commit 1b9bdac12ff0398b06b5b3b8d3f78a64eb08b89c Author: 小蚂蚁云 Date: Tue Oct 20 19:07:36 2026 +0800 签入版本 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bd36bf9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +# ============================================================ +# .dockerignore —— Docker 构建上下文排除清单 +# 位置约定:必须位于构建上下文根目录(项目根),与 docker/Dockerfile 位置无关 +# ============================================================ + +# 版本控制与 IDE +.git +.gitignore +.idea +.vscode +.claude + +# 前端(独立部署,不进后端镜像) +/ui + +# 构建产物与本地运行痕迹 +/bin +/log +/upload + +# Docker 自身配置(不参与镜像构建) +/docker +/.jenkins + +# 开发/文档资产,运行时不需要 +/document +/hack +*.md +Makefile + +# 本地敏感/临时文件 +.env +*.exe +*.db diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..803c07e --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# ============================================================ +# .gitignore —— GoFrame 后端 + Vue3 前端 仓库排除清单 +# 说明:go.sum 必须入库(依赖版本可复现),请勿加入本文件 +# ============================================================ + +# ----- IDE 与编辑器 ----- +.idea/ +.vscode/ +.settings/ +.buildpath +.project +*.swp +*.orig + +# ----- OS 系统文件 ----- +.DS_Store +Thumbs.db + +# ----- Go 构建产物与本地工具 ----- +bin/ +pkg/ +main +cbuild +*.exe +vender/ +vendor/ +# 各产品线 CI 编译产物(goframe_elevue / goframe_antdvue / goframe_naivevue …) +/goframe_* + +# ----- 运行时数据(不入库) ----- +log/ +upload/temp/ + +# ----- 前端(ui/ 独立工程,仅源码入库) ----- +ui/node_modules/ +ui/dist/ +ui/.vite/ +ui/.eslintcache + +# ----- Docker 本地密钥(模板 .env.example 入库) ----- +docker/.env +.env + +# ----- 本地脚本 ----- +gitpush.sh diff --git a/.jenkins/.env.yml b/.jenkins/.env.yml new file mode 100644 index 0000000..b550c29 --- /dev/null +++ b/.jenkins/.env.yml @@ -0,0 +1,16 @@ +masterProfiles: ['','uat','prod'] +Profiles: ['','dev'] +jobConfigFile: "easygoadminConfig.yml" +npmCompileCMD: | + export PATH=/usr/local/node/bin:$PATH + set -e \ + && yarn config set registry https://registry.npmmirror.com \ + && yarn config get registry \ + && yarn config set strict-ssl false \ + && yarn config set sass_binary_site https://repo.huaweicloud.com/node-sass/node-sass \ + && rm -rf package-lock.json \ + && rm -rf node_modules/.cache 2>/dev/null || true \ + && yarn cache clean --force \ + && yarn global add node-gyp@10.3.1 \ + && yarn install --unsafe-perm --legacy-peer-deps --network-timeout 300000 \ + && yarn run build:${PROFILE} \ No newline at end of file diff --git a/.jenkins/Dockerfile b/.jenkins/Dockerfile new file mode 100644 index 0000000..85c376f --- /dev/null +++ b/.jenkins/Dockerfile @@ -0,0 +1,90 @@ +# +====================================================================== +# | 模块: Jenkins 镜像构建文件 +# | 说明: CI 场景将已编译的 Go 二进制打包为应用镜像(编译在 Jenkins 侧完成,镜像内不编译) +# | 适用: 各产品线共用;约定 二进制名 = 产品名 = 镜像名,由流水线统一注入 +# | 构建示例(在项目根执行,产品名因产品线而异): +# | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o <产品名> main.go +# | docker build -f .jenkins/Dockerfile \ +# | --build-arg VAR_BASE_IMAGE=<基础镜像> \ +# | --build-arg VAR_GOFILE=<产品名> \ +# | --build-arg VAR_GOFILE_PATH=. \ +# | --build-arg VAR_PORT=<端口> \ +# | -t <仓库>/<产品名>: . +# | 注意: +# | - 构建参数一律无默认值,必须由流水线显式传入(传什么就是什么,漏传/传错在构建期报错) +# | - VAR_GOFILE 为二进制文件名;VAR_GOFILE_PATH 为二进制所在目录(项目根传 .,可传 bin 等) +# | - 实际取用路径 = VAR_GOFILE_PATH/VAR_GOFILE(如 ./产品名 或 bin/产品名) +# | - 编译产物必须是 linux/amd64 格式(Windows 编译需加 GOOS=linux GOARCH=amd64) +# +====================================================================== + +# 基础镜像地址(如 alpine:3.22,可指向公司私服预置镜像) +ARG VAR_BASE_IMAGE + +# 基础镜像 +FROM ${VAR_BASE_IMAGE} +# 镜像作者 +LABEL MAINTAINER="easygoadmin@163.com" + +# 端口参数(后端监听端口,与 manifest/config/config.yaml 的 server.address 一致) +# 无默认值:未传时 ARG 静默置空,由下方校验层拦截报错 +ARG VAR_PORT +# 二进制文件名参数(= 产品名) +ARG VAR_GOFILE +# 二进制所在目录参数(相对构建上下文;项目根传 .,可传 bin 等自定义目录) +ARG VAR_GOFILE_PATH + +# 设置端口 +ENV PORT=${VAR_PORT} +# 设置二进制文件名 +ENV GOFILE=${VAR_GOFILE} +# 设置时区(日志落盘与 gcron 定时任务依赖系统时区) +ENV TZ=Asia/Shanghai + +# 构建参数校验(紧随 ENV,先于 EXPOSE/COPY) +# ARG 未传时静默置空;若不拦截,EXPOSE 空参、COPY 空路径都会先报 +# "EXPOSE requires at least one argument" / "source path cannot be absolute" +# 这类与参数毫无关联的晦涩错误——在此统一转为明确的参数缺失报错 +RUN set -eux; \ + if [ -z "${PORT}" ] || [ -z "${GOFILE}" ]; then \ + echo "ERROR: 构建参数缺失。必须显式传入:VAR_PORT(监听端口)、VAR_GOFILE(二进制文件名)、VAR_GOFILE_PATH(所在目录,项目根传 .)"; \ + exit 1; \ + fi + +# 暴露端口 +EXPOSE ${PORT} + +# 创建工作区 +RUN mkdir -p /opt/apps +# 设置工作区 +WORKDIR /opt/apps + +# 拷贝运行时资产(四件缺一不可,路径相对项目根) +# manifest/config 运行时配置(敏感项运行时经 GF_ 环境变量覆盖,不改镜像内文件) +# manifest/data ip2region.xdb 归属地库 +# resource 验证码字体、swagger 页面等静态资源 +# templates 代码生成器模板(tool 域运行时读取) +COPY manifest ./manifest +COPY resource ./resource +COPY templates ./templates + +# 拷贝 Jenkins 编译产物(路径 = VAR_GOFILE_PATH/VAR_GOFILE,如 ./产品名 或 bin/产品名) +# 编译建议:CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "-s -w" -o <产品名> main.go +COPY ${VAR_GOFILE_PATH}/${VAR_GOFILE} ./ + +# 构建期校验 + 补执行权限 +# 参数传错/产物缺失在此立即失败并给出指引,不留到启动期以循环报错暴露 +# chmod 防编译产物经跨系统拷贝丢失执行位(COPY 保留源文件权限) +RUN set -eux; \ + if [ -z "${GOFILE}" ] || [ ! -f "./${GOFILE}" ]; then \ + echo "ERROR: 二进制 ./${GOFILE} 不存在。请检查构建参数:VAR_GOFILE(文件名)与 VAR_GOFILE_PATH(所在目录,根目录传 .)、编译产物是否落在构建上下文内"; \ + exit 1; \ + fi; \ + chmod +x "./${GOFILE}" + +# 启动命令 +# ${GOFILE} 为编译产物文件名(= 产品名) +# 配置覆盖示例(部署阶段注入,勿写死镜像): +# -e GF_DATABASE_DEFAULT_LINK="mysql:<用户>:<密码>@tcp(<主机>:<端口>)/<库名>?charset=utf8mb4&parseTime=true&loc=Local" +# -e GF_REDIS_DEFAULT_ADDRESS="<主机>:<端口>" +# -e GF_FILE_UPLOADDIR="/data/upload" # 容器内必须覆盖(配置默认为开发机路径) +CMD ["sh", "-c", "./${GOFILE}"] diff --git a/.jenkins/Jenkinsfile b/.jenkins/Jenkinsfile new file mode 100644 index 0000000..bc5a07b --- /dev/null +++ b/.jenkins/Jenkinsfile @@ -0,0 +1,3 @@ +@Library('share-library@master')_ +orgName = 'easygoadmin' +pipelineGolangService(orgName) \ No newline at end of file diff --git a/.jenkins/Jenkinsfile2 b/.jenkins/Jenkinsfile2 new file mode 100644 index 0000000..bd1223d --- /dev/null +++ b/.jenkins/Jenkinsfile2 @@ -0,0 +1,3 @@ +@Library('share-library@master')_ +orgName = 'easygoadmin' +pipelineNodeService(orgName) \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c6dc028 --- /dev/null +++ b/Makefile @@ -0,0 +1,41 @@ +# ====================================================================== +# EasyGoAdmin_GoFrame_EleVue 常用命令 +# 前置:go install github.com/gogf/gf/cmd/gf/v2@latest +# ====================================================================== +GF ?= gf +APP ?= admin-server +BIN ?= bin/$(APP).exe + +.PHONY: run build test vet fmt gen gen-dao gen-service clean + +## 本地运行 +run: + go run main.go + +## 编译输出 +build: + go build -o $(BIN) main.go + +## 单元测试(全部);单测指定包:go test ./internal/logic/... +test: + go test ./... + +## 静态检查 +vet: + go vet ./... + +## 格式化 +fmt: + gofmt -w . + +## 重新生成 dao + service(表结构变更后执行) +gen: gen-dao gen-service + +gen-dao: + $(GF) gen dao -c hack/config.yaml + +gen-service: + $(GF) gen service -c hack/config.yaml + +clean: + if exist $(BIN) del /f $(BIN) diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..e0fa085 --- /dev/null +++ b/README.md @@ -0,0 +1,171 @@ +## 📚 项目介绍 + +一款基于 GoFrame v2、Vue3、TypeScript、ElementPlus 打造的模块化、高性能前后端分离敏捷开发框架,可快速搭建企业级后台管理系统。框架本着简化开发、提升效率的初衷,内置代码生成器,根据已建表结构一键生成整个模块的增删改查代码,并同步创建菜单、权限节点与字典数据,真正实现低代码开发,缩短研发周期。 + +框架提供体系化的后端能力:多租户数据隔离、RBAC 权限与四档数据权限、JWT 双令牌认证、接口限流、操作日志审计、在线用户/服务器/缓存三组监控、可视化定时任务等开箱即用;并配有全局钩子机制,审计字段、数据软删、租户归属等横切逻辑统一在数据层自动完成,业务开发零负担。 + +数据库全面支持 MySQL、PostgreSQL、SqlServer、Oracle、Sqlite 五种主流数据库并可灵活切换,配套整库迁移工具,跨数据库迁移一条命令完成;前端提供动态菜单、25+ 公共组件与统一请求层,开箱即用;同时提供 Docker 全栈编排,从开发到部署一条龙。 + +## 🍻 项目特点 + ++ 低代码开发:代码生成器读表结构一键产出后端八件套 + 前端整套 CRUD 代码,并同步创建菜单、权限节点、字典数据与路由注册(幂等),支持 HTTP 与 CLI 两种方式 ++ 多数据库支持:内置 MySQL、PostgreSQL、SqlServer、Oracle、Sqlite 五方言驱动适配可灵活切换;配套整库迁移工具,源库内省 → 方言 DDL 翻译 → 分批搬运 → 序列修复,跨库迁移一条命令完成 ++ 企业级多租户:数据层全局租户隔离(全局钩子改写 + dao.Ctx 唯一查询入口),租户 CRUD、专属域名与平台管理开箱即用 ++ 细粒度权限体系:RBAC(用户-角色-菜单)+ 四档数据权限(全部/本部门及以下/本部门/仅本人),多角色自动取最宽松,超管/平台绕过 ++ 安全基座:JWT 双令牌(访问+刷新轮换、黑名单登出/强踢)、图形验证码、固定窗口接口限流(认证入口更严阈值)、操作日志审计、演示环境写保护 ++ 全局钩子机制:审计字段盖章、软删除改写、租户隔离统一在数据层生效,业务代码零侵入 ++ 统一响应与规范:全链路驼峰、统一错误码、分页/筛选三件套定稿,枚举显示名由字典驱动(唯一真相,文案改字典即生效) ++ 现代化前端:Vue3 + TypeScript + Element Plus + Vite,动态菜单路由、封装请求层、25+ 公共组件(表格/表单/上传/富文本/Excel/行政区划等) ++ 完善监控运维:在线用户监控(可强制下线)、服务器监控、缓存监控;Redis 故障自动降级内存,服务不中断 ++ 定时任务框架:数据库可视化配置任务,支持 cron/固定频率/仅一次三种模式与 1 并行/单例/放弃三种执行策略,多实例经分布式锁不双跑 ++ Docker 容器化:内置多阶段 Dockerfile(静态编译、非 root、健康检查)与 docker-compose 全栈编排(后端+MySQL+Redis),敏感配置环境变量注入 ++ 文档与质量保障:140+ 篇开发者文档(架构/实战/部署/规范)、OpenAPI 原生输出(/swagger)、黄金样本接口回归基线 + +## 🍪 内置模块 ++ 用户管理:维护管理系统的用户,常规信息维护与账号设置。 ++ 角色管理:角色菜单管理与权限分配、设置角色所拥有的菜单权限。 ++ 菜单管理:配置系统菜单、操作权限、按钮权限标识等。 ++ 部门管理:管理系统组织架构,对组织架构进行统一管理维护。 ++ 职级管理:管理用户担任的职级。 ++ 岗位管理:管理用户担任的岗位。 ++ 租户管理:多租户开通、配置与停启用,租户级数据全局隔离。 ++ 字典管理:对系统中常用的较为固定的数据进行统一维护,枚举文案改字典即生效。 ++ 配置管理:对系统的常规配置信息进行维护。 ++ 参数管理:系统运行参数的统一维护与读取。 ++ 城市管理:统一对全国行政区划进行维护,对其他模块提供行政区划数据支撑。 ++ 分类管理:树形分类数据的统一维护,支撑内容模块。 ++ 文章管理:文章的发布、维护与富文本内容处理。 ++ 通知公告:系统通知公告信息发布维护。 ++ 友链管理:对系统友情链接、合作伙伴等相关外链进行集成维护管理。 ++ 文件模板管理:富文本/上传等文件模板的统一管理。 ++ 定时任务:可视化配置调度任务(cron/固定频率/仅一次),含执行日志查询。 ++ 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。 ++ 登录日志:系统登录日志记录查询,包含登录异常。 ++ 在线用户监控:当前在线用户查询与强制下线。 ++ 服务器监控:运行时、磁盘与数据库/Redis 连通性监测。 ++ 缓存监控:Redis 运行状态与内存使用监测(降级内存模式可见)。 ++ 个人中心:当前登录用户的个人信息便捷修改。 ++ 代码生成:一键生成模块 CRUD 功能,包括后端和前端等相关代码,并同步创建菜单与权限。 ++ 案例演示:代码生成器一键生成后的演示案例。 + +## 👷 软件信息 +* 软件名称:EasyGoAdmin敏捷开发框架GoFrame+ElementPlus版本 +* 软件作者:@小蚂蚁云 团队 +* 软件协议:Apache-2.0 +* 官网网址:[https://www.easygoadmin.vip](https://www.easygoadmin.vip) +* 文档网址:[http://docs.goframe.elevue.easygoadmin.vip](http://docs.goframe.elevue.easygoadmin.vip) +* 演示地址:[http://manage.goframe.elevue.easygoadmin.vip](http://manage.goframe.elevue.easygoadmin.vip) + +## 📌 版本说明 + +版本名称 | 版本说明 | 版本地址 +---|-------------------------------------|--- +GoFrame+Layui混编版 | 采用GoFrame v2、Layui等框架研发 | https://gitee.com/easygoadmin/EasyGoAdmin_GoFrame_Layui +GoFrame+ElementPlus前后端分离版 | 采用GoFrame v2、Vue3、TypeScript、ElementPlus等框架研发前后端分离版本 | https://gitee.com/easygoadmin/EasyGoAdmin_GoFrame_EleVue +GoFrame+AntDesign前后端分离版 | 采用GoFrame v2、Vue3、TypeScript、AntDesign等框架研发前后端分离版本 | https://gitee.com/easygoadmin/EasyGoAdmin_GoFrame_AntdVue +GoFrame+NaiveUI前后端分离版 | 采用GoFrame v2、Vue3、TypeScript、NaiveUI等框架研发前后端分离版本 | https://gitee.com/easygoadmin/EasyGoAdmin_GoFrame_NaiveVue +Gin+Layui混编版 | 采用Gin、Layui等框架研发 | https://gitee.com/easygoadmin/EasyGoAdmin_Gin_Layui +Gin+ElementPlus前后端分离版 | 采用Gin、Vue3、TypeScript、ElementPlus等框架研发前后端分离版本 | https://gitee.com/easygoadmin/EasyGoAdmin_Gin_EleVue +Gin+AntDesign前后端分离版 | 采用Gin、Vue3、TypeScript、AntDesign等框架研发前后端分离版本 | https://gitee.com/easygoadmin/EasyGoAdmin_Gin_AntdVue +Gin+NaiveUI前后端分离版 | 采用Gin、Vue3、TypeScript、NaiveUI等框架研发前后端分离版本 | https://gitee.com/easygoadmin/EasyGoAdmin_Gin_NaiveVue +Beego+Layui混编版 | 采用Beego、Layui等框架研发 | https://gitee.com/easygoadmin/EasyGoAdmin_Beego_Layui +Beego+ElementPlus前后端分离版 | 采用Beego、Vue3、TypeScript、ElementPlus等框架研发前后端分离版本 | https://gitee.com/easygoadmin/EasyGoAdmin_Beego_EleVue +Beego+AntDesign前后端分离版 | 采用Beego、Vue3、TypeScript、AntDesign等框架研发前后端分离版本 | https://gitee.com/easygoadmin/EasyGoAdmin_Beego_AntdVue +Beego+NaiveUI前后端分离版 | 采用Beego、Vue3、TypeScript、NaiveUI等框架研发前后端分离版本 | https://gitee.com/easygoadmin/EasyGoAdmin_Beego_NaiveVue +Iris+Layui混编版 | 采用Iris、Layui等框架研发 | https://gitee.com/easygoadmin/EasyGoAdmin_Iris_Layui +Iris+ElementPlus前后端分离版 | 采用Iris、Vue3、TypeScript、ElementPlus等框架研发前后端分离版本 | https://gitee.com/easygoadmin/EasyGoAdmin_Iris_EleVue +Iris+AntDesign前后端分离版 | 采用Iris、Vue3、TypeScript、AntDesign等框架研发前后端分离版本 | https://gitee.com/easygoadmin/EasyGoAdmin_Iris_AntdVue +Iris+NaiveUI前后端分离版 | 采用Iris、Vue3、TypeScript、NaiveUI等框架研发前后端分离版本 | https://gitee.com/easygoadmin/EasyGoAdmin_Iris_NaiveVue + +## 🍪 项目结构 + +```text +EasyGoAdmin_GoFrame_EleVue/ +├── main.go # 服务入口:注册五库驱动 + Redis 适配器 → cmd.Run +├── Makefile # run/build/test/vet/fmt/gen(gen-dao)/clean +├── api/ # 全部请求/响应结构体(单包 v1,每模块一文件) +├── cmd/ # CLI 入口 +│ ├── generator/ # 代码生成器(文件+菜单+字典+路由,幂等) +│ └── migrator/ # 整库迁移(源库内省 → 五方言 DDL → 分批搬运) +├── internal/ # 后端核心代码 +│ ├── cmd/ # 启动编排:cache.Init → 依赖接线 → 路由 → 调度器 +│ ├── controller/ # 控制器(单包薄层,仅转发 service) +│ ├── logic/ # 业务逻辑(单包,每模块一文件,同包 init 注册) +│ ├── service/ # 服务接口层(手维护,勿运行 gf gen service) +│ ├── dao/ # 数据访问层(gf gen dao 产出,挂钩子 + Ctx 覆写) +│ ├── model/ # entity / do / hooks(审计盖章、软删、租户隔离) +│ ├── middleware/ # 中间件链:CORS→Response→RateLimit→Demo→JWT→Tenant→OperationLog +│ ├── router/ # 路由注册(router.go)+ gmeta 反射绑定(bind.go) +│ ├── task/ # 定时任务处理器(init 自注册,依赖单向 task→logic) +│ ├── consts/ # 业务错误码 / 缓存键 / 日志类型枚举 +│ └── migrate/ # 整库迁移引擎(内省/翻译/DDL 渲染/搬运) +├── utility/ # 基础能力包(无业务依赖) +│ ├── jwt/ cache/ upload/ # 双令牌 / 缓存降级 / 两段式上传 +│ ├── captcha/ crypto/ excel # 图形验证码 / 加解密 / Excel 导入导出 +│ └── ip2region/ response/ ua # IP 归属地 / 统一响应 / 设备识别 +├── templates/ # 代码生成器模板(后端 {{ }} 定界;ui* 前端 [[ ]] 定界) +├── manifest/ +│ ├── config/config.yaml # 运行时配置(唯一配置文件,GF_ 环境变量可覆盖) +│ └── data/ip2region.xdb # IP 归属地库 +├── resource/ +│ ├── static/ # 验证码字体等静态资源 +│ ├── swagger/ # 本地托管 API 文档页面 +│ └── upload/ # 上传文件静态服务根(/upload/**) +├── hack/ # gf gen dao 配置、自定义模板、黄金样本回归基线 +├── docker/ # Docker 化文件(构建上下文为项目根) +│ ├── Dockerfile # 多阶段构建:静态编译 → alpine 运行(非 root + 健康检查) +│ ├── docker-compose.yml # 三服务编排:后端 + MySQL8 + Redis7 +│ └── .env.example # compose 变量模板(复制为 .env,密钥不入库) +├── .dockerignore # Docker 构建上下文排除清单 +├── ui/ # 前端工程(Vue3 + TypeScript + Element Plus + Vite) +├── document/ # 项目文档(架构定稿、五方言 SQL、wiki) +├── upload/ # 上传物理目录(file.uploadDir,挂载持久卷) +├── log/ # 运行日志 +└── bin/ # 编译产物 +``` + +## 🚀 快速开始 + +**环境要求**:Go ≥ 1.27、Node.js 22.18.0(pnpm 推荐)、MySQL 8.0、Redis + +```sh +# 1. 初始化数据库:导入定稿脚本 +# document/mysql/easygoadmin.goframe.elevue.sql → 库 easygoadmin.goframe.elevue + +# 2. 启动后端(默认 :8000,配置见 manifest/config/config.yaml) +go run main.go + +# 3. 启动前端(默认 :8001,代理转发至 :8000) +cd ui && pnpm install && pnpm dev + +# 或 Docker 一条命令拉起全栈(后端 + MySQL + Redis) +cd docker && copy .env.example .env && docker compose up -d --build +``` + +登录账号 `admin / 123456`。更多启动细节与常见问题见[在线文档·快速入门](http://docs.goframe.elevue.easygoadmin.vip/zh/quickstart/environment.html)。 + +## 🔧 模块展示 + + +| | | +|----------------------------------|----------------------------------| +| ![模块展示1](./upload/demo/1.png) | ![模块展示2](./upload/demo/2.png) | +| ![模块展示3](./upload/demo/3.png) | ![模块展示4](./upload/demo/4.png) | +| ![模块展示5](./upload/demo/5.png) | ![模块展示6](./upload/demo/6.png) | +| ![模块展示7](./upload/demo/7.png) | ![模块展示8](./upload/demo/8.png) | +| ![模块展示9](./upload/demo/9.png) | ![模块展示10](./upload/demo/10.png) | +| ![模块展示11](./upload/demo/11.png) | ![模块展示12](./upload/demo/12.png) | +| ![模块展示13](./upload/demo/13.png) | ![模块展示14](./upload/demo/14.png) | +| ![模块展示15](./upload/demo/15.png) | ![模块展示16](./upload/demo/16.png) | +| ![模块展示17](./upload/demo/17.png) | ![模块展示18](./upload/demo/18.png) | +| ![模块展示19](./upload/demo/19.png) | ![模块展示20](./upload/demo/20.png) | +| ![模块展示21](./upload/demo/21.png) | ![模块展示22](./upload/demo/22.png) | +| ![模块展示23](./upload/demo/23.png) | ![模块展示24](./upload/demo/24.png) | +| ![模块展示25](./upload/demo/25.png) | ![模块展示26](./upload/demo/26.png) | +| ![模块展示27](./upload/demo/27.png) | ![模块展示28](./upload/demo/28.png) | +| ![模块展示29](./upload/demo/29.png) | ![模块展示30](./upload/demo/30.png) | +| ![模块展示31](./upload/demo/31.png) | ![模块展示32](./upload/demo/32.png) | +| ![模块展示33](./upload/demo/33.png) | ![模块展示34](./upload/demo/34.png) | + +## ✨ 特别鸣谢 +感谢[GoFrame](https://goframe.org/#all-updates)、[Vue.js](https://cn.vuejs.org/)、[Element Plus](https://element-plus.org/zh-CN/)、[TypeScript](https://www.typescriptlang.org/)、[Vite](https://cn.vitejs.dev/)、[Pinia](https://pinia.vuejs.org/)等优秀开源项目。 \ No newline at end of file diff --git a/api/v1/article.go b/api/v1/article.go new file mode 100644 index 0000000..271ab31 --- /dev/null +++ b/api/v1/article.go @@ -0,0 +1,184 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 文章模块 ============================== + +// ArticleItem 文章行 +type ArticleItem struct { + Id int64 `json:"id"` // 主键ID + CategoryId int `json:"categoryId"` // 分类ID + CategoryName string `json:"categoryName"` // 分类名称 + Title string `json:"title"` // 标题 + Cover string `json:"cover"` // 封面图 + Intro string `json:"intro"` // 简介 + Author string `json:"author"` // 作者 + Status int `json:"status"` // 状态:0-草稿 1-发布 + StatusText string `json:"statusText"` // 状态文本 + Click int `json:"click"` // 点击量 + Sort int `json:"sort"` // 排序 + CreateUser string `json:"createUser"` // 创建人 + CreateTime *gtime.Time `json:"createTime"` // 创建时间 +} + +// ArticlePageReq 文章分页请求 +type ArticlePageReq struct { + gmeta.Meta `method:"get" path:"/api/article/page" summary:"文章分页" permission:"sys:article:page" log:"文章管理-查询分页记录" logType:"query"` + PageReq // 分页基础参数 + Keyword string `json:"keyword" dc:"关键词(标题/作者模糊)"` // 关键词 + Title string `json:"title" dc:"标题模糊"` // 标题 + CategoryId int `json:"categoryId" dc:"分类ID"` // 分类ID + Status int `json:"status" dc:"状态:0-草稿 1-发布"` // 状态 +} + +// ArticlePageRes 文章分页结果 +type ArticlePageRes = PageData[ArticleItem] + +// ArticleListReq 文章列表请求(下拉/最新) +type ArticleListReq struct { + gmeta.Meta `method:"get" path:"/api/article/list" summary:"文章列表" permission:"sys:article:list" log:"文章管理-查询列表" logType:"query"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 + CategoryId int `json:"categoryId" dc:"分类ID"` // 分类ID + Status int `json:"status" dc:"状态"` // 状态 + Limit int `json:"limit" dc:"条数上限"` // 条数上限 +} + +// ArticleListItem 文章列表行(精简) +type ArticleListItem struct { + Id int64 `json:"id"` // 主键ID + Title string `json:"title"` // 标题 + Status int `json:"status"` // 状态:0-草稿 1-发布 + Sort int `json:"sort"` // 排序 +} + +// ArticleListRes 文章列表结果 +type ArticleListRes = []ArticleListItem + +// ArticleDetailReq 文章详情请求 +type ArticleDetailReq struct { + gmeta.Meta `method:"get" path:"/api/article/detail/:id" summary:"文章详情" permission:"sys:article:detail" log:"文章管理-查询详情" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"文章ID"` // 文章ID +} + +// ArticleDetailRes 文章详情结果 +type ArticleDetailRes struct { + Id int64 `json:"id"` // 主键ID + CategoryId int `json:"categoryId"` // 分类ID + Title string `json:"title"` // 标题 + Cover string `json:"cover"` // 封面图 + Images string `json:"images"` // 多图(逗号分隔) + ImagesList []string `json:"imagesList"` // 多图列表 + Intro string `json:"intro"` // 简介 + Content string `json:"content"` // 正文 + Author string `json:"author"` // 作者 + Status int `json:"status"` // 状态:0-草稿 1-发布 + StatusText string `json:"statusText"` // 状态文本 + Click int `json:"click"` // 点击量 + Sort int `json:"sort"` // 排序 + CreateTime *gtime.Time `json:"createTime"` // 创建时间 +} + +// ArticleAddReq 新增文章请求 +type ArticleAddReq struct { + gmeta.Meta `method:"post" path:"/api/article/add" summary:"新增文章" permission:"sys:article:add" log:"文章管理-新增记录" logType:"add"` + CategoryId int `json:"categoryId" dc:"分类ID"` // 分类ID + Title string `json:"title" v:"required#请输入标题" dc:"标题"` // 标题 + Cover string `json:"cover" dc:"封面图"` // 封面图 + Images string `json:"images" dc:"多图(逗号分隔)"` // 多图 + Intro string `json:"intro" dc:"简介"` // 简介 + Content string `json:"content" dc:"正文"` // 正文 + Author string `json:"author" dc:"作者"` // 作者 + Status int `json:"status" dc:"状态:0-草稿 1-发布"` // 状态 + Sort int `json:"sort" dc:"排序"` // 排序 +} + +// ArticleUpdateReq 修改文章请求 +type ArticleUpdateReq struct { + gmeta.Meta `method:"put" path:"/api/article/update" summary:"修改文章" permission:"sys:article:update" log:"文章管理-修改记录" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"文章ID"` // 文章ID + CategoryId int `json:"categoryId" dc:"分类ID"` // 分类ID + Title string `json:"title" v:"required#请输入标题" dc:"标题"` // 标题 + Cover string `json:"cover" dc:"封面图"` // 封面图 + Images string `json:"images" dc:"多图"` // 多图 + Intro string `json:"intro" dc:"简介"` // 简介 + Content string `json:"content" dc:"正文"` // 正文 + Author string `json:"author" dc:"作者"` // 作者 + Status int `json:"status" dc:"状态"` // 状态 + Click int `json:"click" dc:"点击量"` // 点击量 + Sort int `json:"sort" dc:"排序"` // 排序 +} + +// ArticleUpdateRes 修改文章结果 +type ArticleUpdateRes struct{} + +// ArticleStatusReq 文章状态请求 +type ArticleStatusReq struct { + gmeta.Meta `method:"put" path:"/api/article/status" summary:"文章状态" permission:"sys:article:status" log:"文章管理-设置状态" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"文章ID"` // 文章ID + Status int `json:"status" v:"required#缺少状态" dc:"状态:0-草稿 1-发布"` // 状态 +} + +// ArticleStatusRes 文章状态结果 +type ArticleStatusRes struct{} + +// ArticleDeleteReq 删除文章请求 +type ArticleDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/article/delete/:id" summary:"删除文章" permission:"sys:article:delete" log:"文章管理-删除记录" logType:"delete"` + Id int64 `json:"id" v:"required#缺少ID" dc:"文章ID"` // 文章ID +} + +// ArticleDeleteRes 删除文章结果 +type ArticleDeleteRes struct{} + +// ArticleBatchDeleteReq 批量删除文章请求 +type ArticleBatchDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/article/batchDelete" summary:"批量删除文章" permission:"sys:article:batchDelete" log:"文章管理-批量删除记录" logType:"delete"` + Ids []int64 `json:"ids" v:"required#请选择要删除的记录" dc:"文章ID列表"` // 文章ID列表 +} + +// ArticleBatchDeleteRes 批量删除结果 +type ArticleBatchDeleteRes struct{} diff --git a/api/v1/auth.go b/api/v1/auth.go new file mode 100644 index 0000000..91281f6 --- /dev/null +++ b/api/v1/auth.go @@ -0,0 +1,129 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 认证授权模块 ============================== + +// LoginReq 登录请求 +// 参数支持 query/body 混合提交 +type LoginReq struct { + gmeta.Meta `method:"post" path:"/api/login" summary:"登录" public:"true"` + Username string `json:"username" v:"required#请输入用户名" dc:"用户名"` // 用户名 + Password string `json:"password" v:"required#请输入密码" dc:"密码"` // 密码 + Code string `json:"code" dc:"图形验证码"` // 图形验证码 + Key string `json:"key" dc:"验证码标识(captcha 接口返回的 key)"` // 验证码标识 + TenantCode string `json:"tenantCode" dc:"租户编码,留空按用户名全局定位"` // 租户编码 +} + +// LoginRes 登录结果 +// 客户端从 data.access_token 中读取并存储令牌 +type LoginRes struct { + AccessToken string `json:"access_token"` // 访问令牌 + RefreshToken string `json:"refresh_token"` // 刷新令牌 + ExpiresIn int64 `json:"expires_in"` // access 有效期(秒) + TokenType string `json:"token_type"` // 令牌类型,固定为 Bearer +} + +// LogoutReq 登出请求 +// 拉黑当前 access token,需携带 access token 走 JWT 校验 +type LogoutReq struct { + gmeta.Meta `method:"get" path:"/api/logout" summary:"登出"` +} + +// LogoutRes 登出结果 +type LogoutRes struct{} + +// CaptchaReq 图形验证码请求 +type CaptchaReq struct { + gmeta.Meta `method:"get" path:"/api/captcha" summary:"获取图形验证码" public:"true"` +} + +// CaptchaRes 图形验证码结果 +type CaptchaRes struct { + Key string `json:"key" dc:"验证码标识"` // 验证码标识 + Captcha string `json:"captcha" dc:"SVG data URL 图片"` // SVG data URL 图片 +} + +// RefreshReq 刷新令牌请求 +// 同时兼容 refresh_token 与 password 两种授权模式 +type RefreshReq struct { + gmeta.Meta `method:"post" path:"/api/oauth2/token" summary:"刷新令牌/密码模式兑换" public:"true"` + GrantType string `json:"grant_type" dc:"refresh_token | password"` // 授权类型 + RefreshToken string `json:"refresh_token" dc:"刷新令牌(grant_type=refresh_token 时必填)"` // 刷新令牌 + Username string `json:"username" dc:"用户名(grant_type=password 时必填)"` // 用户名 + Password string `json:"password" dc:"密码(grant_type=password 时必填)"` // 密码 + Code string `json:"code" dc:"验证码"` // 验证码 + Key string `json:"key" dc:"验证码标识"` // 验证码标识 +} + +// RefreshRes 刷新令牌结果 +// 结构与登录结果保持一致 +type RefreshRes struct { + AccessToken string `json:"access_token"` // 访问令牌 + RefreshToken string `json:"refresh_token"` // 刷新令牌 + ExpiresIn int64 `json:"expires_in"` // access 有效期(秒) + TokenType string `json:"token_type"` // 令牌类型 +} + +// CheckTokenReq 校验令牌请求 +type CheckTokenReq struct { + gmeta.Meta `method:"get" path:"/api/oauth2/check/token" summary:"校验令牌"` +} + +// CheckTokenRes 校验令牌结果 +type CheckTokenRes struct { + Valid bool `json:"valid"` // 是否有效 + UserId int64 `json:"userId"` // 用户ID +} + +// RemoveTokenReq 指定令牌拉黑请求 +type RemoveTokenReq struct { + gmeta.Meta `method:"delete" path:"/api/oauth2/remove/:token" summary:"撤销指定令牌"` + Token string `json:"token" v:"required#缺少token" dc:"待撤销的令牌"` // 待撤销的令牌 +} + +// RemoveTokenRes 撤销令牌结果 +type RemoveTokenRes struct{} diff --git a/api/v1/cache_monitor.go b/api/v1/cache_monitor.go new file mode 100644 index 0000000..521a961 --- /dev/null +++ b/api/v1/cache_monitor.go @@ -0,0 +1,82 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 缓存监控模块 ============================== + +// CacheInfoReq 查询缓存信息请求 +type CacheInfoReq struct { + gmeta.Meta `method:"get" path:"/api/cache/info" summary:"缓存监控信息" log:"缓存监控-查询信息" logType:"query"` +} + +// CacheKeySpaceDb 单个库的键空间统计 +type CacheKeySpaceDb struct { + Db string `json:"db"` // 库编号(如 db0) + Keys int64 `json:"keys"` // 键数量 + Expires int64 `json:"expires"` // 带过期键数量 +} + +// CacheInfoRes 缓存监控信息结果 +type CacheInfoRes struct { + Mode string `json:"mode"` // redis / memory + Connected bool `json:"connected"` // 是否连通 + Version string `json:"version"` // Redis 版本 + Uptime string `json:"uptime"` // 运行时长 + UsedMemory string `json:"usedMemory"` // 已用内存 + UsedMemoryPeak string `json:"usedMemoryPeak"` // 内存峰值 + MaxMemory string `json:"maxMemory"` // 内存上限(0 表示无限制) + FragRatio string `json:"fragRatio"` // 内存碎片率 + ConnectedClients int64 `json:"connectedClients"` // 客户端连接数 + Keys int64 `json:"keys"` // 键总数 + Hits int64 `json:"hits"` // 命中次数 + Misses int64 `json:"misses"` // 未命中次数 + HitRate string `json:"hitRate"` // 命中率(%) + ExpiredKeys int64 `json:"expiredKeys"` // 累计过期键数 + EvictedKeys int64 `json:"evictedKeys"` // 累计淘汰键数 + TotalCommands int64 `json:"totalCommands"` // 累计执行命令数 + TotalConns int64 `json:"totalConns"` // 累计连接数 + KeySpace []CacheKeySpaceDb `json:"keySpace"` // 键空间明细 +} diff --git a/api/v1/category.go b/api/v1/category.go new file mode 100644 index 0000000..2cc0cef --- /dev/null +++ b/api/v1/category.go @@ -0,0 +1,139 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 分类模块(树形) ============================== + +// CategoryItem 分类行 +type CategoryItem struct { + Id int64 `json:"id"` // 主键ID + CategoryName string `json:"categoryName"` // 分类名称 + CategoryCode string `json:"categoryCode"` // 分类编码 + ParentId int `json:"parentId"` // 上级ID(0=顶级) + Sort int `json:"sort"` // 排序 + Note string `json:"note"` // 备注 + CreateUser string `json:"createUser"` // 创建人 +} + +// CategoryPageReq 分类分页请求(平铺) +type CategoryPageReq struct { + gmeta.Meta `method:"get" path:"/api/category/page" summary:"分类分页" permission:"sys:category:page" log:"分类管理-查询分页记录" logType:"query"` + PageReq // 分页基础参数 + Keyword string `json:"keyword" dc:"关键词(名称/编码模糊)"` // 关键词 + CategoryName string `json:"categoryName" dc:"分类名称模糊"` // 分类名称 + CategoryCode string `json:"categoryCode" dc:"分类编码模糊"` // 分类编码 +} + +// CategoryPageRes 分类分页结果 +type CategoryPageRes = PageData[CategoryItem] + +// CategoryListReq 分类树形列表请求 +type CategoryListReq struct { + gmeta.Meta `method:"get" path:"/api/category/list" summary:"分类树形列表" permission:"sys:category:list" log:"分类管理-查询树形列表" logType:"query"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 +} + +// CategoryListRes 分类平铺列表结果 +// data 直接为数组,由前端自行构建树形结构 +type CategoryListRes = []*CategoryItem + +// CategoryDetailReq 分类详情请求 +type CategoryDetailReq struct { + gmeta.Meta `method:"get" path:"/api/category/detail/:id" summary:"分类详情" permission:"sys:category:detail" log:"分类管理-查询详情" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"分类ID"` // 分类ID +} + +// CategoryDetailRes 分类详情结果 +type CategoryDetailRes struct { + Id int64 `json:"id"` // 主键ID + CategoryName string `json:"categoryName"` // 分类名称 + CategoryCode string `json:"categoryCode"` // 分类编码 + ParentId int `json:"parentId"` // 上级ID + Sort int `json:"sort"` // 排序 + Note string `json:"note"` // 备注 +} + +// CategoryAddReq 新增分类请求 +type CategoryAddReq struct { + gmeta.Meta `method:"post" path:"/api/category/add" summary:"新增分类" permission:"sys:category:add" log:"分类管理-新增记录" logType:"add"` + CategoryName string `json:"categoryName" v:"required#请输入分类名称" dc:"分类名称"` // 分类名称 + CategoryCode string `json:"categoryCode" dc:"分类编码"` // 分类编码 + ParentId int `json:"parentId" dc:"上级ID(0=顶级)"` // 上级ID + Sort int `json:"sort" dc:"排序"` // 排序 + Note string `json:"note" dc:"备注"` // 备注 +} + +// CategoryUpdateReq 修改分类请求 +type CategoryUpdateReq struct { + gmeta.Meta `method:"put" path:"/api/category/update" summary:"修改分类" permission:"sys:category:update" log:"分类管理-修改记录" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"分类ID"` // 分类ID + CategoryName string `json:"categoryName" v:"required#请输入分类名称" dc:"分类名称"` // 分类名称 + CategoryCode string `json:"categoryCode" dc:"分类编码"` // 分类编码 + ParentId int `json:"parentId" dc:"上级ID"` // 上级ID + Sort int `json:"sort" dc:"排序"` // 排序 + Note string `json:"note" dc:"备注"` // 备注 +} + +// CategoryUpdateRes 修改分类结果 +type CategoryUpdateRes struct{} + +// CategoryDeleteReq 删除分类请求 +type CategoryDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/category/delete/:id" summary:"删除分类" permission:"sys:category:delete" log:"分类管理-删除记录" logType:"delete"` + Id int64 `json:"id" v:"required#缺少ID" dc:"分类ID"` // 分类ID +} + +// CategoryDeleteRes 删除分类结果 +type CategoryDeleteRes struct{} + +// CategoryBatchDeleteReq 批量删除分类请求 +type CategoryBatchDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/category/batchDelete" summary:"批量删除分类" permission:"sys:category:batchDelete" log:"分类管理-批量删除记录" logType:"delete"` + Ids []int64 `json:"ids" v:"required#请选择要删除的记录" dc:"分类ID列表"` // 分类ID列表 +} + +// CategoryBatchDeleteRes 批量删除结果 +type CategoryBatchDeleteRes struct{} diff --git a/api/v1/city.go b/api/v1/city.go new file mode 100644 index 0000000..d8fcdfc --- /dev/null +++ b/api/v1/city.go @@ -0,0 +1,196 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 城市模块 ============================== + +// CityItem 城市行数据 +// 用于分页列表、下拉列表等场景的城市基础信息 +type CityItem struct { + Id int64 `json:"id"` // 主键ID + CityCode string `json:"cityCode"` // 城市编码 + AreaCode string `json:"areaCode"` // 区域编码 + ParentCode string `json:"parentCode"` // 父级编码 + ZipCode string `json:"zipCode"` // 邮政编码 + Level int `json:"level"` // 层级:1-省 2-市 3-区 4-街道 + LevelText string `json:"levelText"` // 层级文本 + Pid int64 `json:"pid"` // 父级ID + Name string `json:"name"` // 城市名称 + ShortName string `json:"shortName"` // 城市简称 + FullName string `json:"fullName"` // 城市全称 + Pinyin string `json:"pinyin"` // 拼音 + Lng string `json:"lng"` // 经度 + Lat string `json:"lat"` // 纬度 +} + +// CityPageReq 城市分页请求 +// 支持关键词、名称、拼音及层级等条件组合查询 +type CityPageReq struct { + gmeta.Meta `method:"get" path:"/api/city/page" summary:"城市分页" permission:"sys:city:page" log:"城市管理-查询分页记录" logType:"query"` + PageReq // 分页基础参数 + Keyword string `json:"keyword" dc:"关键词(名称/拼音模糊)"` // 关键词,名称或拼音模糊匹配 + Name string `json:"name" dc:"城市名称模糊"` // 城市名称模糊匹配 + Pinyin string `json:"pinyin" dc:"拼音模糊"` // 拼音模糊匹配 + Level int `json:"level" dc:"层级:1-省 2-市 3-区 4-街道"` // 层级 +} + +// CityPageRes 城市分页结果 +type CityPageRes = PageData[CityItem] + +// CityListReq 城市列表请求 +// 按父级编码、层级等条件查询,供级联选择场景使用 +type CityListReq struct { + gmeta.Meta `method:"get" path:"/api/city/list" summary:"城市列表" permission:"sys:city:list" log:"城市管理-查询列表" logType:"query"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 + Level int `json:"level" dc:"层级"` // 层级 + ParentCode string `json:"parentCode" dc:"父级编码(顶级为空)"` // 父级编码 + Limit int `json:"limit" dc:"条数上限"` // 条数上限 +} + +// CityListRes 城市列表结果 +type CityListRes = []CityItem + +// CityDetailReq 城市详情请求 +type CityDetailReq struct { + gmeta.Meta `method:"get" path:"/api/city/detail/:id" summary:"城市详情" permission:"sys:city:detail" log:"城市管理-查询详情" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"城市ID"` // 城市ID +} + +// CityDetailRes 城市详情结果 +type CityDetailRes struct { + CityItem + ParentName string `json:"parentName" dc:"父级名称"` // 父级名称 +} + +// CityAddReq 新增城市请求 +type CityAddReq struct { + gmeta.Meta `method:"post" path:"/api/city/add" summary:"新增城市" permission:"sys:city:add" log:"城市管理-新增记录" logType:"add"` + CityCode string `json:"cityCode" v:"required#请输入城市编码" dc:"城市编码"` // 城市编码 + AreaCode string `json:"areaCode" dc:"区域编码"` // 区域编码 + ParentCode string `json:"parentCode" dc:"父级编码"` // 父级编码 + ZipCode string `json:"zipCode" dc:"邮编"` // 邮政编码 + Level int `json:"level" dc:"层级"` // 层级 + Pid int64 `json:"pid" dc:"父级ID"` // 父级ID + Name string `json:"name" v:"required#请输入名称" dc:"名称"` // 城市名称 + ShortName string `json:"shortName" dc:"简称"` // 城市简称 + FullName string `json:"fullName" dc:"全称"` // 城市全称 + Pinyin string `json:"pinyin" dc:"拼音"` // 拼音 + Lng string `json:"lng" dc:"经度"` // 经度 + Lat string `json:"lat" dc:"纬度"` // 纬度 +} + +// CityUpdateReq 修改城市请求 +type CityUpdateReq struct { + gmeta.Meta `method:"put" path:"/api/city/update" summary:"修改城市" permission:"sys:city:update" log:"城市管理-修改记录" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"城市ID"` // 城市ID + CityCode string `json:"cityCode" dc:"城市编码"` // 城市编码 + AreaCode string `json:"areaCode" dc:"区域编码"` // 区域编码 + ParentCode string `json:"parentCode" dc:"父级编码"` // 父级编码 + ZipCode string `json:"zipCode" dc:"邮编"` // 邮政编码 + Level int `json:"level" dc:"层级"` // 层级 + Pid int64 `json:"pid" dc:"父级ID"` // 父级ID + Name string `json:"name" dc:"名称"` // 城市名称 + ShortName string `json:"shortName" dc:"简称"` // 城市简称 + FullName string `json:"fullName" dc:"全称"` // 城市全称 + Pinyin string `json:"pinyin" dc:"拼音"` // 拼音 + Lng string `json:"lng" dc:"经度"` // 经度 + Lat string `json:"lat" dc:"纬度"` // 纬度 +} + +// CityUpdateRes 修改城市结果 +type CityUpdateRes struct{} + +// CityDeleteReq 删除城市请求 +type CityDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/city/delete/:id" summary:"删除城市" permission:"sys:city:delete" log:"城市管理-删除记录" logType:"delete"` + Id int64 `json:"id" v:"required#缺少ID" dc:"城市ID"` // 城市ID +} + +// CityDeleteRes 删除城市结果 +type CityDeleteRes struct{} + +// CityBatchDeleteReq 批量删除城市请求 +type CityBatchDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/city/batchDelete" summary:"批量删除城市" permission:"sys:city:batchDelete" log:"城市管理-批量删除记录" logType:"delete"` + Ids []int64 `json:"ids" v:"required#请选择要删除的记录" dc:"城市ID列表"` // 城市ID列表 +} + +// CityBatchDeleteRes 批量删除结果 +type CityBatchDeleteRes struct{} + +// CityByParentReq 按父编码查询下级城市请求 +// 登录用户即可访问,用于查询指定父级下的直属下级城市 +type CityByParentReq struct { + gmeta.Meta `method:"get" path:"/api/area/findByParentCode" summary:"按父编码查询城市" log:"城市查询-查询下级城市" logType:"query"` + ParentCode string `json:"parentCode" v:"required#请输入父级编码" dc:"父级编码"` // 父级编码 +} + +// CityByParentRes 按父编码查询结果 +type CityByParentRes struct { + List []CityItem `json:"list"` // 城市列表 +} + +// AreaParentReq 省份列表请求 +// 登录用户即可访问,用于查询顶级省份列表 +type AreaParentReq struct { + gmeta.Meta `method:"get" path:"/api/area/getParent" summary:"查询省份列表" log:"城市查询-查询省份列表" logType:"query"` +} + +// AreaParentRes 省份列表结果 +type AreaParentRes struct { + List []CityItem `json:"list"` // 省份列表 +} + +// CityGetListReq 按父编码获取下级城市请求 +// 登录用户即可访问,用于级联选择器懒加载下级城市 +type CityGetListReq struct { + gmeta.Meta `method:"get" path:"/api/city/getCityList/:parentCode" summary:"按父编码获取城市列表" log:"城市查询-查询城市列表" logType:"query"` + ParentCode string `json:"parentCode" v:"required#缺少父级编码" dc:"父级行政编码(顶级为0)"` // 父级行政编码,顶级为0 +} + +// CityGetListRes 城市列表结果 +// data 直接为数组 +type CityGetListRes = []CityItem diff --git a/api/v1/common.go b/api/v1/common.go new file mode 100644 index 0000000..ba512ad --- /dev/null +++ b/api/v1/common.go @@ -0,0 +1,113 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +// ============================== 公共基础模块 ============================== + +// PageReq 分页公共参数 +// 对应前端分页契约:pageNo/pageSize +type PageReq struct { + PageNo int `json:"pageNo" dc:"页码,从1开始,默认1"` // 页码,从1开始 + PageSize int `json:"pageSize" dc:"每页条数,默认10,最大500"` // 每页条数 +} + +// GetPageNo 获取页码 +// 页码小于等于0时返回默认值1 +func (p *PageReq) GetPageNo() int { + if p.PageNo <= 0 { + return 1 + } + return p.PageNo +} + +// GetPageSize 获取每页条数 +// 每页条数小于等于0时返回默认值10,超过500时按500截断 +func (p *PageReq) GetPageSize() int { + if p.PageSize <= 0 { + return 10 + } + if p.PageSize > 500 { + return 500 + } + return p.PageSize +} + +// Offset 计算SQL LIMIT偏移量 +// 偏移量 = (页码 - 1) × 每页条数 +func (p *PageReq) Offset() int { + return (p.GetPageNo() - 1) * p.GetPageSize() +} + +// IdsReq 批量删除入参 +// 通用批量操作请求体,携带待操作的ID列表 +type IdsReq struct { + Ids []int64 `json:"ids" v:"required#请选择要操作的记录" dc:"ID列表"` // ID列表 +} + +// IdRes 通用返回ID结构 +// 各模块新增等接口共用的返回结果 +type IdRes struct { + Id int64 `json:"id"` // 主键ID +} + +// PageData 统一分页壳 +// 对应前端分页契约:records/total/size/current/pages +type PageData[T any] struct { + Records []T `json:"records"` // 当前页记录列表 + Total int `json:"total"` // 总记录数 + Size int `json:"size"` // 请求的每页条数 + Current int `json:"current"` // 请求页码 + Pages int `json:"pages"` // 总页数 +} + +// NewPageData 构造分页壳 +// records 为空时输出空数组而非 null,pages 按总记录数与每页条数计算得出 +func NewPageData[T any](pageNo, pageSize, total int, records []T) *PageData[T] { + if records == nil { + records = []T{} + } + pages := 0 + if pageSize > 0 { + pages = (total + pageSize - 1) / pageSize + } + return &PageData[T]{Records: records, Total: total, Size: pageSize, Current: pageNo, Pages: pages} +} diff --git a/api/v1/config.go b/api/v1/config.go new file mode 100644 index 0000000..1c03b36 --- /dev/null +++ b/api/v1/config.go @@ -0,0 +1,211 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "encoding/json" + "errors" + + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 配置模块 ============================== + +// ConfigItemRow 配置项行数据 +// 内嵌使用,供配置详情、配置列表展示配置项明细 +type ConfigItemRow struct { + Id int64 `json:"id"` // 主键ID + Name string `json:"name"` // 配置项名称 + Code string `json:"code"` // 配置项编码 + Value string `json:"value"` // 配置项值 + Type string `json:"type"` // 控件类型 + Status int `json:"status"` // 状态 + Sort int `json:"sort"` // 排序 +} + +// ConfigItem 配置行数据 +// 用于分页列表等场景的配置基础信息 +type ConfigItem struct { + Id int64 `json:"id"` // 主键ID + Name string `json:"name"` // 配置名称 + Code string `json:"code"` // 配置编码 + Sort int `json:"sort"` // 排序 + Note string `json:"note"` // 备注 + CreateUser string `json:"createUser"` // 创建人 +} + +// ConfigPageReq 配置分页请求 +// 支持关键词、配置名称等条件组合查询 +type ConfigPageReq struct { + gmeta.Meta `method:"get" path:"/api/config/page" summary:"配置分页" permission:"sys:config:page" log:"配置管理-查询分页记录" logType:"query"` + PageReq // 分页基础参数 + Keyword string `json:"keyword" dc:"关键词(名称/编码模糊)"` // 关键词,名称或编码模糊匹配 + Name string `json:"name" dc:"配置名称模糊"` // 配置名称模糊匹配 +} + +// ConfigPageRes 配置分页结果 +type ConfigPageRes = PageData[ConfigItem] + +// ConfigListReq 配置列表请求 +// 返回的每一项均携带其下的配置项集合 +type ConfigListReq struct { + gmeta.Meta `method:"get" path:"/api/config/list" summary:"配置列表" permission:"sys:config:list" log:"配置管理-查询列表" logType:"query"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 +} + +// ConfigListRes 配置列表结果 +// 列表每一项携带其下的配置项集合 +type ConfigListRes = []ConfigWithItems + +// ConfigWithItems 配置及其配置项集合 +// 用于列表、详情等需要同时返回配置与配置项的场景 +type ConfigWithItems struct { + ConfigItem + Items []ConfigItemRow `json:"items"` // 配置项集合 +} + +// ConfigDetailReq 配置详情请求 +type ConfigDetailReq struct { + gmeta.Meta `method:"get" path:"/api/config/detail/:id" summary:"配置详情" permission:"sys:config:detail" log:"配置管理-查询详情" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"配置ID"` // 配置ID +} + +// ConfigDetailRes 配置详情结果 +// 包含配置基础信息及其下的配置项集合 +type ConfigDetailRes struct { + ConfigItem + Items []ConfigItemRow `json:"items"` // 配置项集合 +} + +// ConfigAddReq 新增配置请求 +type ConfigAddReq struct { + gmeta.Meta `method:"post" path:"/api/config/add" summary:"新增配置" permission:"sys:config:add" log:"配置管理-新增记录" logType:"add"` + Name string `json:"name" v:"required#请输入配置名称" dc:"配置名称"` // 配置名称 + Code string `json:"code" v:"required#请输入配置编码" dc:"配置编码"` // 配置编码 + Sort int `json:"sort" dc:"排序"` // 排序 + Note string `json:"note" dc:"备注"` // 备注 +} + +// ConfigUpdateReq 修改配置请求 +type ConfigUpdateReq struct { + gmeta.Meta `method:"put" path:"/api/config/update" summary:"修改配置" permission:"sys:config:update" log:"配置管理-修改记录" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"配置ID"` // 配置ID + Name string `json:"name" v:"required#请输入配置名称" dc:"配置名称"` // 配置名称 + Code string `json:"code" v:"required#请输入配置编码" dc:"配置编码"` // 配置编码 + Sort int `json:"sort" dc:"排序"` // 排序 + Note string `json:"note" dc:"备注"` // 备注 +} + +// ConfigUpdateRes 修改配置结果 +type ConfigUpdateRes struct{} + +// ConfigDeleteReq 删除配置请求 +type ConfigDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/config/delete/:id" summary:"删除配置" permission:"sys:config:delete" log:"配置管理-删除记录" logType:"delete"` + Id int64 `json:"id" v:"required#缺少ID" dc:"配置ID"` // 配置ID +} + +// ConfigDeleteRes 删除配置结果 +type ConfigDeleteRes struct{} + +// ConfigBatchDeleteReq 批量删除配置请求 +type ConfigBatchDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/config/batchDelete" summary:"批量删除配置" permission:"sys:config:batchDelete" log:"配置管理-批量删除记录" logType:"delete"` + Ids []int64 `json:"ids" v:"required#请选择要删除的记录" dc:"配置ID列表"` // 配置ID列表 +} + +// ConfigBatchDeleteRes 批量删除结果 +type ConfigBatchDeleteRes struct{} + +// ConfigWebIndexReq 获取网站配置请求 +// 返回配置分组及其下的配置项,供网站配置页渲染 +type ConfigWebIndexReq struct { + gmeta.Meta `method:"get" path:"/api/config/web/index" summary:"获取网站配置" permission:"sys:configWeb:data" log:"网站配置-获取配置" logType:"query"` +} + +// ConfigWebItem 网站配置项 +// value 按控件类型拼接访问URL;images/files 类型拆分到 valueList;options 解析为 param 映射 +type ConfigWebItem struct { + Id int64 `json:"id"` // 主键ID + Name string `json:"name"` // 配置项名称 + Code string `json:"code"` // 配置项编码 + Value string `json:"value"` // 配置项值 + ValueList []string `json:"valueList"` // 值列表,多值类型使用 + Type string `json:"type"` // 控件类型 + Options string `json:"options"` // 可选项原始串 + Param map[string]string `json:"param"` // 可选项解析后的映射 +} + +// ConfigWebGroup 配置分组 +// 表示一个配置标签页下的配置项集合 +type ConfigWebGroup struct { + ConfigName string `json:"configName"` // 配置分组名称 + DataList []ConfigWebItem `json:"dataList"` // 分组下的配置项集合 +} + +// ConfigWebIndexRes 网站配置聚合结果 +type ConfigWebIndexRes = []ConfigWebGroup + +// ConfigWebSaveReq 保存网站配置请求 +// body 顶层为配置项编码到值的平铺映射,采用自定义解析 +type ConfigWebSaveReq struct { + gmeta.Meta `method:"post" path:"/api/config/web/save" summary:"保存网站配置" permission:"sys:configWeb:save" log:"网站配置-保存配置" logType:"update"` + Data map[string]any `json:"data" dc:"配置项编码 → 值"` // 配置项编码到值的映射 +} + +// UnmarshalBody 自定义请求体解析 +// body 顶层为配置项编码到值的平铺映射,跳过默认的请求参数解析流程 +func (r *ConfigWebSaveReq) UnmarshalBody(req *ghttp.Request) error { + var data map[string]any + if err := json.Unmarshal(req.GetBody(), &data); err != nil { + return err + } + if len(data) == 0 { + return errors.New("请提交配置数据") + } + r.Data = data + return nil +} + +// ConfigWebSaveRes 保存网站配置结果 +type ConfigWebSaveRes struct{} diff --git a/api/v1/config_item.go b/api/v1/config_item.go new file mode 100644 index 0000000..53c0711 --- /dev/null +++ b/api/v1/config_item.go @@ -0,0 +1,166 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 配置项模块 ============================== + +// ConfigItemItem 配置项行数据 +// 用于配置项分页列表、配置项列表等场景的配置项基础信息 +type ConfigItemItem struct { + Id int64 `json:"id"` // 主键ID + ConfigId int64 `json:"configId"` // 所属配置ID + ConfigName string `json:"configName"` // 所属配置名称 + Name string `json:"name"` // 配置项名称 + Code string `json:"code"` // 配置项编码 + Value string `json:"value"` // 配置项值 + Options string `json:"options"` // 可选项(JSON) + Type string `json:"type"` // 控件类型 + Status int `json:"status"` // 状态 + StatusText string `json:"statusText"` // 状态文本 + Sort int `json:"sort"` // 排序 + Note string `json:"note"` // 备注 + CreateUser string `json:"createUser"` // 创建人 +} + +// ConfigItemPageReq 配置项分页请求 +// 支持关键词、配置项名称、所属配置及状态等条件组合查询 +type ConfigItemPageReq struct { + gmeta.Meta `method:"get" path:"/api/config/item/page" summary:"配置项分页" permission:"sys:configitem:page" log:"配置项管理-查询分页记录" logType:"query"` + PageReq // 分页基础参数 + Keyword string `json:"keyword" dc:"关键词(名称/编码模糊)"` // 关键词,名称或编码模糊匹配 + Name string `json:"name" dc:"配置项名称模糊"` // 配置项名称模糊匹配 + ConfigId int64 `json:"configId" dc:"所属配置ID"` // 所属配置ID + Status int `json:"status" dc:"状态"` // 状态 +} + +// ConfigItemPageRes 配置项分页结果 +type ConfigItemPageRes = PageData[ConfigItemItem] + +// ConfigItemListReq 配置项列表请求 +// 按所属配置、状态等条件查询配置项集合 +type ConfigItemListReq struct { + gmeta.Meta `method:"get" path:"/api/config/item/list" summary:"配置项列表" permission:"sys:configitem:list" log:"配置项管理-查询列表" logType:"query"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 + ConfigId int64 `json:"configId" dc:"所属配置ID"` // 所属配置ID + Status int `json:"status" dc:"状态"` // 状态 +} + +// ConfigItemListRes 配置项列表结果 +type ConfigItemListRes = []ConfigItemItem + +// ConfigItemDetailReq 配置项详情请求 +type ConfigItemDetailReq struct { + gmeta.Meta `method:"get" path:"/api/config/item/detail/:id" summary:"配置项详情" permission:"sys:configitem:detail" log:"配置项管理-查询详情" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"配置项ID"` // 配置项ID +} + +// ConfigItemDetailRes 配置项详情结果 +// 包含配置项基础信息、所属配置编码及多值类型拆分后的值列表 +type ConfigItemDetailRes struct { + Id int64 `json:"id"` // 主键ID + ConfigId int64 `json:"configId"` // 所属配置ID + ConfigCode string `json:"configCode"` // 所属配置编码 + Name string `json:"name"` // 配置项名称 + Code string `json:"code"` // 配置项编码 + Value string `json:"value"` // 配置项值 + ValueList []string `json:"valueList"` // 值列表,多值类型使用 + Options string `json:"options"` // 可选项(JSON) + Type string `json:"type"` // 控件类型 + Status int `json:"status"` // 状态 + StatusText string `json:"statusText"` // 状态文本 + Sort int `json:"sort"` // 排序 + Note string `json:"note"` // 备注 +} + +// ConfigItemAddReq 新增配置项请求 +type ConfigItemAddReq struct { + gmeta.Meta `method:"post" path:"/api/config/item/add" summary:"新增配置项" permission:"sys:configitem:add" log:"配置项管理-新增记录" logType:"add"` + ConfigId int64 `json:"configId" v:"required#请选择所属配置" dc:"所属配置ID"` // 所属配置ID + Name string `json:"name" v:"required#请输入配置项名称" dc:"配置项名称"` // 配置项名称 + Code string `json:"code" v:"required#请输入配置项编码" dc:"配置项编码"` // 配置项编码 + Value string `json:"value" dc:"配置项值"` // 配置项值 + Options string `json:"options" dc:"可选项(JSON)"` // 可选项 + Type string `json:"type" dc:"类型:text-单行文本 textarea-多行文本 number-数字 select-下拉 radio-单选 checkbox-复选 image-单图 images-多图 file-单文件 files-多文件(见字典 config_item_type)"` // 控件类型 + Status int `json:"status" dc:"状态"` // 状态 + Sort int `json:"sort" dc:"排序"` // 排序 + Note string `json:"note" dc:"备注"` // 备注 +} + +// ConfigItemUpdateReq 修改配置项请求 +type ConfigItemUpdateReq struct { + gmeta.Meta `method:"put" path:"/api/config/item/update" summary:"修改配置项" permission:"sys:configitem:update" log:"配置项管理-修改记录" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"配置项ID"` // 配置项ID + ConfigId int64 `json:"configId" dc:"所属配置ID"` // 所属配置ID + Name string `json:"name" v:"required#请输入配置项名称" dc:"配置项名称"` // 配置项名称 + Code string `json:"code" v:"required#请输入配置项编码" dc:"配置项编码"` // 配置项编码 + Value string `json:"value" dc:"配置项值"` // 配置项值 + Options string `json:"options" dc:"可选项(JSON)"` // 可选项 + Type string `json:"type" dc:"类型(见字典 config_item_type)"` // 控件类型 + Status int `json:"status" dc:"状态"` // 状态 + Sort int `json:"sort" dc:"排序"` // 排序 + Note string `json:"note" dc:"备注"` // 备注 +} + +// ConfigItemUpdateRes 修改配置项结果 +type ConfigItemUpdateRes struct{} + +// ConfigItemDeleteReq 删除配置项请求 +type ConfigItemDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/config/item/delete/:id" summary:"删除配置项" permission:"sys:configitem:delete" log:"配置项管理-删除记录" logType:"delete"` + Id int64 `json:"id" v:"required#缺少ID" dc:"配置项ID"` // 配置项ID +} + +// ConfigItemDeleteRes 删除配置项结果 +type ConfigItemDeleteRes struct{} + +// ConfigItemBatchDeleteReq 批量删除配置项请求 +type ConfigItemBatchDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/config/item/batchDelete" summary:"批量删除配置项" permission:"sys:configitem:batchDelete" log:"配置项管理-批量删除记录" logType:"delete"` + Ids []int64 `json:"ids" v:"required#请选择要删除的记录" dc:"配置项ID列表"` // 配置项ID列表 +} + +// ConfigItemBatchDeleteRes 批量删除结果 +type ConfigItemBatchDeleteRes struct{} diff --git a/api/v1/dept.go b/api/v1/dept.go new file mode 100644 index 0000000..a2d2fdf --- /dev/null +++ b/api/v1/dept.go @@ -0,0 +1,148 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 部门模块 ============================== + +// DeptItem 部门行数据 +// 用于部门分页列表、树形列表等场景的部门基础信息 +type DeptItem struct { + Id int64 `json:"id"` // 主键ID + TenantId int `json:"tenantId"` // 租户ID + Name string `json:"name"` // 部门名称 + Type int `json:"type"` // 部门类型:1-公司 2-子公司 3-部门 4-小组 + TypeText string `json:"typeText"` // 部门类型文本 + ParentId int `json:"parentId"` // 上级部门ID(0=顶级) + Sort int `json:"sort"` // 排序 + Note string `json:"note"` // 备注 + CreateUser string `json:"createUser"` // 创建人 + CreateTime *gtime.Time `json:"createTime"` // 创建时间 +} + +// DeptPageReq 部门分页请求 +// 支持关键词、部门名称、部门类型等条件组合查询 +type DeptPageReq struct { + gmeta.Meta `method:"get" path:"/api/dept/page" summary:"部门分页" permission:"sys:dept:page" log:"部门管理-查询分页记录" logType:"query"` + PageReq // 分页基础参数 + Keyword string `json:"keyword" dc:"关键词"` // 关键词 + Name string `json:"name" dc:"部门名称模糊"` // 部门名称模糊匹配 + Type int `json:"type" dc:"部门类型:1-公司 2-子公司 3-部门 4-小组"` // 部门类型 +} + +// DeptPageRes 部门分页结果 +type DeptPageRes = PageData[DeptItem] + +// DeptListReq 部门树形列表请求 +// 返回平铺列表,由前端自行构建树形结构 +type DeptListReq struct { + gmeta.Meta `method:"get" path:"/api/dept/list" summary:"部门树形列表" permission:"sys:dept:list" log:"部门管理-查询树形列表" logType:"query"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 + Type int `json:"type" dc:"部门类型"` // 部门类型 +} + +// DeptListRes 部门平铺列表结果 +// data 直接为数组,由前端自行构建树形结构 +type DeptListRes = []*DeptItem + +// DeptDetailReq 部门详情请求 +type DeptDetailReq struct { + gmeta.Meta `method:"get" path:"/api/dept/detail/:id" summary:"部门详情" permission:"sys:dept:detail" log:"部门管理-查询详情" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"部门ID"` // 部门ID +} + +// DeptDetailRes 部门详情结果 +type DeptDetailRes struct { + Id int64 `json:"id"` // 主键ID + Name string `json:"name"` // 部门名称 + Type int `json:"type"` // 部门类型 + TypeText string `json:"typeText"` // 部门类型文本 + ParentId int `json:"parentId"` // 上级部门ID + Sort int `json:"sort"` // 排序 + Note string `json:"note"` // 备注 +} + +// DeptAddReq 新增部门请求 +type DeptAddReq struct { + gmeta.Meta `method:"post" path:"/api/dept/add" summary:"新增部门" permission:"sys:dept:add" log:"部门管理-新增记录" logType:"add"` + Name string `json:"name" v:"required#请输入部门名称" dc:"部门名称"` // 部门名称 + Type int `json:"type" v:"required:1,2,3,4#请选择部门类型" dc:"部门类型:1-公司 2-子公司 3-部门 4-小组"` // 部门类型 + ParentId int `json:"parentId" dc:"上级部门ID(0=顶级)"` // 上级部门ID + Sort int `json:"sort" dc:"排序"` // 排序 + Note string `json:"note" dc:"备注"` // 备注 +} + +// DeptUpdateReq 修改部门请求 +type DeptUpdateReq struct { + gmeta.Meta `method:"put" path:"/api/dept/update" summary:"修改部门" permission:"sys:dept:update" log:"部门管理-修改记录" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"部门ID"` // 部门ID + Name string `json:"name" v:"required#请输入部门名称" dc:"部门名称"` // 部门名称 + Type int `json:"type" dc:"部门类型"` // 部门类型 + ParentId int `json:"parentId" dc:"上级部门ID"` // 上级部门ID + Sort int `json:"sort" dc:"排序"` // 排序 + Note string `json:"note" dc:"备注"` // 备注 +} + +// DeptUpdateRes 修改部门结果 +type DeptUpdateRes struct{} + +// DeptDeleteReq 删除部门请求 +type DeptDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/dept/delete/:id" summary:"删除部门" permission:"sys:dept:delete" log:"部门管理-删除记录" logType:"delete"` + Id int64 `json:"id" v:"required#缺少ID" dc:"部门ID"` // 部门ID +} + +// DeptDeleteRes 删除部门结果 +type DeptDeleteRes struct{} + +// DeptBatchDeleteReq 批量删除部门请求 +type DeptBatchDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/dept/batchDelete" summary:"批量删除部门" permission:"sys:dept:batchDelete" log:"部门管理-批量删除记录" logType:"delete"` + Ids []int64 `json:"ids" v:"required#请选择要删除的记录" dc:"部门ID列表"` // 部门ID列表 +} + +// DeptBatchDeleteRes 批量删除结果 +type DeptBatchDeleteRes struct{} diff --git a/api/v1/dict.go b/api/v1/dict.go new file mode 100644 index 0000000..0eeabfc --- /dev/null +++ b/api/v1/dict.go @@ -0,0 +1,167 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 字典模块 ============================== + +// DictItemRow 字典项行数据 +// 内嵌使用,供字典详情展示字典项明细 +type DictItemRow struct { + Id int64 `json:"id"` // 主键ID + Name string `json:"name"` // 字典项名称 + Value string `json:"value"` // 字典项值 + Sort int `json:"sort"` // 排序 +} + +// DictItem 字典行数据 +// 用于字典分页列表、字典列表等场景的字典基础信息 +type DictItem struct { + Id int64 `json:"id"` // 主键ID + Name string `json:"name"` // 字典名称 + Code string `json:"code"` // 字典编码 + Sort int `json:"sort"` // 排序 + Note string `json:"note"` // 备注 + CreateUser string `json:"createUser"` // 创建人 +} + +// DictPageReq 字典分页请求 +// 支持关键词、字典名称等条件组合查询 +type DictPageReq struct { + gmeta.Meta `method:"get" path:"/api/dict/page" summary:"字典分页" permission:"sys:dict:page" log:"字典管理-查询分页记录" logType:"query"` + PageReq // 分页基础参数 + Keyword string `json:"keyword" dc:"关键词(名称/编码模糊)"` // 关键词,名称或编码模糊匹配 + Name string `json:"name" dc:"字典名称模糊"` // 字典名称模糊匹配 +} + +// DictPageRes 字典分页结果 +type DictPageRes = PageData[DictItem] + +// DictDetailReq 字典详情请求 +// 返回字典基础信息及其下的字典项集合 +type DictDetailReq struct { + gmeta.Meta `method:"get" path:"/api/dict/detail/:id" summary:"字典详情" permission:"sys:dict:detail" log:"字典管理-查询详情" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"字典ID"` // 字典ID +} + +// DictDetailRes 字典详情结果 +// 包含字典基础信息及其下的字典项集合 +type DictDetailRes struct { + DictItem + Items []DictItemRow `json:"items"` // 字典项集合 +} + +// DictAddReq 新增字典请求 +type DictAddReq struct { + gmeta.Meta `method:"post" path:"/api/dict/add" summary:"新增字典" permission:"sys:dict:add" log:"字典管理-新增记录" logType:"add"` + Name string `json:"name" v:"required#请输入字典名称" dc:"字典名称"` // 字典名称 + Code string `json:"code" v:"required#请输入字典编码" dc:"字典编码"` // 字典编码 + Sort int `json:"sort" dc:"排序"` // 排序 + Note string `json:"note" dc:"备注"` // 备注 +} + +// DictUpdateReq 修改字典请求 +type DictUpdateReq struct { + gmeta.Meta `method:"put" path:"/api/dict/update" summary:"修改字典" permission:"sys:dict:update" log:"字典管理-修改记录" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"字典ID"` // 字典ID + Name string `json:"name" v:"required#请输入字典名称" dc:"字典名称"` // 字典名称 + Code string `json:"code" v:"required#请输入字典编码" dc:"字典编码"` // 字典编码 + Sort int `json:"sort" dc:"排序"` // 排序 + Note string `json:"note" dc:"备注"` // 备注 +} + +// DictUpdateRes 修改字典结果 +type DictUpdateRes struct{} + +// DictDeleteReq 删除字典请求 +type DictDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/dict/delete/:id" summary:"删除字典" permission:"sys:dict:delete" log:"字典管理-删除记录" logType:"delete"` + Id int64 `json:"id" v:"required#缺少ID" dc:"字典ID"` // 字典ID +} + +// DictDeleteRes 删除字典结果 +type DictDeleteRes struct{} + +// DictBatchDeleteReq 批量删除字典请求 +type DictBatchDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/dict/batchDelete" summary:"批量删除字典" permission:"sys:dict:batchDelete" log:"字典管理-批量删除记录" logType:"delete"` + Ids []int64 `json:"ids" v:"required#请选择要删除的记录" dc:"字典ID列表"` // 字典ID列表 +} + +// DictBatchDeleteRes 批量删除结果 +type DictBatchDeleteRes struct{} + +// DictRefreshCacheReq 刷新字典缓存请求 +type DictRefreshCacheReq struct { + gmeta.Meta `method:"get" path:"/api/dict/refreshCache" summary:"刷新字典缓存" permission:"sys:dict:cache" log:"字典管理-刷新缓存" logType:"update"` + Code string `json:"code" dc:"字典编码(留空刷新全部)"` // 字典编码,留空刷新全部 +} + +// DictRefreshCacheRes 刷新缓存结果 +type DictRefreshCacheRes struct{} + +// DictAliasListReq 字典别名列表请求 +// 登录用户即可访问,供前端字典查询场景使用 +type DictAliasListReq struct { + gmeta.Meta `method:"get" path:"/api/dictionary/list" summary:"字典列表" log:"字典管理-查询列表" logType:"query"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 +} + +// DictAliasListRes 字典别名列表结果 +type DictAliasListRes = []DictItem + +// DictAliasInfoReq 字典别名详情请求 +// 登录用户即可访问,按字典编码查询字典及其字典项 +type DictAliasInfoReq struct { + gmeta.Meta `method:"get" path:"/api/dictionary/info" summary:"字典详情" log:"字典管理-查询详情" logType:"query"` + Code string `json:"code" v:"required#请输入字典编码" dc:"字典编码"` // 字典编码 +} + +// DictAliasInfoRes 字典别名详情结果 +// 包含字典基础信息及其下的字典项集合 +type DictAliasInfoRes struct { + DictItem + Items []DictItemRow `json:"items"` // 字典项集合 +} diff --git a/api/v1/dict_item.go b/api/v1/dict_item.go new file mode 100644 index 0000000..43c56c1 --- /dev/null +++ b/api/v1/dict_item.go @@ -0,0 +1,151 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 字典项模块 ============================== + +// DictItemItemRow 字典项行数据 +// 用于字典项分页列表等场景的字典项基础信息 +type DictItemItemRow struct { + Id int64 `json:"id"` // 主键ID + DictId int64 `json:"dictId"` // 所属字典ID + DictName string `json:"dictName"` // 所属字典名称 + DictCode string `json:"dictCode"` // 所属字典编码 + Name string `json:"name"` // 字典项名称 + Value string `json:"value"` // 字典项值 + Sort int `json:"sort"` // 排序 + Note string `json:"note"` // 备注 + CreateUser string `json:"createUser"` // 创建人 +} + +// DictItemPageReq 字典项分页请求 +// 支持关键词、字典项名称、所属字典等条件组合查询 +type DictItemPageReq struct { + gmeta.Meta `method:"get" path:"/api/dict/item/page" summary:"字典项分页" permission:"sys:dictitem:page" log:"字典项管理-查询分页记录" logType:"query"` + PageReq // 分页基础参数 + Keyword string `json:"keyword" dc:"关键词(名称/值模糊)"` // 关键词,名称或值模糊匹配 + Name string `json:"name" dc:"字典项名称模糊"` // 字典项名称模糊匹配 + DictId int64 `json:"dictId" dc:"所属字典ID"` // 所属字典ID +} + +// DictItemPageRes 字典项分页结果 +type DictItemPageRes = PageData[DictItemItemRow] + +// DictItemDetailReq 字典项详情请求 +type DictItemDetailReq struct { + gmeta.Meta `method:"get" path:"/api/dict/item/detail/:id" summary:"字典项详情" permission:"sys:dictitem:detail" log:"字典项管理-查询详情" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"字典项ID"` // 字典项ID +} + +// DictItemDetailRes 字典项详情结果 +type DictItemDetailRes struct { + Id int64 `json:"id"` // 主键ID + DictId int64 `json:"dictId"` // 所属字典ID + Name string `json:"name"` // 字典项名称 + Value string `json:"value"` // 字典项值 + Sort int `json:"sort"` // 排序 + Note string `json:"note"` // 备注 +} + +// DictItemAddReq 新增字典项请求 +type DictItemAddReq struct { + gmeta.Meta `method:"post" path:"/api/dict/item/add" summary:"新增字典项" permission:"sys:dictitem:add" log:"字典项管理-新增记录" logType:"add"` + DictId int64 `json:"dictId" v:"required#请选择所属字典" dc:"所属字典ID"` // 所属字典ID + Name string `json:"name" v:"required#请输入字典项名称" dc:"字典项名称"` // 字典项名称 + Value string `json:"value" v:"required#请输入字典项值" dc:"字典项值"` // 字典项值 + Sort int `json:"sort" dc:"排序"` // 排序 + Note string `json:"note" dc:"备注"` // 备注 +} + +// DictItemUpdateReq 修改字典项请求 +type DictItemUpdateReq struct { + gmeta.Meta `method:"put" path:"/api/dict/item/update" summary:"修改字典项" permission:"sys:dictitem:update" log:"字典项管理-修改记录" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"字典项ID"` // 字典项ID + DictId int64 `json:"dictId" dc:"所属字典ID"` // 所属字典ID + Name string `json:"name" v:"required#请输入字典项名称" dc:"字典项名称"` // 字典项名称 + Value string `json:"value" v:"required#请输入字典项值" dc:"字典项值"` // 字典项值 + Sort int `json:"sort" dc:"排序"` // 排序 + Note string `json:"note" dc:"备注"` // 备注 +} + +// DictItemUpdateRes 修改字典项结果 +type DictItemUpdateRes struct{} + +// DictItemDeleteReq 删除字典项请求 +type DictItemDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/dict/item/delete/:id" summary:"删除字典项" permission:"sys:dictitem:delete" log:"字典项管理-删除记录" logType:"delete"` + Id int64 `json:"id" v:"required#缺少ID" dc:"字典项ID"` // 字典项ID +} + +// DictItemDeleteRes 删除字典项结果 +type DictItemDeleteRes struct{} + +// DictItemBatchDeleteReq 批量删除字典项请求 +type DictItemBatchDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/dict/item/batchDelete" summary:"批量删除字典项" permission:"sys:dictitem:batchDelete" log:"字典项管理-批量删除记录" logType:"delete"` + Ids []int64 `json:"ids" v:"required#请选择要删除的记录" dc:"字典项ID列表"` // 字典项ID列表 +} + +// DictItemBatchDeleteRes 批量删除结果 +type DictItemBatchDeleteRes struct{} + +// DictItemQuickListReq 快捷字典项列表请求 +// 登录用户即可访问,按字典编码查询并走缓存 +type DictItemQuickListReq struct { + gmeta.Meta `method:"get" path:"/api/dict/item/getDictItemList/:code" summary:"按编码查询字典项" log:"字典项管理-查询列表" logType:"query"` + Code string `json:"code" v:"required#请输入字典编码" dc:"字典编码"` // 字典编码 +} + +// DictItemQuickRow 快捷字典项行数据 +// 精简结构,仅包含前端下拉等场景所需字段 +type DictItemQuickRow struct { + Id int64 `json:"id"` // 主键ID + Name string `json:"name"` // 字典项名称 + Value string `json:"value"` // 字典项值 +} + +// DictItemQuickListRes 快捷字典项列表结果 +type DictItemQuickListRes = []DictItemQuickRow diff --git a/api/v1/example.go b/api/v1/example.go new file mode 100644 index 0000000..63130f9 --- /dev/null +++ b/api/v1/example.go @@ -0,0 +1,191 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 案例演示模块(goframe_example) ============================== + +// ExampleItem 案例演示行数据 +// 用于分页列表等场景的基础信息 +type ExampleItem struct { + Id int `json:"id"` // 主键ID + Name string `json:"name"` // 案例名称 + Avatar string `json:"avatar"` // 案例图片 + Type int `json:"type"` // 案例类型 + Status int `json:"status"` // 案例状态 + Sort int `json:"sort"` // 案例排序 + CreateUser string `json:"createUser"` // 创建人 + CreateTime *gtime.Time `json:"createTime"` // 创建时间 + StatusText string `json:"statusText"` // 状态显示名(dict_item 解析) +} + +// ExamplePageReq 案例演示分页请求 +type ExamplePageReq struct { + gmeta.Meta `method:"get" path:"/api/example/page" summary:"案例演示分页" permission:"sys:example:page" log:"案例演示-查询分页记录" logType:"query"` + PageReq + Name string `json:"name" dc:"案例名称"` // 案例名称 + Type int `json:"type" dc:"案例类型"` // 案例类型 + Status int `json:"status" dc:"案例状态"` // 案例状态 + Keyword string `json:"keyword" dc:"关键词"` // 关键词 +} + +// ExamplePageRes 案例演示分页结果 +// 统一壳:records/total/size/current/pages +type ExamplePageRes = PageData[ExampleItem] + +// ExampleListReq 案例演示列表请求 +type ExampleListReq struct { + gmeta.Meta `method:"get" path:"/api/example/list" summary:"案例演示列表" permission:"sys:example:list" log:"案例演示-查询列表" logType:"query"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 + Limit int `json:"limit" dc:"条数上限"` // 条数上限 +} + +// ExampleListRes 案例演示平铺列表结果 +// data 直接为数组,树形结构由前端 buildTree 构建 +type ExampleListRes = []ExampleItem + +// ExampleDetailReq 案例演示详情请求 +type ExampleDetailReq struct { + gmeta.Meta `method:"get" path:"/api/example/detail/:id" summary:"案例演示详情" permission:"sys:example:detail" log:"案例演示-查询详情" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"记录ID"` // 记录ID +} + +// ExampleDetailRes 案例演示详情结果 +type ExampleDetailRes struct { + Id int64 `json:"id"` // 主键ID + Name string `json:"name"` // 案例名称 + Avatar string `json:"avatar"` // 案例图片 + Type int `json:"type"` // 案例类型 + Status int `json:"status"` // 案例状态 + Sort int `json:"sort"` // 案例排序 + StatusText string `json:"statusText"` // 状态显示名(dict_item 解析) +} + +// ExampleAddReq 新增案例演示请求 +type ExampleAddReq struct { + gmeta.Meta `method:"post" path:"/api/example/add" summary:"新增案例演示" permission:"sys:example:add" log:"案例演示-新增记录" logType:"add"` + Name string `json:"name" v:"required#请输入案例名称" dc:"案例名称"` // 案例名称 + Avatar string `json:"avatar" dc:"案例图片"` // 案例图片 + Type int `json:"type" dc:"案例类型"` // 案例类型 + Status int `json:"status" dc:"案例状态"` // 案例状态 + Sort int `json:"sort" dc:"案例排序"` // 案例排序 +} + +// ExampleIdRes 新增返回ID结构 +type ExampleIdRes struct { + Id int64 `json:"id"` // 主键ID +} + +// ExampleUpdateReq 修改案例演示请求 +type ExampleUpdateReq struct { + gmeta.Meta `method:"put" path:"/api/example/update" summary:"修改案例演示" permission:"sys:example:update" log:"案例演示-修改记录" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"记录ID"` // 记录ID + Name string `json:"name" v:"required#请输入案例名称" dc:"案例名称"` // 案例名称 + Avatar string `json:"avatar" dc:"案例图片"` // 案例图片 + Type int `json:"type" dc:"案例类型"` // 案例类型 + Status int `json:"status" dc:"案例状态"` // 案例状态 + Sort int `json:"sort" dc:"案例排序"` // 案例排序 +} + +// ExampleUpdateRes 修改案例演示结果 +type ExampleUpdateRes struct{} + +// ExampleStatusReq 案例演示状态请求 +type ExampleStatusReq struct { + gmeta.Meta `method:"put" path:"/api/example/status" summary:"案例演示状态" permission:"sys:example:status" log:"案例演示-设置状态" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"记录ID"` // 记录ID + Status int `json:"status" v:"required#缺少状态" dc:"状态"` // 状态 +} + +// ExampleStatusRes 案例演示状态结果 +type ExampleStatusRes struct{} + +// ExampleDeleteReq 删除案例演示请求 +type ExampleDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/example/delete/:id" summary:"删除案例演示" permission:"sys:example:delete" log:"案例演示-删除记录" logType:"delete"` + Id int64 `json:"id" v:"required#缺少ID" dc:"记录ID"` // 记录ID +} + +// ExampleDeleteRes 删除案例演示结果 +type ExampleDeleteRes struct{} + +// ExampleBatchDeleteReq 批量删除案例演示请求 +type ExampleBatchDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/example/batchDelete" summary:"批量删除案例演示" permission:"sys:example:batchDelete" log:"案例演示-批量删除记录" logType:"delete"` + Ids []int64 `json:"ids" v:"required#请选择要删除的记录" dc:"ID列表"` // ID列表 +} + +// ExampleBatchDeleteRes 批量删除结果 +type ExampleBatchDeleteRes struct{} + +// ExampleImportReq 导入案例演示请求 +type ExampleImportReq struct { + gmeta.Meta `method:"post" path:"/api/example/import" summary:"导入案例演示" permission:"sys:example:import" log:"案例演示-导入数据" logType:"import"` + File *ghttp.UploadFile `p:"file" v:"required#请选择文件" dc:"xls/xlsx 文件"` // 上传文件 +} + +// ExampleImportRes 导入结果 +// 返回导入总数、成功数、失败数及失败原因 +type ExampleImportRes struct { + Total int `json:"total"` // 导入总数 + Success int `json:"success"` // 成功数 + Failed int `json:"failed"` // 失败数 + Reasons []string `json:"reasons"` // 失败原因列表 +} + +// ExampleExportReq 导出案例演示请求 +type ExampleExportReq struct { + gmeta.Meta `method:"get" path:"/api/example/export" summary:"导出案例演示" permission:"sys:example:export" log:"案例演示-导出数据" logType:"export"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 + Name string `json:"name" dc:"案例名称"` // 案例名称 + Type int `json:"type" dc:"案例类型"` // 案例类型 + Status int `json:"status" dc:"案例状态"` // 案例状态 +} + +// ExampleExportRes 导出结果 +// data 直接为文件访问 URL 字符串,前端可 window.open(data) +type ExampleExportRes = string diff --git a/api/v1/file_template.go b/api/v1/file_template.go new file mode 100644 index 0000000..658be41 --- /dev/null +++ b/api/v1/file_template.go @@ -0,0 +1,157 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 文件模板模块 ============================== + +// FileTemplateItem 文件模板行数据 +// 用于文件模板分页列表、文件模板列表等场景的模板基础信息 +type FileTemplateItem struct { + Id int64 `json:"id"` // 主键ID + Name string `json:"name"` // 模板名称 + Code string `json:"code"` // 模板编码 + FileName string `json:"fileName"` // 文件名 + FilePath string `json:"filePath"` // 文件路径 + Note string `json:"note"` // 备注 + Sort int `json:"sort"` // 排序 + CreateUser string `json:"createUser"` // 创建人 + CreateTime *gtime.Time `json:"createTime"` // 创建时间 +} + +// FileTemplatePageReq 文件模板分页请求 +// 支持关键词、模板名称等条件组合查询 +type FileTemplatePageReq struct { + gmeta.Meta `method:"get" path:"/api/file/template/page" summary:"文件模板分页" permission:"sys:fileTemplate:page" log:"文件模板-查询分页记录" logType:"query"` + PageReq // 分页基础参数 + Keyword string `json:"keyword" dc:"关键词(名称/编码模糊)"` // 关键词,名称或编码模糊匹配 + Name string `json:"name" dc:"模板名称模糊"` // 模板名称模糊匹配 +} + +// FileTemplatePageRes 文件模板分页结果 +type FileTemplatePageRes = PageData[FileTemplateItem] + +// FileTemplateListReq 文件模板列表请求 +type FileTemplateListReq struct { + gmeta.Meta `method:"get" path:"/api/file/template/list" summary:"文件模板列表" permission:"sys:fileTemplate:list" log:"文件模板-查询列表" logType:"query"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 +} + +// FileTemplateListRes 文件模板列表结果 +type FileTemplateListRes = []FileTemplateItem + +// FileTemplateDetailReq 文件模板详情请求 +type FileTemplateDetailReq struct { + gmeta.Meta `method:"get" path:"/api/file/template/detail/:id" summary:"文件模板详情" permission:"sys:fileTemplate:detail" log:"文件模板-查询详情" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"模板ID"` // 模板ID +} + +// FileTemplateDetailRes 文件模板详情结果 +type FileTemplateDetailRes struct { + Id int64 `json:"id"` // 主键ID + Name string `json:"name"` // 模板名称 + Code string `json:"code"` // 模板编码 + FileName string `json:"fileName"` // 文件名 + FilePath string `json:"filePath"` // 文件路径 + Note string `json:"note"` // 备注 + Sort int `json:"sort"` // 排序 +} + +// FileTemplateAddReq 新增文件模板请求 +type FileTemplateAddReq struct { + gmeta.Meta `method:"post" path:"/api/file/template/add" summary:"新增文件模板" permission:"sys:fileTemplate:add" log:"文件模板-新增记录" logType:"add"` + Name string `json:"name" v:"required#请输入模板名称" dc:"模板名称"` // 模板名称 + Code string `json:"code" v:"required#请输入模板编码" dc:"模板编码"` // 模板编码 + FileName string `json:"fileName" dc:"文件名"` // 文件名 + FilePath string `json:"filePath" dc:"文件路径"` // 文件路径 + Note string `json:"note" dc:"备注"` // 备注 + Sort int `json:"sort" dc:"排序"` // 排序 +} + +// FileTemplateUpdateReq 修改文件模板请求 +type FileTemplateUpdateReq struct { + gmeta.Meta `method:"put" path:"/api/file/template/update" summary:"修改文件模板" permission:"sys:fileTemplate:update" log:"文件模板-修改记录" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"模板ID"` // 模板ID + Name string `json:"name" v:"required#请输入模板名称" dc:"模板名称"` // 模板名称 + Code string `json:"code" v:"required#请输入模板编码" dc:"模板编码"` // 模板编码 + FileName string `json:"fileName" dc:"文件名"` // 文件名 + FilePath string `json:"filePath" dc:"文件路径"` // 文件路径 + Note string `json:"note" dc:"备注"` // 备注 + Sort int `json:"sort" dc:"排序"` // 排序 +} + +// FileTemplateUpdateRes 修改文件模板结果 +type FileTemplateUpdateRes struct{} + +// FileTemplateDeleteReq 删除文件模板请求 +type FileTemplateDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/file/template/delete/:id" summary:"删除文件模板" permission:"sys:fileTemplate:delete" log:"文件模板-删除记录" logType:"delete"` + Id int64 `json:"id" v:"required#缺少ID" dc:"模板ID"` // 模板ID +} + +// FileTemplateDeleteRes 删除文件模板结果 +type FileTemplateDeleteRes struct{} + +// FileTemplateBatchDeleteReq 批量删除文件模板请求 +type FileTemplateBatchDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/file/template/batchDelete" summary:"批量删除文件模板" permission:"sys:fileTemplate:batchDelete" log:"文件模板-批量删除记录" logType:"delete"` + Ids []int64 `json:"ids" v:"required#请选择要删除的记录" dc:"模板ID列表"` // 模板ID列表 +} + +// FileTemplateBatchDeleteRes 批量删除结果 +type FileTemplateBatchDeleteRes struct{} + +// FileTemplateByCodeReq 按编码查询模板请求 +// 登录用户即可访问,按模板编码查询模板详情 +type FileTemplateByCodeReq struct { + gmeta.Meta `method:"get" path:"/api/file/template/getTemplateByCode/:code" summary:"按编码查询模板" log:"文件模板-查询模板详情" logType:"query"` + Code string `json:"code" v:"required#请输入模板编码" dc:"模板编码"` // 模板编码 +} + +// FileTemplateByCodeRes 按编码查询模板结果 +type FileTemplateByCodeRes struct { + FileTemplateItem +} diff --git a/api/v1/generator.go b/api/v1/generator.go new file mode 100644 index 0000000..1043364 --- /dev/null +++ b/api/v1/generator.go @@ -0,0 +1,162 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 代码生成模块(读数据库结构) ============================== + +// GeneratorTableItem 数据表行数据 +// 用于代码生成模块的数据表分页列表,描述数据库表的元信息 +type GeneratorTableItem struct { + TableName string `json:"tableName"` // 表名 + TableComment string `json:"tableComment"` // 表注释 + Engine string `json:"engine"` // 存储引擎 + TableRows int64 `json:"tableRows"` // 表记录数 + DataLength int64 `json:"dataLength"` // 数据大小(字节) + AutoIncrement int64 `json:"autoIncrement"` // 自增当前值 + TableCollation string `json:"tableCollation"` // 表排序规则 + CreateTime string `json:"createTime"` // 创建时间 +} + +// GeneratorPageReq 数据表分页请求 +// 支持按表名、表注释模糊查询数据库表 +type GeneratorPageReq struct { + gmeta.Meta `method:"get" path:"/api/generator/page" summary:"查询数据表" permission:"sys:generator:page" log:"代码生成-查询数据表" logType:"query"` + PageReq // 分页基础参数 + TableName string `json:"tableName" dc:"表名模糊"` // 表名模糊匹配 + TableComment string `json:"tableComment" dc:"表注释模糊"` // 表注释模糊匹配 +} + +// GeneratorPageRes 数据表分页结果 +type GeneratorPageRes = PageData[GeneratorTableItem] + +// GeneratorPreviewReq 预览代码请求 +// TableNames 元素格式为 "表名|表注释" +type GeneratorPreviewReq struct { + gmeta.Meta `method:"post" path:"/api/generator/preview" summary:"预览代码" permission:"sys:generator:generator" log:"代码生成-预览代码" logType:"generate"` + TableNames []string `json:"tableNames" v:"required#请选择数据表" dc:"数据表列表(表名|表注释)"` // 数据表列表 +} + +// GeneratorFileItem 生成文件 +// 描述单个待生成文件的路径与内容 +type GeneratorFileItem struct { + Path string `json:"path"` // 文件路径 + Content string `json:"content"` // 文件内容 +} + +// GeneratorPreviewRes 预览结果 +type GeneratorPreviewRes struct { + List []GeneratorFileItem `json:"list"` // 生成文件列表 +} + +// GeneratorGenerateReq 一键生成请求 +// TableNames 元素格式为 "表名|表注释" +type GeneratorGenerateReq struct { + gmeta.Meta `method:"post" path:"/api/generator/generate" summary:"一键生成" permission:"sys:generator:generator" log:"代码生成-一键生成" logType:"generate"` + TableNames []string `json:"tableNames" v:"required#请选择数据表" dc:"数据表列表(表名|表注释)"` // 数据表列表 +} + +// GeneratorMenuResult 同步创建菜单结果 +// 描述代码生成过程中同步创建菜单的执行结果 +type GeneratorMenuResult struct { + Success bool `json:"success"` // 是否成功 + MenuId int64 `json:"menuId"` // 菜单ID + Message string `json:"message"` // 结果消息 +} + +// GeneratorDictResult 同步创建字典结果 +// 描述代码生成过程中同步创建字典的执行结果 +type GeneratorDictResult struct { + Dict string `json:"dict"` // 字典编码 + Name string `json:"name,omitempty"` // 字典名称 + Items int `json:"items,omitempty"` // 字典项数量 + Status string `json:"status"` // 执行状态 +} + +// GeneratorRouteResult 同步注册路由结果 +// 描述代码生成过程中同步注册路由的执行结果 +type GeneratorRouteResult struct { + Success bool `json:"success"` // 是否成功 + Message string `json:"message"` // 结果消息 +} + +// GeneratorGenerateRes 生成结果 +// 汇总代码生成的文件写入情况以及菜单、字典、路由的同步结果 +type GeneratorGenerateRes struct { + Generated []string `json:"generated"` // 新生成的文件 + Skipped []string `json:"skipped"` // 跳过的文件 + Overwritten []string `json:"overwritten"` // 覆盖的文件 + Failed []string `json:"failed"` // 失败的文件 + Menu GeneratorMenuResult `json:"menu"` // 菜单创建结果 + Dict []GeneratorDictResult `json:"dict"` // 字典创建结果 + Route GeneratorRouteResult `json:"route"` // 路由注册结果 +} + +// GeneratorBatchGenerateReq 批量生成请求 +// TableNames 元素格式为 "表名|表注释" +type GeneratorBatchGenerateReq struct { + gmeta.Meta `method:"post" path:"/api/generator/batchGenerate" summary:"批量生成" permission:"sys:generator:batchGenerator" log:"代码生成-批量生成" logType:"generate"` + TableNames []string `json:"tableNames" v:"required#请选择数据表" dc:"数据表列表(表名|表注释)"` // 数据表列表 +} + +// GeneratorBatchItem 单表生成结果 +// 描述批量生成中单张表的执行情况 +type GeneratorBatchItem struct { + Table string `json:"table"` // 表名 + Success bool `json:"success"` // 是否成功 + Generated int `json:"generated"` // 新生成文件数 + Skipped int `json:"skipped"` // 跳过文件数 + Overwritten int `json:"overwritten"` // 覆盖文件数 + Failed int `json:"failed"` // 失败文件数 + Message string `json:"message"` // 结果消息 + Menu GeneratorMenuResult `json:"menu"` // 菜单创建结果 + Dict []GeneratorDictResult `json:"dict"` // 字典创建结果 + Route GeneratorRouteResult `json:"route"` // 路由注册结果 +} + +// GeneratorBatchGenerateRes 批量生成结果 +type GeneratorBatchGenerateRes struct { + List []GeneratorBatchItem `json:"list"` // 各表生成结果列表 +} diff --git a/api/v1/index.go b/api/v1/index.go new file mode 100644 index 0000000..343a3c1 --- /dev/null +++ b/api/v1/index.go @@ -0,0 +1,142 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 首页/个人中心模块 ============================== + +// GetUserReq 获取当前登录用户信息请求 +// 返回用户基础信息及其角色与权限集合 +type GetUserReq struct { + gmeta.Meta `method:"get" path:"/api/index/getUser" summary:"获取当前用户信息"` +} + +// GetUserRes 当前登录用户信息结果 +// 返回用户基础信息、角色与权限集合,敏感字段不返回 +type GetUserRes struct { + Id int64 `json:"id"` // 用户ID + Username string `json:"username"` // 用户名 + Realname string `json:"realname"` // 真实姓名 + Gender int `json:"gender"` // 性别:1-男 2-女 3-保密 + Avatar string `json:"avatar"` // 头像 + Mobile string `json:"mobile"` // 手机号 + Email string `json:"email"` // 邮箱 + DeptId int `json:"deptId"` // 部门ID + LevelId int `json:"levelId"` // 职级ID + PositionId int `json:"positionId"` // 岗位ID + City []string `json:"city"` // 省市区街道编码数组 + CityInfo string `json:"cityInfo"` // 省市区信息 + Address string `json:"address"` // 详细地址 + Intro string `json:"intro"` // 个人简介 + Status int `json:"status"` // 状态 + Note string `json:"note"` // 备注 + TenantId int `json:"tenantId"` // 租户ID + Roles []string `json:"roles"` // 角色编码集合 + Permissions []string `json:"permissions"` // 权限标识集合 +} + +// GetMenusReq 获取当前用户菜单请求 +// 无路径参数版本,与带路径参数的菜单路由共用同一处理逻辑 +type GetMenusReq struct { + gmeta.Meta `method:"get" path:"/api/index/getMenus" summary:"获取当前用户菜单"` + Id int64 `json:"id" dc:"菜单ID(路由占位,忽略)"` // 菜单ID,路由占位 +} + +// GetMenusByIdReq 获取当前用户菜单请求(带路径参数) +// 带路径参数版本,与无路径参数的菜单路由共用同一处理逻辑 +type GetMenusByIdReq struct { + gmeta.Meta `method:"get" path:"/api/index/getMenus/:id" summary:"获取当前用户菜单"` + Id int64 `json:"id" dc:"菜单ID(路由占位,忽略)"` // 菜单ID,路由占位 +} + +// MenuTreeItem 菜单树节点 +// 字段与菜单表及前端路由生成契约保持一致 +type MenuTreeItem struct { + Id int64 `json:"id"` // 菜单ID + ParentId int `json:"parentId"` // 上级菜单ID + Name string `json:"name"` // 菜单名称 + Icon string `json:"icon"` // 图标 + Icon2 string `json:"icon2"` // 菜单图标2 + Path string `json:"path"` // 路由路径 + Component string `json:"component"` // 前端组件路径 + Permission string `json:"permission"` // 权限标识 + Target int `json:"target"` // 打开方式 + Type int `json:"type"` // 类型:0-菜单 1-节点 + Status int `json:"status"` // 状态 + Hide int `json:"hide"` // 是否隐藏 + Sort int `json:"sort"` // 排序 + Redirect string `json:"redirect,omitempty"` // 重定向地址 + Children []*MenuTreeItem `json:"children"` // 子节点集合 +} + +// GetMenusRes 菜单树结果 +type GetMenusRes = []*MenuTreeItem + +// UpdatePasswordReq 修改个人密码请求 +type UpdatePasswordReq struct { + gmeta.Meta `method:"put" path:"/api/index/updatePassword" summary:"修改个人密码"` + Password string `json:"password" v:"required#请输入原密码" dc:"原密码"` // 原密码 + NewPassword string `json:"newPassword" v:"required#请输入新密码|length:6,32#新密码长度需在6~32位" dc:"新密码"` // 新密码 +} + +// UpdatePasswordRes 修改个人密码结果 +type UpdatePasswordRes struct{} + +// UpdateProfileReq 更新个人信息请求 +type UpdateProfileReq struct { + gmeta.Meta `method:"put" path:"/api/index/updateProfile" summary:"更新个人信息"` + Realname string `json:"realname" v:"required#请输入姓名" dc:"姓名"` // 真实姓名 + Gender int `json:"gender" dc:"性别:1-男 2-女 3-保密"` // 性别 + Avatar string `json:"avatar" dc:"头像"` // 头像 + Mobile string `json:"mobile" v:"phone#手机号格式不正确" dc:"手机号"` // 手机号 + Email string `json:"email" v:"email#邮箱格式不正确" dc:"邮箱"` // 邮箱 + City []string `json:"city" dc:"省市区街道编码数组"` // 省市区街道编码数组 + CityInfo string `json:"cityInfo" dc:"省市区信息"` // 省市区信息 + Address string `json:"address" dc:"详细地址"` // 详细地址 + Intro string `json:"intro" dc:"个人简介"` // 个人简介 +} + +// UpdateProfileRes 更新个人信息结果 +type UpdateProfileRes struct{} diff --git a/api/v1/job.go b/api/v1/job.go new file mode 100644 index 0000000..074c89b --- /dev/null +++ b/api/v1/job.go @@ -0,0 +1,206 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 定时任务模块 ============================== + +// JobItem 任务行数据 +// 用于定时任务分页列表、任务列表等场景的任务基础信息 +type JobItem struct { + Id int64 `json:"id"` // 主键ID + JobName string `json:"jobName"` // 任务名称 + JobAlias string `json:"jobAlias"` // 任务别名 + JobGroup string `json:"jobGroup"` // 任务分组 + JobTrigger string `json:"jobTrigger"` // 触发器类型 + Status int `json:"status"` // 状态:0-未发布 1-运行中 2-暂停 + StatusText string `json:"statusText"` // 状态文本 + CronExpression string `json:"cronExpression"` // Cron表达式 + ExecutePolicy int `json:"executePolicy"` // 执行策略:1-立即执行 2-执行一次 3-放弃执行 + ExecutePolicyText string `json:"executePolicyText"` // 执行策略文本 + IsSync int `json:"isSync"` // 是否同步:0-否 1-是 + IsSyncText string `json:"isSyncText"` // 是否同步文本 + Url string `json:"url"` // 任务目标:http(s)地址或本地处理方法名 + Params string `json:"params"` // 请求参数(JSON) + Note string `json:"note"` // 备注 + CreateUser string `json:"createUser"` // 创建人 + CreateTime *gtime.Time `json:"createTime"` // 创建时间 +} + +// JobPageReq 定时任务分页请求 +// 支持关键词、任务名称、状态等条件组合查询 +type JobPageReq struct { + gmeta.Meta `method:"get" path:"/api/job/page" summary:"定时任务分页" permission:"sys:job:page" log:"定时任务-查询分页记录" logType:"query"` + PageReq // 分页基础参数 + Keyword string `json:"keyword" dc:"关键词(名称/别名模糊)"` // 关键词,名称或别名模糊匹配 + JobName string `json:"jobName" dc:"任务名称模糊"` // 任务名称模糊匹配 + Status int `json:"status" dc:"状态:0-未发布 1-运行中 2-暂停"` // 状态 +} + +// JobPageRes 定时任务分页结果 +type JobPageRes = PageData[JobItem] + +// JobListReq 定时任务列表请求 +type JobListReq struct { + gmeta.Meta `method:"get" path:"/api/job/list" summary:"定时任务列表" permission:"sys:job:list" log:"定时任务-查询列表" logType:"query"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 + Status int `json:"status" dc:"状态"` // 状态 +} + +// JobListRes 定时任务列表结果 +type JobListRes = []JobItem + +// JobDetailReq 定时任务详情请求 +type JobDetailReq struct { + gmeta.Meta `method:"get" path:"/api/job/detail/:id" summary:"定时任务详情" permission:"sys:job:detail" log:"定时任务-查询详情" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"任务ID"` // 任务ID +} + +// JobDetailRes 定时任务详情结果 +type JobDetailRes struct { + JobItem +} + +// JobAddReq 新增定时任务请求 +type JobAddReq struct { + gmeta.Meta `method:"post" path:"/api/job/add" summary:"新增定时任务" permission:"sys:job:add" log:"定时任务-新增记录" logType:"add"` + JobName string `json:"jobName" v:"required#请输入任务名称" dc:"任务名称"` // 任务名称 + JobAlias string `json:"jobAlias" dc:"任务别名"` // 任务别名 + JobGroup string `json:"jobGroup" dc:"任务分组"` // 任务分组 + JobTrigger string `json:"jobTrigger" dc:"触发器类型"` // 触发器类型 + CronExpression string `json:"cronExpression" v:"required#请输入Cron表达式" dc:"Cron表达式"` // Cron表达式 + ExecutePolicy int `json:"executePolicy" dc:"执行策略:1-立即执行 2-执行一次 3-放弃执行"` // 执行策略 + IsSync int `json:"isSync" dc:"是否同步:0-否 1-是"` // 是否同步 + Url string `json:"url" v:"required#请输入任务目标" dc:"任务目标(http(s)地址或本地处理方法名)"` // 任务目标:http(s)地址或本地处理方法名 + Params string `json:"params" dc:"请求参数(JSON)"` // 请求参数 + Note string `json:"note" dc:"备注"` // 备注 +} + +// JobUpdateReq 修改定时任务请求 +type JobUpdateReq struct { + gmeta.Meta `method:"put" path:"/api/job/update" summary:"修改定时任务" permission:"sys:job:update" log:"定时任务-修改记录" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"任务ID"` // 任务ID + JobName string `json:"jobName" v:"required#请输入任务名称" dc:"任务名称"` // 任务名称 + JobAlias string `json:"jobAlias" dc:"任务别名"` // 任务别名 + JobGroup string `json:"jobGroup" dc:"任务分组"` // 任务分组 + JobTrigger string `json:"jobTrigger" dc:"触发器类型"` // 触发器类型 + CronExpression string `json:"cronExpression" v:"required#请输入Cron表达式" dc:"Cron表达式"` // Cron表达式 + ExecutePolicy int `json:"executePolicy" dc:"执行策略"` // 执行策略 + IsSync int `json:"isSync" dc:"是否同步"` // 是否同步 + Url string `json:"url" v:"required#请输入任务目标" dc:"任务目标(http(s)地址或本地处理方法名)"` // 任务目标:http(s)地址或本地处理方法名 + Params string `json:"params" dc:"请求参数"` // 请求参数 + Note string `json:"note" dc:"备注"` // 备注 +} + +// JobUpdateRes 修改定时任务结果 +type JobUpdateRes struct{} + +// JobDeleteReq 删除定时任务请求 +type JobDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/job/delete/:id" summary:"删除定时任务" permission:"sys:job:delete" log:"定时任务-删除记录" logType:"delete"` + Id int64 `json:"id" v:"required#缺少ID" dc:"任务ID"` // 任务ID +} + +// JobDeleteRes 删除定时任务结果 +type JobDeleteRes struct{} + +// JobBatchDeleteReq 批量删除定时任务请求 +type JobBatchDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/job/batchDelete" summary:"批量删除定时任务" permission:"sys:job:batchDelete" log:"定时任务-批量删除记录" logType:"delete"` + Ids []int64 `json:"ids" v:"required#请选择要删除的记录" dc:"任务ID列表"` // 任务ID列表 +} + +// JobBatchDeleteRes 批量删除结果 +type JobBatchDeleteRes struct{} + +// JobRunOnceReq 立即执行一次任务请求 +type JobRunOnceReq struct { + gmeta.Meta `method:"get" path:"/api/job/runOnce/:id" summary:"立即执行任务" permission:"sys:job:execute" log:"定时任务-执行任务" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"任务ID"` // 任务ID +} + +// JobRunOnceRes 立即执行一次任务结果 +type JobRunOnceRes struct{} + +// JobPauseReq 暂停任务请求 +type JobPauseReq struct { + gmeta.Meta `method:"get" path:"/api/job/pause/:id" summary:"暂停定时任务" permission:"sys:job:pause" log:"定时任务-暂停任务" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"任务ID"` // 任务ID +} + +// JobPauseRes 暂停任务结果 +type JobPauseRes struct{} + +// JobResumeReq 恢复任务请求 +type JobResumeReq struct { + gmeta.Meta `method:"get" path:"/api/job/resume/:id" summary:"恢复定时任务" permission:"sys:job:resume" log:"定时任务-恢复任务" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"任务ID"` // 任务ID +} + +// JobResumeRes 恢复任务结果 +type JobResumeRes struct{} + +// JobStatusReq 设置任务状态请求 +type JobStatusReq struct { + gmeta.Meta `method:"post" path:"/api/job/status" summary:"设置定时任务状态" permission:"sys:job:update" log:"定时任务-设置状态" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"任务ID"` // 任务ID + Status int `json:"status" v:"required#缺少状态" dc:"状态:0-未发布 1-运行中 2-暂停"` // 状态 +} + +// JobStatusRes 设置任务状态结果 +type JobStatusRes struct{} + +// JobDaemonStatusReq 查询调度器状态请求 +type JobDaemonStatusReq struct { + gmeta.Meta `method:"get" path:"/api/job/daemonStatus" summary:"查询调度器状态" log:"定时任务-查询调度器状态" logType:"query"` +} + +// JobDaemonStatusRes 调度器状态结果 +type JobDaemonStatusRes struct { + Running int `json:"running"` // 运行中任务数 + JobIds []string `json:"jobIds"` // 调度器中的任务键 + StartTime string `json:"startTime"` // 调度器启动时间 +} diff --git a/api/v1/job_log.go b/api/v1/job_log.go new file mode 100644 index 0000000..a2e0866 --- /dev/null +++ b/api/v1/job_log.go @@ -0,0 +1,123 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 任务日志模块 ============================== + +// JobLogItem 任务日志行数据 +// 用于任务日志分页列表、任务日志列表等场景的日志基础信息 +type JobLogItem struct { + Id int64 `json:"id"` // 主键ID + JobId int64 `json:"jobId"` // 任务ID + JobName string `json:"jobName"` // 任务名称 + JobGroup string `json:"jobGroup"` // 任务分组 + JobTrigger string `json:"jobTrigger"` // 触发器类型 + JobMessage string `json:"jobMessage"` // 任务执行消息 + CronExpression string `json:"cronExpression"` // Cron表达式 + Status int `json:"status"` // 状态:0-正常 1-失败 + StatusText string `json:"statusText"` // 状态文本 + StartTime *gtime.Time `json:"startTime"` // 开始时间 + EndTime *gtime.Time `json:"endTime"` // 结束时间 + ConsumeTime int64 `json:"consumeTime"` // 耗时(毫秒) + ExceptionInfo string `json:"exceptionInfo"` // 异常信息 + CreateTime *gtime.Time `json:"createTime"` // 创建时间 +} + +// JobLogPageReq 任务日志分页请求 +// 支持关键词、任务名称、任务ID、状态等条件组合查询 +type JobLogPageReq struct { + gmeta.Meta `method:"get" path:"/api/job/log/page" summary:"任务日志分页" permission:"sys:jobLog:page" log:"任务日志-查询分页记录" logType:"query"` + PageReq // 分页基础参数 + Keyword string `json:"keyword" dc:"关键词(任务名模糊)"` // 关键词,任务名模糊匹配 + JobName string `json:"jobName" dc:"任务名称模糊"` // 任务名称模糊匹配 + JobId int64 `json:"jobId" dc:"任务ID"` // 任务ID + Status int `json:"status" dc:"状态:0-正常 1-失败"` // 状态 +} + +// JobLogPageRes 任务日志分页结果 +type JobLogPageRes = PageData[JobLogItem] + +// JobLogListReq 任务日志列表请求 +type JobLogListReq struct { + gmeta.Meta `method:"get" path:"/api/job/log/list" summary:"任务日志列表" permission:"sys:jobLog:list" log:"任务日志-查询列表" logType:"query"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 + JobId int64 `json:"jobId" dc:"任务ID"` // 任务ID + Status int `json:"status" dc:"状态"` // 状态 + Limit int `json:"limit" dc:"条数上限"` // 条数上限 +} + +// JobLogListRes 任务日志列表结果 +type JobLogListRes = []JobLogItem + +// JobLogDetailReq 任务日志详情请求 +type JobLogDetailReq struct { + gmeta.Meta `method:"get" path:"/api/job/log/detail/:id" summary:"任务日志详情" permission:"sys:jobLog:detail" log:"任务日志-查询详情" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"日志ID"` // 日志ID +} + +// JobLogDetailRes 任务日志详情结果 +type JobLogDetailRes struct { + JobLogItem +} + +// JobLogDeleteReq 删除任务日志请求 +type JobLogDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/job/log/delete/:id" summary:"删除任务日志" permission:"sys:jobLog:delete" log:"任务日志-删除记录" logType:"delete"` + Id int64 `json:"id" v:"required#缺少ID" dc:"日志ID"` // 日志ID +} + +// JobLogDeleteRes 删除任务日志结果 +type JobLogDeleteRes struct{} + +// JobLogBatchDeleteReq 批量删除任务日志请求 +type JobLogBatchDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/job/log/batchDelete" summary:"批量删除任务日志" permission:"sys:jobLog:batchDelete" log:"任务日志-批量删除记录" logType:"delete"` + Ids []int64 `json:"ids" v:"required#请选择要删除的记录" dc:"日志ID列表"` // 日志ID列表 +} + +// JobLogBatchDeleteRes 批量删除结果 +type JobLogBatchDeleteRes struct{} diff --git a/api/v1/level.go b/api/v1/level.go new file mode 100644 index 0000000..b4a83f3 --- /dev/null +++ b/api/v1/level.go @@ -0,0 +1,188 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 职级模块 ============================== + +// LevelItem 职级行数据 +// 用于职级分页列表等场景的职级基础信息 +type LevelItem struct { + Id int64 `json:"id"` // 主键ID + TenantId int `json:"tenantId"` // 租户ID + Name string `json:"name"` // 职级名称 + Status int `json:"status"` // 状态:1-正常 2-禁用 + StatusText string `json:"statusText"` // 状态文本 + Sort int `json:"sort"` // 排序 + UserCount int `json:"userCount"` // 关联用户数 + CreateUser string `json:"createUser"` // 创建人 + CreateTime *gtime.Time `json:"createTime"` // 创建时间 +} + +// LevelPageReq 职级分页请求 +// 支持关键词、名称、状态等条件组合查询 +type LevelPageReq struct { + gmeta.Meta `method:"get" path:"/api/level/page" summary:"职级分页" permission:"sys:level:page" log:"职级管理-查询分页记录" logType:"query"` + PageReq // 分页基础参数 + Keyword string `json:"keyword" dc:"关键词"` // 关键词 + Name string `json:"name" dc:"名称模糊"` // 名称模糊匹配 + Status int `json:"status" dc:"状态:1-正常 2-禁用"` // 状态 +} + +// LevelPageRes 职级分页结果 +type LevelPageRes = PageData[LevelItem] + +// LevelListReq 职级全量列表请求 +type LevelListReq struct { + gmeta.Meta `method:"get" path:"/api/level/list" summary:"职级列表" permission:"sys:level:list" log:"职级管理-查询列表" logType:"query"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 + Status int `json:"status" dc:"状态"` // 状态 +} + +// LevelListItem 职级列表行数据 +// 精简结构,仅包含列表展示所需字段 +type LevelListItem struct { + Id int64 `json:"id"` // 主键ID + Name string `json:"name"` // 职级名称 + Status int `json:"status"` // 状态:1-正常 2-禁用 + Sort int `json:"sort"` // 排序 +} + +// LevelListRes 职级列表结果 +type LevelListRes = []LevelListItem + +// LevelDetailReq 职级详情请求 +type LevelDetailReq struct { + gmeta.Meta `method:"get" path:"/api/level/detail/:id" summary:"职级详情" permission:"sys:level:detail" log:"职级管理-查询详情" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"职级ID"` // 职级ID +} + +// LevelDetailRes 职级详情结果 +type LevelDetailRes struct { + Id int64 `json:"id"` // 主键ID + Name string `json:"name"` // 职级名称 + Status int `json:"status"` // 状态:1-正常 2-禁用 + StatusText string `json:"statusText"` // 状态文本 + Sort int `json:"sort"` // 排序 +} + +// LevelAddReq 新增职级请求 +type LevelAddReq struct { + gmeta.Meta `method:"post" path:"/api/level/add" summary:"新增职级" permission:"sys:level:add" log:"职级管理-新增记录" logType:"add"` + Name string `json:"name" v:"required#请输入职级名称" dc:"职级名称"` // 职级名称 + Status int `json:"status" dc:"状态:1-正常 2-禁用"` // 状态 + Sort int `json:"sort" dc:"排序"` // 排序 +} + +// LevelUpdateReq 修改职级请求 +type LevelUpdateReq struct { + gmeta.Meta `method:"put" path:"/api/level/update" summary:"修改职级" permission:"sys:level:update" log:"职级管理-修改记录" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"职级ID"` // 职级ID + Name string `json:"name" v:"required#请输入职级名称" dc:"职级名称"` // 职级名称 + Status int `json:"status" dc:"状态"` // 状态 + Sort int `json:"sort" dc:"排序"` // 排序 +} + +// LevelUpdateRes 修改职级结果 +type LevelUpdateRes struct{} + +// LevelStatusReq 职级状态请求 +type LevelStatusReq struct { + gmeta.Meta `method:"put" path:"/api/level/status" summary:"职级状态" permission:"sys:level:status" log:"职级管理-设置状态" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"职级ID"` // 职级ID + Status int `json:"status" v:"required#缺少状态" dc:"状态:1-正常 2-禁用"` // 状态 +} + +// LevelStatusRes 职级状态结果 +type LevelStatusRes struct{} + +// LevelDeleteReq 删除职级请求 +type LevelDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/level/delete/:id" summary:"删除职级" permission:"sys:level:delete" log:"职级管理-删除记录" logType:"delete"` + Id int64 `json:"id" v:"required#缺少ID" dc:"职级ID"` // 职级ID +} + +// LevelDeleteRes 删除职级结果 +type LevelDeleteRes struct{} + +// LevelBatchDeleteReq 批量删除职级请求 +type LevelBatchDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/level/batchDelete" summary:"批量删除职级" permission:"sys:level:batchDelete" log:"职级管理-批量删除记录" logType:"delete"` + Ids []int64 `json:"ids" v:"required#请选择要删除的记录" dc:"职级ID列表"` // 职级ID列表 +} + +// LevelBatchDeleteRes 批量删除结果 +type LevelBatchDeleteRes struct{} + +// LevelImportReq 导入职级请求 +// 上传 xls/xlsx 文件进行数据导入 +type LevelImportReq struct { + gmeta.Meta `method:"post" path:"/api/level/import" summary:"导入职级" permission:"sys:level:import" log:"职级管理-导入数据" logType:"import"` + File *ghttp.UploadFile `p:"file" v:"required#请选择文件" dc:"xls/xlsx 文件"` // 上传文件 +} + +// LevelImportRes 导入结果 +// 返回导入总数、成功数、失败数及失败原因 +type LevelImportRes struct { + Total int `json:"total"` // 导入总数 + Success int `json:"success"` // 成功数 + Failed int `json:"failed"` // 失败数 + Reasons []string `json:"reasons"` // 失败原因列表 +} + +// LevelExportReq 导出职级请求 +// 按查询条件导出符合条件的记录 +type LevelExportReq struct { + gmeta.Meta `method:"get" path:"/api/level/export" summary:"导出职级" permission:"sys:level:export" log:"职级管理-导出数据" logType:"export"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 + Name string `json:"name" dc:"名称模糊"` // 名称模糊匹配 + Status int `json:"status" dc:"状态"` // 状态 +} + +// LevelExportRes 导出结果 +// data 直接为导出文件的访问地址 +type LevelExportRes = string diff --git a/api/v1/link.go b/api/v1/link.go new file mode 100644 index 0000000..497c522 --- /dev/null +++ b/api/v1/link.go @@ -0,0 +1,152 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 友链模块 ============================== + +// LinkItem 友链行数据 +// 用于友链分页列表等场景的友链基础信息 +type LinkItem struct { + Id int64 `json:"id"` // 主键ID + Name string `json:"name"` // 友链名称 + Type int `json:"type"` // 类型:1-友情链接 2-合作伙伴 + TypeText string `json:"typeText"` // 类型文本 + Url string `json:"url"` // 友链URL + Form int `json:"form"` // 形式:1-文字链接 2-图片链接 + FormText string `json:"formText"` // 形式文本 + Image string `json:"image"` // 友链图片 + Status int `json:"status"` // 状态:1-正常 2-停用 + StatusText string `json:"statusText"` // 状态文本 + Sort int `json:"sort"` // 排序 + Note string `json:"note"` // 备注 + CreateUser string `json:"createUser"` // 创建人 + CreateTime *gtime.Time `json:"createTime"` // 创建时间 +} + +// LinkPageReq 友链分页请求 +// 支持关键词、名称、类型、状态等条件组合查询 +type LinkPageReq struct { + gmeta.Meta `method:"get" path:"/api/link/page" summary:"友链分页" permission:"sys:link:page" log:"友链管理-查询分页记录" logType:"query"` + PageReq // 分页基础参数 + Keyword string `json:"keyword" dc:"关键词(名称模糊)"` // 关键词,名称模糊匹配 + Name string `json:"name" dc:"名称模糊"` // 名称模糊匹配 + Type int `json:"type" dc:"类型:1-友情链接 2-合作伙伴"` // 类型 + Status int `json:"status" dc:"状态:1-正常 2-停用"` // 状态 +} + +// LinkPageRes 友链分页结果 +type LinkPageRes = PageData[LinkItem] + +// LinkDetailReq 友链详情请求 +type LinkDetailReq struct { + gmeta.Meta `method:"get" path:"/api/link/detail/:id" summary:"友链详情" permission:"sys:link:detail" log:"友链管理-查询详情" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"友链ID"` // 友链ID +} + +// LinkDetailRes 友链详情结果 +type LinkDetailRes struct { + Id int64 `json:"id"` // 主键ID + Name string `json:"name"` // 友链名称 + Type int `json:"type"` // 类型:1-友情链接 2-合作伙伴 + TypeText string `json:"typeText"` // 类型文本 + Url string `json:"url"` // 友链URL + Form int `json:"form"` // 形式:1-文字链接 2-图片链接 + FormText string `json:"formText"` // 形式文本 + Image string `json:"image"` // 友链图片 + Status int `json:"status"` // 状态:1-正常 2-停用 + StatusText string `json:"statusText"` // 状态文本 + Sort int `json:"sort"` // 排序 + Note string `json:"note"` // 备注 +} + +// LinkAddReq 新增友链请求 +type LinkAddReq struct { + gmeta.Meta `method:"post" path:"/api/link/add" summary:"新增友链" permission:"sys:link:add" log:"友链管理-新增记录" logType:"add"` + Name string `json:"name" v:"required#请输入友链名称" dc:"友链名称"` // 友链名称 + Type int `json:"type" dc:"类型:1-友情链接 2-合作伙伴"` // 类型 + Url string `json:"url" v:"required#请输入友链地址" dc:"友链URL"` // 友链URL + Form int `json:"form" dc:"形式:1-文字链接 2-图片链接"` // 形式 + Image string `json:"image" dc:"友链图片"` // 友链图片 + Status int `json:"status" dc:"状态:1-正常 2-停用"` // 状态 + Sort int `json:"sort" dc:"排序"` // 排序 + Note string `json:"note" dc:"备注"` // 备注 +} + +// LinkUpdateReq 修改友链请求 +type LinkUpdateReq struct { + gmeta.Meta `method:"put" path:"/api/link/update" summary:"修改友链" permission:"sys:link:update" log:"友链管理-修改记录" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"友链ID"` // 友链ID + Name string `json:"name" v:"required#请输入友链名称" dc:"友链名称"` // 友链名称 + Type int `json:"type" dc:"类型"` // 类型 + Url string `json:"url" v:"required#请输入友链地址" dc:"友链URL"` // 友链URL + Form int `json:"form" dc:"形式"` // 形式 + Image string `json:"image" dc:"友链图片"` // 友链图片 + Status int `json:"status" dc:"状态"` // 状态 + Sort int `json:"sort" dc:"排序"` // 排序 + Note string `json:"note" dc:"备注"` // 备注 +} + +// LinkUpdateRes 修改友链结果 +type LinkUpdateRes struct{} + +// LinkDeleteReq 删除友链请求 +type LinkDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/link/delete/:id" summary:"删除友链" permission:"sys:link:delete" log:"友链管理-删除记录" logType:"delete"` + Id int64 `json:"id" v:"required#缺少ID" dc:"友链ID"` // 友链ID +} + +// LinkDeleteRes 删除友链结果 +type LinkDeleteRes struct{} + +// LinkBatchDeleteReq 批量删除友链请求 +type LinkBatchDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/link/batchDelete" summary:"批量删除友链" permission:"sys:link:batchDelete" log:"友链管理-批量删除记录" logType:"delete"` + Ids []int64 `json:"ids" v:"required#请选择要删除的记录" dc:"友链ID列表"` // 友链ID列表 +} + +// LinkBatchDeleteRes 批量删除结果 +type LinkBatchDeleteRes struct{} diff --git a/api/v1/login_log.go b/api/v1/login_log.go new file mode 100644 index 0000000..6c012d9 --- /dev/null +++ b/api/v1/login_log.go @@ -0,0 +1,131 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 登录日志模块 ============================== + +// LoginLogItem 登录日志行数据 +// 用于登录日志分页列表、登录日志列表等场景的日志基础信息 +type LoginLogItem struct { + Id int64 `json:"id"` // 主键ID + Username string `json:"username"` // 用户名 + LogType int `json:"logType"` // 类型:1-登录 2-退出 + LogTypeText string `json:"logTypeText"` // 类型文本 + Source int `json:"source"` // 来源 + SourceText string `json:"sourceText"` // 来源文本 + Method string `json:"method"` // 处理方法 + RequestMethod string `json:"requestMethod"` // 请求方式 + Url string `json:"url"` // 请求地址 + Param string `json:"param"` // 请求参数 + Result string `json:"result"` // 返回结果 + Ip string `json:"ip"` // 客户端IP + Location string `json:"location"` // 归属地 + Os string `json:"os"` // 操作系统 + Browser string `json:"browser"` // 浏览器 + ConsumeTime int64 `json:"consumeTime"` // 耗时(毫秒) + Status int `json:"status"` // 状态:0-成功 1-失败 + StatusText string `json:"statusText"` // 状态文本 + Error string `json:"error"` // 错误信息 + CreateUser string `json:"createUser"` // 创建人 + CreateTime *gtime.Time `json:"createTime"` // 创建时间 + UpdateTime *gtime.Time `json:"updateTime"` // 更新时间 +} + +// LoginLogPageReq 登录日志分页请求 +// 支持关键词、用户名、类型、状态等条件组合查询 +type LoginLogPageReq struct { + gmeta.Meta `method:"get" path:"/api/login/log/page" summary:"登录日志分页" permission:"sys:loginLog:page" log:"登录日志-查询分页记录" logType:"query"` + PageReq // 分页基础参数 + Keyword string `json:"keyword" dc:"关键词(用户名模糊)"` // 关键词,用户名模糊匹配 + Username string `json:"username" dc:"用户名模糊"` // 用户名模糊匹配 + LogType int `json:"logType" dc:"类型:1-登录 2-退出"` // 类型 + Status int `json:"status" dc:"状态:0-成功 1-失败"` // 状态 +} + +// LoginLogPageRes 登录日志分页结果 +type LoginLogPageRes = PageData[LoginLogItem] + +// LoginLogListReq 登录日志列表请求 +type LoginLogListReq struct { + gmeta.Meta `method:"get" path:"/api/login/log/list" summary:"登录日志列表" permission:"sys:loginLog:list" log:"登录日志-查询列表" logType:"query"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 + LogType int `json:"logType" dc:"类型"` // 类型 + Status int `json:"status" dc:"状态"` // 状态 + Limit int `json:"limit" dc:"条数上限"` // 条数上限 +} + +// LoginLogListRes 登录日志列表结果 +type LoginLogListRes = []LoginLogItem + +// LoginLogDetailReq 登录日志详情请求 +type LoginLogDetailReq struct { + gmeta.Meta `method:"get" path:"/api/login/log/detail/:id" summary:"登录日志详情" permission:"sys:loginLog:detail" log:"登录日志-查询详情" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"日志ID"` // 日志ID +} + +// LoginLogDetailRes 登录日志详情结果 +type LoginLogDetailRes struct { + LoginLogItem +} + +// LoginLogDeleteReq 删除登录日志请求 +type LoginLogDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/login/log/delete/:id" summary:"删除登录日志" permission:"sys:loginLog:delete" log:"登录日志-删除记录" logType:"delete"` + Id int64 `json:"id" v:"required#缺少ID" dc:"日志ID"` // 日志ID +} + +// LoginLogDeleteRes 删除登录日志结果 +type LoginLogDeleteRes struct{} + +// LoginLogBatchDeleteReq 批量删除登录日志请求 +type LoginLogBatchDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/login/log/batchDelete" summary:"批量删除登录日志" permission:"sys:loginLog:batchDelete" log:"登录日志-批量删除记录" logType:"delete"` + Ids []int64 `json:"ids" v:"required#请选择要删除的记录" dc:"日志ID列表"` // 日志ID列表 +} + +// LoginLogBatchDeleteRes 批量删除结果 +type LoginLogBatchDeleteRes struct{} diff --git a/api/v1/menu.go b/api/v1/menu.go new file mode 100644 index 0000000..d6e5686 --- /dev/null +++ b/api/v1/menu.go @@ -0,0 +1,183 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 菜单模块 ============================== + +// MenuItem 菜单行数据 +// 用于菜单分页列表、菜单树形列表等场景的菜单基础信息 +type MenuItem struct { + Id int64 `json:"id"` // 主键ID + ParentId int `json:"parentId"` // 上级菜单ID(0=顶级) + Name string `json:"name"` // 菜单名称 + Icon string `json:"icon"` // 图标 + Icon2 string `json:"icon2"` // 菜单图标2 + Path string `json:"path"` // 路由路径 + Component string `json:"component"` // 前端组件路径 + Permission string `json:"permission"` // 权限节点标识 + Target int `json:"target"` // 打开方式:0-组件 1-内链 2-外链 + Type int `json:"type"` // 类型:0-菜单 1-权限节点 + TypeText string `json:"typeText"` // 类型文本 + Status int `json:"status"` // 状态:0-正常 1-停用 + StatusText string `json:"statusText"` // 状态文本 + Hide int `json:"hide"` // 是否可见:0-可见 1-不可见 + Sort int `json:"sort"` // 排序 + Note string `json:"note"` // 备注 + CreateUser string `json:"createUser"` // 创建人 +} + +// MenuPageReq 菜单分页请求(平铺) +// 支持关键词、名称、路径、类型、状态等条件组合查询 +type MenuPageReq struct { + gmeta.Meta `method:"get" path:"/api/menu/page" summary:"菜单分页" permission:"sys:menu:page" log:"菜单管理-查询分页记录" logType:"query"` + PageReq // 分页基础参数 + Keyword string `json:"keyword" dc:"关键词(名称/路径模糊)"` // 关键词,名称或路径模糊匹配 + Name string `json:"name" dc:"菜单名称模糊"` // 菜单名称模糊匹配 + Path string `json:"path" dc:"路由路径模糊"` // 路由路径模糊匹配 + Type *int `json:"type" dc:"类型:0-菜单 1-权限节点"` // 类型,指针用于区分未传值 + Status *int `json:"status" dc:"状态:0-正常 1-停用"` // 状态,指针用于区分未传值 +} + +// MenuPageRes 菜单分页结果 +type MenuPageRes = PageData[MenuItem] + +// MenuListReq 菜单树形列表请求 +// 返回平铺列表,由前端自行构建树形结构 +type MenuListReq struct { + gmeta.Meta `method:"get" path:"/api/menu/list" summary:"菜单树形列表" permission:"sys:menu:list" log:"菜单管理-查询树形列表" logType:"query"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 + Type *int `json:"type" dc:"类型"` // 类型 + Status *int `json:"status" dc:"状态"` // 状态 +} + +// MenuListRes 菜单平铺列表结果 +// data 直接为数组,由前端自行构建树形结构 +type MenuListRes = []*MenuItem + +// MenuDetailReq 菜单详情请求 +type MenuDetailReq struct { + gmeta.Meta `method:"get" path:"/api/menu/detail/:id" summary:"菜单详情" permission:"sys:menu:detail" log:"菜单管理-查询详情" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"菜单ID"` // 菜单ID +} + +// MenuDetailRes 菜单详情结果 +type MenuDetailRes struct { + Id int64 `json:"id"` // 主键ID + ParentId int `json:"parentId"` // 上级菜单ID + Name string `json:"name"` // 菜单名称 + Icon string `json:"icon"` // 图标 + Icon2 string `json:"icon2"` // 菜单图标2 + Path string `json:"path"` // 路由路径 + Component string `json:"component"` // 前端组件路径 + Permission string `json:"permission"` // 权限节点标识 + Target int `json:"target"` // 打开方式 + Type int `json:"type"` // 类型:0-菜单 1-权限节点 + TypeText string `json:"typeText"` // 类型文本 + Status int `json:"status"` // 状态:0-正常 1-停用 + StatusText string `json:"statusText"` // 状态文本 + Hide int `json:"hide"` // 是否可见 + Sort int `json:"sort"` // 排序 + Note string `json:"note"` // 备注 +} + +// MenuAddReq 新增菜单请求 +type MenuAddReq struct { + gmeta.Meta `method:"post" path:"/api/menu/add" summary:"新增菜单" permission:"sys:menu:add" log:"菜单管理-新增记录" logType:"add"` + ParentId int `json:"parentId" dc:"上级ID(0=顶级)"` // 上级菜单ID + Name string `json:"name" v:"required#请输入菜单名称" dc:"菜单名称"` // 菜单名称 + Icon string `json:"icon" dc:"图标"` // 图标 + Icon2 string `json:"icon2" dc:"菜单图标2"` // 菜单图标2 + Path string `json:"path" dc:"路由路径"` // 路由路径 + Component string `json:"component" dc:"前端组件"` // 前端组件路径 + Permission string `json:"permission" dc:"权限节点"` // 权限节点标识 + Target int `json:"target" dc:"打开方式:0-组件 1-内链 2-外链"` // 打开方式 + Type int `json:"type" dc:"类型:0-菜单 1-权限节点"` // 类型 + Status int `json:"status" dc:"状态:0-正常 1-停用"` // 状态 + Hide int `json:"hide" dc:"是否可见:0-可见 1-不可见"` // 是否可见 + Sort int `json:"sort" dc:"排序"` // 排序 + Note string `json:"note" dc:"备注"` // 备注 +} + +// MenuUpdateReq 修改菜单请求 +// Icon/Icon2/Note 为指针:未传值时该列不更新,保留库中原值;传空串表示清空 +type MenuUpdateReq struct { + gmeta.Meta `method:"put" path:"/api/menu/update" summary:"修改菜单" permission:"sys:menu:update" log:"菜单管理-修改记录" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"菜单ID"` // 菜单ID + ParentId int `json:"parentId" dc:"上级ID"` // 上级菜单ID + Name string `json:"name" v:"required#请输入菜单名称" dc:"菜单名称"` // 菜单名称 + Icon *string `json:"icon" dc:"图标"` // 图标,未传则保留原值 + Icon2 *string `json:"icon2" dc:"菜单图标2"` // 菜单图标2,未传则保留原值 + Path string `json:"path" dc:"路由路径"` // 路由路径 + Component string `json:"component" dc:"前端组件"` // 前端组件路径 + Permission string `json:"permission" dc:"权限节点"` // 权限节点标识 + Target int `json:"target" dc:"打开方式"` // 打开方式 + Type int `json:"type" dc:"类型"` // 类型 + Status int `json:"status" dc:"状态"` // 状态 + Hide int `json:"hide" dc:"是否可见"` // 是否可见 + Sort int `json:"sort" dc:"排序"` // 排序 + Note *string `json:"note" dc:"备注"` // 备注,未传则保留原值 +} + +// MenuUpdateRes 修改菜单结果 +type MenuUpdateRes struct{} + +// MenuDeleteReq 删除菜单请求 +type MenuDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/menu/delete/:id" summary:"删除菜单" permission:"sys:menu:delete" log:"菜单管理-删除记录" logType:"delete"` + Id int64 `json:"id" v:"required#缺少ID" dc:"菜单ID"` // 菜单ID +} + +// MenuDeleteRes 删除菜单结果 +type MenuDeleteRes struct{} + +// MenuBatchDeleteReq 批量删除菜单请求 +type MenuBatchDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/menu/batchDelete" summary:"批量删除菜单" permission:"sys:menu:batchDelete" log:"菜单管理-批量删除记录" logType:"delete"` + Ids []int64 `json:"ids" v:"required#请选择要删除的记录" dc:"菜单ID列表"` // 菜单ID列表 +} + +// MenuBatchDeleteRes 批量删除结果 +type MenuBatchDeleteRes struct{} diff --git a/api/v1/notice.go b/api/v1/notice.go new file mode 100644 index 0000000..2dc685e --- /dev/null +++ b/api/v1/notice.go @@ -0,0 +1,169 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 公告模块 ============================== + +// NoticeItem 公告行数据 +// 用于公告分页列表等场景的公告基础信息 +type NoticeItem struct { + Id int64 `json:"id"` // 主键ID + Title string `json:"title"` // 公告标题 + Cover string `json:"cover"` // 封面图 + Type int `json:"type"` // 类型 + TypeText string `json:"typeText"` // 类型文本 + ClickNum int `json:"clickNum"` // 点击量 + Status int `json:"status"` // 状态:0-草稿 1-发布 + StatusText string `json:"statusText"` // 状态文本 + CreateUser string `json:"createUser"` // 创建人 + CreateTime *gtime.Time `json:"createTime"` // 创建时间 +} + +// NoticePageReq 公告分页请求 +// 支持关键词、标题、类型、状态等条件组合查询 +type NoticePageReq struct { + gmeta.Meta `method:"get" path:"/api/notice/page" summary:"公告分页" permission:"sys:notice:page" log:"公告管理-查询分页记录" logType:"query"` + PageReq // 分页基础参数 + Keyword string `json:"keyword" dc:"关键词(标题模糊)"` // 关键词,标题模糊匹配 + Title string `json:"title" dc:"标题模糊"` // 标题模糊匹配 + Type int `json:"type" dc:"类型"` // 类型 + Status int `json:"status" dc:"状态:0-草稿 1-发布"` // 状态 +} + +// NoticePageRes 公告分页结果 +type NoticePageRes = PageData[NoticeItem] + +// NoticeListReq 公告列表请求 +type NoticeListReq struct { + gmeta.Meta `method:"get" path:"/api/notice/list" summary:"公告列表" permission:"sys:notice:list" log:"公告管理-查询列表" logType:"query"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 + Type int `json:"type" dc:"类型"` // 类型 + Status int `json:"status" dc:"状态"` // 状态 + Limit int `json:"limit" dc:"条数上限"` // 条数上限 +} + +// NoticeListItem 公告列表行数据 +// 精简结构,仅包含列表展示所需字段 +type NoticeListItem struct { + Id int64 `json:"id"` // 主键ID + Title string `json:"title"` // 公告标题 + Type int `json:"type"` // 类型 + Status int `json:"status"` // 状态:0-草稿 1-发布 +} + +// NoticeListRes 公告列表结果 +type NoticeListRes = []NoticeListItem + +// NoticeDetailReq 公告详情请求 +type NoticeDetailReq struct { + gmeta.Meta `method:"get" path:"/api/notice/detail/:id" summary:"公告详情" permission:"sys:notice:detail" log:"公告管理-查询详情" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"公告ID"` // 公告ID +} + +// NoticeDetailRes 公告详情结果 +type NoticeDetailRes struct { + Id int64 `json:"id"` // 主键ID + Title string `json:"title"` // 公告标题 + Cover string `json:"cover"` // 封面图 + Type int `json:"type"` // 类型 + TypeText string `json:"typeText"` // 类型文本 + Status int `json:"status"` // 状态:0-草稿 1-发布 + StatusText string `json:"statusText"` // 状态文本 + Content string `json:"content"` // 正文 +} + +// NoticeAddReq 新增公告请求 +type NoticeAddReq struct { + gmeta.Meta `method:"post" path:"/api/notice/add" summary:"新增公告" permission:"sys:notice:add" log:"公告管理-新增记录" logType:"add"` + Title string `json:"title" v:"required#请输入标题" dc:"标题"` // 公告标题 + Cover string `json:"cover" dc:"封面图"` // 封面图 + Type int `json:"type" dc:"类型"` // 类型 + Status int `json:"status" dc:"状态:0-草稿 1-发布"` // 状态 + Content string `json:"content" dc:"正文"` // 正文 +} + +// NoticeUpdateReq 修改公告请求 +type NoticeUpdateReq struct { + gmeta.Meta `method:"put" path:"/api/notice/update" summary:"修改公告" permission:"sys:notice:update" log:"公告管理-修改记录" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"公告ID"` // 公告ID + Title string `json:"title" v:"required#请输入标题" dc:"标题"` // 公告标题 + Cover string `json:"cover" dc:"封面图"` // 封面图 + Type int `json:"type" dc:"类型"` // 类型 + Status int `json:"status" dc:"状态"` // 状态 + Content string `json:"content" dc:"正文"` // 正文 +} + +// NoticeUpdateRes 修改公告结果 +type NoticeUpdateRes struct{} + +// NoticeStatusReq 公告状态请求 +type NoticeStatusReq struct { + gmeta.Meta `method:"put" path:"/api/notice/status" summary:"公告状态" permission:"sys:notice:status" log:"公告管理-设置状态" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"公告ID"` // 公告ID + Status int `json:"status" v:"required#缺少状态" dc:"状态:0-草稿 1-发布"` // 状态 +} + +// NoticeStatusRes 公告状态结果 +type NoticeStatusRes struct{} + +// NoticeDeleteReq 删除公告请求 +type NoticeDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/notice/delete/:id" summary:"删除公告" permission:"sys:notice:delete" log:"公告管理-删除记录" logType:"delete"` + Id int64 `json:"id" v:"required#缺少ID" dc:"公告ID"` // 公告ID +} + +// NoticeDeleteRes 删除公告结果 +type NoticeDeleteRes struct{} + +// NoticeBatchDeleteReq 批量删除公告请求 +type NoticeBatchDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/notice/batchDelete" summary:"批量删除公告" permission:"sys:notice:batchDelete" log:"公告管理-批量删除记录" logType:"delete"` + Ids []int64 `json:"ids" v:"required#请选择要删除的记录" dc:"公告ID列表"` // 公告ID列表 +} + +// NoticeBatchDeleteRes 批量删除结果 +type NoticeBatchDeleteRes struct{} diff --git a/api/v1/online_user.go b/api/v1/online_user.go new file mode 100644 index 0000000..26d2fec --- /dev/null +++ b/api/v1/online_user.go @@ -0,0 +1,113 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 在线用户模块 ============================== + +// OnlineUserItem 在线用户行数据 +type OnlineUserItem struct { + Id string `json:"id"` // 访问令牌标识(jti) + UserId int `json:"userId"` // 用户ID + Username string `json:"username"` // 用户名 + Realname string `json:"realname"` // 姓名 + Avatar string `json:"avatar"` // 头像(已补全访问域名) + Ip string `json:"ip"` // 登录IP + Location string `json:"location"` // 登录地点 + Oss string `json:"oss"` // 操作系统 + Browser string `json:"browser"` // 浏览器 + LoginTime string `json:"loginTime"` // 登录时间 + IssuedTime string `json:"issuedTime"` // 令牌颁发时间 + ExpireTime string `json:"expireTime"` // 令牌过期时间 + RefreshIssuedTime string `json:"refreshIssuedTime"` // 刷新令牌颁发时间(空表示无) + RefreshExpireTime string `json:"refreshExpireTime"` // 刷新令牌过期时间(空表示无) + SessionNo int `json:"sessionNo"` // 同用户会话序号 + Sessions int `json:"sessions"` // 同用户会话总数 + TokenType string `json:"tokenType"` // 令牌类型(Bearer) + ExpiresIn int64 `json:"expiresIn"` // 访问令牌有效期(秒) + AccessToken string `json:"accessToken"` // 访问令牌原文(仅详情返回) + RefreshToken string `json:"refreshToken"` // 刷新令牌原文(仅详情返回) +} + +// OnlineUserPageReq 在线用户分页请求 +type OnlineUserPageReq struct { + gmeta.Meta `method:"get" path:"/api/onlineUser/page" summary:"在线用户分页" permission:"sys:onlineUser:page" log:"在线用户-查询分页记录" logType:"query"` + PageReq // 分页基础参数 + Username string `json:"username" dc:"用户名模糊"` // 用户名模糊匹配 + Ip string `json:"ip" dc:"登录IP模糊"` // 登录IP模糊匹配 +} + +// OnlineUserPageRes 在线用户分页结果 +type OnlineUserPageRes = PageData[OnlineUserItem] + +// OnlineUserDetailReq 在线会话详情请求 +type OnlineUserDetailReq struct { + gmeta.Meta `method:"get" path:"/api/onlineUser/detail/:id" summary:"在线会话详情" permission:"sys:onlineUser:detail" log:"在线用户-查询会话详情" logType:"query"` + Id string `json:"id" v:"required#缺少令牌标识" dc:"令牌标识"` +} + +// OnlineUserDetailRes 在线会话详情结果 +// 含访问/刷新令牌原文与登录环境信息 +type OnlineUserDetailRes struct { + OnlineUserItem +} + +// OnlineUserForceLogoutReq 强制下线请求 +type OnlineUserForceLogoutReq struct { + gmeta.Meta `method:"get" path:"/api/onlineUser/forceLogout/:id" summary:"强制下线" permission:"sys:onlineUser:forceLogout" log:"在线用户-强制下线" logType:"update"` + Id string `json:"id" v:"required#缺少令牌标识" dc:"令牌标识"` +} + +// OnlineUserForceLogoutRes 强制下线结果 +type OnlineUserForceLogoutRes struct{} + +// OnlineUserForceLogoutUserReq 踢出该用户全部设备请求 +type OnlineUserForceLogoutUserReq struct { + gmeta.Meta `method:"get" path:"/api/onlineUser/forceLogoutUser/:userId" summary:"踢出用户全部设备" permission:"sys:onlineUser:forceLogout" log:"在线用户-踢出全部设备" logType:"update"` + UserId int `json:"userId" v:"required#缺少用户ID" dc:"用户ID"` +} + +// OnlineUserForceLogoutUserRes 踢出该用户全部设备结果 +type OnlineUserForceLogoutUserRes struct{} diff --git a/api/v1/operation_log.go b/api/v1/operation_log.go new file mode 100644 index 0000000..2d6902b --- /dev/null +++ b/api/v1/operation_log.go @@ -0,0 +1,131 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 操作日志模块 ============================== + +// OperLogItem 操作日志行数据 +// 用于操作日志分页列表、操作日志列表等场景的日志基础信息 +type OperLogItem struct { + Id int64 `json:"id"` // 主键ID + Title string `json:"title"` // 日志标题 + Type int `json:"type"` // 类型 + TypeText string `json:"typeText"` // 类型文本 + Source int `json:"source"` // 来源 + SourceText string `json:"sourceText"` // 来源文本 + Method string `json:"method"` // 处理方法 + RequestMethod string `json:"requestMethod"` // 请求方式 + Url string `json:"url"` // 请求地址 + Param string `json:"param"` // 请求参数 + Result string `json:"result"` // 返回结果 + Ip string `json:"ip"` // 客户端IP + Location string `json:"location"` // 归属地 + Os string `json:"os"` // 操作系统 + Browser string `json:"browser"` // 浏览器 + ConsumeTime int64 `json:"consumeTime"` // 耗时(毫秒) + Status int `json:"status"` // 状态:0-正常 1-异常 + StatusText string `json:"statusText"` // 状态文本 + Error string `json:"error"` // 错误信息 + CreateUser string `json:"createUser"` // 操作人 + CreateTime *gtime.Time `json:"createTime"` // 创建时间 + UpdateTime *gtime.Time `json:"updateTime"` // 更新时间 +} + +// OperLogPageReq 操作日志分页请求 +// 支持关键词、日志标题、类型、状态等条件组合查询 +type OperLogPageReq struct { + gmeta.Meta `method:"get" path:"/api/oper/log/page" summary:"操作日志分页" permission:"sys:operLog:page" log:"操作日志-查询分页记录" logType:"query"` + PageReq // 分页基础参数 + Keyword string `json:"keyword" dc:"关键词(标题/URL模糊)"` // 关键词,标题或URL模糊匹配 + Title string `json:"title" dc:"日志标题模糊"` // 日志标题模糊匹配 + Type int `json:"type" dc:"类型(logType)"` // 类型 + Status int `json:"status" dc:"状态:0-正常 1-异常"` // 状态 +} + +// OperLogPageRes 操作日志分页结果 +type OperLogPageRes = PageData[OperLogItem] + +// OperLogListReq 操作日志列表请求 +type OperLogListReq struct { + gmeta.Meta `method:"get" path:"/api/oper/log/list" summary:"操作日志列表" permission:"sys:operLog:list" log:"操作日志-查询列表" logType:"query"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 + Type int `json:"type" dc:"类型"` // 类型 + Status int `json:"status" dc:"状态"` // 状态 + Limit int `json:"limit" dc:"条数上限"` // 条数上限 +} + +// OperLogListRes 操作日志列表结果 +type OperLogListRes = []OperLogItem + +// OperLogDetailReq 操作日志详情请求 +type OperLogDetailReq struct { + gmeta.Meta `method:"get" path:"/api/oper/log/detail/:id" summary:"操作日志详情" permission:"sys:operLog:detail" log:"操作日志-查询详情" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"日志ID"` // 日志ID +} + +// OperLogDetailRes 操作日志详情结果 +type OperLogDetailRes struct { + OperLogItem +} + +// OperLogDeleteReq 删除操作日志请求 +type OperLogDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/oper/log/delete/:id" summary:"删除操作日志" permission:"sys:operLog:delete" log:"操作日志-删除记录" logType:"delete"` + Id int64 `json:"id" v:"required#缺少ID" dc:"日志ID"` // 日志ID +} + +// OperLogDeleteRes 删除操作日志结果 +type OperLogDeleteRes struct{} + +// OperLogBatchDeleteReq 批量删除操作日志请求 +type OperLogBatchDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/oper/log/batchDelete" summary:"批量删除操作日志" permission:"sys:operLog:batchDelete" log:"操作日志-批量删除记录" logType:"delete"` + Ids []int64 `json:"ids" v:"required#请选择要删除的记录" dc:"日志ID列表"` // 日志ID列表 +} + +// OperLogBatchDeleteRes 批量删除结果 +type OperLogBatchDeleteRes struct{} diff --git a/api/v1/param.go b/api/v1/param.go new file mode 100644 index 0000000..6ddc04b --- /dev/null +++ b/api/v1/param.go @@ -0,0 +1,155 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 参数模块 ============================== + +// ParamItem 参数行数据 +// 用于参数分页列表、参数列表等场景的参数基础信息 +type ParamItem struct { + Id int64 `json:"id"` // 主键ID + Name string `json:"name"` // 参数名称 + Code string `json:"code"` // 参数编码 + Value string `json:"value"` // 参数值 + Type int `json:"type"` // 类型:1-文本 2-数字 3-开关 + TypeText string `json:"typeText"` // 类型文本 + Status int `json:"status"` // 状态 + StatusText string `json:"statusText"` // 状态文本 + Sort int `json:"sort"` // 排序 + Note string `json:"note"` // 备注 + CreateUser string `json:"createUser"` // 创建人 +} + +// ParamPageReq 参数分页请求 +// 支持关键词、参数名称、类型、状态等条件组合查询 +type ParamPageReq struct { + gmeta.Meta `method:"get" path:"/api/param/page" summary:"参数分页" permission:"sys:param:page" log:"参数管理-查询分页记录" logType:"query"` + PageReq // 分页基础参数 + Keyword string `json:"keyword" dc:"关键词(名称/编码模糊)"` // 关键词,名称或编码模糊匹配 + Name string `json:"name" dc:"参数名称模糊"` // 参数名称模糊匹配 + Type int `json:"type" dc:"类型:1-文本 2-数字 3-开关"` // 类型 + Status int `json:"status" dc:"状态"` // 状态 +} + +// ParamPageRes 参数分页结果 +type ParamPageRes = PageData[ParamItem] + +// ParamListReq 参数列表请求 +type ParamListReq struct { + gmeta.Meta `method:"get" path:"/api/param/list" summary:"参数列表" permission:"sys:param:list" log:"参数管理-查询列表" logType:"query"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 + Type int `json:"type" dc:"类型"` // 类型 + Status int `json:"status" dc:"状态"` // 状态 +} + +// ParamListRes 参数列表结果 +type ParamListRes = []ParamItem + +// ParamDetailReq 参数详情请求 +type ParamDetailReq struct { + gmeta.Meta `method:"get" path:"/api/param/detail/:id" summary:"参数详情" permission:"sys:param:detail" log:"参数管理-查询详情" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"参数ID"` // 参数ID +} + +// ParamDetailRes 参数详情结果 +type ParamDetailRes struct { + Id int64 `json:"id"` // 主键ID + Name string `json:"name"` // 参数名称 + Code string `json:"code"` // 参数编码 + Value string `json:"value"` // 参数值 + Type int `json:"type"` // 类型:1-文本 2-数字 3-开关 + TypeText string `json:"typeText"` // 类型文本 + Status int `json:"status"` // 状态 + StatusText string `json:"statusText"` // 状态文本 + Sort int `json:"sort"` // 排序 + Note string `json:"note"` // 备注 +} + +// ParamAddReq 新增参数请求 +type ParamAddReq struct { + gmeta.Meta `method:"post" path:"/api/param/add" summary:"新增参数" permission:"sys:param:add" log:"参数管理-新增记录" logType:"add"` + Name string `json:"name" v:"required#请输入参数名称" dc:"参数名称"` // 参数名称 + Code string `json:"code" v:"required#请输入参数编码" dc:"参数编码"` // 参数编码 + Value string `json:"value" dc:"参数值"` // 参数值 + Type int `json:"type" dc:"类型:1-文本 2-数字 3-开关"` // 类型 + Status int `json:"status" dc:"状态"` // 状态 + Sort int `json:"sort" dc:"排序"` // 排序 + Note string `json:"note" dc:"备注"` // 备注 +} + +// ParamUpdateReq 修改参数请求 +type ParamUpdateReq struct { + gmeta.Meta `method:"put" path:"/api/param/update" summary:"修改参数" permission:"sys:param:update" log:"参数管理-修改记录" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"参数ID"` // 参数ID + Name string `json:"name" v:"required#请输入参数名称" dc:"参数名称"` // 参数名称 + Code string `json:"code" v:"required#请输入参数编码" dc:"参数编码"` // 参数编码 + Value string `json:"value" dc:"参数值"` // 参数值 + Type int `json:"type" dc:"类型"` // 类型 + Status int `json:"status" dc:"状态"` // 状态 + Sort int `json:"sort" dc:"排序"` // 排序 + Note string `json:"note" dc:"备注"` // 备注 +} + +// ParamUpdateRes 修改参数结果 +type ParamUpdateRes struct{} + +// ParamDeleteReq 删除参数请求 +type ParamDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/param/delete/:id" summary:"删除参数" permission:"sys:param:delete" log:"参数管理-删除记录" logType:"delete"` + Id int64 `json:"id" v:"required#缺少ID" dc:"参数ID"` // 参数ID +} + +// ParamDeleteRes 删除参数结果 +type ParamDeleteRes struct{} + +// ParamBatchDeleteReq 批量删除参数请求 +type ParamBatchDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/param/batchDelete" summary:"批量删除参数" permission:"sys:param:batchDelete" log:"参数管理-批量删除记录" logType:"delete"` + Ids []int64 `json:"ids" v:"required#请选择要删除的记录" dc:"参数ID列表"` // 参数ID列表 +} + +// ParamBatchDeleteRes 批量删除结果 +type ParamBatchDeleteRes struct{} diff --git a/api/v1/position.go b/api/v1/position.go new file mode 100644 index 0000000..41b8152 --- /dev/null +++ b/api/v1/position.go @@ -0,0 +1,158 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 岗位模块 ============================== + +// PositionItem 岗位行数据 +// 用于岗位分页列表等场景的岗位基础信息 +type PositionItem struct { + Id int64 `json:"id"` // 主键ID + TenantId int `json:"tenantId"` // 租户ID + Name string `json:"name"` // 岗位名称 + Status int `json:"status"` // 状态:1-正常 2-禁用 + StatusText string `json:"statusText"` // 状态文本 + Sort int `json:"sort"` // 排序 + UserCount int `json:"userCount"` // 关联用户数 + CreateUser string `json:"createUser"` // 创建人 + CreateTime *gtime.Time `json:"createTime"` // 创建时间 +} + +// PositionPageReq 岗位分页请求 +// 支持关键词、岗位名称、状态等条件组合查询 +type PositionPageReq struct { + gmeta.Meta `method:"get" path:"/api/position/page" summary:"岗位分页" permission:"sys:position:page" log:"岗位管理-查询分页记录" logType:"query"` + PageReq // 分页基础参数 + Keyword string `json:"keyword" dc:"关键词"` // 关键词 + Name string `json:"name" dc:"岗位名称模糊"` // 岗位名称模糊匹配 + Status int `json:"status" dc:"状态:1-正常 2-禁用"` // 状态 +} + +// PositionPageRes 岗位分页结果 +type PositionPageRes = PageData[PositionItem] + +// PositionListReq 岗位全量列表请求 +type PositionListReq struct { + gmeta.Meta `method:"get" path:"/api/position/list" summary:"岗位列表" permission:"sys:position:list" log:"岗位管理-查询列表" logType:"query"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 + Status int `json:"status" dc:"状态"` // 状态 +} + +// PositionListItem 岗位列表行数据 +// 精简结构,仅包含列表展示所需字段 +type PositionListItem struct { + Id int64 `json:"id"` // 主键ID + Name string `json:"name"` // 岗位名称 + Status int `json:"status"` // 状态:1-正常 2-禁用 + Sort int `json:"sort"` // 排序 +} + +// PositionListRes 岗位列表结果 +type PositionListRes = []PositionListItem + +// PositionDetailReq 岗位详情请求 +type PositionDetailReq struct { + gmeta.Meta `method:"get" path:"/api/position/detail/:id" summary:"岗位详情" permission:"sys:position:detail" log:"岗位管理-查询详情" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"岗位ID"` // 岗位ID +} + +// PositionDetailRes 岗位详情结果 +type PositionDetailRes struct { + Id int64 `json:"id"` // 主键ID + Name string `json:"name"` // 岗位名称 + Status int `json:"status"` // 状态:1-正常 2-禁用 + StatusText string `json:"statusText"` // 状态文本 + Sort int `json:"sort"` // 排序 +} + +// PositionAddReq 新增岗位请求 +type PositionAddReq struct { + gmeta.Meta `method:"post" path:"/api/position/add" summary:"新增岗位" permission:"sys:position:add" log:"岗位管理-新增记录" logType:"add"` + Name string `json:"name" v:"required#请输入岗位名称" dc:"岗位名称"` // 岗位名称 + Status int `json:"status" dc:"状态:1-正常 2-禁用"` // 状态 + Sort int `json:"sort" dc:"排序"` // 排序 +} + +// PositionUpdateReq 修改岗位请求 +type PositionUpdateReq struct { + gmeta.Meta `method:"put" path:"/api/position/update" summary:"修改岗位" permission:"sys:position:update" log:"岗位管理-修改记录" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"岗位ID"` // 岗位ID + Name string `json:"name" v:"required#请输入岗位名称" dc:"岗位名称"` // 岗位名称 + Status int `json:"status" dc:"状态"` // 状态 + Sort int `json:"sort" dc:"排序"` // 排序 +} + +// PositionUpdateRes 修改岗位结果 +type PositionUpdateRes struct{} + +// PositionStatusReq 岗位状态请求 +type PositionStatusReq struct { + gmeta.Meta `method:"put" path:"/api/position/status" summary:"岗位状态" permission:"sys:position:status" log:"岗位管理-设置状态" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"岗位ID"` // 岗位ID + Status int `json:"status" v:"required#缺少状态" dc:"状态:1-正常 2-禁用"` // 状态 +} + +// PositionStatusRes 岗位状态结果 +type PositionStatusRes struct{} + +// PositionDeleteReq 删除岗位请求 +type PositionDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/position/delete/:id" summary:"删除岗位" permission:"sys:position:delete" log:"岗位管理-删除记录" logType:"delete"` + Id int64 `json:"id" v:"required#缺少ID" dc:"岗位ID"` // 岗位ID +} + +// PositionDeleteRes 删除岗位结果 +type PositionDeleteRes struct{} + +// PositionBatchDeleteReq 批量删除岗位请求 +type PositionBatchDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/position/batchDelete" summary:"批量删除岗位" permission:"sys:position:batchDelete" log:"岗位管理-批量删除记录" logType:"delete"` + Ids []int64 `json:"ids" v:"required#请选择要删除的记录" dc:"岗位ID列表"` // 岗位ID列表 +} + +// PositionBatchDeleteRes 批量删除结果 +type PositionBatchDeleteRes struct{} diff --git a/api/v1/role.go b/api/v1/role.go new file mode 100644 index 0000000..0c3a468 --- /dev/null +++ b/api/v1/role.go @@ -0,0 +1,193 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 角色模块 ============================== + +// RolePageReq 角色分页请求 +// 支持关键词、角色名称等条件组合查询 +type RolePageReq struct { + gmeta.Meta `method:"get" path:"/api/role/page" summary:"角色分页" permission:"sys:role:page" log:"角色管理-查询分页记录" logType:"query"` + PageReq // 分页基础参数 + Keyword string `json:"keyword" dc:"关键词(名称/编码模糊)"` // 关键词,名称或编码模糊匹配 + Name string `json:"name" dc:"角色名称模糊"` // 角色名称模糊匹配 +} + +// RoleItem 角色行数据 +// 用于角色分页列表等场景的角色基础信息 +type RoleItem struct { + Id int64 `json:"id"` // 主键ID + TenantId int `json:"tenantId"` // 租户ID + Name string `json:"name"` // 角色名称 + Code string `json:"code"` // 角色编码 + DataScope int `json:"dataScope"` // 数据范围:1-全部 2-本部门及以下 3-本部门 4-仅本人 + DataScopeText string `json:"dataScopeText"` // 数据范围文案(字典驱动) + Sort int `json:"sort"` // 排序 + Note string `json:"note"` // 备注 + UserCount int `json:"userCount"` // 关联用户数 + CreateUser string `json:"createUser"` // 创建人 + CreateTime *gtime.Time `json:"createTime"` // 创建时间 +} + +// RolePageRes 角色分页结果 +type RolePageRes = PageData[RoleItem] + +// RoleListReq 角色全量列表请求 +type RoleListReq struct { + gmeta.Meta `method:"get" path:"/api/role/list" summary:"角色列表" permission:"sys:role:list" log:"角色管理-查询列表" logType:"query"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 +} + +// RoleListItem 角色列表行数据 +// 精简结构,仅包含列表展示所需字段 +type RoleListItem struct { + Id int64 `json:"id"` // 主键ID + Name string `json:"name"` // 角色名称 + Code string `json:"code"` // 角色编码 + Sort int `json:"sort"` // 排序 +} + +// RoleListRes 角色列表结果 +type RoleListRes = []RoleListItem + +// RoleDetailReq 角色详情请求 +type RoleDetailReq struct { + gmeta.Meta `method:"get" path:"/api/role/detail/:id" summary:"角色详情" permission:"sys:role:detail" log:"角色管理-查询详情" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"角色ID"` // 角色ID +} + +// RoleDetailRes 角色详情结果 +type RoleDetailRes struct { + Id int64 `json:"id"` // 主键ID + TenantId int `json:"tenantId"` // 租户ID + Name string `json:"name"` // 角色名称 + Code string `json:"code"` // 角色编码 + DataScope int `json:"dataScope"` // 数据范围:1-全部 2-本部门及以下 3-本部门 4-仅本人 + Sort int `json:"sort"` // 排序 + Note string `json:"note"` // 备注 +} + +// RoleAddReq 新增角色请求 +type RoleAddReq struct { + gmeta.Meta `method:"post" path:"/api/role/add" summary:"新增角色" permission:"sys:role:add" log:"角色管理-新增记录" logType:"add"` + Name string `json:"name" v:"required#请输入角色名称" dc:"角色名称"` // 角色名称 + Code string `json:"code" v:"required#请输入角色编码|length:2,30#编码长度需在2~30位" dc:"角色编码"` // 角色编码 + DataScope int `json:"dataScope" v:"in:1,2,3,4#数据范围不合法" d:"1" dc:"数据范围:1-全部 2-本部门及以下 3-本部门 4-仅本人"` // 数据范围 + Sort int `json:"sort" dc:"排序"` // 排序 + Note string `json:"note" dc:"备注"` // 备注 +} + +// RoleUpdateReq 修改角色请求 +type RoleUpdateReq struct { + gmeta.Meta `method:"put" path:"/api/role/update" summary:"修改角色" permission:"sys:role:update" log:"角色管理-修改记录" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"角色ID"` // 角色ID + Name string `json:"name" v:"required#请输入角色名称" dc:"角色名称"` // 角色名称 + Code string `json:"code" dc:"角色编码(不允许修改)"` // 角色编码,不允许修改 + DataScope int `json:"dataScope" v:"in:1,2,3,4#数据范围不合法" dc:"数据范围:1-全部 2-本部门及以下 3-本部门 4-仅本人"` // 数据范围 + Sort int `json:"sort" dc:"排序"` // 排序 + Note string `json:"note" dc:"备注"` // 备注 +} + +// RoleUpdateRes 修改角色结果 +type RoleUpdateRes struct{} + +// RoleDeleteReq 删除角色请求 +type RoleDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/role/delete/:id" summary:"删除角色" permission:"sys:role:delete" log:"角色管理-删除记录" logType:"delete"` + Id int64 `json:"id" v:"required#缺少ID" dc:"角色ID"` // 角色ID +} + +// RoleDeleteRes 删除角色结果 +type RoleDeleteRes struct{} + +// RoleBatchDeleteReq 批量删除角色请求 +type RoleBatchDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/role/batchDelete" summary:"批量删除角色" permission:"sys:role:batchDelete" log:"角色管理-批量删除记录" logType:"delete"` + Ids []int64 `json:"ids" v:"required#请选择要删除的记录" dc:"角色ID列表"` // 角色ID列表 +} + +// RoleBatchDeleteRes 批量删除结果 +type RoleBatchDeleteRes struct{} + +// RoleMenuListReq 角色菜单权限请求 +// 返回菜单平铺列表并附带勾选状态 +type RoleMenuListReq struct { + gmeta.Meta `method:"get" path:"/api/role/menu/list/:roleId" summary:"查询角色菜单权限" permission:"sys:role:getPermission" log:"角色管理-查询角色菜单" logType:"query"` + RoleId int64 `json:"roleId" v:"required#缺少角色ID" dc:"角色ID"` // 角色ID +} + +// RoleMenuItem 菜单勾选行数据 +// 供前端构建树形结构并依据 checked 标记勾选状态 +type RoleMenuItem struct { + Id int64 `json:"id"` // 菜单ID + ParentId int `json:"parentId"` // 上级菜单ID + Name string `json:"name"` // 菜单名称 + Icon string `json:"icon"` // 图标 + Icon2 string `json:"icon2"` // 菜单图标2 + Path string `json:"path"` // 路由路径 + Component string `json:"component"` // 前端组件路径 + Permission string `json:"permission"` // 权限节点标识 + Target int `json:"target"` // 打开方式 + Type int `json:"type"` // 类型:0-菜单 1-权限节点 + Status int `json:"status"` // 状态 + Hide int `json:"hide"` // 是否可见 + Sort int `json:"sort"` // 排序 + Checked bool `json:"checked"` // 是否已勾选 +} + +// RoleMenuListRes 角色菜单勾选列表结果(平铺) +type RoleMenuListRes = []RoleMenuItem + +// RoleSavePermissionReq 保存角色菜单权限请求 +type RoleSavePermissionReq struct { + gmeta.Meta `method:"post" path:"/api/role/menu/save" summary:"保存角色菜单权限" permission:"sys:role:savePermission" log:"角色管理-保存权限" logType:"update"` + RoleId int64 `json:"roleId" v:"required#缺少角色ID" dc:"角色ID"` // 角色ID + MenuIds []int64 `json:"menuIds" dc:"菜单ID列表(含半选父级)"` // 菜单ID列表 +} + +// RoleSavePermissionRes 保存角色菜单权限结果 +type RoleSavePermissionRes struct{} diff --git a/api/v1/server_monitor.go b/api/v1/server_monitor.go new file mode 100644 index 0000000..c8cac64 --- /dev/null +++ b/api/v1/server_monitor.go @@ -0,0 +1,113 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 服务器监控模块 ============================== + +// ServerInfoReq 查询服务器信息请求 +type ServerInfoReq struct { + gmeta.Meta `method:"get" path:"/api/server/info" summary:"服务器监控信息" log:"服务器监控-查询信息" logType:"query"` +} + +// ServerOsInfo 操作系统与进程信息 +type ServerOsInfo struct { + Hostname string `json:"hostname"` // 主机名 + GoVersion string `json:"goVersion"` // Go 运行时版本 + Arch string `json:"arch"` // 架构 + NumCPU int `json:"numCpu"` // CPU 核数 + Goroutines int `json:"goroutines"` // 协程数 + Uptime string `json:"uptime"` // 进程运行时长 +} + +// ServerMemInfo 进程内存信息(MB) +type ServerMemInfo struct { + AllocMB float64 `json:"allocMb"` // 已分配 + TotalAllocMB float64 `json:"totalAllocMb"` // 累计分配 + SysMB float64 `json:"sysMb"` // 系统占用 + HeapAllocMB float64 `json:"heapAllocMb"` // 堆已分配 + HeapSysMB float64 `json:"heapSysMb"` // 堆系统占用 + HeapObjects uint64 `json:"heapObjects"` // 堆对象数 + GCCycles uint32 `json:"gcCycles"` // GC 次数 +} + +// ServerDiskInfo 磁盘分区信息 +type ServerDiskInfo struct { + Path string `json:"path"` // 路径(工作目录所在分区) + TotalGB float64 `json:"totalGb"` // 总容量(GB) + FreeGB float64 `json:"freeGb"` // 可用容量(GB) + UsedPercent float64 `json:"usedPercent"` // 使用率(%) +} + +// ServerDbInfo 数据库连通信息 +type ServerDbInfo struct { + Version string `json:"version"` // 数据库版本 + PingMs int64 `json:"pingMs"` // 连通耗时(毫秒) +} + +// ServerCacheBrief 缓存连通信息 +type ServerCacheBrief struct { + Mode string `json:"mode"` // redis / memory + Connected bool `json:"connected"` // 是否连通 +} + +// ServerSchedulerInfo 定时任务调度器状态 +type ServerSchedulerInfo struct { + Enabled bool `json:"enabled"` // 调度器是否已启动 + Healthy bool `json:"healthy"` // 是否健康(挂载数不小于应挂载数) + Mounted int `json:"mounted"` // 已挂载任务数 + Expected int `json:"expected"` // 应挂载任务数(status=1) + StartTime string `json:"startTime"` // 调度器启动时间 +} + +// ServerInfoRes 服务器监控信息结果 +type ServerInfoRes struct { + Server ServerOsInfo `json:"server"` // 操作系统与进程 + Mem ServerMemInfo `json:"mem"` // 内存 + Disk []ServerDiskInfo `json:"disk"` // 磁盘 + Db ServerDbInfo `json:"db"` // 数据库 + Cache ServerCacheBrief `json:"cache"` // 缓存 + Scheduler ServerSchedulerInfo `json:"scheduler"` // 定时任务调度器 +} diff --git a/api/v1/tenant.go b/api/v1/tenant.go new file mode 100644 index 0000000..0eaa4a2 --- /dev/null +++ b/api/v1/tenant.go @@ -0,0 +1,236 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 租户模块 ============================== + +// TenantItem 租户行数据 +// 用于租户分页列表等场景的租户基础信息 +type TenantItem struct { + Id int64 `json:"id"` // 主键ID + Name string `json:"name"` // 租户名称 + Code string `json:"code"` // 租户编码 + Contact string `json:"contact"` // 联系人 + Mobile string `json:"mobile"` // 联系电话 + Email string `json:"email"` // 邮箱 + Address string `json:"address"` // 地址 + Logo string `json:"logo"` // Logo + Status int `json:"status"` // 状态:1-正常 2-禁用 + StatusText string `json:"statusText"` // 状态文本 + ExpireTime *gtime.Time `json:"expireTime"` // 过期时间 + MaxUserNum int `json:"maxUserNum"` // 用户数上限(0=不限) + UserCount int `json:"userCount"` // 已用用户数 + Note string `json:"note"` // 备注 + CreateUser string `json:"createUser"` // 创建人 + CreateTime *gtime.Time `json:"createTime"` // 创建时间 +} + +// TenantPageReq 租户分页请求 +// 支持关键词、租户名称、租户编码、状态等条件组合查询 +type TenantPageReq struct { + gmeta.Meta `method:"get" path:"/api/tenant/page" summary:"租户分页" permission:"sys:tenant:page" log:"租户管理-查询分页记录" logType:"query"` + PageReq // 分页基础参数 + Keyword string `json:"keyword" dc:"关键词(名称/编码/联系人模糊)"` // 关键词,名称、编码或联系人模糊匹配 + Name string `json:"name" dc:"租户名称模糊"` // 租户名称模糊匹配 + Code string `json:"code" dc:"租户编码模糊"` // 租户编码模糊匹配 + Status int `json:"status" dc:"状态:1-正常 2-禁用"` // 状态 +} + +// TenantPageRes 租户分页结果 +type TenantPageRes = PageData[TenantItem] + +// TenantListReq 租户全量列表请求 +type TenantListReq struct { + gmeta.Meta `method:"get" path:"/api/tenant/list" summary:"租户列表" permission:"sys:tenant:list" log:"租户管理-查询列表" logType:"query"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 + Status int `json:"status" dc:"状态"` // 状态 +} + +// TenantListItem 租户列表行数据 +// 精简结构,仅包含列表展示所需字段 +type TenantListItem struct { + Id int64 `json:"id"` // 主键ID + Name string `json:"name"` // 租户名称 + Code string `json:"code"` // 租户编码 + Status int `json:"status"` // 状态:1-正常 2-禁用 +} + +// TenantListRes 租户列表结果 +type TenantListRes = []TenantListItem + +// TenantDetailReq 租户详情请求 +type TenantDetailReq struct { + gmeta.Meta `method:"get" path:"/api/tenant/detail/:id" summary:"租户详情" permission:"sys:tenant:detail" log:"租户管理-查询详情" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"租户ID"` // 租户ID +} + +// TenantDetailRes 租户详情结果 +type TenantDetailRes struct { + Id int64 `json:"id"` // 主键ID + Name string `json:"name"` // 租户名称 + Code string `json:"code"` // 租户编码 + Contact string `json:"contact"` // 联系人 + Mobile string `json:"mobile"` // 联系电话 + Email string `json:"email"` // 邮箱 + Address string `json:"address"` // 地址 + Logo string `json:"logo"` // Logo + Status int `json:"status"` // 状态:1-正常 2-禁用 + StatusText string `json:"statusText"` // 状态文本 + ExpireTime *gtime.Time `json:"expireTime"` // 过期时间 + MaxUserNum int `json:"maxUserNum"` // 用户数上限 + Note string `json:"note"` // 备注 +} + +// TenantAddReq 新增租户请求 +type TenantAddReq struct { + gmeta.Meta `method:"post" path:"/api/tenant/add" summary:"新增租户" permission:"sys:tenant:add" log:"租户管理-新增记录" logType:"add"` + Name string `json:"name" v:"required#请输入租户名称" dc:"租户名称"` // 租户名称 + Code string `json:"code" dc:"租户编码(留空自动生成)"` // 租户编码 + Contact string `json:"contact" dc:"联系人"` // 联系人 + Mobile string `json:"mobile" v:"phone#手机号格式不正确" dc:"联系电话"` // 联系电话 + Email string `json:"email" v:"email#邮箱格式不正确" dc:"邮箱"` // 邮箱 + Address string `json:"address" dc:"地址"` // 地址 + Logo string `json:"logo" dc:"Logo"` // Logo + Status int `json:"status" dc:"状态:1-正常 2-禁用"` // 状态 + ExpireTime string `json:"expireTime" dc:"过期时间(Y-m-d H:i:s,留空永久)"` // 过期时间 + MaxUserNum int `json:"maxUserNum" dc:"用户数上限(0=不限)"` // 用户数上限 + Note string `json:"note" dc:"备注"` // 备注 +} + +// TenantUpdateReq 修改租户请求 +type TenantUpdateReq struct { + gmeta.Meta `method:"put" path:"/api/tenant/update" summary:"修改租户" permission:"sys:tenant:update" log:"租户管理-修改记录" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"租户ID"` // 租户ID + Name string `json:"name" v:"required#请输入租户名称" dc:"租户名称"` // 租户名称 + Code string `json:"code" dc:"租户编码(不允许修改)"` // 租户编码,不允许修改 + Contact string `json:"contact" dc:"联系人"` // 联系人 + Mobile string `json:"mobile" v:"phone#手机号格式不正确" dc:"联系电话"` // 联系电话 + Email string `json:"email" v:"email#邮箱格式不正确" dc:"邮箱"` // 邮箱 + Address string `json:"address" dc:"地址"` // 地址 + Logo string `json:"logo" dc:"Logo"` // Logo + Status int `json:"status" dc:"状态"` // 状态 + ExpireTime string `json:"expireTime" dc:"过期时间"` // 过期时间 + MaxUserNum int `json:"maxUserNum" dc:"用户数上限"` // 用户数上限 + Note string `json:"note" dc:"备注"` // 备注 +} + +// TenantUpdateRes 修改租户结果 +type TenantUpdateRes struct{} + +// TenantStatusReq 租户状态请求 +type TenantStatusReq struct { + gmeta.Meta `method:"put" path:"/api/tenant/status" summary:"租户状态" permission:"sys:tenant:status" log:"租户管理-设置状态" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"租户ID"` // 租户ID + Status int `json:"status" v:"required#缺少状态" dc:"状态:1-正常 2-禁用"` // 状态 +} + +// TenantStatusRes 租户状态结果 +type TenantStatusRes struct{} + +// TenantDeleteReq 删除租户请求 +type TenantDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/tenant/delete/:id" summary:"删除租户" permission:"sys:tenant:delete" log:"租户管理-删除记录" logType:"delete"` + Id int64 `json:"id" v:"required#缺少ID" dc:"租户ID"` // 租户ID +} + +// TenantDeleteRes 删除租户结果 +type TenantDeleteRes struct{} + +// TenantBatchDeleteReq 批量删除租户请求 +type TenantBatchDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/tenant/batchDelete" summary:"批量删除租户" permission:"sys:tenant:batchDelete" log:"租户管理-批量删除记录" logType:"delete"` + Ids []int64 `json:"ids" v:"required#请选择要删除的记录" dc:"租户ID列表"` // 租户ID列表 +} + +// TenantBatchDeleteRes 批量删除结果 +type TenantBatchDeleteRes struct{} + +// TenantAccountReq 创建租户账号请求 +// 同时创建租户及其初始管理员账号 +type TenantAccountReq struct { + gmeta.Meta `method:"post" path:"/api/tenant/account" summary:"创建租户账号" permission:"sys:tenant:add" log:"租户管理-创建租户账号" logType:"add"` + Name string `json:"name" v:"required#请输入租户名称" dc:"租户名称"` // 租户名称 + Contact string `json:"contact" dc:"联系人"` // 联系人 + Mobile string `json:"mobile" v:"phone#手机号格式不正确" dc:"联系电话"` // 联系电话 + Email string `json:"email" v:"email#邮箱格式不正确" dc:"邮箱"` // 邮箱 + Address string `json:"address" dc:"地址"` // 地址 + ExpireTime string `json:"expireTime" dc:"过期时间"` // 过期时间 + MaxUserNum int `json:"maxUserNum" dc:"用户数上限"` // 用户数上限 + Note string `json:"note" dc:"备注"` // 备注 + Username string `json:"username" v:"required#请输入管理员账号|length:2,30#账号长度需在2~30位" dc:"初始管理员账号"` // 初始管理员账号 + Password string `json:"password" v:"required#请输入密码|length:6,32#密码长度需在6~32位" dc:"初始管理员密码"` // 初始管理员密码 + Realname string `json:"realname" dc:"管理员姓名"` // 管理员姓名 +} + +// TenantAccountRes 创建租户账号结果 +type TenantAccountRes struct { + TenantId int64 `json:"tenantId"` // 租户ID + Code string `json:"code"` // 租户编码 + UserId int64 `json:"userId"` // 管理员用户ID +} + +// TenantUsersReq 查询租户用户请求 +type TenantUsersReq struct { + gmeta.Meta `method:"get" path:"/api/tenant/users/:tenantId" summary:"查询租户用户" permission:"sys:tenant:users" log:"租户管理-查询租户用户" logType:"query"` + TenantId int64 `json:"tenantId" v:"required#缺少租户ID" dc:"租户ID"` // 租户ID +} + +// TenantUserItem 租户用户行数据 +// 精简结构,仅包含租户用户列表展示所需字段 +type TenantUserItem struct { + Id int64 `json:"id"` // 用户ID + Username string `json:"username"` // 用户名 + Realname string `json:"realname"` // 真实姓名 + Mobile string `json:"mobile"` // 手机号 + Email string `json:"email"` // 邮箱 + Status int `json:"status"` // 状态 +} + +// TenantUsersRes 查询租户用户结果 +type TenantUsersRes struct { + List []TenantUserItem `json:"list"` // 租户用户列表 +} diff --git a/api/v1/upload.go b/api/v1/upload.go new file mode 100644 index 0000000..ac3d986 --- /dev/null +++ b/api/v1/upload.go @@ -0,0 +1,81 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 文件上传模块(存储根目录见 file.uploadDir) ============================== + +// UploadFileReq 上传文件请求 +// 以 multipart 表单方式提交单个文件 +type UploadFileReq struct { + gmeta.Meta `method:"post" path:"/api/upload/uploadFile" summary:"上传文件" permission:"sys:upload:file" log:"文件上传-上传文件" logType:"add"` + File *ghttp.UploadFile `p:"file" v:"required#请选择文件" dc:"上传文件"` // 上传文件 + Type string `json:"type" dc:"类型:images/files/video(前端约定,可空)"` // 文件类型 +} + +// UploadFileRes 上传文件结果 +// 供前端编辑器、上传组件使用的文件信息 +type UploadFileRes struct { + OriginalName string `json:"originalName"` // 原始文件名 + FileExtension string `json:"fileExtension"` // 扩展名 + FileType string `json:"fileType"` // MIME 类型 + FileSize int64 `json:"fileSize"` // 文件大小(字节) + FileName string `json:"fileName"` // 存储文件名(basename,如 32位随机.png) + FilePath string `json:"filePath"` // 相对 uploadDir 的路径(入库形态) + FileUrl string `json:"fileUrl"` // 文件访问地址 +} + +// UploadFilesReq 批量上传文件请求 +// 以 multipart 表单方式提交多个文件 +type UploadFilesReq struct { + gmeta.Meta `method:"post" path:"/api/upload/uploadFiles" summary:"批量上传文件" permission:"sys:upload:files" log:"文件上传-批量上传" logType:"add"` + Files []*ghttp.UploadFile `p:"files" v:"required#请选择文件" dc:"上传文件列表"` // 上传文件列表 +} + +// UploadFilesRes 批量上传文件结果 +type UploadFilesRes struct { + List []UploadFileRes `json:"list"` // 上传文件列表 +} diff --git a/api/v1/user.go b/api/v1/user.go new file mode 100644 index 0000000..093278d --- /dev/null +++ b/api/v1/user.go @@ -0,0 +1,285 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package v1 + +import ( + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gmeta" +) + +// ============================== 用户模块 ============================== + +// UserPageReq 用户分页请求 +// 支持关键词、姓名、状态、部门、角色、职级、岗位、租户等条件组合查询 +type UserPageReq struct { + gmeta.Meta `method:"get" path:"/api/user/page" summary:"用户分页" permission:"sys:user:page" log:"用户管理-查询分页记录" logType:"query"` + PageReq // 分页基础参数 + Keyword string `json:"keyword" dc:"关键词(用户名/姓名/手机号模糊)"` // 关键词,用户名、姓名或手机号模糊匹配 + Realname string `json:"realname" dc:"姓名模糊"` // 姓名模糊匹配 + Status int `json:"status" dc:"状态:1-正常 2-禁用"` // 状态 + DeptId int `json:"deptId" dc:"部门ID"` // 部门ID + RoleId int `json:"roleId" dc:"角色ID"` // 角色ID + Role int `json:"role" dc:"角色ID(前端参数名兼容)"` // 角色ID,前端参数名兼容 + LevelId int `json:"levelId" dc:"职级ID"` // 职级ID + PositionId int `json:"positionId" dc:"岗位ID"` // 岗位ID + TenantId int `json:"tenantId" dc:"租户ID"` // 租户ID +} + +// UserRoleRef 用户角色引用 +// 仅包含角色ID与名称,用于用户详情中的角色输出 +type UserRoleRef struct { + Id int64 `json:"id"` // 角色ID + Name string `json:"name"` // 角色名称 +} + +// UserItem 用户行数据 +// 用于用户分页列表等场景的用户基础信息 +type UserItem struct { + Id int64 `json:"id"` // 主键ID + Username string `json:"username"` // 用户名 + Code string `json:"code"` // 用户编码 + Realname string `json:"realname"` // 真实姓名 + Avatar string `json:"avatar"` // 头像,已补全访问域名 + Gender int `json:"gender"` // 性别:1-男 2-女 3-保密 + GenderText string `json:"genderText"` // 性别文本 + Mobile string `json:"mobile"` // 手机号 + Email string `json:"email"` // 邮箱 + Roles []UserRoleRef `json:"roles"` // 角色引用数组 + City []string `json:"city"` // 省市区街道编码数组 + LevelName string `json:"levelName"` // 职级名称 + PositionName string `json:"positionName"` // 岗位名称 + DeptName string `json:"deptName"` // 部门名称 + Status int `json:"status"` // 状态:1-正常 2-禁用 + StatusText string `json:"statusText"` // 状态文本 + Sort int `json:"sort"` // 排序 + Note string `json:"note"` // 备注 + CreateUser string `json:"createUser"` // 创建人 + CreateTime *gtime.Time `json:"createTime"` // 创建时间 +} + +// UserPageRes 用户分页结果 +type UserPageRes = PageData[UserItem] + +// UserListReq 用户全量列表请求 +// 用于下拉选择场景,支持关键词与状态过滤 +type UserListReq struct { + gmeta.Meta `method:"get" path:"/api/user/list" summary:"用户列表" permission:"sys:user:list" log:"用户管理-查询列表" logType:"query"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 + Status int `json:"status" dc:"状态"` // 状态 +} + +// UserListItem 用户列表行数据 +// 精简结构,仅包含列表展示所需字段 +type UserListItem struct { + Id int64 `json:"id"` // 用户ID + Username string `json:"username"` // 用户名 + Realname string `json:"realname"` // 真实姓名 + Role string `json:"role"` // 角色 +} + +// UserListRes 用户列表结果 +type UserListRes = []UserListItem + +// UserDetailReq 用户详情请求 +type UserDetailReq struct { + gmeta.Meta `method:"get" path:"/api/user/detail/:id" summary:"用户详情" permission:"sys:user:detail" log:"用户管理-查询详情" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"用户ID"` // 用户ID +} + +// UserDetailRes 用户详情结果 +// 满足前端编辑表单契约:roles 为角色ID数组,city 为四级区划编码 +type UserDetailRes struct { + Id int64 `json:"id"` // 用户ID + Username string `json:"username"` // 用户名 + Realname string `json:"realname"` // 真实姓名 + Avatar string `json:"avatar"` // 头像 + Gender int `json:"gender"` // 性别:1-男 2-女 3-保密 + GenderText string `json:"genderText"` // 性别文本 + Mobile string `json:"mobile"` // 手机号 + Email string `json:"email"` // 邮箱 + Roles []int64 `json:"roles"` // 角色ID数组 + DeptId int `json:"deptId"` // 部门ID + LevelId int `json:"levelId"` // 职级ID + PositionId int `json:"positionId"` // 岗位ID + City []string `json:"city"` // 省市区街道编码数组 + CityInfo string `json:"cityInfo"` // 省市区信息 + Address string `json:"address"` // 详细地址 + Intro string `json:"intro"` // 个人简介 + Status int `json:"status"` // 状态:1-正常 2-禁用 + StatusText string `json:"statusText"` // 状态文本 + Sort int `json:"sort"` // 排序 + Note string `json:"note"` // 备注 +} + +// UserAddReq 新增用户请求 +type UserAddReq struct { + gmeta.Meta `method:"post" path:"/api/user/add" summary:"新增用户" permission:"sys:user:add" log:"用户管理-新增记录" logType:"add"` + Username string `json:"username" v:"required#请输入用户名|length:2,30#用户名长度需在2~30位" dc:"用户名"` // 用户名 + Password string `json:"password" v:"required#请输入密码|length:6,32#密码长度需在6~32位" dc:"密码"` // 密码 + Realname string `json:"realname" v:"required#请输入姓名" dc:"姓名"` // 真实姓名 + Gender int `json:"gender" dc:"性别:1-男 2-女 3-保密"` // 性别 + Mobile string `json:"mobile" v:"phone#手机号格式不正确" dc:"手机号"` // 手机号 + Email string `json:"email" v:"email#邮箱格式不正确" dc:"邮箱"` // 邮箱 + Roles []int64 `json:"roles" dc:"角色ID列表"` // 角色ID列表 + DeptId int `json:"deptId" dc:"部门ID"` // 部门ID + LevelId int `json:"levelId" dc:"职级ID"` // 职级ID + PositionId int `json:"positionId" dc:"岗位ID"` // 岗位ID + City []string `json:"city" dc:"省市区街道编码数组"` // 省市区街道编码数组 + CityInfo string `json:"cityInfo" dc:"省市区信息"` // 省市区信息 + Address string `json:"address" dc:"详细地址"` // 详细地址 + Avatar string `json:"avatar" dc:"头像"` // 头像 + Intro string `json:"intro" dc:"个人简介"` // 个人简介 + Status int `json:"status" dc:"状态:1-正常 2-禁用"` // 状态 + Sort int `json:"sort" dc:"排序"` // 排序 + Note string `json:"note" dc:"备注"` // 备注 +} + +// UserUpdateReq 修改用户请求 +type UserUpdateReq struct { + gmeta.Meta `method:"put" path:"/api/user/update" summary:"修改用户" permission:"sys:user:update" log:"用户管理-修改记录" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"用户ID"` // 用户ID + Username string `json:"username" v:"required#请输入用户名" dc:"用户名"` // 用户名 + Password string `json:"password" dc:"密码(留空不修改)"` // 密码,留空不修改 + Realname string `json:"realname" v:"required#请输入姓名" dc:"姓名"` // 真实姓名 + Gender int `json:"gender" dc:"性别"` // 性别 + Mobile string `json:"mobile" v:"phone#手机号格式不正确" dc:"手机号"` // 手机号 + Email string `json:"email" v:"email#邮箱格式不正确" dc:"邮箱"` // 邮箱 + Roles []int64 `json:"roles" dc:"角色ID列表"` // 角色ID列表 + DeptId int `json:"deptId" dc:"部门ID"` // 部门ID + LevelId int `json:"levelId" dc:"职级ID"` // 职级ID + PositionId int `json:"positionId" dc:"岗位ID"` // 岗位ID + City []string `json:"city" dc:"省市区街道编码数组"` // 省市区街道编码数组 + CityInfo string `json:"cityInfo" dc:"省市区信息"` // 省市区信息 + Address string `json:"address" dc:"详细地址"` // 详细地址 + Avatar string `json:"avatar" dc:"头像"` // 头像 + Intro string `json:"intro" dc:"个人简介"` // 个人简介 + Status int `json:"status" dc:"状态"` // 状态 + Sort int `json:"sort" dc:"排序"` // 排序 + Note string `json:"note" dc:"备注"` // 备注 +} + +// UserUpdateRes 修改用户结果 +type UserUpdateRes struct{} + +// UserStatusReq 用户状态请求 +type UserStatusReq struct { + gmeta.Meta `method:"put" path:"/api/user/status" summary:"用户状态" permission:"sys:user:status" log:"用户管理-设置状态" logType:"update"` + Id int64 `json:"id" v:"required#缺少ID" dc:"用户ID"` // 用户ID + Status int `json:"status" v:"required#缺少状态" dc:"状态:1-正常 2-禁用"` // 状态 +} + +// UserStatusRes 用户状态结果 +type UserStatusRes struct{} + +// UserResetPwdReq 重置用户密码请求 +type UserResetPwdReq struct { + gmeta.Meta `method:"put" path:"/api/user/resetPwd" summary:"重置用户密码" permission:"sys:user:resetPwd" log:"用户管理-重置密码" logType:"reset"` + UserId int64 `json:"userId" v:"required#缺少用户ID" dc:"用户ID"` // 用户ID + Password string `json:"password" v:"length:6,32#密码长度需在6~32位" dc:"新密码(为空时重置为系统默认密码 DEFAULT_PASSWORD)"` // 新密码 +} + +// UserResetPwdRes 重置用户密码结果 +type UserResetPwdRes struct{} + +// UserDeleteReq 删除用户请求 +type UserDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/user/delete/:id" summary:"删除用户" permission:"sys:user:delete" log:"用户管理-删除记录" logType:"delete"` + Id int64 `json:"id" v:"required#缺少ID" dc:"用户ID"` // 用户ID +} + +// UserDeleteRes 删除用户结果 +type UserDeleteRes struct{} + +// UserBatchDeleteReq 批量删除用户请求 +type UserBatchDeleteReq struct { + gmeta.Meta `method:"delete" path:"/api/user/batchDelete" summary:"批量删除用户" permission:"sys:user:batchDelete" log:"用户管理-批量删除记录" logType:"delete"` + Ids []int64 `json:"ids" v:"required#请选择要删除的记录" dc:"用户ID列表"` // 用户ID列表 +} + +// UserBatchDeleteRes 批量删除结果 +type UserBatchDeleteRes struct{} + +// UserImportReq 导入用户请求 +// 上传 xls/xlsx 文件进行数据导入 +type UserImportReq struct { + gmeta.Meta `method:"post" path:"/api/user/import" summary:"导入用户" permission:"sys:user:import" log:"用户管理-导入数据" logType:"import"` + File *ghttp.UploadFile `p:"file" v:"required#请选择文件" dc:"xls/xlsx 文件"` // 上传文件 +} + +// UserImportRes 导入结果 +// 返回导入总数、成功数、失败数及失败原因 +type UserImportRes struct { + Total int `json:"total"` // 导入总数 + Success int `json:"success"` // 成功数 + Failed int `json:"failed"` // 失败数 + Reasons []string `json:"reasons"` // 失败原因列表 +} + +// UserExportReq 导出用户请求 +// 按查询条件导出符合条件的记录 +type UserExportReq struct { + gmeta.Meta `method:"get" path:"/api/user/export" summary:"导出用户" permission:"sys:user:export" log:"用户管理-导出数据" logType:"export"` + Keyword string `json:"keyword" dc:"关键词"` // 关键词 + Realname string `json:"realname" dc:"姓名模糊"` // 姓名模糊匹配 + Status int `json:"status" dc:"状态"` // 状态 +} + +// UserExportRes 导出结果 +// data 直接为导出文件的访问地址 +type UserExportRes = string + +// UserDocumentReq 用户打印信息请求 +type UserDocumentReq struct { + gmeta.Meta `method:"get" path:"/api/user/document/:id" summary:"用户打印信息" permission:"sys:user:detail" log:"用户管理-查询打印信息" logType:"query"` + Id int64 `json:"id" v:"required#缺少ID" dc:"用户ID"` // 用户ID +} + +// UserDocumentRes 用户打印信息结果 +// 在用户详情基础上追加关联的角色、部门、职级、岗位名称 +type UserDocumentRes struct { + UserDetailRes + RoleNames []string `json:"roleNames"` // 角色名称列表 + DeptName string `json:"deptName"` // 部门名称 + LevelName string `json:"levelName"` // 职级名称 + PositionName string `json:"positionName"` // 岗位名称 +} diff --git a/cmd/generator/main.go b/cmd/generator/main.go new file mode 100644 index 0000000..bba7f0c --- /dev/null +++ b/cmd/generator/main.go @@ -0,0 +1,125 @@ +// Command generator 代码生成命令行 +// +// 用法(仓库根目录执行): +// +// go run ./cmd/generator -tables "goframe_order|订单表,goframe_sku|商品规格" # 一键生成(文件+菜单+字典+路由) +// go run ./cmd/generator -tables "goframe_order|订单表" -preview # 仅预览渲染结果 +// go run ./cmd/generator -tables "goframe_order|订单表" -batch # 批量模式逐表统计 +// +// 覆盖模式由配置 generator.overwrite 统一控制(true=生成即覆盖;false/缺省=缺失补生成、存在跳过)。 +// 生成后需手动执行:go build ./... +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gctx" + + _ "github.com/gogf/gf/contrib/drivers/mssql/v2" + _ "github.com/gogf/gf/contrib/drivers/mysql/v2" + _ "github.com/gogf/gf/contrib/drivers/oracle/v2" + _ "github.com/gogf/gf/contrib/drivers/pgsql/v2" + _ "github.com/gogf/gf/contrib/drivers/sqlite/v2" + + "easygoadmin/api/v1" + _ "easygoadmin/internal/logic" // 注册全部业务服务(init) + "easygoadmin/internal/service" +) + +// main 代码生成命令行入口 +// 解析命令行参数后,按预览、批量或一键生成模式执行代码生成 +func main() { + tables := flag.String("tables", "", "表列表,格式: 表名|表注释,表名|表注释(与前端一键生成入参一致)") + preview := flag.Bool("preview", false, "仅预览渲染结果,不写文件、不建菜单/字典/路由") + batch := flag.Bool("batch", false, "批量模式(逐表统计输出)") + flag.Parse() + + ctx := gctx.New() + + // 解析目标表列表,为空时打印用法并退出 + list := splitTables(*tables) + if len(list) == 0 { + flag.Usage() + fmt.Println("\n示例: go run ./cmd/generator -tables \"goframe_order|订单表\"") + os.Exit(2) + } + g.Log().Infof(ctx, "目标表 %d 张: %v", len(list), list) + + switch { + // 预览模式:仅渲染并输出文件清单,不落盘 + case *preview: + res, err := service.Generator().Preview(ctx, &v1.GeneratorPreviewReq{TableNames: list}) + if err != nil { + fatal("预览失败: %v", err) + } + for _, f := range res.List { + fmt.Printf("── %s (%d bytes)\n", f.Path, len(f.Content)) + } + fmt.Printf("预览完成:共 %d 个文件\n", len(res.List)) + + // 批量模式:逐表生成并输出各表统计结果 + case *batch: + res, err := service.Generator().BatchGenerate(ctx, &v1.GeneratorBatchGenerateReq{TableNames: list}) + if err != nil { + fatal("批量生成失败: %v", err) + } + for _, item := range res.List { + mark := "OK" + if !item.Success { + mark = "FAIL" + } + fmt.Printf("[%s] %s — %s | 菜单:%s | 路由:%s\n", + mark, item.Table, item.Message, item.Menu.Message, item.Route.Message) + for _, d := range item.Dict { + fmt.Printf(" 字典 %s: %s\n", d.Dict, d.Status) + } + } + + // 默认模式:一键生成文件、菜单、字典与路由 + default: + res, err := service.Generator().Generate(ctx, &v1.GeneratorGenerateReq{TableNames: list}) + if err != nil { + fatal("生成失败: %v", err) + } + for _, p := range res.Generated { + fmt.Println(" + 生成", p) + } + for _, p := range res.Skipped { + fmt.Println(" - 跳过", p) + } + for _, p := range res.Overwritten { + fmt.Println(" * 覆盖", p) + } + for _, p := range res.Failed { + fmt.Println(" ! 失败", p) + } + fmt.Println(" 菜单:", res.Menu.Message) + for _, d := range res.Dict { + fmt.Printf(" 字典 %s: %s\n", d.Dict, d.Status) + } + fmt.Println(" 路由:", res.Route.Message) + fmt.Println("下一步: go build ./... 并重启服务") + } +} + +// fatal 输出错误信息并终止进程 +func fatal(format string, args ...any) { + fmt.Fprintf(os.Stderr, format+"\n", args...) + os.Exit(1) +} + +// splitTables 解析表列表参数 +// 按逗号分隔并去除空白项,返回有效的表列表 +func splitTables(raw string) []string { + out := []string{} + for _, item := range strings.Split(raw, ",") { + if s := strings.TrimSpace(item); s != "" { + out = append(out, s) + } + } + return out +} diff --git a/cmd/migrator/main.go b/cmd/migrator/main.go new file mode 100644 index 0000000..77ea91f --- /dev/null +++ b/cmd/migrator/main.go @@ -0,0 +1,95 @@ +// Command migrator 整库迁移命令行 +// +// 用法(仓库根目录执行): +// +// go run ./cmd/migrator \ +// -src-type=mysql -src-host=127.0.0.1 -src-hostport=3306 \ +// -src-database=easygoadmin.goframe.elevue -src-username=root -src-password=root \ +// -dst-type=pgsql -dst-host=127.0.0.1 -dst-hostport=5432 \ +// -dst-database=easygoadmin.goframe.elevue -dst-username=postgres -dst-password=123456 \ +// -drop-target-first +// +// 可选:-tables "goframe_user,goframe_level"(只迁指定表)、-batch-size=1000(数据批大小) +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/gogf/gf/v2/os/gctx" + + _ "github.com/gogf/gf/contrib/drivers/mssql/v2" + _ "github.com/gogf/gf/contrib/drivers/mysql/v2" + _ "github.com/gogf/gf/contrib/drivers/oracle/v2" + _ "github.com/gogf/gf/contrib/drivers/pgsql/v2" + _ "github.com/gogf/gf/contrib/drivers/sqlite/v2" + + "easygoadmin/internal/migrate" +) + +func main() { + // 源库连接要素 + srcType := flag.String("src-type", "mysql", "源库类型:mysql/pgsql/sqlite/mssql/oracle") + srcHost := flag.String("src-host", "127.0.0.1", "源库地址") + srcPort := flag.String("src-hostport", "3306", "源库端口") + srcDB := flag.String("src-database", "", "源库库名(sqlite 填文件路径)") + srcUser := flag.String("src-username", "root", "源库用户名") + srcPass := flag.String("src-password", "", "源库密码") + // 目标库连接要素 + dstType := flag.String("dst-type", "pgsql", "目标库类型:mysql/pgsql/sqlite/mssql/oracle") + dstHost := flag.String("dst-host", "127.0.0.1", "目标库地址") + dstPort := flag.String("dst-hostport", "5432", "目标库端口") + dstDB := flag.String("dst-database", "", "目标库库名(sqlite 填文件路径)") + dstUser := flag.String("dst-username", "postgres", "目标库用户名") + dstPass := flag.String("dst-password", "", "目标库密码") + // 迁移行为 + dropFirst := flag.Bool("drop-target-first", false, "迁移前清空目标库全部表") + tables := flag.String("tables", "", "只迁指定表(逗号分隔,留空迁全部)") + batchSize := flag.Int("batch-size", 1000, "数据搬运批大小") + flag.Parse() + + if *srcDB == "" || *dstDB == "" { + flag.Usage() + fmt.Fprintln(os.Stderr, "\n示例: go run ./cmd/migrator -src-database=easygoadmin -dst-database=easygoadmin -drop-target-first") + os.Exit(2) + } + + ctx := gctx.New() + opt := migrate.Options{ + Src: migrate.DSN{ + Type: *srcType, Host: *srcHost, Port: *srcPort, + Database: *srcDB, Username: *srcUser, Password: *srcPass, + }, + Dst: migrate.DSN{ + Type: *dstType, Host: *dstHost, Port: *dstPort, + Database: *dstDB, Username: *dstUser, Password: *dstPass, + }, + DropTargetFirst: *dropFirst, + Tables: splitList(*tables), + BatchSize: *batchSize, + } + + _, err := migrate.Run(ctx, opt, func(format string, args ...any) { + fmt.Printf(format, args...) + }) + if err != nil { + fmt.Fprintf(os.Stderr, "\n迁移失败: %v\n", err) + os.Exit(1) + } +} + +// splitList 逗号分隔列表拆分(去空白) +func splitList(raw string) []string { + if strings.TrimSpace(raw) == "" { + return nil + } + out := make([]string, 0) + for _, item := range strings.Split(raw, ",") { + if s := strings.TrimSpace(item); s != "" { + out = append(out, s) + } + } + return out +} diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 0000000..adfecb3 --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,46 @@ +# ============================================================ +# Docker Compose 环境变量模板 +# 用法:复制为同目录 .env(同目录被 .gitignore 与 .dockerignore 双重排除) +# copy .env.example .env +# 说明:本文件使用 MYSQL_HOST 等友好名,由 docker-compose.yml 的 +# environment 块映射为 GF_ 规范键(如 GF_DATABASE_DEFAULT_LINK) +# 注入容器——应用只认 GF_ 前缀键,勿将友好名原样透传给应用 +# 注意:值中含 $ 须写成 $$(compose 变量插值会吞掉单 $, +# 如密码 abc#$^ 应写为 abc#$$^) +# ============================================================ + +# ===== 应用 ===== +APP_PORT=8000 + +# JWT 签名密钥(生产必改):要求 ≥32 字节密码学随机,禁止词典词/项目名 +# 生成方式(任选其一,生成后替换下方示例值): +# Linux 部署机: openssl rand -hex 32 +# 无 openssl 时: python3 -c "import secrets; print(secrets.token_hex(32))" +# Windows: [Convert]::ToBase64String([Security.Cryptography.RandomNumberGenerator]::GetBytes(32)) +# 生成示例:a3f8c2d91e4b7f05...(64 位十六进制,各环境独立生成、勿复用同一值) +# 注意:轮换此值后所有已签发令牌立即失效,用户需重新登录(安排发版窗口执行) +JWT_SECRET=please-change-this-jwt-secret + +# ===== MySQL ===== +# 数据库地址(容器网络内地址) +# 连 compose 自带 mysql 服务:保持默认服务名 mysql +# 连宿主机数据库:Mac/Windows Docker Desktop 用 host.docker.internal, +# Linux 用宿主机实际 IP 或 --add-host=host.docker.internal:host-gateway +# 注意:容器内 127.0.0.1 指向容器自身,连不上宿主机数据库 +MYSQL_HOST=mysql +# 数据库监听端口(进连接串;连外部库时填外部库端口) +# 注:自带 mysql/redis 服务的宿主映射端口已在 compose 写死(3306/6379), +# 仅供宿主机客户端直连调试,占用冲突时改 compose 的 ports +MYSQL_PORT=3306 +MYSQL_USER=root +MYSQL_PASSWORD=root +MYSQL_DATABASE=easygoadmin.goframe.elevue + +# ===== Redis ===== +# 同 MYSQL_HOST 规则;容器内 127.0.0.1 指向容器自身,连不上宿主机 Redis +REDIS_HOST=redis +# 连宿主机 Redis 时改为(Windows/Mac Docker Desktop): +# REDIS_HOST=host.docker.internal +# Redis 监听端口(进连接串;连外部库时填外部端口) +REDIS_PORT=6379 +REDIS_PASSWORD=123456 diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..285dc5f --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,97 @@ +# ============================================================ +# Dockerfile:EasyGoAdmin 后端镜像(多阶段构建) +# 构建命令(上下文为项目根目录,在项目根执行): +# docker build -f docker/Dockerfile -t easygoadmin:latest . +# +# 设计要点 +# - 纯 Go 项目(sqlite 驱动为 modernc 系,无 CGO),CGO_ENABLED=0 静态编译 +# - builder 只负责编译,runtime 仅含二进制 + 运行时资产,镜像最小化 +# - 运行时资产四件:manifest/config、manifest/data(ip2region.xdb)、 +# resource(字体/swagger)、templates(代码生成器模板) +# - 敏感配置不改镜像内文件,运行时用 GF_ 前缀环境变量覆盖 +# (见 manifest/config/config.yaml 头部说明) +# - 源码 COPY 放在依赖下载之后,保护 go mod 层缓存 +# +# 构建参数 +# VAR_PORT 应用监听端口 +# ============================================================ + + +# ------------------------------------------------------------ +# 全局构建参数 +# 在 FROM 之前声明,可供后续各阶段引用 +# ------------------------------------------------------------ +ARG VAR_GOLANG_IMAGE=golang:1.27-alpine +ARG VAR_RUNTIME_IMAGE=alpine:3.22 +ARG VAR_PORT=8000 + + +# ============================================================ +# 阶段一:builder —— 编译静态二进制 +# ============================================================ +FROM ${VAR_GOLANG_IMAGE} AS builder + +LABEL MAINTAINER="easygoadmin@163.com" + +# 国内代理:加速 go mod 下载(海外环境可改为 https://proxy.golang.org,direct) +ENV GOPROXY=https://goproxy.cn,direct \ + GO111MODULE=on \ + CGO_ENABLED=0 \ + GOOS=linux + +WORKDIR /build + +# 先拷依赖清单,利用层缓存:源码变更但 go.mod/go.sum 未变时不重复下载 +COPY go.mod go.sum ./ +RUN go mod download + +# 拷源码并编译(-trimpath 去除本地路径,-s -w 去除符号表与调试信息) +COPY . . +RUN go build -trimpath -ldflags "-s -w" -o /build/bin/admin-server main.go + + +# ============================================================ +# 阶段二:runtime —— 最终业务镜像 +# ============================================================ +FROM ${VAR_RUNTIME_IMAGE} AS runtime + +LABEL MAINTAINER="easygoadmin@163.com" + +# 时区(应用配置 app.timezone=Asia/Shanghai,日志与定时任务依赖系统时区) +ENV TZ=Asia/Shanghai + +# tzdata 提供时区数据库;ca-certificates 供 HTTPS 外呼(无外呼场景可去掉) +RUN set -eux; \ + sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories; \ + apk add --no-cache tzdata ca-certificates; \ + # 非 root 运行(低权限用户跑业务) + addgroup -g 1000 app && adduser -u 1000 -G app -s /sbin/nologin -D app; \ + ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime + +WORKDIR /opt/easygoadmin + +# 运行时资产(先于二进制 COPY,业务代码变动不破坏资产层缓存) +COPY --chown=app:app manifest/config ./manifest/config +COPY --chown=app:app manifest/data ./manifest/data +COPY --chown=app:app resource ./resource +COPY --chown=app:app templates ./templates + +# 二进制 +COPY --chown=app:app --from=builder /build/bin/admin-server ./bin/admin-server + +# 上传目录与日志目录:预建并授权,compose 挂载宿主卷时属主一致 +RUN mkdir -p /data/upload /opt/easygoadmin/log && \ + chown -R app:app /data/upload /opt/easygoadmin/log + +USER app + +# ARG 转 ENV,运行期与 compose/HEALTHCHECK 可读 +ENV VAR_PORT=${VAR_PORT} +EXPOSE ${VAR_PORT} + +# 健康检查(/api/health 为公开白名单路径,不带 /v1) +HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ + CMD wget -qO- "http://127.0.0.1:${VAR_PORT}/api/health" || exit 1 + +# 前台运行;优雅重启由框架 graceful 配置处理 +ENTRYPOINT ["./bin/admin-server"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..94d0d17 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,110 @@ +# +====================================================================== +# | 模块: Docker Compose 配置 +# | 说明: EasyGoAdmin 后端服务容器编排(后端 + MySQL + Redis) +# | 位置: 位于 docker/ 目录,宿主路径均以 ../ 相对项目根 +# | 启动(在 docker/ 目录执行): +# | copy .env.example .env # 首次,按需修改密钥 +# | docker compose up -d --build +# +====================================================================== + +services: + # ------------------------------------------------------------ + # 后端服务(GoFrame) + # ------------------------------------------------------------ + easygoadmin: + image: easygoadmin:latest + container_name: easygoadmin + build: + # 上下文为项目根目录(compose 文件所在目录的上一级) + context: .. + dockerfile: docker/Dockerfile + restart: always + ports: + - "${APP_PORT:-8000}:8000" + # 敏感/环境相关配置一律 GF_ 前缀覆盖,不改镜像内 config.yaml + # (GF_ 键名 = 配置路径大写下划线,如 file.uploadDir → GF_FILE_UPLOADDIR) + environment: + TZ: Asia/Shanghai + GF_DATABASE_DEFAULT_LINK: "mysql:${MYSQL_USER:-root}:${MYSQL_PASSWORD:-root}@tcp(${MYSQL_HOST:-mysql}:${MYSQL_PORT:-3306})/${MYSQL_DATABASE:-easygoadmin.goframe.elevue}?charset=utf8mb4&parseTime=true&loc=Local" + GF_REDIS_DEFAULT_ADDRESS: "${REDIS_HOST:-redis}:${REDIS_PORT:-6379}" + GF_REDIS_DEFAULT_PASS: "${REDIS_PASSWORD:-123456}" + GF_FILE_UPLOADDIR: "/data/upload" + GF_JWT_SECRET: "${JWT_SECRET:-please-change-this-jwt-secret}" + GF_CAPTCHA_BYPASSCODE: "" # 生产环境清空万能验证码 + GF_APP_ERRORMSG: "false" # 生产环境关闭详细错误信息 + volumes: + # 上传文件持久化(两段式上传的 temp 与正式目录都在 uploadDir 下) + - ../upload:/data/upload + # 日志目录持久化 + - ../log:/opt/easygoadmin/log + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + logging: + driver: "json-file" + options: + max-size: "500m" + max-file: "10" + + # ------------------------------------------------------------ + # MySQL 8(首次启动自动导入定稿库结构) + # ------------------------------------------------------------ + mysql: + image: mysql:8.0 + container_name: easygoadmin-mysql + restart: always + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD:-root} + MYSQL_DATABASE: ${MYSQL_DATABASE:-easygoadmin.goframe.elevue} + TZ: Asia/Shanghai + ports: + # 宿主映射端口写死(仅供宿主机客户端直连调试;宿主机 3306 被占用时改此处) + - "3306:3306" + volumes: + - mysql-data:/var/lib/mysql + # 首次初始化:导入定稿 SQL(仅空数据卷时执行一次) + - ../document/mysql/easygoadmin.goframe.elevue.sql:/docker-entrypoint-initdb.d/init.sql:ro + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + - --lower_case_table_names=1 + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_PASSWORD:-root}"] + interval: 10s + timeout: 5s + retries: 10 + logging: + driver: "json-file" + options: + max-size: "500m" + max-file: "10" + + # ------------------------------------------------------------ + # Redis(JWT黑名单/验证码/字典/身份缓存;宕机时应用自动降级内存) + # ------------------------------------------------------------ + redis: + image: redis:7-alpine + container_name: easygoadmin-redis + restart: always + command: ["redis-server", "--requirepass", "${REDIS_PASSWORD:-123456}", "--appendonly", "yes"] + ports: + # 宿主映射端口写死(仅供宿主机客户端直连调试;宿主机 6379 被占用时改此处) + - "6379:6379" + volumes: + - redis-data:/data + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-123456}", "ping"] + interval: 10s + timeout: 5s + retries: 10 + logging: + driver: "json-file" + options: + max-size: "500m" + max-file: "10" + +volumes: + mysql-data: + redis-data: diff --git a/document/.gitignore b/document/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/document/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4ee52aa --- /dev/null +++ b/go.mod @@ -0,0 +1,69 @@ +module easygoadmin + +go 1.27.2 + +require ( + github.com/gogf/gf/contrib/drivers/mssql/v2 v2.10.3 + github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.3 + github.com/gogf/gf/contrib/drivers/oracle/v2 v2.10.3 + github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.3 + github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.3 + github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.3 + github.com/gogf/gf/v2 v2.10.3 + github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20261008081544-02f04aa559ac + github.com/xuri/excelize/v2 v2.11.0 + golang.org/x/crypto v0.58.0 + golang.org/x/image v0.38.0 + golang.org/x/sys v0.49.0 +) + +require ( + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clbanning/mxj/v2 v2.7.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-sql-driver/mysql v1.7.1 // indirect + github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect + github.com/golang-sql/sqlexp v0.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/grokify/html-strip-tags-go v0.1.0 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/microsoft/go-mssqldb v1.7.1 // indirect + github.com/olekukonko/errors v1.1.0 // indirect + github.com/olekukonko/ll v0.0.9 // indirect + github.com/olekukonko/tablewriter v1.1.0 // indirect + github.com/redis/go-redis/v9 v9.12.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/richardlehane/mscfb v1.0.7 // indirect + github.com/richardlehane/msoleps v1.0.6 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/sijms/go-ora/v2 v2.7.10 // indirect + github.com/tiendc/go-deepcopy v1.7.2 // indirect + github.com/xuri/efp v0.0.1 // indirect + github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + golang.org/x/net v0.60.0 // indirect + golang.org/x/text v0.43.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c691134 --- /dev/null +++ b/go.sum @@ -0,0 +1,168 @@ +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ= +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/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA= +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/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.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= +github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/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/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/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU= +github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/gogf/gf/contrib/drivers/mssql/v2 v2.10.3 h1:IRoBZeBwl0+YF82ZT/Uw0wYbtEZz0PddZUan4BsciNY= +github.com/gogf/gf/contrib/drivers/mssql/v2 v2.10.3/go.mod h1:X9txuMimO0z0uMVI+n+YBv1fLpgjXo6EqzvUmizXwIQ= +github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.3 h1:GKe0qcxB0kCkrFDS+oXodHxT1SnRgqZ6dyIu1DMtNMA= +github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.3/go.mod h1:o9/0jHDXrmBDVGWq2hh84r0NYsMmv8W5gl3K8pOL7+s= +github.com/gogf/gf/contrib/drivers/oracle/v2 v2.10.3 h1:Sg9q07o/dqKhFwJIylAEGxNY0+Forvr512iOvV5C1HY= +github.com/gogf/gf/contrib/drivers/oracle/v2 v2.10.3/go.mod h1:Js5uPrzNh5VQV2dd1K6ZF9m/RWnjVOPp5wGUarD2ebw= +github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.3 h1:5OzgqO5rxNVBuUCI08MBtucE30U73onIZJ58noSBNgo= +github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.3/go.mod h1:mDsLu8SZbB1W6prrK/HvDYcqJrIuKmir5YOQL+eN5SY= +github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.3 h1:/lsdBYwdTAZxo3hdp8ON+daETjIwtwNv0V9eN0JBbFU= +github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.3/go.mod h1:B3lop3H1Lz3QD6yObUmd+7wwbfdVtE1LJdX+yznyrxw= +github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.3 h1:ayc2yhrBNZRit8o9veM994LpfkxPqAOjflA6x3dYh4k= +github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.3/go.mod h1:ZgywMX6mxmvzKWi/zTu0LrGUVN2MrcXEHZ+vyxq0qCI= +github.com/gogf/gf/v2 v2.10.3 h1:tZvEk+58x9uyuOAMgT5VV8yAPHGPBwJkPYRuXj2cfBI= +github.com/gogf/gf/v2 v2.10.3/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +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/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4= +github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20261008081544-02f04aa559ac h1:QzJvikmi/EVRBnoRfXJKs+DgS6NsdG1oQIZKXszGOJ8= +github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20261008081544-02f04aa559ac/go.mod h1:sj5LMpsqB4IWdwIrcmmBJM6m+rW/uOQLSGUPhKkqdh8= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +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-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microsoft/go-mssqldb v1.7.1 h1:KU/g8aWeM3Hx7IMOFpiwYiUkU+9zeISb4+tx3ScVfsM= +github.com/microsoft/go-mssqldb v1.7.1/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= +github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= +github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= +github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= +github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY= +github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.12.1 h1:k5iquqv27aBtnTm2tIkROUDp8JBXhXZIVu1InSgvovg= +github.com/redis/go-redis/v9 v9.12.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +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.7 h1:oeoiM0WE79vHwE8RpIYYvIAc8ajTH2mb6UZm55/+EB0= +github.com/richardlehane/mscfb v1.0.7/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo= +github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg= +github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sijms/go-ora/v2 v2.7.10 h1:GSLdj0PYYgSndhsnm7b6p32OqgnwnUZSkFb3j+htfhI= +github.com/sijms/go-ora/v2 v2.7.10/go.mod h1:EHxlY6x7y9HAsdfumurRfTd+v8NrEOTR3Xl4FWlH6xk= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44= +github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= +github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= +github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.11.0 h1:HxaEFl6sRN2+8J5a8HaKq+0M4FsjBGMnWWtjOCPSG88= +github.com/xuri/excelize/v2 v2.11.0/go.mod h1:jxFLbzaIwGQ5ufFNvYfUOHqXhfPaNmP14KWfmNz2Uak= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54= +golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= +golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE= +golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= diff --git a/hack/config.yaml b/hack/config.yaml new file mode 100644 index 0000000..850e34b --- /dev/null +++ b/hack/config.yaml @@ -0,0 +1,23 @@ +# ====================================================================== +# gf gen 代码生成配置(键名已对照 gf v2.10.3 gendao/genservice 源码核实) +# 用法:gf gen dao -c hack/config.yaml && gf gen service -c hack/config.yaml +# ====================================================================== +gfcli: + gen: + dao: + link: "mysql:root:root@tcp(127.0.0.1:3306)/easygoadmin.goframe.elevue?charset=utf8mb4" + removePrefix: "goframe_" # goframe_user → User/UserDao + group: "default" + jsonCase: "CamelLower" + descriptionTag: true # 列注释写入 entity + overwriteDao: false # 保留已定制的 dao 扩展文件 + tplDaoIndexPath: "hack/tpl/dao_index.tpl" # 自定义 dao 扩展模板(挂载全局钩子) + # 生成路径(相对 path) + path: "internal" + daoPath: "dao" + doPath: "model/do" + entityPath: "model/entity" + service: + srcFolder: "internal/logic" # 扫描 sUser/sRole…(默认 stPattern ^s([A-Z]\w+)$) + dstFolder: "internal/service" + clear: false # gf v2.10.3 Windows 下 clear 逻辑路径分隔符不匹配,会误删刚生成的文件,故关闭 diff --git a/hack/tpl/dao_index.tpl b/hack/tpl/dao_index.tpl new file mode 100644 index 0000000..c7a039e --- /dev/null +++ b/hack/tpl/dao_index.tpl @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package {{.TplPackageName}} + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "{{.TplImportPrefix}}/internal" + "easygoadmin/internal/model/hooks" +) + +// {{.TplTableNameCamelLowerCase}}Dao is the data access object for the table {{.TplTableName}}. +// You can define custom methods on it to extend its functionality as needed. +type {{.TplTableNameCamelLowerCase}}Dao struct { + *internal.{{.TplTableNameCamelCase}}Dao +} + +var ( + // {{.TplTableNameCamelCase}} is a globally accessible object for table {{.TplTableName}} operations. + {{.TplTableNameCamelCase}} = {{.TplTableNameCamelLowerCase}}Dao{ + internal.New{{.TplTableNameCamelCase}}Dao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *{{.TplTableNameCamelLowerCase}}Dao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.{{.TplTableNameCamelCase}}Dao.Ctx(ctx), "{{.TplTableName}}") +} + +// Add your custom methods and functionality below. diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go new file mode 100644 index 0000000..43a45a1 --- /dev/null +++ b/internal/cmd/cmd.go @@ -0,0 +1,100 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package cmd + +import ( + "context" + "time" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gcron" + + "easygoadmin/internal/logic" + "easygoadmin/internal/router" + "easygoadmin/utility/cache" + "easygoadmin/utility/config" + + // 空白导入:触发定时任务处理器 init 自注册 + _ "easygoadmin/internal/task" +) + +// Run 服务启动入口 +// +// 启动流程: +// 1. 缓存初始化(Redis 探测,失败降级内存) +// 2. 中间件依赖接线(身份/租户加载器、日志落地器,由业务模块注册) +// 3. 注册中间件链与全部路由 +// 4. 阻塞运行 HTTP 服务 +func Run(ctx context.Context) { + // 配置环境变量覆盖接线(GF_ 前缀环境变量 > 配置文件,12-Factor 优先级) + // 须最先执行:缓存/数据库/Redis 初始化都依赖配置读取 + config.RegisterEnvOverride() + + // 缓存初始化(JWT黑名单/验证码/字典/身份缓存) + cache.Init(ctx) + + // 依赖接线(业务模块通过 init 注册,见 internal/logic 各包) + wireDependencies(ctx) + + // 创建服务实例,注册中间件链与全部路由 + s := g.Server("easygoadmin") + router.Register(s) + + g.Log().Info(ctx, "中间件链: CORS → Response → RateLimit → Demo → JWT → Tenant → OperationLog → Controller") + + // 定时任务调度器:延时确保 HTTP 监听就绪后,加载 status=1 的任务 + go func() { + time.Sleep(2 * time.Second) + logic.DaemonInit(ctx) + }() + + // 上传临时目录清理:每日 03:30 清理超过 file.tempExpireDays 天的 temp/日期 目录 + _, _ = gcron.AddSingleton(ctx, "0 30 3 * * *", func(c context.Context) { + days := g.Cfg().MustGet(c, "file.tempExpireDays").Int() + if n := logic.CleanTemp(c, days); n > 0 { + g.Log().Infof(c, "临时文件清理完成:删除 %d 个过期目录(保留 %d 天)", n, days) + } + }, "upload_temp_clean") + + // 阻塞运行 HTTP 服务 + s.Run() +} diff --git a/internal/cmd/wire.go b/internal/cmd/wire.go new file mode 100644 index 0000000..f848540 --- /dev/null +++ b/internal/cmd/wire.go @@ -0,0 +1,76 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package cmd + +import ( + "context" + + "github.com/gogf/gf/v2/frame/g" + + "easygoadmin/internal/middleware" +) + +// ctxLog 启动期提示 +// 以警告级别输出启动阶段的提示信息,便于排查依赖接线缺失 +func ctxLog(ctx context.Context, msg string) { + g.Log().Warning(ctx, msg) +} + +// wireDependencies 中间件依赖接线 +// +// 各业务包在 init() 中通过 SetXxx 完成注册(见 internal/logic/auth、internal/logic/system), +// 此处仅做启动期校验与日志。 +func wireDependencies(ctx context.Context) { + // 由业务包 init 注册: + // middleware.SetIdentityLoader(authlogic.LoadIdentity) // internal/logic/auth + // middleware.SetTenantLoader(systemlogic.LoadTenant) // internal/logic/system + // middleware.SetLogSink(monitorlogic.SaveOperationLog) // internal/logic/monitor + if middleware.IdentityLoader == nil { + ctxLog(ctx, "IdentityLoader 未注册(认证模块尚未接入)") + } + if middleware.TenantLoader == nil { + ctxLog(ctx, "TenantLoader 未注册(租户校验将跳过)") + } + if middleware.LogSink == nil { + ctxLog(ctx, "LogSink 未注册(操作日志仅输出到调试日志)") + } +} diff --git a/internal/consts/biz_err.go b/internal/consts/biz_err.go new file mode 100644 index 0000000..bb3f99d --- /dev/null +++ b/internal/consts/biz_err.go @@ -0,0 +1,90 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package consts + +import ( + "github.com/gogf/gf/v2/errors/gcode" +) + +// ============================== 业务错误码 ============================== + +// 业务错误码定义 +// +// 响应层统一映射:401 保持原样(前端跳登录),其余非 0 错误码一律降级为 1 +// (前端 ResultEnum.ERROR=1 才会弹出 msg 提示),故本文件的码值仅用于日志与语义区分。 +var ( + CodeFailed = gcode.New(1, "操作失败", nil) // 通用操作失败 + CodeUnauthorized = gcode.New(401, "登录已过期,请重新登录", nil) // 未认证 + CodeForbidden = gcode.New(403, "没有该操作权限", nil) // 无权限 + CodeParamError = gcode.New(400, "参数校验失败", nil) // 参数校验失败 + CodeRecordNotFound = gcode.New(1010, "记录不存在", nil) // 记录不存在 + CodeRecordExists = gcode.New(1009, "记录已存在", nil) // 记录已存在 + CodeServerError = gcode.New(500, "服务器内部错误", nil) // 服务器内部错误 + CodeTooManyRequests = gcode.New(429, "请求过于频繁,请稍后再试", nil) // 限流 + + // 认证与账户 + CodeUserNotFound = gcode.New(1001, "用户不存在", nil) // 用户不存在 + CodeUserDisabled = gcode.New(1002, "账号已被禁用", nil) // 账号已被禁用 + CodePasswordWrong = gcode.New(1003, "密码错误", nil) // 密码错误 + CodeCaptchaError = gcode.New(1004, "验证码错误或已过期", nil) // 验证码错误或已过期 + CodeTokenInvalid = gcode.New(1008, "Token无效或已失效", nil) // Token无效或已失效 + CodeOldPwdWrong = gcode.New(1012, "原密码错误", nil) // 原密码错误 + + // 租户 + CodeTenantExpired = gcode.New(1005, "租户已过期", nil) // 租户已过期 + CodeTenantDisabled = gcode.New(1006, "租户已禁用", nil) // 租户已禁用 + CodeTenantUserLimit = gcode.New(1007, "租户用户数已达上限", nil) // 租户用户数已达上限 + CodeTenantNotFound = gcode.New(1013, "租户不存在", nil) // 租户不存在 + + // 业务约束 + CodeHasChildren = gcode.New(1011, "存在下级或关联记录,不能删除", nil) // 存在下级或关联记录 + CodeInUse = gcode.New(1014, "记录已被引用,不能删除", nil) // 记录已被引用 + CodeDemoMode = gcode.New(1015, "演示环境,禁止操作!", nil) // 演示环境禁止操作 +) + +// RoleCodeSuperAdmin 超级管理员角色编码 +// 与初始化数据中超级管理员角色的 code 保持一致 +const RoleCodeSuperAdmin = "ROLE_SUPER_ADMIN" + +// PermWildcard 超级管理员通配权限码 +// 前端识别 *:*:* 后放开全部按钮权限 +const PermWildcard = "*:*:*" diff --git a/internal/consts/cache_key.go b/internal/consts/cache_key.go new file mode 100644 index 0000000..50d7290 --- /dev/null +++ b/internal/consts/cache_key.go @@ -0,0 +1,100 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package consts + +import "fmt" + +// ============================== 缓存键 ============================== + +// CacheKeyPrefix Redis缓存键统一前缀 +const CacheKeyPrefix = "easygoadmin:" + +// CacheKeyTokenBlacklist Token黑名单缓存键 +// TTL 为 token 剩余有效期 +func CacheKeyTokenBlacklist(jti string) string { + return fmt.Sprintf("%stoken:blacklist:%s", CacheKeyPrefix, jti) +} + +// CacheKeyOnlineJtiSet 在线令牌有序集合键(score=访问令牌过期时间戳) +func CacheKeyOnlineJtiSet() string { + return fmt.Sprintf("%sonline:jti:set", CacheKeyPrefix) +} + +// CacheKeyOnlineInfo 在线令牌详情键(value=在线信息 JSON) +func CacheKeyOnlineInfo(jti string) string { + return fmt.Sprintf("%sonline:info:%s", CacheKeyPrefix, jti) +} + +// CacheKeyCaptcha 图形验证码缓存键 +// TTL 为 captcha.expire 配置值 +func CacheKeyCaptcha(uuid string) string { + return fmt.Sprintf("%scaptcha:%s", CacheKeyPrefix, uuid) +} + +// CacheKeyDictItem 字典项集合缓存键 +// 按字典编码缓存 +func CacheKeyDictItem(code string) string { + return fmt.Sprintf("%sdict:item:%s", CacheKeyPrefix, code) +} + +// CacheKeyTenantInfo 租户信息缓存键 +// TTL 为 60 秒,用于状态与过期校验热路径 +func CacheKeyTenantInfo(tenantID int) string { + return fmt.Sprintf("%stenant:info:%d", CacheKeyPrefix, tenantID) +} + +// CacheKeyUserPerms 用户权限点集合缓存键 +// 角色变更或菜单变更时删除 +func CacheKeyUserPerms(userID int) string { + return fmt.Sprintf("%suser:perms:%d", CacheKeyPrefix, userID) +} + +// CacheKeyUserRoles 用户角色编码集合缓存键 +func CacheKeyUserRoles(userID int) string { + return fmt.Sprintf("%suser:roles:%d", CacheKeyPrefix, userID) +} + +// CacheKeyUserIdentity 用户身份缓存键 +// 由 roles 与 perms 合并为 JSON,TTL 为 300 秒,权限变更时删除 +func CacheKeyUserIdentity(userID int) string { + return fmt.Sprintf("%suser:identity:%d", CacheKeyPrefix, userID) +} diff --git a/internal/consts/context_key.go b/internal/consts/context_key.go new file mode 100644 index 0000000..643242d --- /dev/null +++ b/internal/consts/context_key.go @@ -0,0 +1,288 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package consts + +import ( + "context" + "time" + + "github.com/gogf/gf/v2/frame/g" +) + +// ============================== 请求上下文键与读写工具 ============================== + +// contextKey 上下文键私有类型 +// 使用私有类型避免跨包键名冲突 +type contextKey string + +// 请求上下文键 +// 由 JWT、租户中间件写入,供全链路读取 +const ( + KeyUserID contextKey = "userID" // 当前登录用户ID + KeyTenantID contextKey = "tenantID" // 当前租户ID(0=平台级) + KeyUsername contextKey = "username" // 当前登录用户名 + KeyRealname contextKey = "realname" // 当前登录用户姓名 + KeyRoles contextKey = "roles" // 角色编码集合 []string + KeyPerms contextKey = "perms" // 权限节点集合 []string,如 sys:user:page + KeyTokenJTI contextKey = "tokenJTI" // 当前令牌 jti + KeyTokenExp contextKey = "tokenExpire" // 令牌过期时间 time.Time,用于登出黑名单TTL + KeyDataScope contextKey = "dataScope" // 角色数据范围(多角色取最宽松),见 DataScopeXxx 常量 + KeyDeptID contextKey = "deptID" // 当前登录用户部门ID(0=未分配) + + // 请求元信息,由 Response 中间件写入,供登录日志、操作日志读取 + KeyClientIP contextKey = "clientIP" // 客户端IP + KeyUserAgent contextKey = "userAgent" // User-Agent + KeyReqMethod contextKey = "reqMethod" // HTTP方法 + KeyReqURL contextKey = "reqURL" // 请求URL +) + +// WithUserID 写入当前登录用户ID +func WithUserID(ctx context.Context, id int) context.Context { + return context.WithValue(ctx, KeyUserID, id) +} + +// GetUserID 读取当前登录用户ID +// 未登录时返回 0 +func GetUserID(ctx context.Context) int { + v, _ := ctx.Value(KeyUserID).(int) + return v +} + +// WithTenantID 写入当前租户ID +// 0 表示平台级 +func WithTenantID(ctx context.Context, id int) context.Context { + return context.WithValue(ctx, KeyTenantID, id) +} + +// CtxTenantID 读取上下文原始租户ID +// +// 未登录时返回 0(平台级)。仅供租户隔离设施使用(超管判断、查询过滤、中间件校验), +// 业务模块取归属租户请使用 GetTenantID。 +func CtxTenantID(ctx context.Context) int { + v, _ := ctx.Value(KeyTenantID).(int) + return v +} + +// DefaultTenantID 默认归属租户ID +// +// 超管新增租户级数据时使用。读取配置 tenant.defaultId,未配置或非法值(≤0)回退 1。 +func DefaultTenantID(ctx context.Context) int { + v := g.Cfg().MustGet(ctx, "tenant.defaultId").Int() + if v <= 0 { + return 1 + } + return v +} + +// GetTenantID 读取当前上下文的归属租户ID +// +// 租户用户返回自身租户ID,超管返回默认租户ID。 +// 业务模块新增、导入、唯一校验等取归属租户统一使用本方法。 +func GetTenantID(ctx context.Context) int { + if t := CtxTenantID(ctx); t > 0 { + return t + } + return DefaultTenantID(ctx) +} + +// IsPlatform 是否平台级租户 +// 上下文租户ID为0时表示平台级(超管) +func IsPlatform(ctx context.Context) bool { + return CtxTenantID(ctx) == 0 +} + +// WithUsername 写入当前登录用户名 +func WithUsername(ctx context.Context, username string) context.Context { + return context.WithValue(ctx, KeyUsername, username) +} + +// GetUsername 读取当前登录用户名 +func GetUsername(ctx context.Context) string { + v, _ := ctx.Value(KeyUsername).(string) + return v +} + +// WithRealname 写入当前登录用户姓名 +func WithRealname(ctx context.Context, realname string) context.Context { + return context.WithValue(ctx, KeyRealname, realname) +} + +// GetRealname 读取当前登录用户姓名 +// 审计字段 create_user 优先使用本方法 +func GetRealname(ctx context.Context) string { + v, _ := ctx.Value(KeyRealname).(string) + return v +} + +// WithRoles 写入当前用户角色编码 +func WithRoles(ctx context.Context, roles []string) context.Context { + return context.WithValue(ctx, KeyRoles, roles) +} + +// GetRoles 读取当前用户角色编码 +func GetRoles(ctx context.Context) []string { + v, _ := ctx.Value(KeyRoles).([]string) + return v +} + +// WithPerms 写入当前用户权限节点 +func WithPerms(ctx context.Context, perms []string) context.Context { + return context.WithValue(ctx, KeyPerms, perms) +} + +// GetPerms 读取当前用户权限节点 +func GetPerms(ctx context.Context) []string { + v, _ := ctx.Value(KeyPerms).([]string) + return v +} + +// HasPerm 校验权限节点 +// 权限节点为空视为无需校验;超级管理员拥有全部权限 +func HasPerm(ctx context.Context, permission string) bool { + if permission == "" { + return true + } + for _, r := range GetRoles(ctx) { + if r == RoleCodeSuperAdmin { + return true + } + } + for _, p := range GetPerms(ctx) { + if p == permission { + return true + } + } + return false +} + +// 数据范围档位(角色 data_scope 列值;数值越小可见面越宽) +const ( + DataScopeAll = 1 // 全部(租户隔离之内) + DataScopeDeptAndBelow = 2 // 本部门及以下 + DataScopeDept = 3 // 本部门 + DataScopeSelf = 4 // 仅本人 +) + +// WithDataScope 写入当前用户数据范围 +func WithDataScope(ctx context.Context, scope int) context.Context { + return context.WithValue(ctx, KeyDataScope, scope) +} + +// GetDataScope 读取当前用户数据范围 +// 未注入时返回 DataScopeAll(视为全量可见) +func GetDataScope(ctx context.Context) int { + v, _ := ctx.Value(KeyDataScope).(int) + if v <= 0 { + return DataScopeAll + } + return v +} + +// WithDeptID 写入当前登录用户部门ID +func WithDeptID(ctx context.Context, id int) context.Context { + return context.WithValue(ctx, KeyDeptID, id) +} + +// GetDeptID 读取当前登录用户部门ID +// 未分配或未登录时返回 0 +func GetDeptID(ctx context.Context) int { + v, _ := ctx.Value(KeyDeptID).(int) + return v +} + +// WithTokenJTI 写入当前令牌 jti +// 登出时依据该值将令牌加入黑名单 +func WithTokenJTI(ctx context.Context, jti string) context.Context { + return context.WithValue(ctx, KeyTokenJTI, jti) +} + +// GetTokenJTI 读取当前令牌 jti +func GetTokenJTI(ctx context.Context) string { + v, _ := ctx.Value(KeyTokenJTI).(string) + return v +} + +// WithTokenExpire 写入当前令牌过期时间 +// 黑名单 TTL 取令牌剩余有效期 +func WithTokenExpire(ctx context.Context, t time.Time) context.Context { + return context.WithValue(ctx, KeyTokenExp, t) +} + +// GetTokenExpire 读取当前令牌过期时间 +func GetTokenExpire(ctx context.Context) time.Time { + v, _ := ctx.Value(KeyTokenExp).(time.Time) + return v +} + +// WithRequestMeta 写入请求元信息 +// 由 Response 中间件调用 +func WithRequestMeta(ctx context.Context, ip, userAgent, method, url string) context.Context { + ctx = context.WithValue(ctx, KeyClientIP, ip) + ctx = context.WithValue(ctx, KeyUserAgent, userAgent) + ctx = context.WithValue(ctx, KeyReqMethod, method) + ctx = context.WithValue(ctx, KeyReqURL, url) + return ctx +} + +// GetClientIP 读取客户端IP +func GetClientIP(ctx context.Context) string { + v, _ := ctx.Value(KeyClientIP).(string) + return v +} + +// GetUserAgent 读取 User-Agent +func GetUserAgent(ctx context.Context) string { + v, _ := ctx.Value(KeyUserAgent).(string) + return v +} + +// GetReqMethod 读取HTTP方法 +func GetReqMethod(ctx context.Context) string { + v, _ := ctx.Value(KeyReqMethod).(string) + return v +} + +// GetReqURL 读取请求URL +func GetReqURL(ctx context.Context) string { + v, _ := ctx.Value(KeyReqURL).(string) + return v +} diff --git a/internal/consts/log_type.go b/internal/consts/log_type.go new file mode 100644 index 0000000..b7a9b31 --- /dev/null +++ b/internal/consts/log_type.go @@ -0,0 +1,155 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package consts + +// ============================== 日志类型与来源 ============================== + +// 操作日志类型 +// +// 对应操作日志表 type 列以及 gmeta logType 标签。 +// 数值定义勿随意改动(前端操作日志类型按此映射显示)。 +const ( + LogTypeAdd = 1 // 新增 + LogTypeUpdate = 2 // 修改 + LogTypeDelete = 3 // 删除 + LogTypeQuery = 4 // 查询 + LogTypeImport = 5 // 导入 + LogTypeExport = 6 // 导出 + LogTypeApprove = 7 // 审批 + LogTypeReject = 8 // 驳回 + LogTypeSubmit = 9 // 提交 + LogTypeWithdraw = 10 // 撤回 + LogTypeClose = 11 // 关闭 + LogTypeOpen = 12 // 开启 + LogTypeEnable = 13 // 启用 + LogTypeDisable = 14 // 禁用 + LogTypeUpload = 15 // 上传 + LogTypeDownload = 16 // 下载 + LogTypeSend = 17 // 发送 + LogTypeSync = 18 // 同步 + LogTypeGenerate = 19 // 代码生成 + LogTypeClear = 20 // 清空 + LogTypeRestore = 21 // 还原 + LogTypeLogin = 22 // 登录 + LogTypeLogout = 23 // 登出 + LogTypeReset = 24 // 重置 + LogTypeAssign = 25 // 分配 + LogTypeAuthorize = 26 // 授权 + LogTypeExecute = 27 // 执行 + LogTypePause = 28 // 暂停 + LogTypeResume = 29 // 恢复 + LogTypePublish = 30 // 发布 + LogTypeUnpublish = 31 // 下架 + LogTypeCopy = 32 // 复制 + LogTypeMove = 33 // 移动 + LogTypeSort = 34 // 排序 + LogTypeLock = 35 // 锁定 + LogTypeUnlock = 36 // 解锁 + LogTypeArchive = 37 // 归档 + LogTypeUnarchive = 38 // 取消归档 + LogTypeBind = 39 // 绑定 + LogTypeUnbind = 40 // 解绑 + LogTypeOther = 99 // 其他 + LogTypeAuto = 0 // 自动推断:由中间件按 HTTP 方法判定(GET→查询,其余→修改) +) + +// LogTypeNames 操作日志类型值到中文名的映射 +// 供日志展示使用 +var LogTypeNames = map[int]string{ + LogTypeAdd: "新增", LogTypeUpdate: "修改", LogTypeDelete: "删除", LogTypeQuery: "查询", + LogTypeImport: "导入", LogTypeExport: "导出", LogTypeApprove: "审批", LogTypeReject: "驳回", + LogTypeSubmit: "提交", LogTypeWithdraw: "撤回", LogTypeClose: "关闭", LogTypeOpen: "开启", + LogTypeEnable: "启用", LogTypeDisable: "禁用", LogTypeUpload: "上传", LogTypeDownload: "下载", + LogTypeSend: "发送", LogTypeSync: "同步", LogTypeGenerate: "代码生成", LogTypeClear: "清空", + LogTypeRestore: "还原", LogTypeLogin: "登录", LogTypeLogout: "登出", LogTypeReset: "重置", + LogTypeAssign: "分配", LogTypeAuthorize: "授权", LogTypeExecute: "执行", LogTypePause: "暂停", + LogTypeResume: "恢复", LogTypePublish: "发布", LogTypeUnpublish: "下架", LogTypeCopy: "复制", + LogTypeMove: "移动", LogTypeSort: "排序", LogTypeLock: "锁定", LogTypeUnlock: "解锁", + LogTypeArchive: "归档", LogTypeUnarchive: "取消归档", LogTypeBind: "绑定", LogTypeUnbind: "解绑", + LogTypeOther: "其他", +} + +// LogTypeMap gmeta logType 标签字符串到枚举值的映射 +// 空字符串映射为自动推断 +var LogTypeMap = map[string]int{ + "add": LogTypeAdd, + "update": LogTypeUpdate, + "delete": LogTypeDelete, + "query": LogTypeQuery, + "import": LogTypeImport, + "export": LogTypeExport, + "approve": LogTypeApprove, + "reject": LogTypeReject, + "submit": LogTypeSubmit, + "withdraw": LogTypeWithdraw, + "enable": LogTypeEnable, + "disable": LogTypeDisable, + "upload": LogTypeUpload, + "download": LogTypeDownload, + "generate": LogTypeGenerate, + "login": LogTypeLogin, + "logout": LogTypeLogout, + "reset": LogTypeReset, + "assign": LogTypeAssign, + "authorize": LogTypeAuthorize, + "execute": LogTypeExecute, + "pause": LogTypePause, + "resume": LogTypeResume, + "publish": LogTypePublish, + "sort": LogTypeSort, + "other": LogTypeOther, + "": LogTypeAuto, +} + +// 登录日志类型 +// 对应登录日志表 log_type 列 +const ( + LoginLogTypeLogin = 1 // 登录 + LoginLogTypeLogout = 2 // 退出 +) + +// 登录来源 +// 对应登录日志表 source 列 +const ( + LoginSourceAdmin = 0 // 后台 + LoginSourceFront = 1 // 前台 +) diff --git a/internal/controller/article.go b/internal/controller/article.go new file mode 100644 index 0000000..06ce611 --- /dev/null +++ b/internal/controller/article.go @@ -0,0 +1,103 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 文章控制器 ============================== + +// CArticle 文章控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.Article() 处理 +type CArticle struct{} + +// Page 文章分页 +// 按查询条件分页返回文章列表 +func (c *CArticle) Page(ctx context.Context, req *v1.ArticlePageReq) (res *v1.ArticlePageRes, err error) { + return service.Article().Page(ctx, req) +} + +// List 文章列表 +// 返回符合条件的文章精简列表,供下拉或最新展示使用 +func (c *CArticle) List(ctx context.Context, req *v1.ArticleListReq) (res v1.ArticleListRes, err error) { + return service.Article().List(ctx, req) +} + +// Detail 文章详情 +// 按文章ID查询详情 +func (c *CArticle) Detail(ctx context.Context, req *v1.ArticleDetailReq) (res *v1.ArticleDetailRes, err error) { + return service.Article().Detail(ctx, req) +} + +// Add 新增文章 +// 新增成功后返回新记录ID +func (c *CArticle) Add(ctx context.Context, req *v1.ArticleAddReq) (res *v1.IdRes, err error) { + return service.Article().Add(ctx, req) +} + +// Update 修改文章 +// 按文章ID更新记录 +func (c *CArticle) Update(ctx context.Context, req *v1.ArticleUpdateReq) (res *v1.ArticleUpdateRes, err error) { + return service.Article().Update(ctx, req) +} + +// Status 文章状态 +// 设置文章的草稿或发布状态 +func (c *CArticle) Status(ctx context.Context, req *v1.ArticleStatusReq) (res *v1.ArticleStatusRes, err error) { + return service.Article().Status(ctx, req) +} + +// Delete 删除文章 +// 按文章ID删除单条记录 +func (c *CArticle) Delete(ctx context.Context, req *v1.ArticleDeleteReq) (res *v1.ArticleDeleteRes, err error) { + return service.Article().Delete(ctx, req) +} + +// BatchDelete 批量删除文章 +// 按文章ID列表批量删除记录 +func (c *CArticle) BatchDelete(ctx context.Context, req *v1.ArticleBatchDeleteReq) (res *v1.ArticleBatchDeleteRes, err error) { + return service.Article().BatchDelete(ctx, req) +} diff --git a/internal/controller/auth.go b/internal/controller/auth.go new file mode 100644 index 0000000..5cfc83c --- /dev/null +++ b/internal/controller/auth.go @@ -0,0 +1,91 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 认证控制器 ============================== + +// CAuth 认证控制器 +// 薄层控制器,参数解析、权限与日志切面由 router.Bind 统一处理 +type CAuth struct{} + +// Login 登录 +// 校验用户名、密码及验证码,成功后签发访问令牌与刷新令牌 +func (c *CAuth) Login(ctx context.Context, req *v1.LoginReq) (res *v1.LoginRes, err error) { + return service.Auth().Login(ctx, req) +} + +// Logout 登出 +// 将当前访问令牌加入黑名单,使其立即失效 +func (c *CAuth) Logout(ctx context.Context, req *v1.LogoutReq) (res *v1.LogoutRes, err error) { + return service.Auth().Logout(ctx, req) +} + +// Captcha 图形验证码 +// 生成图形验证码并返回验证码标识与图片内容 +func (c *CAuth) Captcha(ctx context.Context, req *v1.CaptchaReq) (res *v1.CaptchaRes, err error) { + return service.Auth().Captcha(ctx, req) +} + +// Refresh 刷新令牌 +// 支持 refresh_token 刷新与 password 密码模式两种授权方式 +func (c *CAuth) Refresh(ctx context.Context, req *v1.RefreshReq) (res *v1.RefreshRes, err error) { + return service.Auth().Refresh(ctx, req) +} + +// CheckToken 校验令牌 +// 校验令牌有效性并返回用户ID +func (c *CAuth) CheckToken(ctx context.Context, req *v1.CheckTokenReq) (res *v1.CheckTokenRes, err error) { + return service.Auth().CheckToken(ctx, req) +} + +// RemoveToken 撤销指定令牌 +// 将指定令牌加入黑名单,实现踢下线 +func (c *CAuth) RemoveToken(ctx context.Context, req *v1.RemoveTokenReq) (res *v1.RemoveTokenRes, err error) { + return service.Auth().RemoveToken(ctx, req) +} diff --git a/internal/controller/cache_monitor.go b/internal/controller/cache_monitor.go new file mode 100644 index 0000000..639a624 --- /dev/null +++ b/internal/controller/cache_monitor.go @@ -0,0 +1,59 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 缓存监控控制器 ============================== + +// CCacheMonitor 缓存监控控制器 +type CCacheMonitor struct{} + +// Info 缓存监控信息 +func (c *CCacheMonitor) Info(ctx context.Context, req *v1.CacheInfoReq) (res *v1.CacheInfoRes, err error) { + return service.CacheMonitor().Info(ctx, req) +} diff --git a/internal/controller/category.go b/internal/controller/category.go new file mode 100644 index 0000000..1fc368c --- /dev/null +++ b/internal/controller/category.go @@ -0,0 +1,97 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 分类控制器 ============================== + +// CCategory 分类控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.Category() 处理 +type CCategory struct{} + +// Page 分类分页 +// 按查询条件分页返回平铺的分类列表 +func (c *CCategory) Page(ctx context.Context, req *v1.CategoryPageReq) (res *v1.CategoryPageRes, err error) { + return service.Category().Page(ctx, req) +} + +// List 分类平铺列表 +// 返回分类平铺列表,由前端自行构建树形结构 +func (c *CCategory) List(ctx context.Context, req *v1.CategoryListReq) (res v1.CategoryListRes, err error) { + return service.Category().List(ctx, req) +} + +// Detail 分类详情 +// 按分类ID查询详情 +func (c *CCategory) Detail(ctx context.Context, req *v1.CategoryDetailReq) (res *v1.CategoryDetailRes, err error) { + return service.Category().Detail(ctx, req) +} + +// Add 新增分类 +// 新增成功后返回新记录ID +func (c *CCategory) Add(ctx context.Context, req *v1.CategoryAddReq) (res *v1.IdRes, err error) { + return service.Category().Add(ctx, req) +} + +// Update 修改分类 +// 按分类ID更新记录 +func (c *CCategory) Update(ctx context.Context, req *v1.CategoryUpdateReq) (res *v1.CategoryUpdateRes, err error) { + return service.Category().Update(ctx, req) +} + +// Delete 删除分类 +// 按分类ID删除单条记录 +func (c *CCategory) Delete(ctx context.Context, req *v1.CategoryDeleteReq) (res *v1.CategoryDeleteRes, err error) { + return service.Category().Delete(ctx, req) +} + +// BatchDelete 批量删除分类 +// 按分类ID列表批量删除记录 +func (c *CCategory) BatchDelete(ctx context.Context, req *v1.CategoryBatchDeleteReq) (res *v1.CategoryBatchDeleteRes, err error) { + return service.Category().BatchDelete(ctx, req) +} diff --git a/internal/controller/city.go b/internal/controller/city.go new file mode 100644 index 0000000..3737940 --- /dev/null +++ b/internal/controller/city.go @@ -0,0 +1,115 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 城市控制器 ============================== + +// CCity 城市控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.City() 处理 +type CCity struct{} + +// Page 城市分页 +// 按查询条件分页返回城市列表 +func (c *CCity) Page(ctx context.Context, req *v1.CityPageReq) (res *v1.CityPageRes, err error) { + return service.City().Page(ctx, req) +} + +// List 城市列表 +// 按父级编码、层级等条件返回城市列表,供级联选择使用 +func (c *CCity) List(ctx context.Context, req *v1.CityListReq) (res v1.CityListRes, err error) { + return service.City().List(ctx, req) +} + +// Detail 城市详情 +// 按城市ID查询详情 +func (c *CCity) Detail(ctx context.Context, req *v1.CityDetailReq) (res *v1.CityDetailRes, err error) { + return service.City().Detail(ctx, req) +} + +// Add 新增城市 +// 新增成功后返回新记录ID +func (c *CCity) Add(ctx context.Context, req *v1.CityAddReq) (res *v1.IdRes, err error) { + return service.City().Add(ctx, req) +} + +// Update 修改城市 +// 按城市ID更新记录 +func (c *CCity) Update(ctx context.Context, req *v1.CityUpdateReq) (res *v1.CityUpdateRes, err error) { + return service.City().Update(ctx, req) +} + +// Delete 删除城市 +// 按城市ID删除单条记录 +func (c *CCity) Delete(ctx context.Context, req *v1.CityDeleteReq) (res *v1.CityDeleteRes, err error) { + return service.City().Delete(ctx, req) +} + +// BatchDelete 批量删除城市 +// 按城市ID列表批量删除记录 +func (c *CCity) BatchDelete(ctx context.Context, req *v1.CityBatchDeleteReq) (res *v1.CityBatchDeleteRes, err error) { + return service.City().BatchDelete(ctx, req) +} + +// ByParent 按父编码查询下级城市 +// 登录用户即可访问,返回指定父级下的直属下级城市 +func (c *CCity) ByParent(ctx context.Context, req *v1.CityByParentReq) (res *v1.CityByParentRes, err error) { + return service.City().ByParent(ctx, req) +} + +// GetCityList 按父编码获取城市列表 +// 登录用户即可访问,供级联选择器懒加载下级城市 +func (c *CCity) GetCityList(ctx context.Context, req *v1.CityGetListReq) (res v1.CityGetListRes, err error) { + return service.City().GetCityList(ctx, req) +} + +// AreaProvince 省份列表 +// 登录用户即可访问,返回顶级省份列表 +func (c *CCity) AreaProvince(ctx context.Context, req *v1.AreaParentReq) (res *v1.AreaParentRes, err error) { + return service.City().AreaProvince(ctx, req) +} diff --git a/internal/controller/config.go b/internal/controller/config.go new file mode 100644 index 0000000..970f502 --- /dev/null +++ b/internal/controller/config.go @@ -0,0 +1,109 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 配置控制器 ============================== + +// CConfig 配置控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.Config() 处理 +type CConfig struct{} + +// Page 配置分页 +// 按查询条件分页返回配置列表 +func (c *CConfig) Page(ctx context.Context, req *v1.ConfigPageReq) (res *v1.ConfigPageRes, err error) { + return service.Config().Page(ctx, req) +} + +// List 配置列表 +// 返回配置列表,每项携带其下的配置项集合 +func (c *CConfig) List(ctx context.Context, req *v1.ConfigListReq) (res v1.ConfigListRes, err error) { + return service.Config().List(ctx, req) +} + +// Detail 配置详情 +// 按配置ID查询详情及其下的配置项集合 +func (c *CConfig) Detail(ctx context.Context, req *v1.ConfigDetailReq) (res *v1.ConfigDetailRes, err error) { + return service.Config().Detail(ctx, req) +} + +// Add 新增配置 +// 新增成功后返回新记录ID +func (c *CConfig) Add(ctx context.Context, req *v1.ConfigAddReq) (res *v1.IdRes, err error) { + return service.Config().Add(ctx, req) +} + +// Update 修改配置 +// 按配置ID更新记录 +func (c *CConfig) Update(ctx context.Context, req *v1.ConfigUpdateReq) (res *v1.ConfigUpdateRes, err error) { + return service.Config().Update(ctx, req) +} + +// Delete 删除配置 +// 按配置ID删除单条记录 +func (c *CConfig) Delete(ctx context.Context, req *v1.ConfigDeleteReq) (res *v1.ConfigDeleteRes, err error) { + return service.Config().Delete(ctx, req) +} + +// BatchDelete 批量删除配置 +// 按配置ID列表批量删除记录 +func (c *CConfig) BatchDelete(ctx context.Context, req *v1.ConfigBatchDeleteReq) (res *v1.ConfigBatchDeleteRes, err error) { + return service.Config().BatchDelete(ctx, req) +} + +// WebIndex 获取网站配置 +// 返回配置分组及其下的配置项,供网站配置页渲染 +func (c *CConfig) WebIndex(ctx context.Context, req *v1.ConfigWebIndexReq) (res *v1.ConfigWebIndexRes, err error) { + return service.Config().WebIndex(ctx, req) +} + +// WebSave 保存网站配置 +// 接收配置项编码到值的平铺映射并批量保存 +func (c *CConfig) WebSave(ctx context.Context, req *v1.ConfigWebSaveReq) (res *v1.ConfigWebSaveRes, err error) { + return service.Config().WebSave(ctx, req) +} diff --git a/internal/controller/config_item.go b/internal/controller/config_item.go new file mode 100644 index 0000000..e1772f7 --- /dev/null +++ b/internal/controller/config_item.go @@ -0,0 +1,97 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 配置项控制器 ============================== + +// CConfigItem 配置项控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.ConfigItem() 处理 +type CConfigItem struct{} + +// Page 配置项分页 +// 按查询条件分页返回配置项列表 +func (c *CConfigItem) Page(ctx context.Context, req *v1.ConfigItemPageReq) (res *v1.ConfigItemPageRes, err error) { + return service.ConfigItem().Page(ctx, req) +} + +// List 配置项列表 +// 按所属配置、状态等条件返回配置项集合 +func (c *CConfigItem) List(ctx context.Context, req *v1.ConfigItemListReq) (res v1.ConfigItemListRes, err error) { + return service.ConfigItem().List(ctx, req) +} + +// Detail 配置项详情 +// 按配置项ID查询详情及其值列表 +func (c *CConfigItem) Detail(ctx context.Context, req *v1.ConfigItemDetailReq) (res *v1.ConfigItemDetailRes, err error) { + return service.ConfigItem().Detail(ctx, req) +} + +// Add 新增配置项 +// 新增成功后返回新记录ID +func (c *CConfigItem) Add(ctx context.Context, req *v1.ConfigItemAddReq) (res *v1.IdRes, err error) { + return service.ConfigItem().Add(ctx, req) +} + +// Update 修改配置项 +// 按配置项ID更新记录 +func (c *CConfigItem) Update(ctx context.Context, req *v1.ConfigItemUpdateReq) (res *v1.ConfigItemUpdateRes, err error) { + return service.ConfigItem().Update(ctx, req) +} + +// Delete 删除配置项 +// 按配置项ID删除单条记录 +func (c *CConfigItem) Delete(ctx context.Context, req *v1.ConfigItemDeleteReq) (res *v1.ConfigItemDeleteRes, err error) { + return service.ConfigItem().Delete(ctx, req) +} + +// BatchDelete 批量删除配置项 +// 按配置项ID列表批量删除记录 +func (c *CConfigItem) BatchDelete(ctx context.Context, req *v1.ConfigItemBatchDeleteReq) (res *v1.ConfigItemBatchDeleteRes, err error) { + return service.ConfigItem().BatchDelete(ctx, req) +} diff --git a/internal/controller/dept.go b/internal/controller/dept.go new file mode 100644 index 0000000..ab24c1f --- /dev/null +++ b/internal/controller/dept.go @@ -0,0 +1,97 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 部门控制器 ============================== + +// CDept 部门控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.Dept() 处理 +type CDept struct{} + +// Page 部门分页 +// 按查询条件分页返回部门列表 +func (c *CDept) Page(ctx context.Context, req *v1.DeptPageReq) (res *v1.DeptPageRes, err error) { + return service.Dept().Page(ctx, req) +} + +// List 部门平铺列表 +// 返回部门平铺列表,由前端自行构建树形结构 +func (c *CDept) List(ctx context.Context, req *v1.DeptListReq) (res v1.DeptListRes, err error) { + return service.Dept().List(ctx, req) +} + +// Detail 部门详情 +// 按部门ID查询详情 +func (c *CDept) Detail(ctx context.Context, req *v1.DeptDetailReq) (res *v1.DeptDetailRes, err error) { + return service.Dept().Detail(ctx, req) +} + +// Add 新增部门 +// 新增成功后返回新记录ID +func (c *CDept) Add(ctx context.Context, req *v1.DeptAddReq) (res *v1.IdRes, err error) { + return service.Dept().Add(ctx, req) +} + +// Update 修改部门 +// 按部门ID更新记录 +func (c *CDept) Update(ctx context.Context, req *v1.DeptUpdateReq) (res *v1.DeptUpdateRes, err error) { + return service.Dept().Update(ctx, req) +} + +// Delete 删除部门 +// 按部门ID删除单条记录 +func (c *CDept) Delete(ctx context.Context, req *v1.DeptDeleteReq) (res *v1.DeptDeleteRes, err error) { + return service.Dept().Delete(ctx, req) +} + +// BatchDelete 批量删除部门 +// 按部门ID列表批量删除记录 +func (c *CDept) BatchDelete(ctx context.Context, req *v1.DeptBatchDeleteReq) (res *v1.DeptBatchDeleteRes, err error) { + return service.Dept().BatchDelete(ctx, req) +} diff --git a/internal/controller/dict.go b/internal/controller/dict.go new file mode 100644 index 0000000..0f168be --- /dev/null +++ b/internal/controller/dict.go @@ -0,0 +1,109 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 字典控制器 ============================== + +// CDict 字典控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.Dict() 处理 +type CDict struct{} + +// Page 字典分页 +// 按查询条件分页返回字典列表 +func (c *CDict) Page(ctx context.Context, req *v1.DictPageReq) (res *v1.DictPageRes, err error) { + return service.Dict().Page(ctx, req) +} + +// Detail 字典详情 +// 按字典ID查询详情及其下的字典项集合 +func (c *CDict) Detail(ctx context.Context, req *v1.DictDetailReq) (res *v1.DictDetailRes, err error) { + return service.Dict().Detail(ctx, req) +} + +// Add 新增字典 +// 新增成功后返回新记录ID +func (c *CDict) Add(ctx context.Context, req *v1.DictAddReq) (res *v1.IdRes, err error) { + return service.Dict().Add(ctx, req) +} + +// Update 修改字典 +// 按字典ID更新记录 +func (c *CDict) Update(ctx context.Context, req *v1.DictUpdateReq) (res *v1.DictUpdateRes, err error) { + return service.Dict().Update(ctx, req) +} + +// Delete 删除字典 +// 按字典ID删除单条记录 +func (c *CDict) Delete(ctx context.Context, req *v1.DictDeleteReq) (res *v1.DictDeleteRes, err error) { + return service.Dict().Delete(ctx, req) +} + +// BatchDelete 批量删除字典 +// 按字典ID列表批量删除记录 +func (c *CDict) BatchDelete(ctx context.Context, req *v1.DictBatchDeleteReq) (res *v1.DictBatchDeleteRes, err error) { + return service.Dict().BatchDelete(ctx, req) +} + +// RefreshCache 刷新字典缓存 +// 按字典编码刷新缓存,编码为空时刷新全部 +func (c *CDict) RefreshCache(ctx context.Context, req *v1.DictRefreshCacheReq) (res *v1.DictRefreshCacheRes, err error) { + return service.Dict().RefreshCache(ctx, req) +} + +// AliasList 字典别名列表 +// 登录用户即可访问,供前端字典查询场景使用 +func (c *CDict) AliasList(ctx context.Context, req *v1.DictAliasListReq) (res v1.DictAliasListRes, err error) { + return service.Dict().AliasList(ctx, req) +} + +// AliasInfo 字典别名详情 +// 登录用户即可访问,按字典编码查询字典及其字典项 +func (c *CDict) AliasInfo(ctx context.Context, req *v1.DictAliasInfoReq) (res *v1.DictAliasInfoRes, err error) { + return service.Dict().AliasInfo(ctx, req) +} diff --git a/internal/controller/dict_item.go b/internal/controller/dict_item.go new file mode 100644 index 0000000..4ff6057 --- /dev/null +++ b/internal/controller/dict_item.go @@ -0,0 +1,97 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 字典项控制器 ============================== + +// CDictItem 字典项控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.DictItem() 处理 +type CDictItem struct{} + +// Page 字典项分页 +// 按查询条件分页返回字典项列表 +func (c *CDictItem) Page(ctx context.Context, req *v1.DictItemPageReq) (res *v1.DictItemPageRes, err error) { + return service.DictItem().Page(ctx, req) +} + +// Detail 字典项详情 +// 按字典项ID查询详情 +func (c *CDictItem) Detail(ctx context.Context, req *v1.DictItemDetailReq) (res *v1.DictItemDetailRes, err error) { + return service.DictItem().Detail(ctx, req) +} + +// Add 新增字典项 +// 新增成功后返回新记录ID +func (c *CDictItem) Add(ctx context.Context, req *v1.DictItemAddReq) (res *v1.IdRes, err error) { + return service.DictItem().Add(ctx, req) +} + +// Update 修改字典项 +// 按字典项ID更新记录 +func (c *CDictItem) Update(ctx context.Context, req *v1.DictItemUpdateReq) (res *v1.DictItemUpdateRes, err error) { + return service.DictItem().Update(ctx, req) +} + +// Delete 删除字典项 +// 按字典项ID删除单条记录 +func (c *CDictItem) Delete(ctx context.Context, req *v1.DictItemDeleteReq) (res *v1.DictItemDeleteRes, err error) { + return service.DictItem().Delete(ctx, req) +} + +// BatchDelete 批量删除字典项 +// 按字典项ID列表批量删除记录 +func (c *CDictItem) BatchDelete(ctx context.Context, req *v1.DictItemBatchDeleteReq) (res *v1.DictItemBatchDeleteRes, err error) { + return service.DictItem().BatchDelete(ctx, req) +} + +// QuickList 快捷字典项列表 +// 登录用户即可访问,按字典编码查询并走缓存 +func (c *CDictItem) QuickList(ctx context.Context, req *v1.DictItemQuickListReq) (res v1.DictItemQuickListRes, err error) { + return service.DictItem().QuickList(ctx, req) +} diff --git a/internal/controller/example.go b/internal/controller/example.go new file mode 100644 index 0000000..fb1e7e9 --- /dev/null +++ b/internal/controller/example.go @@ -0,0 +1,115 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 案例演示控制器 ============================== + +// CExample 案例演示控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.Example() 处理 +type CExample struct{} + +// Page 案例演示分页 +// 按查询条件分页返回案例演示列表 +func (c *CExample) Page(ctx context.Context, req *v1.ExamplePageReq) (res *v1.ExamplePageRes, err error) { + return service.Example().Page(ctx, req) +} + +// List 案例演示平铺列表 +// data 直接为数组,树形结构由前端 buildTree 构建 +func (c *CExample) List(ctx context.Context, req *v1.ExampleListReq) (res v1.ExampleListRes, err error) { + return service.Example().List(ctx, req) +} + +// Detail 案例演示详情 +// 按主键ID查询详情 +func (c *CExample) Detail(ctx context.Context, req *v1.ExampleDetailReq) (res *v1.ExampleDetailRes, err error) { + return service.Example().Detail(ctx, req) +} + +// Add 新增案例演示 +// 新增成功后返回新记录ID +func (c *CExample) Add(ctx context.Context, req *v1.ExampleAddReq) (res *v1.ExampleIdRes, err error) { + return service.Example().Add(ctx, req) +} + +// Update 修改案例演示 +// 按主键ID更新记录 +func (c *CExample) Update(ctx context.Context, req *v1.ExampleUpdateReq) (res *v1.ExampleUpdateRes, err error) { + return service.Example().Update(ctx, req) +} + +// Status 案例演示状态 +// 设置记录的状态字段 +func (c *CExample) Status(ctx context.Context, req *v1.ExampleStatusReq) (res *v1.ExampleStatusRes, err error) { + return service.Example().Status(ctx, req) +} + +// Delete 删除案例演示 +// 按主键ID删除单条记录 +func (c *CExample) Delete(ctx context.Context, req *v1.ExampleDeleteReq) (res *v1.ExampleDeleteRes, err error) { + return service.Example().Delete(ctx, req) +} + +// BatchDelete 批量删除案例演示 +// 按主键ID列表批量删除记录 +func (c *CExample) BatchDelete(ctx context.Context, req *v1.ExampleBatchDeleteReq) (res *v1.ExampleBatchDeleteRes, err error) { + return service.Example().BatchDelete(ctx, req) +} + +// Import 导入案例演示 +// 接收上传的 xls/xlsx 文件并批量导入数据 +func (c *CExample) Import(ctx context.Context, req *v1.ExampleImportReq) (res *v1.ExampleImportRes, err error) { + return service.Example().Import(ctx, req) +} + +// Export 导出案例演示 +// 按查询条件导出数据并返回文件访问地址 +func (c *CExample) Export(ctx context.Context, req *v1.ExampleExportReq) (res *v1.ExampleExportRes, err error) { + return service.Example().Export(ctx, req) +} diff --git a/internal/controller/file_template.go b/internal/controller/file_template.go new file mode 100644 index 0000000..12c8c64 --- /dev/null +++ b/internal/controller/file_template.go @@ -0,0 +1,103 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 文件模板控制器 ============================== + +// CFileTemplate 文件模板控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.FileTemplate() 处理 +type CFileTemplate struct{} + +// Page 文件模板分页 +// 按查询条件分页返回文件模板列表 +func (c *CFileTemplate) Page(ctx context.Context, req *v1.FileTemplatePageReq) (res *v1.FileTemplatePageRes, err error) { + return service.FileTemplate().Page(ctx, req) +} + +// List 文件模板列表 +// 按关键词返回文件模板列表 +func (c *CFileTemplate) List(ctx context.Context, req *v1.FileTemplateListReq) (res v1.FileTemplateListRes, err error) { + return service.FileTemplate().List(ctx, req) +} + +// Detail 文件模板详情 +// 按模板ID查询详情 +func (c *CFileTemplate) Detail(ctx context.Context, req *v1.FileTemplateDetailReq) (res *v1.FileTemplateDetailRes, err error) { + return service.FileTemplate().Detail(ctx, req) +} + +// Add 新增文件模板 +// 新增成功后返回新记录ID +func (c *CFileTemplate) Add(ctx context.Context, req *v1.FileTemplateAddReq) (res *v1.IdRes, err error) { + return service.FileTemplate().Add(ctx, req) +} + +// Update 修改文件模板 +// 按模板ID更新记录 +func (c *CFileTemplate) Update(ctx context.Context, req *v1.FileTemplateUpdateReq) (res *v1.FileTemplateUpdateRes, err error) { + return service.FileTemplate().Update(ctx, req) +} + +// Delete 删除文件模板 +// 按模板ID删除单条记录 +func (c *CFileTemplate) Delete(ctx context.Context, req *v1.FileTemplateDeleteReq) (res *v1.FileTemplateDeleteRes, err error) { + return service.FileTemplate().Delete(ctx, req) +} + +// BatchDelete 批量删除文件模板 +// 按模板ID列表批量删除记录 +func (c *CFileTemplate) BatchDelete(ctx context.Context, req *v1.FileTemplateBatchDeleteReq) (res *v1.FileTemplateBatchDeleteRes, err error) { + return service.FileTemplate().BatchDelete(ctx, req) +} + +// ByCode 按编码查询模板 +// 登录用户即可访问,按模板编码查询模板详情 +func (c *CFileTemplate) ByCode(ctx context.Context, req *v1.FileTemplateByCodeReq) (res *v1.FileTemplateByCodeRes, err error) { + return service.FileTemplate().ByCode(ctx, req) +} diff --git a/internal/controller/generator.go b/internal/controller/generator.go new file mode 100644 index 0000000..c091a79 --- /dev/null +++ b/internal/controller/generator.go @@ -0,0 +1,79 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 代码生成控制器 ============================== + +// CGenerator 代码生成控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.Generator() 处理 +type CGenerator struct{} + +// Page 数据表分页 +// 分页返回数据库表元信息列表 +func (c *CGenerator) Page(ctx context.Context, req *v1.GeneratorPageReq) (res *v1.GeneratorPageRes, err error) { + return service.Generator().Page(ctx, req) +} + +// Preview 预览代码 +// 根据所选数据表渲染代码,仅返回文件清单不落盘 +func (c *CGenerator) Preview(ctx context.Context, req *v1.GeneratorPreviewReq) (res *v1.GeneratorPreviewRes, err error) { + return service.Generator().Preview(ctx, req) +} + +// Generate 一键生成 +// 生成代码文件并同步创建菜单、字典与路由 +func (c *CGenerator) Generate(ctx context.Context, req *v1.GeneratorGenerateReq) (res *v1.GeneratorGenerateRes, err error) { + return service.Generator().Generate(ctx, req) +} + +// BatchGenerate 批量生成 +// 按数据表逐表生成,并返回各表的执行结果 +func (c *CGenerator) BatchGenerate(ctx context.Context, req *v1.GeneratorBatchGenerateReq) (res *v1.GeneratorBatchGenerateRes, err error) { + return service.Generator().BatchGenerate(ctx, req) +} diff --git a/internal/controller/index.go b/internal/controller/index.go new file mode 100644 index 0000000..f4baaec --- /dev/null +++ b/internal/controller/index.go @@ -0,0 +1,85 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 个人中心与菜单聚合控制器 ============================== + +// CIndex 个人中心与菜单聚合控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.Index() 处理 +type CIndex struct{} + +// GetUser 获取当前登录用户信息 +// 返回用户基础信息、角色与权限集合 +func (c *CIndex) GetUser(ctx context.Context, req *v1.GetUserReq) (res *v1.GetUserRes, err error) { + return service.Index().GetUser(ctx, req) +} + +// GetMenus 获取当前用户菜单树 +// 返回当前用户可见的菜单树形结构 +func (c *CIndex) GetMenus(ctx context.Context, req *v1.GetMenusReq) (res *v1.GetMenusRes, err error) { + return service.Index().GetMenus(ctx, req) +} + +// GetMenusById 获取当前用户菜单树(带路径参数) +// 与无路径参数的菜单路由共用同一处理逻辑 +func (c *CIndex) GetMenusById(ctx context.Context, req *v1.GetMenusByIdReq) (res *v1.GetMenusRes, err error) { + return service.Index().GetMenusById(ctx, req) +} + +// UpdatePassword 修改个人密码 +// 校验原密码后更新为新密码 +func (c *CIndex) UpdatePassword(ctx context.Context, req *v1.UpdatePasswordReq) (res *v1.UpdatePasswordRes, err error) { + return service.Index().UpdatePassword(ctx, req) +} + +// UpdateProfile 更新个人信息 +// 更新当前登录用户的个人资料 +func (c *CIndex) UpdateProfile(ctx context.Context, req *v1.UpdateProfileReq) (res *v1.UpdateProfileRes, err error) { + return service.Index().UpdateProfile(ctx, req) +} diff --git a/internal/controller/job.go b/internal/controller/job.go new file mode 100644 index 0000000..9ef4d6e --- /dev/null +++ b/internal/controller/job.go @@ -0,0 +1,127 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 定时任务控制器 ============================== + +// CJob 定时任务控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.Job() 处理 +type CJob struct{} + +// Page 任务分页 +// 按查询条件分页返回定时任务列表 +func (c *CJob) Page(ctx context.Context, req *v1.JobPageReq) (res *v1.JobPageRes, err error) { + return service.Job().Page(ctx, req) +} + +// List 任务列表 +// 按关键词、状态等条件返回定时任务列表 +func (c *CJob) List(ctx context.Context, req *v1.JobListReq) (res v1.JobListRes, err error) { + return service.Job().List(ctx, req) +} + +// Detail 任务详情 +// 按任务ID查询详情 +func (c *CJob) Detail(ctx context.Context, req *v1.JobDetailReq) (res *v1.JobDetailRes, err error) { + return service.Job().Detail(ctx, req) +} + +// Add 新增任务 +// 新增成功后返回新记录ID +func (c *CJob) Add(ctx context.Context, req *v1.JobAddReq) (res *v1.IdRes, err error) { + return service.Job().Add(ctx, req) +} + +// Update 修改任务 +// 按任务ID更新记录 +func (c *CJob) Update(ctx context.Context, req *v1.JobUpdateReq) (res *v1.JobUpdateRes, err error) { + return service.Job().Update(ctx, req) +} + +// Delete 删除任务 +// 按任务ID删除单条记录 +func (c *CJob) Delete(ctx context.Context, req *v1.JobDeleteReq) (res *v1.JobDeleteRes, err error) { + return service.Job().Delete(ctx, req) +} + +// BatchDelete 批量删除任务 +// 按任务ID列表批量删除记录 +func (c *CJob) BatchDelete(ctx context.Context, req *v1.JobBatchDeleteReq) (res *v1.JobBatchDeleteRes, err error) { + return service.Job().BatchDelete(ctx, req) +} + +// RunOnce 立即执行一次任务 +// 触发指定任务立即执行一次 +func (c *CJob) RunOnce(ctx context.Context, req *v1.JobRunOnceReq) (res *v1.JobRunOnceRes, err error) { + return service.Job().RunOnce(ctx, req) +} + +// Pause 暂停任务 +// 暂停指定任务的调度 +func (c *CJob) Pause(ctx context.Context, req *v1.JobPauseReq) (res *v1.JobPauseRes, err error) { + return service.Job().Pause(ctx, req) +} + +// Resume 恢复任务 +// 恢复指定任务的调度 +func (c *CJob) Resume(ctx context.Context, req *v1.JobResumeReq) (res *v1.JobResumeRes, err error) { + return service.Job().Resume(ctx, req) +} + +// Status 设置任务状态 +// 设置任务的未发布、运行中或暂停状态 +func (c *CJob) Status(ctx context.Context, req *v1.JobStatusReq) (res *v1.JobStatusRes, err error) { + return service.Job().Status(ctx, req) +} + +// DaemonStatus 查询调度器状态 +// 返回调度器中运行的任务数、任务键与启动时间 +func (c *CJob) DaemonStatus(ctx context.Context, req *v1.JobDaemonStatusReq) (res *v1.JobDaemonStatusRes, err error) { + return service.Job().DaemonStatus(ctx, req) +} diff --git a/internal/controller/job_log.go b/internal/controller/job_log.go new file mode 100644 index 0000000..e3b115e --- /dev/null +++ b/internal/controller/job_log.go @@ -0,0 +1,85 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 任务日志控制器 ============================== + +// CJobLog 任务日志控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.JobLog() 处理 +type CJobLog struct{} + +// Page 任务日志分页 +// 按查询条件分页返回任务日志列表 +func (c *CJobLog) Page(ctx context.Context, req *v1.JobLogPageReq) (res *v1.JobLogPageRes, err error) { + return service.JobLog().Page(ctx, req) +} + +// List 任务日志列表 +// 按关键词、任务ID、状态等条件返回任务日志列表 +func (c *CJobLog) List(ctx context.Context, req *v1.JobLogListReq) (res v1.JobLogListRes, err error) { + return service.JobLog().List(ctx, req) +} + +// Detail 任务日志详情 +// 按日志ID查询详情 +func (c *CJobLog) Detail(ctx context.Context, req *v1.JobLogDetailReq) (res *v1.JobLogDetailRes, err error) { + return service.JobLog().Detail(ctx, req) +} + +// Delete 删除任务日志 +// 按日志ID删除单条记录 +func (c *CJobLog) Delete(ctx context.Context, req *v1.JobLogDeleteReq) (res *v1.JobLogDeleteRes, err error) { + return service.JobLog().Delete(ctx, req) +} + +// BatchDelete 批量删除任务日志 +// 按日志ID列表批量删除记录 +func (c *CJobLog) BatchDelete(ctx context.Context, req *v1.JobLogBatchDeleteReq) (res *v1.JobLogBatchDeleteRes, err error) { + return service.JobLog().BatchDelete(ctx, req) +} diff --git a/internal/controller/level.go b/internal/controller/level.go new file mode 100644 index 0000000..73f0936 --- /dev/null +++ b/internal/controller/level.go @@ -0,0 +1,115 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 职级控制器 ============================== + +// CLevel 职级控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.Level() 处理 +type CLevel struct{} + +// Page 职级分页 +// 按查询条件分页返回职级列表 +func (c *CLevel) Page(ctx context.Context, req *v1.LevelPageReq) (res *v1.LevelPageRes, err error) { + return service.Level().Page(ctx, req) +} + +// List 职级列表 +// 按关键词、状态等条件返回职级列表 +func (c *CLevel) List(ctx context.Context, req *v1.LevelListReq) (res v1.LevelListRes, err error) { + return service.Level().List(ctx, req) +} + +// Detail 职级详情 +// 按职级ID查询详情 +func (c *CLevel) Detail(ctx context.Context, req *v1.LevelDetailReq) (res *v1.LevelDetailRes, err error) { + return service.Level().Detail(ctx, req) +} + +// Add 新增职级 +// 新增成功后返回新记录ID +func (c *CLevel) Add(ctx context.Context, req *v1.LevelAddReq) (res *v1.IdRes, err error) { + return service.Level().Add(ctx, req) +} + +// Update 修改职级 +// 按职级ID更新记录 +func (c *CLevel) Update(ctx context.Context, req *v1.LevelUpdateReq) (res *v1.LevelUpdateRes, err error) { + return service.Level().Update(ctx, req) +} + +// Status 职级状态 +// 设置职级的正常或禁用状态 +func (c *CLevel) Status(ctx context.Context, req *v1.LevelStatusReq) (res *v1.LevelStatusRes, err error) { + return service.Level().Status(ctx, req) +} + +// Delete 删除职级 +// 按职级ID删除单条记录 +func (c *CLevel) Delete(ctx context.Context, req *v1.LevelDeleteReq) (res *v1.LevelDeleteRes, err error) { + return service.Level().Delete(ctx, req) +} + +// BatchDelete 批量删除职级 +// 按职级ID列表批量删除记录 +func (c *CLevel) BatchDelete(ctx context.Context, req *v1.LevelBatchDeleteReq) (res *v1.LevelBatchDeleteRes, err error) { + return service.Level().BatchDelete(ctx, req) +} + +// Import 导入职级 +// 接收上传的 xls/xlsx 文件并批量导入数据 +func (c *CLevel) Import(ctx context.Context, req *v1.LevelImportReq) (res *v1.LevelImportRes, err error) { + return service.Level().Import(ctx, req) +} + +// Export 导出职级 +// 按查询条件导出职级数据并返回文件访问地址 +func (c *CLevel) Export(ctx context.Context, req *v1.LevelExportReq) (res *v1.LevelExportRes, err error) { + return service.Level().Export(ctx, req) +} diff --git a/internal/controller/link.go b/internal/controller/link.go new file mode 100644 index 0000000..1c22555 --- /dev/null +++ b/internal/controller/link.go @@ -0,0 +1,91 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 友链控制器 ============================== + +// CLink 友链控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.Link() 处理 +type CLink struct{} + +// Page 友链分页 +// 按查询条件分页返回友链列表 +func (c *CLink) Page(ctx context.Context, req *v1.LinkPageReq) (res *v1.LinkPageRes, err error) { + return service.Link().Page(ctx, req) +} + +// Detail 友链详情 +// 按友链ID查询详情 +func (c *CLink) Detail(ctx context.Context, req *v1.LinkDetailReq) (res *v1.LinkDetailRes, err error) { + return service.Link().Detail(ctx, req) +} + +// Add 新增友链 +// 新增成功后返回新记录ID +func (c *CLink) Add(ctx context.Context, req *v1.LinkAddReq) (res *v1.IdRes, err error) { + return service.Link().Add(ctx, req) +} + +// Update 修改友链 +// 按友链ID更新记录 +func (c *CLink) Update(ctx context.Context, req *v1.LinkUpdateReq) (res *v1.LinkUpdateRes, err error) { + return service.Link().Update(ctx, req) +} + +// Delete 删除友链 +// 按友链ID删除单条记录 +func (c *CLink) Delete(ctx context.Context, req *v1.LinkDeleteReq) (res *v1.LinkDeleteRes, err error) { + return service.Link().Delete(ctx, req) +} + +// BatchDelete 批量删除友链 +// 按友链ID列表批量删除记录 +func (c *CLink) BatchDelete(ctx context.Context, req *v1.LinkBatchDeleteReq) (res *v1.LinkBatchDeleteRes, err error) { + return service.Link().BatchDelete(ctx, req) +} diff --git a/internal/controller/login_log.go b/internal/controller/login_log.go new file mode 100644 index 0000000..803ff7f --- /dev/null +++ b/internal/controller/login_log.go @@ -0,0 +1,85 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 登录日志控制器 ============================== + +// CLoginLog 登录日志控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.LoginLog() 处理 +type CLoginLog struct{} + +// Page 登录日志分页 +// 按查询条件分页返回登录日志列表 +func (c *CLoginLog) Page(ctx context.Context, req *v1.LoginLogPageReq) (res *v1.LoginLogPageRes, err error) { + return service.LoginLog().Page(ctx, req) +} + +// List 登录日志列表 +// 按关键词、类型、状态等条件返回登录日志列表 +func (c *CLoginLog) List(ctx context.Context, req *v1.LoginLogListReq) (res v1.LoginLogListRes, err error) { + return service.LoginLog().List(ctx, req) +} + +// Detail 登录日志详情 +// 按日志ID查询详情 +func (c *CLoginLog) Detail(ctx context.Context, req *v1.LoginLogDetailReq) (res *v1.LoginLogDetailRes, err error) { + return service.LoginLog().Detail(ctx, req) +} + +// Delete 删除登录日志 +// 按日志ID删除单条记录 +func (c *CLoginLog) Delete(ctx context.Context, req *v1.LoginLogDeleteReq) (res *v1.LoginLogDeleteRes, err error) { + return service.LoginLog().Delete(ctx, req) +} + +// BatchDelete 批量删除登录日志 +// 按日志ID列表批量删除记录 +func (c *CLoginLog) BatchDelete(ctx context.Context, req *v1.LoginLogBatchDeleteReq) (res *v1.LoginLogBatchDeleteRes, err error) { + return service.LoginLog().BatchDelete(ctx, req) +} diff --git a/internal/controller/menu.go b/internal/controller/menu.go new file mode 100644 index 0000000..95d93e8 --- /dev/null +++ b/internal/controller/menu.go @@ -0,0 +1,97 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 菜单控制器 ============================== + +// CMenu 菜单控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.Menu() 处理 +type CMenu struct{} + +// Page 菜单分页 +// 按查询条件分页返回平铺的菜单列表 +func (c *CMenu) Page(ctx context.Context, req *v1.MenuPageReq) (res *v1.MenuPageRes, err error) { + return service.Menu().Page(ctx, req) +} + +// List 菜单平铺列表 +// 返回菜单平铺列表,由前端自行构建树形结构 +func (c *CMenu) List(ctx context.Context, req *v1.MenuListReq) (res v1.MenuListRes, err error) { + return service.Menu().List(ctx, req) +} + +// Detail 菜单详情 +// 按菜单ID查询详情 +func (c *CMenu) Detail(ctx context.Context, req *v1.MenuDetailReq) (res *v1.MenuDetailRes, err error) { + return service.Menu().Detail(ctx, req) +} + +// Add 新增菜单 +// 新增成功后返回新记录ID +func (c *CMenu) Add(ctx context.Context, req *v1.MenuAddReq) (res *v1.IdRes, err error) { + return service.Menu().Add(ctx, req) +} + +// Update 修改菜单 +// 按菜单ID更新记录 +func (c *CMenu) Update(ctx context.Context, req *v1.MenuUpdateReq) (res *v1.MenuUpdateRes, err error) { + return service.Menu().Update(ctx, req) +} + +// Delete 删除菜单 +// 按菜单ID删除单条记录 +func (c *CMenu) Delete(ctx context.Context, req *v1.MenuDeleteReq) (res *v1.MenuDeleteRes, err error) { + return service.Menu().Delete(ctx, req) +} + +// BatchDelete 批量删除菜单 +// 按菜单ID列表批量删除记录 +func (c *CMenu) BatchDelete(ctx context.Context, req *v1.MenuBatchDeleteReq) (res *v1.MenuBatchDeleteRes, err error) { + return service.Menu().BatchDelete(ctx, req) +} diff --git a/internal/controller/notice.go b/internal/controller/notice.go new file mode 100644 index 0000000..3ed0fb3 --- /dev/null +++ b/internal/controller/notice.go @@ -0,0 +1,103 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 公告控制器 ============================== + +// CNotice 公告控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.Notice() 处理 +type CNotice struct{} + +// Page 公告分页 +// 按查询条件分页返回公告列表 +func (c *CNotice) Page(ctx context.Context, req *v1.NoticePageReq) (res *v1.NoticePageRes, err error) { + return service.Notice().Page(ctx, req) +} + +// List 公告列表 +// 按关键词、类型、状态等条件返回公告精简列表 +func (c *CNotice) List(ctx context.Context, req *v1.NoticeListReq) (res v1.NoticeListRes, err error) { + return service.Notice().List(ctx, req) +} + +// Detail 公告详情 +// 按公告ID查询详情 +func (c *CNotice) Detail(ctx context.Context, req *v1.NoticeDetailReq) (res *v1.NoticeDetailRes, err error) { + return service.Notice().Detail(ctx, req) +} + +// Add 新增公告 +// 新增成功后返回新记录ID +func (c *CNotice) Add(ctx context.Context, req *v1.NoticeAddReq) (res *v1.IdRes, err error) { + return service.Notice().Add(ctx, req) +} + +// Update 修改公告 +// 按公告ID更新记录 +func (c *CNotice) Update(ctx context.Context, req *v1.NoticeUpdateReq) (res *v1.NoticeUpdateRes, err error) { + return service.Notice().Update(ctx, req) +} + +// Status 公告状态 +// 设置公告的草稿或发布状态 +func (c *CNotice) Status(ctx context.Context, req *v1.NoticeStatusReq) (res *v1.NoticeStatusRes, err error) { + return service.Notice().Status(ctx, req) +} + +// Delete 删除公告 +// 按公告ID删除单条记录 +func (c *CNotice) Delete(ctx context.Context, req *v1.NoticeDeleteReq) (res *v1.NoticeDeleteRes, err error) { + return service.Notice().Delete(ctx, req) +} + +// BatchDelete 批量删除公告 +// 按公告ID列表批量删除记录 +func (c *CNotice) BatchDelete(ctx context.Context, req *v1.NoticeBatchDeleteReq) (res *v1.NoticeBatchDeleteRes, err error) { + return service.Notice().BatchDelete(ctx, req) +} diff --git a/internal/controller/online_user.go b/internal/controller/online_user.go new file mode 100644 index 0000000..cdb5b47 --- /dev/null +++ b/internal/controller/online_user.go @@ -0,0 +1,74 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 在线用户控制器 ============================== + +// COnlineUser 在线用户控制器 +type COnlineUser struct{} + +// Page 在线用户分页 +func (c *COnlineUser) Page(ctx context.Context, req *v1.OnlineUserPageReq) (res *v1.OnlineUserPageRes, err error) { + return service.OnlineUser().Page(ctx, req) +} + +// Detail 在线会话详情 +func (c *COnlineUser) Detail(ctx context.Context, req *v1.OnlineUserDetailReq) (res *v1.OnlineUserDetailRes, err error) { + return service.OnlineUser().Detail(ctx, req) +} + +// ForceLogout 强制下线 +func (c *COnlineUser) ForceLogout(ctx context.Context, req *v1.OnlineUserForceLogoutReq) (res *v1.OnlineUserForceLogoutRes, err error) { + return service.OnlineUser().ForceLogout(ctx, req) +} + +// ForceLogoutUser 踢出该用户全部设备 +func (c *COnlineUser) ForceLogoutUser(ctx context.Context, req *v1.OnlineUserForceLogoutUserReq) (res *v1.OnlineUserForceLogoutUserRes, err error) { + return service.OnlineUser().ForceLogoutUser(ctx, req) +} diff --git a/internal/controller/operation_log.go b/internal/controller/operation_log.go new file mode 100644 index 0000000..06006ec --- /dev/null +++ b/internal/controller/operation_log.go @@ -0,0 +1,85 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 操作日志控制器 ============================== + +// COperLog 操作日志控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.OperLog() 处理 +type COperLog struct{} + +// Page 操作日志分页 +// 按查询条件分页返回操作日志列表 +func (c *COperLog) Page(ctx context.Context, req *v1.OperLogPageReq) (res *v1.OperLogPageRes, err error) { + return service.OperLog().Page(ctx, req) +} + +// List 操作日志列表 +// 按关键词、类型、状态等条件返回操作日志列表 +func (c *COperLog) List(ctx context.Context, req *v1.OperLogListReq) (res v1.OperLogListRes, err error) { + return service.OperLog().List(ctx, req) +} + +// Detail 操作日志详情 +// 按日志ID查询详情 +func (c *COperLog) Detail(ctx context.Context, req *v1.OperLogDetailReq) (res *v1.OperLogDetailRes, err error) { + return service.OperLog().Detail(ctx, req) +} + +// Delete 删除操作日志 +// 按日志ID删除单条记录 +func (c *COperLog) Delete(ctx context.Context, req *v1.OperLogDeleteReq) (res *v1.OperLogDeleteRes, err error) { + return service.OperLog().Delete(ctx, req) +} + +// BatchDelete 批量删除操作日志 +// 按日志ID列表批量删除记录 +func (c *COperLog) BatchDelete(ctx context.Context, req *v1.OperLogBatchDeleteReq) (res *v1.OperLogBatchDeleteRes, err error) { + return service.OperLog().BatchDelete(ctx, req) +} diff --git a/internal/controller/param.go b/internal/controller/param.go new file mode 100644 index 0000000..4df500f --- /dev/null +++ b/internal/controller/param.go @@ -0,0 +1,97 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 参数控制器 ============================== + +// CParam 参数控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.Param() 处理 +type CParam struct{} + +// Page 参数分页 +// 按查询条件分页返回参数列表 +func (c *CParam) Page(ctx context.Context, req *v1.ParamPageReq) (res *v1.ParamPageRes, err error) { + return service.Param().Page(ctx, req) +} + +// List 参数列表 +// 按关键词、类型、状态等条件返回参数列表 +func (c *CParam) List(ctx context.Context, req *v1.ParamListReq) (res v1.ParamListRes, err error) { + return service.Param().List(ctx, req) +} + +// Detail 参数详情 +// 按参数ID查询详情 +func (c *CParam) Detail(ctx context.Context, req *v1.ParamDetailReq) (res *v1.ParamDetailRes, err error) { + return service.Param().Detail(ctx, req) +} + +// Add 新增参数 +// 新增成功后返回新记录ID +func (c *CParam) Add(ctx context.Context, req *v1.ParamAddReq) (res *v1.IdRes, err error) { + return service.Param().Add(ctx, req) +} + +// Update 修改参数 +// 按参数ID更新记录 +func (c *CParam) Update(ctx context.Context, req *v1.ParamUpdateReq) (res *v1.ParamUpdateRes, err error) { + return service.Param().Update(ctx, req) +} + +// Delete 删除参数 +// 按参数ID删除单条记录 +func (c *CParam) Delete(ctx context.Context, req *v1.ParamDeleteReq) (res *v1.ParamDeleteRes, err error) { + return service.Param().Delete(ctx, req) +} + +// BatchDelete 批量删除参数 +// 按参数ID列表批量删除记录 +func (c *CParam) BatchDelete(ctx context.Context, req *v1.ParamBatchDeleteReq) (res *v1.ParamBatchDeleteRes, err error) { + return service.Param().BatchDelete(ctx, req) +} diff --git a/internal/controller/position.go b/internal/controller/position.go new file mode 100644 index 0000000..11a2811 --- /dev/null +++ b/internal/controller/position.go @@ -0,0 +1,103 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 岗位控制器 ============================== + +// CPosition 岗位控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.Position() 处理 +type CPosition struct{} + +// Page 岗位分页 +// 按查询条件分页返回岗位列表 +func (c *CPosition) Page(ctx context.Context, req *v1.PositionPageReq) (res *v1.PositionPageRes, err error) { + return service.Position().Page(ctx, req) +} + +// List 岗位列表 +// 按关键词、状态等条件返回岗位列表 +func (c *CPosition) List(ctx context.Context, req *v1.PositionListReq) (res v1.PositionListRes, err error) { + return service.Position().List(ctx, req) +} + +// Detail 岗位详情 +// 按岗位ID查询详情 +func (c *CPosition) Detail(ctx context.Context, req *v1.PositionDetailReq) (res *v1.PositionDetailRes, err error) { + return service.Position().Detail(ctx, req) +} + +// Add 新增岗位 +// 新增成功后返回新记录ID +func (c *CPosition) Add(ctx context.Context, req *v1.PositionAddReq) (res *v1.IdRes, err error) { + return service.Position().Add(ctx, req) +} + +// Update 修改岗位 +// 按岗位ID更新记录 +func (c *CPosition) Update(ctx context.Context, req *v1.PositionUpdateReq) (res *v1.PositionUpdateRes, err error) { + return service.Position().Update(ctx, req) +} + +// Status 岗位状态 +// 设置岗位的正常或禁用状态 +func (c *CPosition) Status(ctx context.Context, req *v1.PositionStatusReq) (res *v1.PositionStatusRes, err error) { + return service.Position().Status(ctx, req) +} + +// Delete 删除岗位 +// 按岗位ID删除单条记录 +func (c *CPosition) Delete(ctx context.Context, req *v1.PositionDeleteReq) (res *v1.PositionDeleteRes, err error) { + return service.Position().Delete(ctx, req) +} + +// BatchDelete 批量删除岗位 +// 按岗位ID列表批量删除记录 +func (c *CPosition) BatchDelete(ctx context.Context, req *v1.PositionBatchDeleteReq) (res *v1.PositionBatchDeleteRes, err error) { + return service.Position().BatchDelete(ctx, req) +} diff --git a/internal/controller/role.go b/internal/controller/role.go new file mode 100644 index 0000000..613ecf1 --- /dev/null +++ b/internal/controller/role.go @@ -0,0 +1,109 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 角色控制器 ============================== + +// CRole 角色控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.Role() 处理 +type CRole struct{} + +// Page 角色分页 +// 按查询条件分页返回角色列表 +func (c *CRole) Page(ctx context.Context, req *v1.RolePageReq) (res *v1.RolePageRes, err error) { + return service.Role().Page(ctx, req) +} + +// List 角色列表 +// 按关键词返回角色精简列表 +func (c *CRole) List(ctx context.Context, req *v1.RoleListReq) (res v1.RoleListRes, err error) { + return service.Role().List(ctx, req) +} + +// Detail 角色详情 +// 按角色ID查询详情 +func (c *CRole) Detail(ctx context.Context, req *v1.RoleDetailReq) (res *v1.RoleDetailRes, err error) { + return service.Role().Detail(ctx, req) +} + +// Add 新增角色 +// 新增成功后返回新记录ID +func (c *CRole) Add(ctx context.Context, req *v1.RoleAddReq) (res *v1.IdRes, err error) { + return service.Role().Add(ctx, req) +} + +// Update 修改角色 +// 按角色ID更新记录 +func (c *CRole) Update(ctx context.Context, req *v1.RoleUpdateReq) (res *v1.RoleUpdateRes, err error) { + return service.Role().Update(ctx, req) +} + +// Delete 删除角色 +// 按角色ID删除单条记录 +func (c *CRole) Delete(ctx context.Context, req *v1.RoleDeleteReq) (res *v1.RoleDeleteRes, err error) { + return service.Role().Delete(ctx, req) +} + +// BatchDelete 批量删除角色 +// 按角色ID列表批量删除记录 +func (c *CRole) BatchDelete(ctx context.Context, req *v1.RoleBatchDeleteReq) (res *v1.RoleBatchDeleteRes, err error) { + return service.Role().BatchDelete(ctx, req) +} + +// MenuList 角色菜单勾选列表 +// 返回菜单平铺列表并附带勾选状态,供权限配置使用 +func (c *CRole) MenuList(ctx context.Context, req *v1.RoleMenuListReq) (res v1.RoleMenuListRes, err error) { + return service.Role().MenuList(ctx, req) +} + +// SavePermission 保存角色菜单权限 +// 按角色ID保存其可访问的菜单ID列表 +func (c *CRole) SavePermission(ctx context.Context, req *v1.RoleSavePermissionReq) (res *v1.RoleSavePermissionRes, err error) { + return service.Role().SavePermission(ctx, req) +} diff --git a/internal/controller/server_monitor.go b/internal/controller/server_monitor.go new file mode 100644 index 0000000..b38cd9e --- /dev/null +++ b/internal/controller/server_monitor.go @@ -0,0 +1,59 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 服务器监控控制器 ============================== + +// CServerMonitor 服务器监控控制器 +type CServerMonitor struct{} + +// Info 服务器监控信息 +func (c *CServerMonitor) Info(ctx context.Context, req *v1.ServerInfoReq) (res *v1.ServerInfoRes, err error) { + return service.ServerMonitor().Info(ctx, req) +} diff --git a/internal/controller/tenant.go b/internal/controller/tenant.go new file mode 100644 index 0000000..080bedd --- /dev/null +++ b/internal/controller/tenant.go @@ -0,0 +1,115 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 租户控制器 ============================== + +// CTenant 租户控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.Tenant() 处理 +type CTenant struct{} + +// Page 租户分页 +// 按查询条件分页返回租户列表 +func (c *CTenant) Page(ctx context.Context, req *v1.TenantPageReq) (res *v1.TenantPageRes, err error) { + return service.Tenant().Page(ctx, req) +} + +// List 租户列表 +// 按关键词、状态等条件返回租户精简列表 +func (c *CTenant) List(ctx context.Context, req *v1.TenantListReq) (res v1.TenantListRes, err error) { + return service.Tenant().List(ctx, req) +} + +// Detail 租户详情 +// 按租户ID查询详情 +func (c *CTenant) Detail(ctx context.Context, req *v1.TenantDetailReq) (res *v1.TenantDetailRes, err error) { + return service.Tenant().Detail(ctx, req) +} + +// Add 新增租户 +// 新增成功后返回新记录ID +func (c *CTenant) Add(ctx context.Context, req *v1.TenantAddReq) (res *v1.IdRes, err error) { + return service.Tenant().Add(ctx, req) +} + +// Update 修改租户 +// 按租户ID更新记录 +func (c *CTenant) Update(ctx context.Context, req *v1.TenantUpdateReq) (res *v1.TenantUpdateRes, err error) { + return service.Tenant().Update(ctx, req) +} + +// Status 租户状态 +// 设置租户的正常或禁用状态 +func (c *CTenant) Status(ctx context.Context, req *v1.TenantStatusReq) (res *v1.TenantStatusRes, err error) { + return service.Tenant().Status(ctx, req) +} + +// Delete 删除租户 +// 按租户ID删除单条记录 +func (c *CTenant) Delete(ctx context.Context, req *v1.TenantDeleteReq) (res *v1.TenantDeleteRes, err error) { + return service.Tenant().Delete(ctx, req) +} + +// BatchDelete 批量删除租户 +// 按租户ID列表批量删除记录 +func (c *CTenant) BatchDelete(ctx context.Context, req *v1.TenantBatchDeleteReq) (res *v1.TenantBatchDeleteRes, err error) { + return service.Tenant().BatchDelete(ctx, req) +} + +// Account 创建租户账号 +// 同时创建租户及其初始管理员账号 +func (c *CTenant) Account(ctx context.Context, req *v1.TenantAccountReq) (res *v1.TenantAccountRes, err error) { + return service.Tenant().Account(ctx, req) +} + +// Users 租户用户列表 +// 按租户ID查询该租户下的用户列表 +func (c *CTenant) Users(ctx context.Context, req *v1.TenantUsersReq) (res *v1.TenantUsersRes, err error) { + return service.Tenant().Users(ctx, req) +} diff --git a/internal/controller/upload.go b/internal/controller/upload.go new file mode 100644 index 0000000..e433e4a --- /dev/null +++ b/internal/controller/upload.go @@ -0,0 +1,67 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 文件上传控制器 ============================== + +// CUpload 文件上传控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.Upload() 处理 +type CUpload struct{} + +// UploadFile 上传单文件 +// 接收单个上传文件并返回文件访问地址与基本信息 +func (c *CUpload) UploadFile(ctx context.Context, req *v1.UploadFileReq) (res *v1.UploadFileRes, err error) { + return service.Upload().UploadFile(ctx, req) +} + +// UploadFiles 批量上传文件 +// 接收多个上传文件并返回各文件的访问地址与基本信息 +func (c *CUpload) UploadFiles(ctx context.Context, req *v1.UploadFilesReq) (res *v1.UploadFilesRes, err error) { + return service.Upload().UploadFiles(ctx, req) +} diff --git a/internal/controller/user.go b/internal/controller/user.go new file mode 100644 index 0000000..1601ddb --- /dev/null +++ b/internal/controller/user.go @@ -0,0 +1,127 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package controller + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" +) + +// ============================== 用户控制器 ============================== + +// CUser 用户控制器 +// 薄层控制器,仅负责参数透传,业务逻辑由 service.User() 处理 +type CUser struct{} + +// Page 用户分页 +// 按查询条件分页返回用户列表 +func (c *CUser) Page(ctx context.Context, req *v1.UserPageReq) (res *v1.UserPageRes, err error) { + return service.User().Page(ctx, req) +} + +// List 用户列表 +// 按关键词、状态等条件返回用户精简列表,供下拉选择使用 +func (c *CUser) List(ctx context.Context, req *v1.UserListReq) (res v1.UserListRes, err error) { + return service.User().List(ctx, req) +} + +// Detail 用户详情 +// 按用户ID查询详情,含角色ID数组与四级区划编码 +func (c *CUser) Detail(ctx context.Context, req *v1.UserDetailReq) (res *v1.UserDetailRes, err error) { + return service.User().Detail(ctx, req) +} + +// Add 新增用户 +// 新增成功后返回新记录ID +func (c *CUser) Add(ctx context.Context, req *v1.UserAddReq) (res *v1.IdRes, err error) { + return service.User().Add(ctx, req) +} + +// Update 修改用户 +// 按用户ID更新记录,密码留空时不做修改 +func (c *CUser) Update(ctx context.Context, req *v1.UserUpdateReq) (res *v1.UserUpdateRes, err error) { + return service.User().Update(ctx, req) +} + +// Status 用户状态 +// 设置用户的正常或禁用状态 +func (c *CUser) Status(ctx context.Context, req *v1.UserStatusReq) (res *v1.UserStatusRes, err error) { + return service.User().Status(ctx, req) +} + +// ResetPwd 重置用户密码 +// 密码为空时重置为系统默认密码 +func (c *CUser) ResetPwd(ctx context.Context, req *v1.UserResetPwdReq) (res *v1.UserResetPwdRes, err error) { + return service.User().ResetPwd(ctx, req) +} + +// Delete 删除用户 +// 按用户ID删除单条记录 +func (c *CUser) Delete(ctx context.Context, req *v1.UserDeleteReq) (res *v1.UserDeleteRes, err error) { + return service.User().Delete(ctx, req) +} + +// BatchDelete 批量删除用户 +// 按用户ID列表批量删除记录 +func (c *CUser) BatchDelete(ctx context.Context, req *v1.UserBatchDeleteReq) (res *v1.UserBatchDeleteRes, err error) { + return service.User().BatchDelete(ctx, req) +} + +// Import 导入用户 +// 接收上传的 xls/xlsx 文件并批量导入数据 +func (c *CUser) Import(ctx context.Context, req *v1.UserImportReq) (res *v1.UserImportRes, err error) { + return service.User().Import(ctx, req) +} + +// Export 导出用户 +// 按查询条件导出用户数据并返回文件访问地址 +func (c *CUser) Export(ctx context.Context, req *v1.UserExportReq) (res *v1.UserExportRes, err error) { + return service.User().Export(ctx, req) +} + +// Document 用户打印信息 +// 返回用户详情及关联的角色、部门、职级、岗位名称 +func (c *CUser) Document(ctx context.Context, req *v1.UserDocumentReq) (res *v1.UserDocumentRes, err error) { + return service.User().Document(ctx, req) +} diff --git a/internal/dao/article.go b/internal/dao/article.go new file mode 100644 index 0000000..d8101f9 --- /dev/null +++ b/internal/dao/article.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// articleDao is the data access object for the table goframe_article. +// You can define custom methods on it to extend its functionality as needed. +type articleDao struct { + *internal.ArticleDao +} + +var ( + // Article is a globally accessible object for table goframe_article operations. + Article = articleDao{ + internal.NewArticleDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *articleDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.ArticleDao.Ctx(ctx), "goframe_article") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/category.go b/internal/dao/category.go new file mode 100644 index 0000000..5f51d9f --- /dev/null +++ b/internal/dao/category.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// categoryDao is the data access object for the table goframe_category. +// You can define custom methods on it to extend its functionality as needed. +type categoryDao struct { + *internal.CategoryDao +} + +var ( + // Category is a globally accessible object for table goframe_category operations. + Category = categoryDao{ + internal.NewCategoryDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *categoryDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.CategoryDao.Ctx(ctx), "goframe_category") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/city.go b/internal/dao/city.go new file mode 100644 index 0000000..8266c9f --- /dev/null +++ b/internal/dao/city.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// cityDao is the data access object for the table goframe_city. +// You can define custom methods on it to extend its functionality as needed. +type cityDao struct { + *internal.CityDao +} + +var ( + // City is a globally accessible object for table goframe_city operations. + City = cityDao{ + internal.NewCityDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *cityDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.CityDao.Ctx(ctx), "goframe_city") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/config.go b/internal/dao/config.go new file mode 100644 index 0000000..b04f52d --- /dev/null +++ b/internal/dao/config.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// configDao is the data access object for the table goframe_config. +// You can define custom methods on it to extend its functionality as needed. +type configDao struct { + *internal.ConfigDao +} + +var ( + // Config is a globally accessible object for table goframe_config operations. + Config = configDao{ + internal.NewConfigDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *configDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.ConfigDao.Ctx(ctx), "goframe_config") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/config_item.go b/internal/dao/config_item.go new file mode 100644 index 0000000..cf5754f --- /dev/null +++ b/internal/dao/config_item.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// configItemDao is the data access object for the table goframe_config_item. +// You can define custom methods on it to extend its functionality as needed. +type configItemDao struct { + *internal.ConfigItemDao +} + +var ( + // ConfigItem is a globally accessible object for table goframe_config_item operations. + ConfigItem = configItemDao{ + internal.NewConfigItemDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *configItemDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.ConfigItemDao.Ctx(ctx), "goframe_config_item") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/dept.go b/internal/dao/dept.go new file mode 100644 index 0000000..e3da4dc --- /dev/null +++ b/internal/dao/dept.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// deptDao is the data access object for the table goframe_dept. +// You can define custom methods on it to extend its functionality as needed. +type deptDao struct { + *internal.DeptDao +} + +var ( + // Dept is a globally accessible object for table goframe_dept operations. + Dept = deptDao{ + internal.NewDeptDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *deptDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.DeptDao.Ctx(ctx), "goframe_dept") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/dict.go b/internal/dao/dict.go new file mode 100644 index 0000000..0458da0 --- /dev/null +++ b/internal/dao/dict.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// dictDao is the data access object for the table goframe_dict. +// You can define custom methods on it to extend its functionality as needed. +type dictDao struct { + *internal.DictDao +} + +var ( + // Dict is a globally accessible object for table goframe_dict operations. + Dict = dictDao{ + internal.NewDictDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *dictDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.DictDao.Ctx(ctx), "goframe_dict") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/dict_item.go b/internal/dao/dict_item.go new file mode 100644 index 0000000..b87e008 --- /dev/null +++ b/internal/dao/dict_item.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// dictItemDao is the data access object for the table goframe_dict_item. +// You can define custom methods on it to extend its functionality as needed. +type dictItemDao struct { + *internal.DictItemDao +} + +var ( + // DictItem is a globally accessible object for table goframe_dict_item operations. + DictItem = dictItemDao{ + internal.NewDictItemDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *dictItemDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.DictItemDao.Ctx(ctx), "goframe_dict_item") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/example.go b/internal/dao/example.go new file mode 100644 index 0000000..f2e9313 --- /dev/null +++ b/internal/dao/example.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// exampleDao is the data access object for the table goframe_example. +// You can define custom methods on it to extend its functionality as needed. +type exampleDao struct { + *internal.ExampleDao +} + +var ( + // Example is a globally accessible object for table goframe_example operations. + Example = exampleDao{ + internal.NewExampleDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *exampleDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.ExampleDao.Ctx(ctx), "goframe_example") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/file_template.go b/internal/dao/file_template.go new file mode 100644 index 0000000..21153d7 --- /dev/null +++ b/internal/dao/file_template.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// fileTemplateDao is the data access object for the table goframe_file_template. +// You can define custom methods on it to extend its functionality as needed. +type fileTemplateDao struct { + *internal.FileTemplateDao +} + +var ( + // FileTemplate is a globally accessible object for table goframe_file_template operations. + FileTemplate = fileTemplateDao{ + internal.NewFileTemplateDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *fileTemplateDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.FileTemplateDao.Ctx(ctx), "goframe_file_template") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/internal/article.go b/internal/dao/internal/article.go new file mode 100644 index 0000000..68fbe01 --- /dev/null +++ b/internal/dao/internal/article.go @@ -0,0 +1,111 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// ArticleDao is the data access object for the table goframe_article. +type ArticleDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns ArticleColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// ArticleColumns defines and stores column names for the table goframe_article. +type ArticleColumns struct { + Id string // 主键ID + TenantId string // 租户ID:0=平台级 + CategoryId string // 分类ID + Title string // 文章标题 + Cover string // 文章封面 + Images string // 文章图集 + Intro string // 文章导读 + Content string // 文章内容 + Author string // 文章作者 + Status string // 文章状态:0-正常 1-下架 + Click string // 文章点击率 + Sort string // 文章排序 + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// articleColumns holds the columns for the table goframe_article. +var articleColumns = ArticleColumns{ + Id: "id", + TenantId: "tenant_id", + CategoryId: "category_id", + Title: "title", + Cover: "cover", + Images: "images", + Intro: "intro", + Content: "content", + Author: "author", + Status: "status", + Click: "click", + Sort: "sort", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewArticleDao creates and returns a new DAO object for table data access. +func NewArticleDao(handlers ...gdb.ModelHandler) *ArticleDao { + return &ArticleDao{ + group: "default", + table: "goframe_article", + columns: articleColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *ArticleDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *ArticleDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *ArticleDao) Columns() ArticleColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *ArticleDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *ArticleDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *ArticleDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/category.go b/internal/dao/internal/category.go new file mode 100644 index 0000000..c22d49e --- /dev/null +++ b/internal/dao/internal/category.go @@ -0,0 +1,101 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// CategoryDao is the data access object for the table goframe_category. +type CategoryDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns CategoryColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// CategoryColumns defines and stores column names for the table goframe_category. +type CategoryColumns struct { + Id string // 主键ID + TenantId string // 租户ID:0=平台级 + CategoryName string // 分类名称 + CategoryCode string // 分类编码 + ParentId string // 上级ID + Sort string // 排序 + Note string // 分类备注 + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// categoryColumns holds the columns for the table goframe_category. +var categoryColumns = CategoryColumns{ + Id: "id", + TenantId: "tenant_id", + CategoryName: "category_name", + CategoryCode: "category_code", + ParentId: "parent_id", + Sort: "sort", + Note: "note", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewCategoryDao creates and returns a new DAO object for table data access. +func NewCategoryDao(handlers ...gdb.ModelHandler) *CategoryDao { + return &CategoryDao{ + group: "default", + table: "goframe_category", + columns: categoryColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *CategoryDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *CategoryDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *CategoryDao) Columns() CategoryColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *CategoryDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *CategoryDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *CategoryDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/city.go b/internal/dao/internal/city.go new file mode 100644 index 0000000..b8d0169 --- /dev/null +++ b/internal/dao/internal/city.go @@ -0,0 +1,113 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// CityDao is the data access object for the table goframe_city. +type CityDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns CityColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// CityColumns defines and stores column names for the table goframe_city. +type CityColumns struct { + Id string // 主键ID + CityCode string // 城市区号 + AreaCode string // 行政编码 + ParentCode string // 上级行政编码 + ZipCode string // 邮政编码 + Level string // 城市级别:0-省份 1-城市 2-县区 3-街道 4-社区 + Pid string // 上级城市ID + Name string // 城市名称 + ShortName string // 城市简称 + FullName string // 城市全称 + Pinyin string // 城市拼音 + Lng string // 城市经度 + Lat string // 城市纬度 + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// cityColumns holds the columns for the table goframe_city. +var cityColumns = CityColumns{ + Id: "id", + CityCode: "city_code", + AreaCode: "area_code", + ParentCode: "parent_code", + ZipCode: "zip_code", + Level: "level", + Pid: "pid", + Name: "name", + ShortName: "short_name", + FullName: "full_name", + Pinyin: "pinyin", + Lng: "lng", + Lat: "lat", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewCityDao creates and returns a new DAO object for table data access. +func NewCityDao(handlers ...gdb.ModelHandler) *CityDao { + return &CityDao{ + group: "default", + table: "goframe_city", + columns: cityColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *CityDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *CityDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *CityDao) Columns() CityColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *CityDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *CityDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *CityDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/config.go b/internal/dao/internal/config.go new file mode 100644 index 0000000..692e434 --- /dev/null +++ b/internal/dao/internal/config.go @@ -0,0 +1,97 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// ConfigDao is the data access object for the table goframe_config. +type ConfigDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns ConfigColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// ConfigColumns defines and stores column names for the table goframe_config. +type ConfigColumns struct { + Id string // 主键ID + Name string // 配置名称 + Code string // 配置编码 + Sort string // 配置排序 + Note string // 配置备注 + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// configColumns holds the columns for the table goframe_config. +var configColumns = ConfigColumns{ + Id: "id", + Name: "name", + Code: "code", + Sort: "sort", + Note: "note", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewConfigDao creates and returns a new DAO object for table data access. +func NewConfigDao(handlers ...gdb.ModelHandler) *ConfigDao { + return &ConfigDao{ + group: "default", + table: "goframe_config", + columns: configColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *ConfigDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *ConfigDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *ConfigDao) Columns() ConfigColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *ConfigDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *ConfigDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *ConfigDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/config_item.go b/internal/dao/internal/config_item.go new file mode 100644 index 0000000..098da69 --- /dev/null +++ b/internal/dao/internal/config_item.go @@ -0,0 +1,107 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// ConfigItemDao is the data access object for the table goframe_config_item. +type ConfigItemDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns ConfigItemColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// ConfigItemColumns defines and stores column names for the table goframe_config_item. +type ConfigItemColumns struct { + Id string // 主键ID + Name string // 配置项名称 + Code string // 配置项编码 + Value string // 配置项值 + Options string // 配置选项 + ConfigId string // 配置ID + Type string // 配置类型 + Status string // 配置状态:1-正常 2-停用 + Sort string // 配置项顺序 + Note string // 配置项备注 + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// configItemColumns holds the columns for the table goframe_config_item. +var configItemColumns = ConfigItemColumns{ + Id: "id", + Name: "name", + Code: "code", + Value: "value", + Options: "options", + ConfigId: "config_id", + Type: "type", + Status: "status", + Sort: "sort", + Note: "note", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewConfigItemDao creates and returns a new DAO object for table data access. +func NewConfigItemDao(handlers ...gdb.ModelHandler) *ConfigItemDao { + return &ConfigItemDao{ + group: "default", + table: "goframe_config_item", + columns: configItemColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *ConfigItemDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *ConfigItemDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *ConfigItemDao) Columns() ConfigItemColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *ConfigItemDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *ConfigItemDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *ConfigItemDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/dept.go b/internal/dao/internal/dept.go new file mode 100644 index 0000000..36f261b --- /dev/null +++ b/internal/dao/internal/dept.go @@ -0,0 +1,101 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// DeptDao is the data access object for the table goframe_dept. +type DeptDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns DeptColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// DeptColumns defines and stores column names for the table goframe_dept. +type DeptColumns struct { + Id string // 主键ID + TenantId string // 租户ID:0=平台级 + Name string // 部门名称 + Type string // 部门类型:1-公司 2-子公司 3-部门 4-小组 + ParentId string // 上级部门ID + Sort string // 部门排序 + Note string // 备注 + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// deptColumns holds the columns for the table goframe_dept. +var deptColumns = DeptColumns{ + Id: "id", + TenantId: "tenant_id", + Name: "name", + Type: "type", + ParentId: "parent_id", + Sort: "sort", + Note: "note", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewDeptDao creates and returns a new DAO object for table data access. +func NewDeptDao(handlers ...gdb.ModelHandler) *DeptDao { + return &DeptDao{ + group: "default", + table: "goframe_dept", + columns: deptColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *DeptDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *DeptDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *DeptDao) Columns() DeptColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *DeptDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *DeptDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *DeptDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/dict.go b/internal/dao/internal/dict.go new file mode 100644 index 0000000..c48c5c9 --- /dev/null +++ b/internal/dao/internal/dict.go @@ -0,0 +1,97 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// DictDao is the data access object for the table goframe_dict. +type DictDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns DictColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// DictColumns defines and stores column names for the table goframe_dict. +type DictColumns struct { + Id string // 主键ID + Name string // 字典名称 + Code string // 字典编码 + Sort string // 字典排序 + Note string // 字典备注 + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// dictColumns holds the columns for the table goframe_dict. +var dictColumns = DictColumns{ + Id: "id", + Name: "name", + Code: "code", + Sort: "sort", + Note: "note", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewDictDao creates and returns a new DAO object for table data access. +func NewDictDao(handlers ...gdb.ModelHandler) *DictDao { + return &DictDao{ + group: "default", + table: "goframe_dict", + columns: dictColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *DictDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *DictDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *DictDao) Columns() DictColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *DictDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *DictDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *DictDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/dict_item.go b/internal/dao/internal/dict_item.go new file mode 100644 index 0000000..1554637 --- /dev/null +++ b/internal/dao/internal/dict_item.go @@ -0,0 +1,99 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// DictItemDao is the data access object for the table goframe_dict_item. +type DictItemDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns DictItemColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// DictItemColumns defines and stores column names for the table goframe_dict_item. +type DictItemColumns struct { + Id string // 主键ID + Name string // 字典项名称 + Value string // 字典项值 + DictId string // 字典ID + Sort string // 字典项顺序 + Note string // 字典项备注 + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// dictItemColumns holds the columns for the table goframe_dict_item. +var dictItemColumns = DictItemColumns{ + Id: "id", + Name: "name", + Value: "value", + DictId: "dict_id", + Sort: "sort", + Note: "note", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewDictItemDao creates and returns a new DAO object for table data access. +func NewDictItemDao(handlers ...gdb.ModelHandler) *DictItemDao { + return &DictItemDao{ + group: "default", + table: "goframe_dict_item", + columns: dictItemColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *DictItemDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *DictItemDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *DictItemDao) Columns() DictItemColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *DictItemDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *DictItemDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *DictItemDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/example.go b/internal/dao/internal/example.go new file mode 100644 index 0000000..01c04dd --- /dev/null +++ b/internal/dao/internal/example.go @@ -0,0 +1,99 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// ExampleDao is the data access object for the table goframe_example. +type ExampleDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns ExampleColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// ExampleColumns defines and stores column names for the table goframe_example. +type ExampleColumns struct { + Id string // 主键ID + Name string // 案例名称 + Avatar string // 案例图片 + Type string // 案例类型:1-类型1 2-类型2 3-类型3 4-类型4 + Status string // 案例状态:1-在用 2-停用 + Sort string // 案例排序 + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// exampleColumns holds the columns for the table goframe_example. +var exampleColumns = ExampleColumns{ + Id: "id", + Name: "name", + Avatar: "avatar", + Type: "type", + Status: "status", + Sort: "sort", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewExampleDao creates and returns a new DAO object for table data access. +func NewExampleDao(handlers ...gdb.ModelHandler) *ExampleDao { + return &ExampleDao{ + group: "default", + table: "goframe_example", + columns: exampleColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *ExampleDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *ExampleDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *ExampleDao) Columns() ExampleColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *ExampleDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *ExampleDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *ExampleDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/file_template.go b/internal/dao/internal/file_template.go new file mode 100644 index 0000000..0276a0d --- /dev/null +++ b/internal/dao/internal/file_template.go @@ -0,0 +1,101 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// FileTemplateDao is the data access object for the table goframe_file_template. +type FileTemplateDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns FileTemplateColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// FileTemplateColumns defines and stores column names for the table goframe_file_template. +type FileTemplateColumns struct { + Id string // 主键ID + Name string // 模板名称 + Code string // 模板编码 + FileName string // 文件名称 + FilePath string // 文件路径 + Note string // 文件模板备注 + Sort string // 文件模板顺序 + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// fileTemplateColumns holds the columns for the table goframe_file_template. +var fileTemplateColumns = FileTemplateColumns{ + Id: "id", + Name: "name", + Code: "code", + FileName: "file_name", + FilePath: "file_path", + Note: "note", + Sort: "sort", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewFileTemplateDao creates and returns a new DAO object for table data access. +func NewFileTemplateDao(handlers ...gdb.ModelHandler) *FileTemplateDao { + return &FileTemplateDao{ + group: "default", + table: "goframe_file_template", + columns: fileTemplateColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *FileTemplateDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *FileTemplateDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *FileTemplateDao) Columns() FileTemplateColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *FileTemplateDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *FileTemplateDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *FileTemplateDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/job.go b/internal/dao/internal/job.go new file mode 100644 index 0000000..1d2a21a --- /dev/null +++ b/internal/dao/internal/job.go @@ -0,0 +1,111 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// JobDao is the data access object for the table goframe_job. +type JobDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns JobColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// JobColumns defines and stores column names for the table goframe_job. +type JobColumns struct { + Id string // 主键ID + JobName string // 任务名称 + JobAlias string // 任务别名 + JobGroup string // 任务分组 + JobTrigger string // 任务触发器 + Status string // 任务状态:0-未发布 1-运行中 2-暂停 3-删除 + CronExpression string // Cron表达式 + ExecutePolicy string // 执行策略:1-立即执行 2-执行一次 3-放弃执行 + IsSync string // 是否同步任务:0-否 1-是 + Url string // 任务URL + Params string // 任务参数 + Note string // 任务备注 + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// jobColumns holds the columns for the table goframe_job. +var jobColumns = JobColumns{ + Id: "id", + JobName: "job_name", + JobAlias: "job_alias", + JobGroup: "job_group", + JobTrigger: "job_trigger", + Status: "status", + CronExpression: "cron_expression", + ExecutePolicy: "execute_policy", + IsSync: "is_sync", + Url: "url", + Params: "params", + Note: "note", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewJobDao creates and returns a new DAO object for table data access. +func NewJobDao(handlers ...gdb.ModelHandler) *JobDao { + return &JobDao{ + group: "default", + table: "goframe_job", + columns: jobColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *JobDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *JobDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *JobDao) Columns() JobColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *JobDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *JobDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *JobDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/job_log.go b/internal/dao/internal/job_log.go new file mode 100644 index 0000000..6b221ae --- /dev/null +++ b/internal/dao/internal/job_log.go @@ -0,0 +1,111 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// JobLogDao is the data access object for the table goframe_job_log. +type JobLogDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns JobLogColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// JobLogColumns defines and stores column names for the table goframe_job_log. +type JobLogColumns struct { + Id string // 主键ID + JobId string // 任务ID + JobName string // 任务名称 + JobGroup string // 任务分组 + JobTrigger string // 任务触发器 + JobMessage string // 任务日志信息 + CronExpression string // Cron表达式 + Status string // 执行状态:0-正常 1-失败 + StartTime string // 执行开始时间 + EndTime string // 执行结束时间 + ConsumeTime string // 执行耗时,单位:毫秒 + ExceptionInfo string // 异常信息 + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// jobLogColumns holds the columns for the table goframe_job_log. +var jobLogColumns = JobLogColumns{ + Id: "id", + JobId: "job_id", + JobName: "job_name", + JobGroup: "job_group", + JobTrigger: "job_trigger", + JobMessage: "job_message", + CronExpression: "cron_expression", + Status: "status", + StartTime: "start_time", + EndTime: "end_time", + ConsumeTime: "consume_time", + ExceptionInfo: "exception_info", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewJobLogDao creates and returns a new DAO object for table data access. +func NewJobLogDao(handlers ...gdb.ModelHandler) *JobLogDao { + return &JobLogDao{ + group: "default", + table: "goframe_job_log", + columns: jobLogColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *JobLogDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *JobLogDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *JobLogDao) Columns() JobLogColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *JobLogDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *JobLogDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *JobLogDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/level.go b/internal/dao/internal/level.go new file mode 100644 index 0000000..43d9f21 --- /dev/null +++ b/internal/dao/internal/level.go @@ -0,0 +1,97 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// LevelDao is the data access object for the table goframe_level. +type LevelDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns LevelColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// LevelColumns defines and stores column names for the table goframe_level. +type LevelColumns struct { + Id string // 主键ID + TenantId string // 租户ID:0=平台级 + Name string // 职级名称 + Status string // 职级状态:1-在用 2-停用 + Sort string // 职级排序 + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// levelColumns holds the columns for the table goframe_level. +var levelColumns = LevelColumns{ + Id: "id", + TenantId: "tenant_id", + Name: "name", + Status: "status", + Sort: "sort", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewLevelDao creates and returns a new DAO object for table data access. +func NewLevelDao(handlers ...gdb.ModelHandler) *LevelDao { + return &LevelDao{ + group: "default", + table: "goframe_level", + columns: levelColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *LevelDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *LevelDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *LevelDao) Columns() LevelColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *LevelDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *LevelDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *LevelDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/link.go b/internal/dao/internal/link.go new file mode 100644 index 0000000..42b4fde --- /dev/null +++ b/internal/dao/internal/link.go @@ -0,0 +1,107 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// LinkDao is the data access object for the table goframe_link. +type LinkDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns LinkColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// LinkColumns defines and stores column names for the table goframe_link. +type LinkColumns struct { + Id string // 主键ID + TenantId string // 租户ID:0=平台级 + Name string // 友链名称 + Type string // 友链类型:1-友情链接 2-合作伙伴 + Url string // 友链URL + Form string // 友链形式:1-文字链接 2-图片链接 + Image string // 友链图片 + Status string // 友链状态:1-正常 2-停用 + Sort string // 友链顺序 + Note string // 友链备注 + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// linkColumns holds the columns for the table goframe_link. +var linkColumns = LinkColumns{ + Id: "id", + TenantId: "tenant_id", + Name: "name", + Type: "type", + Url: "url", + Form: "form", + Image: "image", + Status: "status", + Sort: "sort", + Note: "note", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewLinkDao creates and returns a new DAO object for table data access. +func NewLinkDao(handlers ...gdb.ModelHandler) *LinkDao { + return &LinkDao{ + group: "default", + table: "goframe_link", + columns: linkColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *LinkDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *LinkDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *LinkDao) Columns() LinkColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *LinkDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *LinkDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *LinkDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/login_log.go b/internal/dao/internal/login_log.go new file mode 100644 index 0000000..b3cc53a --- /dev/null +++ b/internal/dao/internal/login_log.go @@ -0,0 +1,119 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// LoginLogDao is the data access object for the table goframe_login_log. +type LoginLogDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns LoginLogColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// LoginLogColumns defines and stores column names for the table goframe_login_log. +type LoginLogColumns struct { + Id string // 主键ID + Username string // 登录账号 + LogType string // 日志类型:1-登录,2-退出 + Source string // 登录来源:0-后台,1-前台 + Method string // 请求方法 + RequestMethod string // 请求方式 + Url string // 请求URL + Param string // 请求参数 + Result string // 返回结果 + Ip string // IP地址 + Location string // IP归属地 + Os string // 操作系统 + Browser string // 浏览器 + ConsumeTime string // 请求耗时 + Status string // 操作状态:0-正常,1-异常 + Error string // 错误信息 + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// loginLogColumns holds the columns for the table goframe_login_log. +var loginLogColumns = LoginLogColumns{ + Id: "id", + Username: "username", + LogType: "log_type", + Source: "source", + Method: "method", + RequestMethod: "request_method", + Url: "url", + Param: "param", + Result: "result", + Ip: "ip", + Location: "location", + Os: "os", + Browser: "browser", + ConsumeTime: "consume_time", + Status: "status", + Error: "error", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewLoginLogDao creates and returns a new DAO object for table data access. +func NewLoginLogDao(handlers ...gdb.ModelHandler) *LoginLogDao { + return &LoginLogDao{ + group: "default", + table: "goframe_login_log", + columns: loginLogColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *LoginLogDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *LoginLogDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *LoginLogDao) Columns() LoginLogColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *LoginLogDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *LoginLogDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *LoginLogDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/menu.go b/internal/dao/internal/menu.go new file mode 100644 index 0000000..cd5638d --- /dev/null +++ b/internal/dao/internal/menu.go @@ -0,0 +1,115 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// MenuDao is the data access object for the table goframe_menu. +type MenuDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns MenuColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// MenuColumns defines and stores column names for the table goframe_menu. +type MenuColumns struct { + Id string // 主键ID + Name string // 菜单名称 + Icon string // 菜单图标 + Icon2 string // 菜单图标 + ParentId string // 上级ID + Path string // 菜单路径 + Component string // 菜单组件 + Permission string // 权限节点 + Target string // 打开方式:0-组件 1-内链 2-外链 + Type string // 菜单类型:0-菜单 1-节点 + Status string // 菜单状态:0-正常 1-停用 + Hide string // 是否可见:0-可见 1-不可见 + Sort string // 排序 + Note string // 备注 + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// menuColumns holds the columns for the table goframe_menu. +var menuColumns = MenuColumns{ + Id: "id", + Name: "name", + Icon: "icon", + Icon2: "icon2", + ParentId: "parent_id", + Path: "path", + Component: "component", + Permission: "permission", + Target: "target", + Type: "type", + Status: "status", + Hide: "hide", + Sort: "sort", + Note: "note", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewMenuDao creates and returns a new DAO object for table data access. +func NewMenuDao(handlers ...gdb.ModelHandler) *MenuDao { + return &MenuDao{ + group: "default", + table: "goframe_menu", + columns: menuColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *MenuDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *MenuDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *MenuDao) Columns() MenuColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *MenuDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *MenuDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *MenuDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/notice.go b/internal/dao/internal/notice.go new file mode 100644 index 0000000..a890123 --- /dev/null +++ b/internal/dao/internal/notice.go @@ -0,0 +1,103 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// NoticeDao is the data access object for the table goframe_notice. +type NoticeDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns NoticeColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// NoticeColumns defines and stores column names for the table goframe_notice. +type NoticeColumns struct { + Id string // 主键ID + TenantId string // 租户ID:0=平台级 + Title string // 通知标题 + Cover string // 通知封面 + Type string // 通知类型:1-通知 2-公告 + ClickNum string // 点击率 + Status string // 通知状态:1-正常 2-停用 + Content string // 通知公告内容 + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// noticeColumns holds the columns for the table goframe_notice. +var noticeColumns = NoticeColumns{ + Id: "id", + TenantId: "tenant_id", + Title: "title", + Cover: "cover", + Type: "type", + ClickNum: "click_num", + Status: "status", + Content: "content", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewNoticeDao creates and returns a new DAO object for table data access. +func NewNoticeDao(handlers ...gdb.ModelHandler) *NoticeDao { + return &NoticeDao{ + group: "default", + table: "goframe_notice", + columns: noticeColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *NoticeDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *NoticeDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *NoticeDao) Columns() NoticeColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *NoticeDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *NoticeDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *NoticeDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/operation_log.go b/internal/dao/internal/operation_log.go new file mode 100644 index 0000000..54dd02d --- /dev/null +++ b/internal/dao/internal/operation_log.go @@ -0,0 +1,119 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// OperationLogDao is the data access object for the table goframe_operation_log. +type OperationLogDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns OperationLogColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// OperationLogColumns defines and stores column names for the table goframe_operation_log. +type OperationLogColumns struct { + Id string // 主键ID + Title string // 请求标题 + Type string // 请求类型 + Source string // 请求来源:0-后台,1-前台 + Method string // 请求方法 + RequestMethod string // 请求方式 + Url string // 请求URL + Param string // 请求参数 + Result string // 请求结果 + Ip string // 请求IP + Location string // 请求所在区域 + Os string // 操作系统 + Browser string // 浏览器 + ConsumeTime string // 请求耗时 + Status string // 请求状态:0-正常,1-异常 + Error string // 错误描述 + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// operationLogColumns holds the columns for the table goframe_operation_log. +var operationLogColumns = OperationLogColumns{ + Id: "id", + Title: "title", + Type: "type", + Source: "source", + Method: "method", + RequestMethod: "request_method", + Url: "url", + Param: "param", + Result: "result", + Ip: "ip", + Location: "location", + Os: "os", + Browser: "browser", + ConsumeTime: "consume_time", + Status: "status", + Error: "error", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewOperationLogDao creates and returns a new DAO object for table data access. +func NewOperationLogDao(handlers ...gdb.ModelHandler) *OperationLogDao { + return &OperationLogDao{ + group: "default", + table: "goframe_operation_log", + columns: operationLogColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *OperationLogDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *OperationLogDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *OperationLogDao) Columns() OperationLogColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *OperationLogDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *OperationLogDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *OperationLogDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/param.go b/internal/dao/internal/param.go new file mode 100644 index 0000000..12f3ce3 --- /dev/null +++ b/internal/dao/internal/param.go @@ -0,0 +1,103 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// ParamDao is the data access object for the table goframe_param. +type ParamDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns ParamColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// ParamColumns defines and stores column names for the table goframe_param. +type ParamColumns struct { + Id string // 主键ID + Name string // 参数名称 + Code string // 参数编码 + Value string // 参数值 + Type string // 参数类型:0-系统 1-业务 + Status string // 参数状态:1-正常 2-禁用 + Sort string // 参数排序 + Note string // 参数备注 + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// paramColumns holds the columns for the table goframe_param. +var paramColumns = ParamColumns{ + Id: "id", + Name: "name", + Code: "code", + Value: "value", + Type: "type", + Status: "status", + Sort: "sort", + Note: "note", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewParamDao creates and returns a new DAO object for table data access. +func NewParamDao(handlers ...gdb.ModelHandler) *ParamDao { + return &ParamDao{ + group: "default", + table: "goframe_param", + columns: paramColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *ParamDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *ParamDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *ParamDao) Columns() ParamColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *ParamDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *ParamDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *ParamDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/position.go b/internal/dao/internal/position.go new file mode 100644 index 0000000..65e5630 --- /dev/null +++ b/internal/dao/internal/position.go @@ -0,0 +1,97 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// PositionDao is the data access object for the table goframe_position. +type PositionDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns PositionColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// PositionColumns defines and stores column names for the table goframe_position. +type PositionColumns struct { + Id string // 主键ID + TenantId string // 租户ID:0=平台级 + Name string // 岗位名称 + Status string // 岗位状态:1-在用 2-停用 + Sort string // 岗位排序 + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// positionColumns holds the columns for the table goframe_position. +var positionColumns = PositionColumns{ + Id: "id", + TenantId: "tenant_id", + Name: "name", + Status: "status", + Sort: "sort", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewPositionDao creates and returns a new DAO object for table data access. +func NewPositionDao(handlers ...gdb.ModelHandler) *PositionDao { + return &PositionDao{ + group: "default", + table: "goframe_position", + columns: positionColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *PositionDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *PositionDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *PositionDao) Columns() PositionColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *PositionDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *PositionDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *PositionDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/role.go b/internal/dao/internal/role.go new file mode 100644 index 0000000..56dd2c1 --- /dev/null +++ b/internal/dao/internal/role.go @@ -0,0 +1,101 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// RoleDao is the data access object for the table goframe_role. +type RoleDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns RoleColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// RoleColumns defines and stores column names for the table goframe_role. +type RoleColumns struct { + Id string // 主键ID + TenantId string // 租户ID:0=平台级 + Name string // 角色名称 + Code string // 角色编码 + DataScope string // 数据范围:1-全部 2-本部门及以下 3-本部门 4-仅本人 + Sort string // 角色排序 + Note string // 角色备注 + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// roleColumns holds the columns for the table goframe_role. +var roleColumns = RoleColumns{ + Id: "id", + TenantId: "tenant_id", + Name: "name", + Code: "code", + DataScope: "data_scope", + Sort: "sort", + Note: "note", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewRoleDao creates and returns a new DAO object for table data access. +func NewRoleDao(handlers ...gdb.ModelHandler) *RoleDao { + return &RoleDao{ + group: "default", + table: "goframe_role", + columns: roleColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *RoleDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *RoleDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *RoleDao) Columns() RoleColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *RoleDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *RoleDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *RoleDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/role_menu.go b/internal/dao/internal/role_menu.go new file mode 100644 index 0000000..1731d0c --- /dev/null +++ b/internal/dao/internal/role_menu.go @@ -0,0 +1,93 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// RoleMenuDao is the data access object for the table goframe_role_menu. +type RoleMenuDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns RoleMenuColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// RoleMenuColumns defines and stores column names for the table goframe_role_menu. +type RoleMenuColumns struct { + Id string // 主键ID + RoleId string // 角色ID + MenuId string // 菜单ID + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// roleMenuColumns holds the columns for the table goframe_role_menu. +var roleMenuColumns = RoleMenuColumns{ + Id: "id", + RoleId: "role_id", + MenuId: "menu_id", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewRoleMenuDao creates and returns a new DAO object for table data access. +func NewRoleMenuDao(handlers ...gdb.ModelHandler) *RoleMenuDao { + return &RoleMenuDao{ + group: "default", + table: "goframe_role_menu", + columns: roleMenuColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *RoleMenuDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *RoleMenuDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *RoleMenuDao) Columns() RoleMenuColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *RoleMenuDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *RoleMenuDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *RoleMenuDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/tenant.go b/internal/dao/internal/tenant.go new file mode 100644 index 0000000..6c7644f --- /dev/null +++ b/internal/dao/internal/tenant.go @@ -0,0 +1,111 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TenantDao is the data access object for the table goframe_tenant. +type TenantDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TenantColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TenantColumns defines and stores column names for the table goframe_tenant. +type TenantColumns struct { + Id string // + Name string // 租户名称 + Code string // 租户编码(系统自动生成:T+4位ID左补零) + Contact string // 联系人 + Mobile string // 联系电话 + Email string // 邮箱 + Address string // 地址 + Logo string // 租户LOGO + Status string // 状态:1=正常 2=禁用 + ExpireTime string // 过期时间 + MaxUserNum string // 最大用户数 + Note string // 备注 + CreateUser string // + CreateTime string // + UpdateUser string // + UpdateTime string // + DelFlag string // +} + +// tenantColumns holds the columns for the table goframe_tenant. +var tenantColumns = TenantColumns{ + Id: "id", + Name: "name", + Code: "code", + Contact: "contact", + Mobile: "mobile", + Email: "email", + Address: "address", + Logo: "logo", + Status: "status", + ExpireTime: "expire_time", + MaxUserNum: "max_user_num", + Note: "note", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewTenantDao creates and returns a new DAO object for table data access. +func NewTenantDao(handlers ...gdb.ModelHandler) *TenantDao { + return &TenantDao{ + group: "default", + table: "goframe_tenant", + columns: tenantColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TenantDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TenantDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TenantDao) Columns() TenantColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TenantDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TenantDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TenantDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/user.go b/internal/dao/internal/user.go new file mode 100644 index 0000000..7160f15 --- /dev/null +++ b/internal/dao/internal/user.go @@ -0,0 +1,135 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// UserDao is the data access object for the table goframe_user. +type UserDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns UserColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// UserColumns defines and stores column names for the table goframe_user. +type UserColumns struct { + Id string // 主键ID + TenantId string // 租户ID:0=平台用户 + Code string // 用户编码 + Realname string // 用户姓名 + Gender string // 性别:1-男 2-女 3-保密 + Avatar string // 用户头像 + Mobile string // 手机号 + Email string // 邮箱 + DeptId string // 部门ID + LevelId string // 职级ID + PositionId string // 岗位ID + ProvinceCode string // 省份编码 + CityCode string // 城市编码 + DistrictCode string // 县区编码 + StreetCode string // 街道编码 + CityInfo string // 省市区信息 + Address string // 详细地址 + Username string // 用户名 + Password string // 密码 + Salt string // 加密盐 + Intro string // 个人简介 + Status string // 状态:1-正常 2-禁用 + Sort string // 排序 + Note string // 个人备注 + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// userColumns holds the columns for the table goframe_user. +var userColumns = UserColumns{ + Id: "id", + TenantId: "tenant_id", + Code: "code", + Realname: "realname", + Gender: "gender", + Avatar: "avatar", + Mobile: "mobile", + Email: "email", + DeptId: "dept_id", + LevelId: "level_id", + PositionId: "position_id", + ProvinceCode: "province_code", + CityCode: "city_code", + DistrictCode: "district_code", + StreetCode: "street_code", + CityInfo: "city_info", + Address: "address", + Username: "username", + Password: "password", + Salt: "salt", + Intro: "intro", + Status: "status", + Sort: "sort", + Note: "note", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewUserDao creates and returns a new DAO object for table data access. +func NewUserDao(handlers ...gdb.ModelHandler) *UserDao { + return &UserDao{ + group: "default", + table: "goframe_user", + columns: userColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *UserDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *UserDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *UserDao) Columns() UserColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *UserDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *UserDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *UserDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/user_role.go b/internal/dao/internal/user_role.go new file mode 100644 index 0000000..6f9a037 --- /dev/null +++ b/internal/dao/internal/user_role.go @@ -0,0 +1,93 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// UserRoleDao is the data access object for the table goframe_user_role. +type UserRoleDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns UserRoleColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// UserRoleColumns defines and stores column names for the table goframe_user_role. +type UserRoleColumns struct { + Id string // 主键ID + UserId string // 用户ID + RoleId string // 角色ID + CreateUser string // 创建人 + CreateTime string // 创建时间 + UpdateUser string // 更新人 + UpdateTime string // 更新时间 + DelFlag string // 删除标识:0-正常 1-已删除 +} + +// userRoleColumns holds the columns for the table goframe_user_role. +var userRoleColumns = UserRoleColumns{ + Id: "id", + UserId: "user_id", + RoleId: "role_id", + CreateUser: "create_user", + CreateTime: "create_time", + UpdateUser: "update_user", + UpdateTime: "update_time", + DelFlag: "del_flag", +} + +// NewUserRoleDao creates and returns a new DAO object for table data access. +func NewUserRoleDao(handlers ...gdb.ModelHandler) *UserRoleDao { + return &UserRoleDao{ + group: "default", + table: "goframe_user_role", + columns: userRoleColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *UserRoleDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *UserRoleDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *UserRoleDao) Columns() UserRoleColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *UserRoleDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *UserRoleDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *UserRoleDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/job.go b/internal/dao/job.go new file mode 100644 index 0000000..c66593b --- /dev/null +++ b/internal/dao/job.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// jobDao is the data access object for the table goframe_job. +// You can define custom methods on it to extend its functionality as needed. +type jobDao struct { + *internal.JobDao +} + +var ( + // Job is a globally accessible object for table goframe_job operations. + Job = jobDao{ + internal.NewJobDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *jobDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.JobDao.Ctx(ctx), "goframe_job") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/job_log.go b/internal/dao/job_log.go new file mode 100644 index 0000000..2676a87 --- /dev/null +++ b/internal/dao/job_log.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// jobLogDao is the data access object for the table goframe_job_log. +// You can define custom methods on it to extend its functionality as needed. +type jobLogDao struct { + *internal.JobLogDao +} + +var ( + // JobLog is a globally accessible object for table goframe_job_log operations. + JobLog = jobLogDao{ + internal.NewJobLogDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *jobLogDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.JobLogDao.Ctx(ctx), "goframe_job_log") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/level.go b/internal/dao/level.go new file mode 100644 index 0000000..f533e34 --- /dev/null +++ b/internal/dao/level.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// levelDao is the data access object for the table goframe_level. +// You can define custom methods on it to extend its functionality as needed. +type levelDao struct { + *internal.LevelDao +} + +var ( + // Level is a globally accessible object for table goframe_level operations. + Level = levelDao{ + internal.NewLevelDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *levelDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.LevelDao.Ctx(ctx), "goframe_level") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/link.go b/internal/dao/link.go new file mode 100644 index 0000000..a5a8436 --- /dev/null +++ b/internal/dao/link.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// linkDao is the data access object for the table goframe_link. +// You can define custom methods on it to extend its functionality as needed. +type linkDao struct { + *internal.LinkDao +} + +var ( + // Link is a globally accessible object for table goframe_link operations. + Link = linkDao{ + internal.NewLinkDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *linkDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.LinkDao.Ctx(ctx), "goframe_link") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/login_log.go b/internal/dao/login_log.go new file mode 100644 index 0000000..e7c65ab --- /dev/null +++ b/internal/dao/login_log.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// loginLogDao is the data access object for the table goframe_login_log. +// You can define custom methods on it to extend its functionality as needed. +type loginLogDao struct { + *internal.LoginLogDao +} + +var ( + // LoginLog is a globally accessible object for table goframe_login_log operations. + LoginLog = loginLogDao{ + internal.NewLoginLogDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *loginLogDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.LoginLogDao.Ctx(ctx), "goframe_login_log") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/menu.go b/internal/dao/menu.go new file mode 100644 index 0000000..a2fe86f --- /dev/null +++ b/internal/dao/menu.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// menuDao is the data access object for the table goframe_menu. +// You can define custom methods on it to extend its functionality as needed. +type menuDao struct { + *internal.MenuDao +} + +var ( + // Menu is a globally accessible object for table goframe_menu operations. + Menu = menuDao{ + internal.NewMenuDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *menuDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.MenuDao.Ctx(ctx), "goframe_menu") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/notice.go b/internal/dao/notice.go new file mode 100644 index 0000000..fe49d9f --- /dev/null +++ b/internal/dao/notice.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// noticeDao is the data access object for the table goframe_notice. +// You can define custom methods on it to extend its functionality as needed. +type noticeDao struct { + *internal.NoticeDao +} + +var ( + // Notice is a globally accessible object for table goframe_notice operations. + Notice = noticeDao{ + internal.NewNoticeDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *noticeDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.NoticeDao.Ctx(ctx), "goframe_notice") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/operation_log.go b/internal/dao/operation_log.go new file mode 100644 index 0000000..0705146 --- /dev/null +++ b/internal/dao/operation_log.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// operationLogDao is the data access object for the table goframe_operation_log. +// You can define custom methods on it to extend its functionality as needed. +type operationLogDao struct { + *internal.OperationLogDao +} + +var ( + // OperationLog is a globally accessible object for table goframe_operation_log operations. + OperationLog = operationLogDao{ + internal.NewOperationLogDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *operationLogDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.OperationLogDao.Ctx(ctx), "goframe_operation_log") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/param.go b/internal/dao/param.go new file mode 100644 index 0000000..d9d9b92 --- /dev/null +++ b/internal/dao/param.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// paramDao is the data access object for the table goframe_param. +// You can define custom methods on it to extend its functionality as needed. +type paramDao struct { + *internal.ParamDao +} + +var ( + // Param is a globally accessible object for table goframe_param operations. + Param = paramDao{ + internal.NewParamDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *paramDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.ParamDao.Ctx(ctx), "goframe_param") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/position.go b/internal/dao/position.go new file mode 100644 index 0000000..6c603ad --- /dev/null +++ b/internal/dao/position.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// positionDao is the data access object for the table goframe_position. +// You can define custom methods on it to extend its functionality as needed. +type positionDao struct { + *internal.PositionDao +} + +var ( + // Position is a globally accessible object for table goframe_position operations. + Position = positionDao{ + internal.NewPositionDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *positionDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.PositionDao.Ctx(ctx), "goframe_position") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/role.go b/internal/dao/role.go new file mode 100644 index 0000000..92efdf6 --- /dev/null +++ b/internal/dao/role.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// roleDao is the data access object for the table goframe_role. +// You can define custom methods on it to extend its functionality as needed. +type roleDao struct { + *internal.RoleDao +} + +var ( + // Role is a globally accessible object for table goframe_role operations. + Role = roleDao{ + internal.NewRoleDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *roleDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.RoleDao.Ctx(ctx), "goframe_role") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/role_menu.go b/internal/dao/role_menu.go new file mode 100644 index 0000000..74252ef --- /dev/null +++ b/internal/dao/role_menu.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// roleMenuDao is the data access object for the table goframe_role_menu. +// You can define custom methods on it to extend its functionality as needed. +type roleMenuDao struct { + *internal.RoleMenuDao +} + +var ( + // RoleMenu is a globally accessible object for table goframe_role_menu operations. + RoleMenu = roleMenuDao{ + internal.NewRoleMenuDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *roleMenuDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.RoleMenuDao.Ctx(ctx), "goframe_role_menu") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/tenant.go b/internal/dao/tenant.go new file mode 100644 index 0000000..36209c7 --- /dev/null +++ b/internal/dao/tenant.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// tenantDao is the data access object for the table goframe_tenant. +// You can define custom methods on it to extend its functionality as needed. +type tenantDao struct { + *internal.TenantDao +} + +var ( + // Tenant is a globally accessible object for table goframe_tenant operations. + Tenant = tenantDao{ + internal.NewTenantDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *tenantDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.TenantDao.Ctx(ctx), "goframe_tenant") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/user.go b/internal/dao/user.go new file mode 100644 index 0000000..87da972 --- /dev/null +++ b/internal/dao/user.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// userDao is the data access object for the table goframe_user. +// You can define custom methods on it to extend its functionality as needed. +type userDao struct { + *internal.UserDao +} + +var ( + // User is a globally accessible object for table goframe_user operations. + User = userDao{ + internal.NewUserDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *userDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.UserDao.Ctx(ctx), "goframe_user") +} + +// Add your custom methods and functionality below. diff --git a/internal/dao/user_role.go b/internal/dao/user_role.go new file mode 100644 index 0000000..460592c --- /dev/null +++ b/internal/dao/user_role.go @@ -0,0 +1,42 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// 自定义模板:hack/tpl/dao_index.tpl +// - 构造时挂载全局模型钩子(审计字段 / 软删除改写;主键交数据库自增) +// - Ctx 覆写注入软删除与租户隔离条件(架构红线执行点) +// ================================================================================= + +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/dao/internal" + "easygoadmin/internal/model/hooks" +) + +// userRoleDao is the data access object for the table goframe_user_role. +// You can define custom methods on it to extend its functionality as needed. +type userRoleDao struct { + *internal.UserRoleDao +} + +var ( + // UserRole is a globally accessible object for table goframe_user_role operations. + UserRole = userRoleDao{ + internal.NewUserRoleDao(func(m *gdb.Model) *gdb.Model { + // 全局钩子:Insert 补齐审计字段/租户ID(主键自增);Update 盖章;Delete 改写为软删除 + return m.Hook(hooks.Handler()) + }), + } +) + +// Ctx 覆写:在 SQL 构建之前注入架构红线条件 —— +// 所有表追加 del_flag=0;租户级表对租户上下文追加 tenant_id IN (0, 当前租户)。 +// 业务代码必须经由本方法访问模型,禁止绕过 dao 直连 DB。 +func (d *userRoleDao) Ctx(ctx context.Context) *gdb.Model { + return hooks.ApplyCtx(ctx, d.UserRoleDao.Ctx(ctx), "goframe_user_role") +} + +// Add your custom methods and functionality below. diff --git a/internal/logic/article.go b/internal/logic/article.go new file mode 100644 index 0000000..2784936 --- /dev/null +++ b/internal/logic/article.go @@ -0,0 +1,226 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + "strings" + + "easygoadmin/api/v1" + "easygoadmin/internal/consts" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" +) + +// ============================== 文章业务实现 ============================== + +// sArticle 文章业务实现 +// 实现 service.IArticle 接口,承载文章模块的业务逻辑 +type sArticle struct{} + +// articleSerializeMaps 枚举字段到字典编码的映射 +// {field}Text 由 dict_item 解析,字典管理页可修改文案 +var articleSerializeMaps = map[string]string{ + "status": "article_status", +} + +// articleSpec 文章模块声明 +// +// Status 特有方法与文件字段保存在 Add/Update 手写体内处理; +// 文件与富文本读侧还原由基座 readTransform 按下列声明自动完成 +var articleSpec = ModuleSpec[v1.ArticleItem, v1.ArticleListItem, v1.ArticleDetailRes]{ + Model: dao.Article.Ctx, + LikeFields: []string{"title", "author"}, + EqFields: []string{"status", "category_id"}, // 零值不筛(列表初始显示全部) + OrderBy: "sort, id DESC", + ListOrderBy: "sort, id", + SerializeMaps: articleSerializeMaps, + FileFields: []string{"cover"}, // 读侧补域名 + MultiFileFields: []string{"images"}, // 逗号多图:读侧逐个补域名 + ContentFields: []string{"content"}, // 富文本:读侧 [IMG_URL] 还原 + FileSaveDir: "article", + AfterPageList: articleExpandCategory, // 行内分类名批量补全 + AfterDetail: articleSplitImagesList, // 多图拆分为 imagesList 数组 +} + +// articleExpandCategory 分页行分类名批量补全 +// 将分类ID映射为分类名称,避免逐行查询 +func articleExpandCategory(ctx context.Context, list []*v1.ArticleItem, req any) error { + catNames := (&sArticle{}).categoryNames(ctx) + for _, item := range list { + item.CategoryName = catNames[int64(item.CategoryId)] + } + return nil +} + +func init() { + service.RegisterArticle(&sArticle{}) +} + +// Page 文章分页 +// 按查询条件分页返回文章列表 +func (s *sArticle) Page(ctx context.Context, req *v1.ArticlePageReq) (res *v1.ArticlePageRes, err error) { + list, total, err := articleSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize()) + if err != nil { + return nil, err + } + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil +} + +// List 文章全量列表 +// 按查询条件返回文章精简列表 +func (s *sArticle) List(ctx context.Context, req *v1.ArticleListReq) (res v1.ArticleListRes, err error) { + list, err := articleSpec.List(ctx, req) + if err != nil { + return nil, err + } + return derefSlice(list), nil +} + +// Detail 文章详情 +// 按文章ID查询详情 +func (s *sArticle) Detail(ctx context.Context, req *v1.ArticleDetailReq) (res *v1.ArticleDetailRes, err error) { + return articleSpec.Detail(ctx, req.Id, req) +} + +// articleSplitImagesList 详情装配 +// 将多图逗号串(读转换已补域名)拆分为 imagesList 数组 +func articleSplitImagesList(ctx context.Context, item *v1.ArticleDetailRes, req any) error { + if item.Images == "" { + return nil + } + for _, u := range strings.Split(item.Images, ",") { + if u = strings.TrimSpace(u); u != "" { + item.ImagesList = append(item.ImagesList, u) + } + } + return nil +} + +// Add 新增文章 +// 封面与多图由临时目录迁移至正式目录,正文富文本本地化后落库 +func (s *sArticle) Add(ctx context.Context, req *v1.ArticleAddReq) (res *v1.IdRes, err error) { + // 封面/多图 temp → 正式目录;正文富文本本地化([IMG_URL] 占位符契约) + req.Cover = SaveFileField(ctx, req.Cover, "article") + req.Images = SaveMultiFileField(ctx, req.Images, "article") + req.Content = LocalizeContentFields(ctx, req.Content, "article") + result, err := dao.Article.Ctx(ctx).Data(map[string]any{ + "category_id": req.CategoryId, + "title": req.Title, + "cover": req.Cover, + "images": req.Images, + "intro": req.Intro, + "content": req.Content, + "author": req.Author, + "status": req.Status, + "sort": req.Sort, + "tenant_id": consts.GetTenantID(ctx), + }).Insert() + if err != nil { + return nil, err + } + idv, _ := result.LastInsertId() + return &v1.IdRes{Id: idv}, nil +} + +// Update 修改文章 +// 封面与多图由临时目录迁移至正式目录,正文富文本本地化后更新 +func (s *sArticle) Update(ctx context.Context, req *v1.ArticleUpdateReq) (res *v1.ArticleUpdateRes, err error) { + req.Cover = SaveFileField(ctx, req.Cover, "article") + req.Images = SaveMultiFileField(ctx, req.Images, "article") + req.Content = LocalizeContentFields(ctx, req.Content, "article") + if _, err = dao.Article.Ctx(ctx).Where("id", req.Id).Data(map[string]any{ + "category_id": req.CategoryId, + "title": req.Title, + "cover": req.Cover, + "images": req.Images, + "intro": req.Intro, + "content": req.Content, + "author": req.Author, + "status": req.Status, + "click": req.Click, + "sort": req.Sort, + }).Update(); err != nil { + return nil, err + } + return &v1.ArticleUpdateRes{}, nil +} + +// Status 文章状态 +// 设置文章的草稿或发布状态 +func (s *sArticle) Status(ctx context.Context, req *v1.ArticleStatusReq) (res *v1.ArticleStatusRes, err error) { + if _, err = dao.Article.Ctx(ctx).Where("id", req.Id).Data(map[string]any{"status": req.Status}).Update(); err != nil { + return nil, err + } + return &v1.ArticleStatusRes{}, nil +} + +// Delete 删除文章 +// 按文章ID删除单条记录 +func (s *sArticle) Delete(ctx context.Context, req *v1.ArticleDeleteReq) (res *v1.ArticleDeleteRes, err error) { + if err = articleSpec.Delete(ctx, req.Id); err != nil { + return nil, err + } + return &v1.ArticleDeleteRes{}, nil +} + +// BatchDelete 批量删除文章 +// 按文章ID列表批量删除记录 +func (s *sArticle) BatchDelete(ctx context.Context, req *v1.ArticleBatchDeleteReq) (res *v1.ArticleBatchDeleteRes, err error) { + if err = articleSpec.BatchDelete(ctx, req.Ids); err != nil { + return nil, err + } + return &v1.ArticleBatchDeleteRes{}, nil +} + +// categoryNames 查询分类ID到名称的映射 +func (s *sArticle) categoryNames(ctx context.Context) map[int64]string { + result := make(map[int64]string) + rows, err := dao.Category.Ctx(ctx).Fields("id, category_name").All() + if err != nil { + return result + } + for _, r := range rows { + result[r["id"].Int64()] = r["category_name"].String() + } + return result +} diff --git a/internal/logic/auth.go b/internal/logic/auth.go new file mode 100644 index 0000000..07a211e --- /dev/null +++ b/internal/logic/auth.go @@ -0,0 +1,494 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + "encoding/json" + "strings" + "time" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + + "easygoadmin/api/v1" + "easygoadmin/internal/consts" + "easygoadmin/internal/dao" + "easygoadmin/internal/middleware" + "easygoadmin/internal/service" + "easygoadmin/utility/cache" + "easygoadmin/utility/captcha" + "easygoadmin/utility/crypto" + "easygoadmin/utility/ip2region" + jwtutil "easygoadmin/utility/jwt" + "easygoadmin/utility/ua" +) + +// ============================== 认证业务实现 ============================== + +// sAuth 认证业务实现 +// 实现 service.IAuth 接口,承载登录、登出、验证码、令牌等业务逻辑 +type sAuth struct{} + +func init() { + service.RegisterAuth(&sAuth{}) + // 中间件依赖接线:身份扩展加载 / 租户状态校验 + middleware.SetIdentityLoader(LoadIdentity) + middleware.SetTenantLoader(LoadTenant) +} + +// Login 登录 +// 流程:验证码校验 → 定位用户 → 账号状态与租户校验 → 密码校验 → 签发双令牌 → 写入登录日志 +func (s *sAuth) Login(ctx context.Context, req *v1.LoginReq) (res *v1.LoginRes, err error) { + start := time.Now() + username := strings.TrimSpace(req.Username) + + // 1. 图形验证码(提供 key 时强制校验) + if !captcha.Verify(ctx, req.Key, req.Code) { + s.writeLoginLog(ctx, username, consts.LoginLogTypeLogin, 1, "验证码错误或已过期", start, req) + return nil, gerror.NewCode(consts.CodeCaptchaError) + } + + // 2. 定位用户(用户名在租户内唯一;未指定租户时全局匹配,优先平台租户) + user, err := s.findUser(ctx, username, req.TenantCode) + if err != nil { + s.writeLoginLog(ctx, username, consts.LoginLogTypeLogin, 1, err.Error(), start, req) + return nil, err + } + + // 3. 账号状态 + if user.Status != 1 { + s.writeLoginLog(ctx, username, consts.LoginLogTypeLogin, 1, "账号已被禁用", start, req) + return nil, gerror.NewCode(consts.CodeUserDisabled) + } + + // 4. 租户状态与过期校验 + if info := mustLoadTenant(ctx, user.TenantId); info != nil { + if info.Status != 1 { + s.writeLoginLog(ctx, username, consts.LoginLogTypeLogin, 1, "租户已禁用", start, req) + return nil, gerror.NewCode(consts.CodeTenantDisabled) + } + if info.ExpireTime != nil && info.ExpireTime.Time.Before(time.Now()) { + s.writeLoginLog(ctx, username, consts.LoginLogTypeLogin, 1, "租户已过期", start, req) + return nil, gerror.NewCode(consts.CodeTenantExpired) + } + } + + // 5. 密码校验(外部 salt + bcrypt(password+salt);salt 为空兼容旧数据) + if !crypto.IsBcrypt(user.Password) || !crypto.VerifyPassword(user.Password, req.Password, user.Salt) { + s.writeLoginLog(ctx, username, consts.LoginLogTypeLogin, 1, "密码错误", start, req) + return nil, gerror.NewCode(consts.CodePasswordWrong) + } + + // 6. 签发双令牌 + loginCtx := consts.WithUserID(consts.WithUsername(ctx, username), int(user.Id)) + loginCtx = consts.WithTenantID(loginCtx, user.TenantId) + access, accessClaims, err := jwtutil.Sign(loginCtx, jwtutil.TokenTypeAccess, + int(user.Id), user.TenantId, user.Username, user.Realname) + if err != nil { + return nil, err + } + refresh, refreshClaims, err := jwtutil.Sign(loginCtx, jwtutil.TokenTypeRefresh, + int(user.Id), user.TenantId, user.Username, user.Realname) + if err != nil { + return nil, err + } + + // 7. 登录日志(成功) + s.writeLoginLog(ctx, username, consts.LoginLogTypeLogin, 0, "", start, req) + + // 在线登记(访问令牌 + 刷新令牌信息) + MarkOnline(ctx, accessClaims, refreshClaims, access, refresh, int(user.Id), user.Username, user.Realname, + consts.GetClientIP(ctx), consts.GetUserAgent(ctx)) + + expireSec := int64(jwtutil.ExpireDuration(ctx, jwtutil.TokenTypeAccess).Seconds()) + return &v1.LoginRes{ + AccessToken: access, + RefreshToken: refresh, + ExpiresIn: expireSec, + TokenType: "Bearer", + }, nil +} + +// Logout 登出 +// 将当前 access token 的 jti 加入黑名单,并写入登录日志 +func (s *sAuth) Logout(ctx context.Context, req *v1.LogoutReq) (res *v1.LogoutRes, err error) { + start := time.Now() + jti := consts.GetTokenJTI(ctx) + if jti != "" { + ttl := int(time.Until(consts.GetTokenExpire(ctx)).Seconds()) + if ttl > 0 { + _ = cache.Set(ctx, consts.CacheKeyTokenBlacklist(jti), "1", ttl) + } + MarkOffline(ctx, jti) + } + s.writeLoginLog(ctx, consts.GetUsername(ctx), consts.LoginLogTypeLogout, 0, "", start, nil) + return &v1.LogoutRes{}, nil +} + +// Captcha 生成图形验证码 +// 返回验证码标识与图片内容 +func (s *sAuth) Captcha(ctx context.Context, req *v1.CaptchaReq) (res *v1.CaptchaRes, err error) { + key, dataURL, err := captcha.Generate(ctx) + if err != nil { + return nil, err + } + return &v1.CaptchaRes{Key: key, Captcha: dataURL}, nil +} + +// Refresh 刷新令牌 +// 支持 refresh_token 轮换,并兼容密码模式兑换 +func (s *sAuth) Refresh(ctx context.Context, req *v1.RefreshReq) (res *v1.RefreshRes, err error) { + // 密码模式(前端 login2 变体):复用 Login + if req.GrantType == "password" || req.RefreshToken == "" { + loginRes, lerr := s.Login(ctx, &v1.LoginReq{ + Username: req.Username, + Password: req.Password, + Code: req.Code, + Key: req.Key, + }) + if lerr != nil { + return nil, lerr + } + return &v1.RefreshRes{ + AccessToken: loginRes.AccessToken, + RefreshToken: loginRes.RefreshToken, + ExpiresIn: loginRes.ExpiresIn, + TokenType: loginRes.TokenType, + }, nil + } + + // refresh_token 模式 + claims, err := jwtutil.Parse(ctx, req.RefreshToken) + if err != nil { + return nil, err + } + if claims.Type != jwtutil.TokenTypeRefresh { + return nil, gerror.NewCode(consts.CodeTokenInvalid) + } + // 旧 refresh 已拉黑则失效 + if cache.Exists(ctx, consts.CacheKeyTokenBlacklist(claims.ID)) { + return nil, gerror.NewCode(consts.CodeTokenInvalid) + } + // 轮换:旧 refresh 入黑名单 + if claims.ExpiresAt != nil { + ttl := int(time.Until(claims.ExpiresAt.Time).Seconds()) + if ttl > 0 { + _ = cache.Set(ctx, consts.CacheKeyTokenBlacklist(claims.ID), "1", ttl) + } + } + access, accessClaims, err := jwtutil.Sign(ctx, jwtutil.TokenTypeAccess, + claims.UserID, claims.TenantID, claims.Username, claims.Realname) + if err != nil { + return nil, err + } + // 令牌轮换后刷新在线登记 + refresh, refreshClaims, err := jwtutil.Sign(ctx, jwtutil.TokenTypeRefresh, + claims.UserID, claims.TenantID, claims.Username, claims.Realname) + if err != nil { + return nil, err + } + // 令牌轮换后刷新在线登记 + MarkOnline(ctx, accessClaims, refreshClaims, access, refresh, claims.UserID, claims.Username, claims.Realname, + consts.GetClientIP(ctx), consts.GetUserAgent(ctx)) + return &v1.RefreshRes{ + AccessToken: access, + RefreshToken: refresh, + ExpiresIn: int64(jwtutil.ExpireDuration(ctx, jwtutil.TokenTypeAccess).Seconds()), + TokenType: "Bearer", + }, nil +} + +// CheckToken 校验令牌有效性 +// JWT 中间件已通过即视为有效,返回当前登录用户ID +func (s *sAuth) CheckToken(ctx context.Context, req *v1.CheckTokenReq) (res *v1.CheckTokenRes, err error) { + return &v1.CheckTokenRes{ + Valid: true, + UserId: int64(consts.GetUserID(ctx)), + }, nil +} + +// RemoveToken 指定令牌撤销(踢下线) +// 将指定令牌加入黑名单 +func (s *sAuth) RemoveToken(ctx context.Context, req *v1.RemoveTokenReq) (res *v1.RemoveTokenRes, err error) { + claims, err := jwtutil.Parse(ctx, req.Token) + if err != nil { + return nil, err + } + if claims.ExpiresAt != nil { + ttl := int(time.Until(claims.ExpiresAt.Time).Seconds()) + if ttl > 0 { + _ = cache.Set(ctx, consts.CacheKeyTokenBlacklist(claims.ID), "1", ttl) + } + } + return &v1.RemoveTokenRes{}, nil +} + +// ---------- 定位与校验 ---------- + +// findUser 按用户名与可选租户编码定位用户 +// 未指定租户时全局匹配,多租户同名优先平台租户,仍歧义则提示携带租户编码 +func (s *sAuth) findUser(ctx context.Context, username, tenantCode string) (*entityUser, error) { + if username == "" { + return nil, gerror.NewCode(consts.CodeUserNotFound) + } + query := dao.User.Ctx(ctx).Where("username", username) + tenantID := 0 + if tenantCode != "" { + tOne, terr := dao.Tenant.Ctx(ctx).Where("code", tenantCode).One() + if terr != nil || len(tOne) == 0 { + return nil, gerror.NewCode(consts.CodeTenantNotFound) + } + tenantID = tOne["id"].Int() + query = query.Where("tenant_id", tenantID) + } + list, err := query.All() + if err != nil { + return nil, err + } + if len(list) == 0 { + return nil, gerror.NewCode(consts.CodeUserNotFound) + } + // 多租户同名:优先平台租户(tenant_id=0),仍歧义则要求携带租户编码 + if len(list) == 1 { + return rowToUser(list[0]), nil + } + for _, row := range list { + if row["tenant_id"].Int() == 0 { + return rowToUser(row), nil + } + } + return nil, gerror.NewCodef(consts.CodeUserNotFound, "该用户名存在于多个租户,请携带租户编码登录") +} + +// entityUser 用户摘要 +// 登录所需的最小字段集合 +type entityUser struct { + Id int64 // 用户ID + TenantId int // 租户ID + Username string // 用户名 + Password string // 密码 + Salt string // 密码盐 + Realname string // 真实姓名 + Status int // 状态:1-正常 2-禁用 +} + +// rowToUser 数据库记录转换为用户摘要 +func rowToUser(row gdb.Record) *entityUser { + return &entityUser{ + Id: row["id"].Int64(), + TenantId: row["tenant_id"].Int(), + Username: row["username"].String(), + Password: row["password"].String(), + Salt: row["salt"].String(), + Realname: row["realname"].String(), + Status: row["status"].Int(), + } +} + +// ---------- 中间件加载器 ---------- + +// LoadIdentity 加载用户角色与权限 +// 由 JWT 中间件调用,结果由中间件缓存 300 秒 +func LoadIdentity(ctx context.Context, userID int) (*middleware.Identity, error) { + // DataScope 以 0 为"未取到"哨兵,角色循环内取最小(最宽松),结束时回退"全部" + idn := &middleware.Identity{Roles: []string{}, Perms: []string{}} + if userID <= 0 { + return idn, nil + } + // 所属部门(数据权限过滤用) + if one, e := dao.User.Ctx(ctx).Where("id", userID).Fields("dept_id").One(); e == nil && len(one) > 0 { + idn.DeptId = one["dept_id"].Int() + } + // 角色编码 + roleRows, err := dao.UserRole.Ctx(ctx).Where("user_id", userID).All() + if err != nil || len(roleRows) == 0 { + return idn, err + } + roleIds := make([]int64, 0, len(roleRows)) + for _, r := range roleRows { + roleIds = append(roleIds, r["role_id"].Int64()) + } + roleRows2, err := dao.Role.Ctx(ctx).WhereIn("id", roleIds).All() + if err != nil { + return idn, err + } + for _, r := range roleRows2 { + idn.Roles = append(idn.Roles, r["code"].String()) + // 数据范围多角色取最宽松(数值越小越宽;缺列值按"全部"参与比较) + sc := r["data_scope"].Int() + if sc <= 0 { + sc = consts.DataScopeAll + } + if idn.DataScope == 0 || sc < idn.DataScope { + idn.DataScope = sc + } + } + if idn.DataScope == 0 { + idn.DataScope = consts.DataScopeAll // 无角色或角色均无该列时回退 + } + // 权限节点(role → menu.permission) + menuRows, err := dao.RoleMenu.Ctx(ctx).WhereIn("role_id", roleIds).All() + if err != nil || len(menuRows) == 0 { + return idn, err + } + menuIds := make([]int64, 0, len(menuRows)) + for _, r := range menuRows { + menuIds = append(menuIds, r["menu_id"].Int64()) + } + permRows, err := dao.Menu.Ctx(ctx).WhereIn("id", menuIds). + Where("type", 1).Where("status", 0).Where("hide", 0).All() // 仅取权限节点(type=1),启用且非隐藏 + if err != nil { + return idn, err + } + seen := make(map[string]bool) + for _, r := range permRows { + p := r["permission"].String() + if p != "" && !seen[p] { + seen[p] = true + idn.Perms = append(idn.Perms, p) + } + } + return idn, nil +} + +// LoadTenant 加载租户状态摘要 +// 由 Tenant 中间件调用,缓存 60 秒;租户不存在按禁用处理,实现失效关闭 +func LoadTenant(ctx context.Context, tenantID int) (*middleware.TenantInfo, error) { + if tenantID == 0 { + return &middleware.TenantInfo{ID: 0, Status: 1}, nil + } + one, err := dao.Tenant.Ctx(ctx).Where("id", tenantID).One() + if err != nil { + return nil, err // 基础设施错误:中间件放行 + } + if len(one) == 0 { + return &middleware.TenantInfo{ID: tenantID, Status: 2}, nil // 租户不存在 → 禁用 + } + info := &middleware.TenantInfo{ + ID: tenantID, + Status: one["status"].Int(), + } + if v := one["expire_time"]; !v.IsNil() { + t := v.GTime() + if t != nil { + info.ExpireTime = t + } + } + return info, nil +} + +// mustLoadTenant 登录路径上的租户校验 +// 带 60 秒缓存,避免登录过程中重复查询租户信息 +func mustLoadTenant(ctx context.Context, tenantID int) *middleware.TenantInfo { + ck := consts.CacheKeyTenantInfo(tenantID) + if v, ok := cache.Get(ctx, ck); ok { + info := &middleware.TenantInfo{} + if json.Unmarshal([]byte(v), info) == nil { + return info + } + } + info, err := LoadTenant(ctx, tenantID) + if err != nil || info == nil { + return nil + } + if b, berr := json.Marshal(info); berr == nil { + _ = cache.Set(ctx, ck, string(b), 60) + } + return info +} + +// writeLoginLog 写入登录日志 +// 覆盖登录日志表所有 NOT NULL 字段,写入失败仅记录警告不影响主流程 +func (s *sAuth) writeLoginLog(ctx context.Context, username string, logType, status int, errMsg string, start time.Time, req *v1.LoginReq) { + param := "" + if req != nil { + if b, err := json.Marshal(map[string]any{ + "username": req.Username, + "code": maskValue(req.Code), + "key": req.Key, + }); err == nil { + param = string(b) + } + } + result := "成功" + if status != 0 { + result = "失败" + } + // 登录/退出日志的 create_user 显式取本次登录账号: + // 登录走免登录公开路由,ctx 中无登录身份,审计钩子取不到操作人 + createUser := username + if createUser == "" { + createUser = consts.GetUsername(ctx) + } + _, err := dao.LoginLog.Ctx(ctx).Data(map[string]any{ + "username": username, + "log_type": logType, + "source": consts.LoginSourceAdmin, + "method": consts.GetReqMethod(ctx), + "request_method": consts.GetReqMethod(ctx), + "url": consts.GetReqURL(ctx), + "param": param, + "result": result, + "ip": consts.GetClientIP(ctx), + "location": ip2region.Location(consts.GetClientIP(ctx)), + "os": ua.ParseOS(consts.GetUserAgent(ctx)), + "browser": ua.ParseBrowser(consts.GetUserAgent(ctx)), + "consume_time": int(time.Since(start).Milliseconds()), + "status": status, + "error": errMsg, + "create_user": createUser, + }).Insert() + if err != nil { + g.Log().Warningf(ctx, "登录日志写入失败: %v", err) + } +} + +// maskValue 敏感值脱敏 +// 空值返回空串,非空统一返回掩码 +func maskValue(s string) string { + if s == "" { + return "" + } + return "***" +} diff --git a/internal/logic/base.go b/internal/logic/base.go new file mode 100644 index 0000000..b812d5f --- /dev/null +++ b/internal/logic/base.go @@ -0,0 +1,419 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +// ============================================================================ +// logic 共享能力层:跨模块复用的运行时横切能力(组合式,非继承) +// +// 【架构约定 · 企业级纪律】 +// 1. 本文件按能力分区:文件字段 / 富文本 / 枚举文本 / 唯一校验 / 事务 / 分块查询 / 树 +// 2. 依赖方向单向:base 只允许依赖 dao / utility / 内部 consts,禁止引用任何模块文件 +// 3. 导出函数上限 15 个:超出即按能力拆为 internal/logic/base/ 子包(防"万能工具箱"腐化) +// 4. CheckUnique 仅为 UX 快速报错(先查后插存在 TOCTOU 竞态),并发唯一性以数据库 +// 唯一索引(uk_*)为准,调用点必须保证对应 uk 索引存在,冲突时以错误1062翻译提示 +// 5. 富文本占位符 [IMG_URL] 是持久化契约(见 architecture.md),还原只能走 ExpandContentFields +// ============================================================================ + +import ( + "context" + "fmt" + "os" + "path/filepath" + "reflect" + "regexp" + "sort" + "strings" + "sync" + "time" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + + "easygoadmin/internal/consts" + "easygoadmin/internal/dao" + "easygoadmin/utility/upload" +) + +// =========================================================================== +// 一、文件字段(保存前:临时文件转正式目录) +// 声明式用法(ModuleSpec 逃生舱手写 Add/Update 内联场景): +// FileFields: []string{"avatar"}, FileSaveDir: "user" // 字段名单 + 模块统一目录 +// SaveFileFields(ctx, data, spec.FileFields, spec.FileSaveDir) // 临时 → 正式,原地更新 +// =========================================================================== + +// SaveFileFields 单文件字段批量处理 +// 值为临时文件URL时移动到正式目录,其余原样保留 +func SaveFileFields(ctx context.Context, data map[string]any, fields []string, saveDir string) { + for _, field := range fields { + if v, ok := data[field]; ok { + if s, ok2 := v.(string); ok2 && s != "" { + data[field] = upload.SaveFromTemp(ctx, s, saveDir) + } + } + } +} + +// SaveMultiFileFields 多文件字段(逗号分隔)批量处理 +// 各文件为临时文件URL时移动到正式目录,其余原样保留 +func SaveMultiFileFields(ctx context.Context, data map[string]any, fields []string, saveDir string) { + for _, field := range fields { + if v, ok := data[field]; ok { + if s, ok2 := v.(string); ok2 && s != "" { + data[field] = upload.SaveFromTempMulti(ctx, s, saveDir) + } + } + } +} + +// SaveFileField 单值便捷版 +// 供模块内联场景使用,临时文件URL转正式目录 +func SaveFileField(ctx context.Context, fileUrl, saveDir string) string { + return upload.SaveFromTemp(ctx, fileUrl, saveDir) +} + +// SaveMultiFileField 逗号分隔多文件串便捷版 +// 供模块内联场景使用,临时文件URL批量转正式目录 +func SaveMultiFileField(ctx context.Context, fileUrls, saveDir string) string { + return upload.SaveFromTempMulti(ctx, fileUrls, saveDir) +} + +// =========================================================================== +// 二、富文本(存库占位符化 / 读取还原) +// 存库契约:正文内本站文件引用统一写为 [IMG_URL]/{相对uploadDir路径} 占位符 +// - temp 文件URL → 移动转正后写占位符 +// - 本站正式文件 → 剥离域名/静态前缀写占位符 +// - 外站 URL → 不处理(规避 SSRF) +// 读取契约:ExpandContentFields 是唯一还原出口(占位符 → 静态访问基址拼接) +// =========================================================================== + +var ( + contentSrcRe = regexp.MustCompile(`(?i)<(?:img|video|source|embed)[^>]+src=["']?([^"'>\s]+)`) + contentHrefRe = regexp.MustCompile(`(?i)]+href=["']?([^"'>\s]+\.\w{2,5})["']?`) +) + +// LocalizeContentFields 富文本存库前本地化 +// 临时文件转正,并将本站URL统一写为 [IMG_URL] 占位符 +func LocalizeContentFields(ctx context.Context, content, saveDir string) string { + if content == "" { + return content + } + urlMap := map[string]string{} + collect := func(re *regexp.Regexp) { + for _, m := range re.FindAllStringSubmatch(content, -1) { + if len(m) < 2 { + continue + } + url := m[1] + if _, seen := urlMap[url]; seen { + continue + } + if newURL := localizeOne(ctx, url, saveDir); newURL != "" { + urlMap[url] = newURL + } + } + } + collect(contentSrcRe) + collect(contentHrefRe) + for old, newV := range urlMap { + content = strings.ReplaceAll(content, old, newV) + } + return content +} + +// localizeOne 单个URL本地化处理 +// 临时文件转正为相对路径写占位符;本站正式文件剥离访问基址写占位符;其他返回空表示跳过 +func localizeOne(ctx context.Context, url, saveDir string) string { + if strings.Contains(url, "/temp/") { + newURL := upload.SaveFromTemp(ctx, url, saveDir) + if newURL == "" || strings.HasPrefix(newURL, "http://") || strings.HasPrefix(newURL, "https://") { + return "" + } + return "[IMG_URL]/" + strings.TrimLeft(newURL, "/") + } + // 本站正式文件:剥离访问基址(domainUrl)后剩余的 "/" 开头路径即相对路径; + // 未剥离成功的(仍含协议头或未匹配基址)视为外站URL,跳过 + rest := strings.TrimPrefix(url, upload.GetAccessBase(ctx)) + if strings.HasPrefix(rest, "/") && !strings.Contains(rest, "://") { + return "[IMG_URL]" + rest + } + return "" +} + +// ExpandContentFields 富文本读取侧还原 +// 将 [IMG_URL]/{相对路径} 拼接访问基址,是唯一的还原出口 +func ExpandContentFields(ctx context.Context, content string) string { + if content == "" { + return content + } + return strings.ReplaceAll(content, "[IMG_URL]", upload.GetAccessBase(ctx)) +} + +// =========================================================================== +// 三、枚举文本(字典值 → 显示名,详情/列表补 {field}Text) +// =========================================================================== + +// dictTextCache 进程内字典文本缓存 +// 读多写少,TTL 300 秒,修改字典后最迟 5 分钟生效 +var dictTextCache sync.Map // key: {code}:{value} → dictTextEntry + +// dictTextEntry 字典文本缓存条目 +type dictTextEntry struct { + text string // 显示名 + expires time.Time // 过期时间 +} + +// DictGetText 按字典编码与值获取显示名 +// dict_item 查询为唯一真相,未命中时返回原值文本 +func DictGetText(ctx context.Context, dictCode string, value any) string { + val := strings.TrimSpace(fmt.Sprintf("%v", value)) + if dictCode == "" || val == "" { + return val + } + key := dictCode + ":" + val + if e, ok := dictTextCache.Load(key); ok { + ent := e.(dictTextEntry) + if time.Now().Before(ent.expires) { + return ent.text + } + } + dict, err := dao.Dict.Ctx(ctx).Where("code", dictCode).One() + if err != nil || len(dict) == 0 { + return val + } + item, err := dao.DictItem.Ctx(ctx).Where("dict_id", dict["id"].Int64()).Where("value", val).One() + if err != nil || len(item) == 0 { + return val + } + text := item["name"].String() + dictTextCache.Store(key, dictTextEntry{text: text, expires: time.Now().Add(300 * time.Second)}) + return text +} + +// SerializeMapOnRead 详情/列表读取后补 {field}Text 显示名 +// 直接在传入的 map 上原地更新 +func SerializeMapOnRead(ctx context.Context, data map[string]any, fieldDict map[string]string) { + for field, dictCode := range fieldDict { + if v, ok := data[field]; ok { + data[field+"Text"] = DictGetText(ctx, dictCode, v) + } + } +} + +// LogTypeText 操作日志类型转中文名 +// +// 该字段存储的是注解常量值(1=新增…40=解绑),与字典编码体系不一致, +// 故使用静态映射表而非字典;未知值回退原数字。 +func LogTypeText(v int) string { + if n, ok := consts.LogTypeNames[v]; ok { + return n + } + return fmt.Sprintf("%d", v) +} + +// =========================================================================== +// 四、唯一校验(TOCTOU 说明见文件头纪律4) +// m 传入 dao.X.Ctx(ctx)(自带 del_flag/租户过滤) +// =========================================================================== + +// CheckUnique 唯一性快速校验 +// label 为字段中文名,例如"角色编码" +func CheckUnique(ctx context.Context, m *gdb.Model, column string, value any, excludeId int64, label string) error { + mm := m.Where(column, value) + if excludeId > 0 { + mm = mm.WhereNot("id", excludeId) + } + n, err := mm.Count() + if err != nil { + return err + } + if n > 0 { + return gerror.NewCodef(consts.CodeRecordExists, "%s已存在", label) + } + return nil +} + +// =========================================================================== +// 五、事务(gdb.WithTX 注入上下文,dao.Ctx(ctx) 自动走事务连接) +// 纪律:事务内不做 HTTP/文件IO(保持短事务) +// =========================================================================== + +// WithTrans 事务包裹 +// fn 返回错误时自动回滚 +func WithTrans(ctx context.Context, fn func(ctx context.Context) error) error { + tx, err := g.DB().Begin(ctx) + if err != nil { + return err + } + if err = fn(gdb.WithTX(ctx, tx)); err != nil { + _ = tx.Rollback() + return err + } + return tx.Commit() +} + +// =========================================================================== +// 六、分块查询(导出防OOM) +// build 每页重建查询(gdb Model 不可复用执行),快照语义:并发写入可能翻页漂移 +// =========================================================================== + +// ChunkQuery 分块遍历 +// build(page, size) 返回当页模型,fn 处理每页结果 +func ChunkQuery(ctx context.Context, build func(page, size int) *gdb.Model, size int, fn func(gdb.Result) error) error { + if size <= 0 { + size = 500 + } + for page := 1; ; page++ { + rows, err := build(page, size).All() + if err != nil { + return err + } + if len(rows) == 0 { + return nil + } + if err = fn(rows); err != nil { + return err + } + if len(rows) < size { + return nil + } + } +} + +// fillUserCount 按 user 表外键列统计引用数 +// 批量填充 DTO 的 UserCount 字段,供 level、position 等带 userCount 展示的模块复用; +// list 元素需包含 Id 与 UserCount 字段 +func fillUserCount(ctx context.Context, column string, list any) { + rv := reflect.ValueOf(list) + if rv.Kind() != reflect.Ptr || rv.Elem().Kind() != reflect.Slice { + return + } + slice := rv.Elem() + counts := make(map[int64]int) + rows, err := dao.User.Ctx(ctx).Fields("id, " + column).All() + if err == nil { + for _, r := range rows { + if v := r[column].Int64(); v > 0 { + counts[v]++ + } + } + } + for i := 0; i < slice.Len(); i++ { + item := slice.Index(i) + for item.Kind() == reflect.Ptr { + item = item.Elem() + } + if item.Kind() != reflect.Struct { + continue + } + idField := item.FieldByName("Id") + cntField := item.FieldByName("UserCount") + if idField.IsValid() && cntField.IsValid() && cntField.CanSet() && cntField.Kind() == reflect.Int { + cntField.SetInt(int64(counts[idField.Int()])) + } + } +} + +// =========================================================================== +// 七、树构建(泛型,供 menu/dept/category 等复用) +// =========================================================================== + +// BuildTree 平铺列表转树 +// 父节点不在集合内则挂根;childrenOf 返回子级切片指针;less 排序,为 nil 时保持原序 +func BuildTree[T any](rows []T, idOf func(T) int64, pidOf func(T) int, + childrenOf func(*T) *[]*T, less func(a, b T) bool) []*T { + + nodes := make(map[int64]*T, len(rows)) + ids := make([]int64, 0, len(rows)) + for _, r := range rows { + item := r + nodes[idOf(item)] = &item + ids = append(ids, idOf(item)) + } + roots := make([]*T, 0, len(ids)) + for _, idv := range ids { + node := nodes[idv] + if pid := pidOf(*node); pid != 0 { + if parent, ok := nodes[int64(pid)]; ok { + pc := childrenOf(parent) + *pc = append(*pc, node) + continue + } + } + roots = append(roots, node) + } + if less != nil { + sort.SliceStable(roots, func(i, j int) bool { return less(*roots[i], *roots[j]) }) + } + return roots +} + +// =========================================================================== +// 临时目录清理(企业级运营闭环:避免 temp 孤儿文件堆积) +// =========================================================================== + +// CleanTemp 清理上传临时目录 +// 删除 upload/temp 下超过 expireDays 天的日期子目录或散文件,返回删除数量 +func CleanTemp(ctx context.Context, expireDays int) int { + if expireDays <= 0 { + expireDays = 7 + } + base := upload.TempBaseDir(ctx) + entries, err := os.ReadDir(base) + if err != nil { + return 0 + } + deadline := time.Now().AddDate(0, 0, -expireDays) + removed := 0 + for _, e := range entries { + if !e.IsDir() { + continue + } + // 目录名即日期(Yyyymmdd) + if day, e2 := time.Parse("20060102", e.Name()); e2 == nil && day.Before(deadline) { + if os.RemoveAll(filepath.Join(base, e.Name())) == nil { + removed++ + } + } + } + return removed +} diff --git a/internal/logic/cache_monitor.go b/internal/logic/cache_monitor.go new file mode 100644 index 0000000..3b91227 --- /dev/null +++ b/internal/logic/cache_monitor.go @@ -0,0 +1,141 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + "strconv" + "strings" + "time" + + "github.com/gogf/gf/v2/frame/g" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" + "easygoadmin/utility/cache" +) + +// ============================== 缓存监控业务实现 ============================== + +// sCacheMonitor 缓存监控业务实现 +// Redis 模式解析 INFO 各节;内存降级模式仅返回模式标识 +type sCacheMonitor struct{} + +func init() { + service.RegisterCacheMonitor(&sCacheMonitor{}) +} + +// Info 查询缓存监控信息 +func (s *sCacheMonitor) Info(ctx context.Context, req *v1.CacheInfoReq) (res *v1.CacheInfoRes, err error) { + if cache.UsingMemory() { + return &v1.CacheInfoRes{Mode: "memory", Connected: false}, nil + } + res = &v1.CacheInfoRes{Mode: "redis", Connected: true} + sections := map[string]map[string]string{} + for _, name := range []string{"server", "memory", "stats", "clients", "keyspace"} { + v, err := g.Redis().Do(ctx, "INFO", name) + if err != nil { + res.Connected = false + return res, nil + } + sections[name] = parseRedisInfo(v.String()) + } + + res.Version = sections["server"]["redis_version"] + if sec, e := strconv.ParseInt(sections["server"]["uptime_in_seconds"], 10, 64); e == nil { + res.Uptime = (time.Duration(sec) * time.Second).String() + } + res.UsedMemory = sections["memory"]["used_memory_human"] + res.UsedMemoryPeak = sections["memory"]["used_memory_peak_human"] + res.MaxMemory = sections["memory"]["maxmemory_human"] + res.FragRatio = sections["memory"]["mem_fragmentation_ratio"] + res.ConnectedClients, _ = strconv.ParseInt(sections["clients"]["connected_clients"], 10, 64) + res.ExpiredKeys, _ = strconv.ParseInt(sections["stats"]["expired_keys"], 10, 64) + res.EvictedKeys, _ = strconv.ParseInt(sections["stats"]["evicted_keys"], 10, 64) + res.TotalCommands, _ = strconv.ParseInt(sections["stats"]["total_commands_processed"], 10, 64) + res.TotalConns, _ = strconv.ParseInt(sections["stats"]["total_connections_received"], 10, 64) + res.Hits, _ = strconv.ParseInt(sections["stats"]["keyspace_hits"], 10, 64) + res.Misses, _ = strconv.ParseInt(sections["stats"]["keyspace_misses"], 10, 64) + if total := res.Hits + res.Misses; total > 0 { + res.HitRate = strconv.FormatFloat(float64(res.Hits)/float64(total)*100, 'f', 2, 64) + } else { + res.HitRate = "0.00" + } + if v, e := g.Redis().Do(ctx, "DBSIZE"); e == nil { + res.Keys = v.Int64() + } + for k, line := range sections["keyspace"] { + if !strings.HasPrefix(k, "db") { + continue + } + entry := v1.CacheKeySpaceDb{Db: k} + for _, kv := range strings.Split(line, ",") { + parts := strings.SplitN(kv, "=", 2) + if len(parts) != 2 { + continue + } + switch parts[0] { + case "keys": + entry.Keys, _ = strconv.ParseInt(parts[1], 10, 64) + case "expires": + entry.Expires, _ = strconv.ParseInt(parts[1], 10, 64) + } + } + res.KeySpace = append(res.KeySpace, entry) + } + return res, nil +} + +// parseRedisInfo 解析 INFO 输出为 键→值 映射 +func parseRedisInfo(s string) map[string]string { + out := map[string]string{} + for _, line := range strings.Split(s, "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + if idx := strings.Index(line, ":"); idx > 0 { + out[line[:idx]] = strings.TrimSpace(line[idx+1:]) + } + } + return out +} diff --git a/internal/logic/category.go b/internal/logic/category.go new file mode 100644 index 0000000..4e81e13 --- /dev/null +++ b/internal/logic/category.go @@ -0,0 +1,222 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + + "github.com/gogf/gf/v2/errors/gerror" + + "easygoadmin/api/v1" + "easygoadmin/internal/consts" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" +) + +// ============================== 分类业务实现 ============================== + +// sCategory 分类业务实现 +// 实现 service.ICategory 接口,接口定义见 api/v1/category.go +type sCategory struct{} + +func init() { + service.RegisterCategory(&sCategory{}) +} + +// categorySpec 分类模块声明 +// +// 平铺列表 + 上级循环校验 + 删除守卫;树形结构由前端 buildTree 构建 +var categorySpec = ModuleSpec[v1.CategoryItem, v1.CategoryItem, v1.CategoryDetailRes]{ + Model: dao.Category.Ctx, + LikeFields: []string{"category_name", "category_code"}, + OrderBy: "sort, id", + ToData: categoryToData, + BeforeUpdate: categoryParentGuard, // 禁止挂到自身/后代下 + BeforeDelete: categoryDeleteGuard, // 存在性 + 有子分类/文章引用禁止 +} + +// categoryToData 全量表单语义转换 +// 将新增或修改请求转换为落库字段,tenant_id 由 hooks.ApplyCtx 自动填充 +func categoryToData(req any) map[string]any { + switch r := req.(type) { + case *v1.CategoryAddReq: + return map[string]any{ + "category_name": r.CategoryName, "category_code": r.CategoryCode, + "parent_id": r.ParentId, "sort": r.Sort, "note": r.Note, + } + case *v1.CategoryUpdateReq: + return map[string]any{ + "category_name": r.CategoryName, "category_code": r.CategoryCode, + "parent_id": r.ParentId, "sort": r.Sort, "note": r.Note, + } + } + panic("categoryToData: 不支持的入参类型") +} + +// categoryParentGuard 上级循环校验 +// 禁止将上级设置为自身或自己的后代节点 +func categoryParentGuard(ctx context.Context, data map[string]any, id int64, req any) error { + r := mustAs[*v1.CategoryUpdateReq](req) + if r.ParentId <= 0 { + return nil + } + if int64(r.ParentId) == id { + return gerror.NewCodef(consts.CodeParamError, "上级不能选择自己") + } + descIds, err := categoryDescendantIds(ctx, id) + if err != nil { + return err + } + for _, did := range descIds { + if did == int64(r.ParentId) { + return gerror.NewCodef(consts.CodeParamError, "上级不能选择自己的下级") + } + } + return nil +} + +// categoryDeleteGuard 删除守卫 +// 校验记录存在性,存在子分类或文章引用时禁止删除 +func categoryDeleteGuard(ctx context.Context, id int64) error { + if n, err := dao.Category.Ctx(ctx).Where("id", id).Count(); err != nil { + return err + } else if n == 0 { + return gerror.NewCode(consts.CodeRecordNotFound) + } + if n, err := dao.Category.Ctx(ctx).Where("parent_id", id).Count(); err != nil { + return err + } else if n > 0 { + return gerror.NewCode(consts.CodeHasChildren) + } + if n, err := dao.Article.Ctx(ctx).Where("category_id", id).Count(); err != nil { + return err + } else if n > 0 { + return gerror.NewCode(consts.CodeInUse) + } + return nil +} + +// categoryDescendantIds 递归获取全部后代ID +// 采用广度优先遍历,返回指定分类下所有层级的后代节点ID +func categoryDescendantIds(ctx context.Context, categoryId int64) ([]int64, error) { + result := make([]int64, 0) + queue := []int64{categoryId} + for len(queue) > 0 { + current := queue[0] + queue = queue[1:] + rows, err := dao.Category.Ctx(ctx).Where("parent_id", current).Fields("id").All() + if err != nil { + return nil, err + } + for _, r := range rows { + idv := r["id"].Int64() + result = append(result, idv) + queue = append(queue, idv) + } + } + return result, nil +} + +// ---------- 标准方法:3行委托 ---------- + +// Page 分类分页(平铺) +// 按查询条件分页返回平铺的分类列表 +func (s *sCategory) Page(ctx context.Context, req *v1.CategoryPageReq) (res *v1.CategoryPageRes, err error) { + list, total, err := categorySpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize()) + if err != nil { + return nil, err + } + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil +} + +// List 分类平铺列表 +// data 直接为数组,树形结构由前端 buildTree 构建 +func (s *sCategory) List(ctx context.Context, req *v1.CategoryListReq) (res v1.CategoryListRes, err error) { + list, err := categorySpec.List(ctx, req) + if err != nil { + return nil, err + } + return list, nil +} + +// Detail 分类详情 +// 按分类ID查询详情 +func (s *sCategory) Detail(ctx context.Context, req *v1.CategoryDetailReq) (res *v1.CategoryDetailRes, err error) { + return categorySpec.Detail(ctx, req.Id, req) +} + +// Add 新增分类 +// 新增成功后返回新记录ID +func (s *sCategory) Add(ctx context.Context, req *v1.CategoryAddReq) (res *v1.IdRes, err error) { + id, err := categorySpec.Add(ctx, req) + if err != nil { + return nil, err + } + return &v1.IdRes{Id: id}, nil +} + +// Update 修改分类 +// 按分类ID更新记录 +func (s *sCategory) Update(ctx context.Context, req *v1.CategoryUpdateReq) (res *v1.CategoryUpdateRes, err error) { + if err = categorySpec.Update(ctx, req.Id, req); err != nil { + return nil, err + } + return &v1.CategoryUpdateRes{}, nil +} + +// Delete 删除分类 +// 按分类ID删除单条记录 +func (s *sCategory) Delete(ctx context.Context, req *v1.CategoryDeleteReq) (res *v1.CategoryDeleteRes, err error) { + if err = categorySpec.Delete(ctx, req.Id); err != nil { + return nil, err + } + return &v1.CategoryDeleteRes{}, nil +} + +// BatchDelete 批量删除分类 +// 按分类ID列表批量删除记录 +func (s *sCategory) BatchDelete(ctx context.Context, req *v1.CategoryBatchDeleteReq) (res *v1.CategoryBatchDeleteRes, err error) { + if err = categorySpec.BatchDelete(ctx, req.Ids); err != nil { + return nil, err + } + return &v1.CategoryBatchDeleteRes{}, nil +} diff --git a/internal/logic/city.go b/internal/logic/city.go new file mode 100644 index 0000000..3f903a7 --- /dev/null +++ b/internal/logic/city.go @@ -0,0 +1,345 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + "strings" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gerror" + + "easygoadmin/api/v1" + "easygoadmin/internal/consts" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" +) + +// ============================== 城市业务实现 ============================== + +// sCity 城市业务实现 +// 实现 service.ICity 接口,承载城市模块的业务逻辑 +type sCity struct{} + +// citySerializeMaps 枚举字段到字典编码的映射 +// {field}Text 由 dict_item 解析,字典管理页可修改文案 +var citySerializeMaps = map[string]string{ + "level": "city_level", +} + +func init() { + service.RegisterCity(&sCity{}) +} + +// citySpec 城市模块声明 +// +// 仅 Page 采用声明式;List 含 parentCode、limit 等特有查询,Detail 使用嵌入结构,均手写实现 +var citySpec = ModuleSpec[v1.CityItem, v1.CityItem, v1.CityDetailRes]{ + Model: dao.City.Ctx, + LikeFields: []string{"name", "pinyin"}, + EqFields: []string{"level"}, + OrderBy: "id", + SerializeMaps: citySerializeMaps, + ToData: cityToData, +} + +// cityToData 全量表单语义转换 +// 兼容新增与修改请求,将入参转换为落库字段 +func cityToData(req any) map[string]any { + switch r := req.(type) { + case *v1.CityAddReq: + return map[string]any{ + "city_code": r.CityCode, "area_code": r.AreaCode, "parent_code": r.ParentCode, + "zip_code": r.ZipCode, "level": r.Level, "pid": r.Pid, "name": r.Name, + "short_name": r.ShortName, "full_name": r.FullName, "pinyin": r.Pinyin, + "lng": r.Lng, "lat": r.Lat, + } + case *v1.CityUpdateReq: + return map[string]any{ + "city_code": r.CityCode, "area_code": r.AreaCode, "parent_code": r.ParentCode, + "zip_code": r.ZipCode, "level": r.Level, "pid": r.Pid, "name": r.Name, + "short_name": r.ShortName, "full_name": r.FullName, "pinyin": r.Pinyin, + "lng": r.Lng, "lat": r.Lat, + } + } + panic("cityToData: 不支持的入参类型") +} + +// Page 城市分页 +// 按查询条件分页返回城市列表,采用声明式实现 +func (s *sCity) Page(ctx context.Context, req *v1.CityPageReq) (res *v1.CityPageRes, err error) { + list, total, err := citySpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize()) + if err != nil { + return nil, err + } + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil +} + +// List 城市列表 +// 按父级编码、层级、关键词等条件返回城市列表,供级联选择器使用 +func (s *sCity) List(ctx context.Context, req *v1.CityListReq) (res v1.CityListRes, err error) { + m := dao.City.Ctx(ctx) + if req.ParentCode != "" { + m = m.Where("parent_code", req.ParentCode) + } else if req.Level > 0 { + m = m.Where("level", req.Level) + } + if req.Keyword != "" { + kw := "%" + req.Keyword + "%" + m = m.WhereLike("name", kw).WhereOrLike("pinyin", kw) + } + if req.Limit > 0 { + m = m.Limit(req.Limit) + } + rows, err := m.Order("id").All() + if err != nil { + return nil, err + } + list := make([]v1.CityItem, 0, len(rows)) + for _, r := range rows { + list = append(list, s.toItem(ctx, r)) + } + return list, nil +} + +// Detail 城市详情 +// 按城市ID查询详情,并补充父级名称 +func (s *sCity) Detail(ctx context.Context, req *v1.CityDetailReq) (res *v1.CityDetailRes, err error) { + one, err := dao.City.Ctx(ctx).Where("id", req.Id).One() + if err != nil { + return nil, err + } + if len(one) == 0 { + return nil, gerror.NewCode(consts.CodeRecordNotFound) + } + res = &v1.CityDetailRes{CityItem: s.toItem(ctx, one)} + if pid := one["pid"].Int64(); pid > 0 { + if parent, e := dao.City.Ctx(ctx).Where("id", pid).Fields("name").One(); e == nil && len(parent) > 0 { + res.ParentName = parent["name"].String() + } + } + return res, nil +} + +// Add 新增城市 +// 新增成功后返回新记录ID,采用声明式实现 +func (s *sCity) Add(ctx context.Context, req *v1.CityAddReq) (res *v1.IdRes, err error) { + id, err := citySpec.Add(ctx, req) + if err != nil { + return nil, err + } + return &v1.IdRes{Id: id}, nil +} + +// Update 修改城市 +// 按城市ID更新记录,采用声明式实现 +func (s *sCity) Update(ctx context.Context, req *v1.CityUpdateReq) (res *v1.CityUpdateRes, err error) { + if err = citySpec.Update(ctx, req.Id, req); err != nil { + return nil, err + } + return &v1.CityUpdateRes{}, nil +} + +// Delete 删除城市 +// 按城市ID删除单条记录,采用声明式实现 +func (s *sCity) Delete(ctx context.Context, req *v1.CityDeleteReq) (res *v1.CityDeleteRes, err error) { + if err = citySpec.Delete(ctx, req.Id); err != nil { + return nil, err + } + return &v1.CityDeleteRes{}, nil +} + +// BatchDelete 批量删除城市 +// 按城市ID列表批量删除记录,采用声明式实现 +func (s *sCity) BatchDelete(ctx context.Context, req *v1.CityBatchDeleteReq) (res *v1.CityBatchDeleteRes, err error) { + if err = citySpec.BatchDelete(ctx, req.Ids); err != nil { + return nil, err + } + return &v1.CityBatchDeleteRes{}, nil +} + +// ByParent 按父编码查询下级城市 +// 登录用户即可访问,返回指定父级下的直属下级城市 +func (s *sCity) ByParent(ctx context.Context, req *v1.CityByParentReq) (res *v1.CityByParentRes, err error) { + rows, err := dao.City.Ctx(ctx).Where("parent_code", req.ParentCode).Order("id").All() + if err != nil { + return nil, err + } + list := make([]v1.CityItem, 0, len(rows)) + for _, r := range rows { + list = append(list, s.toItem(ctx, r)) + } + return &v1.CityByParentRes{List: list}, nil +} + +// GetCityList 按父编码获取下级城市 +// 登录用户即可访问,供级联选择器懒加载;parentCode 为 0 时返回顶级 +func (s *sCity) GetCityList(ctx context.Context, req *v1.CityGetListReq) (res v1.CityGetListRes, err error) { + rows, err := dao.City.Ctx(ctx).Where("parent_code", req.ParentCode).Order("id").All() + if err != nil { + return nil, err + } + list := make([]v1.CityItem, 0, len(rows)) + for _, r := range rows { + list = append(list, s.toItem(ctx, r)) + } + return list, nil +} + +// AreaProvince 省份列表 +// 登录用户即可访问,返回层级为省的顶级城市列表 +func (s *sCity) AreaProvince(ctx context.Context, req *v1.AreaParentReq) (res *v1.AreaParentRes, err error) { + rows, err := dao.City.Ctx(ctx).Where("level", 1).Order("id").All() + if err != nil { + return nil, err + } + list := make([]v1.CityItem, 0, len(rows)) + for _, r := range rows { + list = append(list, s.toItem(ctx, r)) + } + return &v1.AreaParentRes{List: list}, nil +} + +// ============================================================ +// 城市编解码(名称 ↔ 编码互转) +// ============================================================ + +// ResolveCityNames 城市编码数组转名称串 +// +// 按 area_code 查询 name,保留输入顺序,空编码跳过。 +// 例:["320000000000","320100000000","320105000000"] 转换为 "江苏省,南京市,建邺区" +func ResolveCityNames(ctx context.Context, codes []string, separator string) string { + clean := make([]string, 0, len(codes)) + for _, c := range codes { + if c != "" { + clean = append(clean, c) + } + } + if len(clean) == 0 { + return "" + } + rows, err := dao.City.Ctx(ctx).WhereIn("area_code", clean).Fields("area_code, name").All() + if err != nil { + return "" + } + nameByCode := make(map[string]string, len(rows)) + for _, r := range rows { + nameByCode[r["area_code"].String()] = r["name"].String() + } + names := make([]string, 0, len(clean)) + for _, code := range clean { + if n := nameByCode[code]; n != "" { + names = append(names, n) + } + } + if separator == "" { + separator = "," + } + return strings.Join(names, separator) +} + +// ResolveCityCodes 名称串转四级编码数组 +// +// 逐级以 parent 约束避免同名歧义。 +// 例:"江苏省,南京市,建邺区" 转换为 ["320000000000","320100000000","320105000000",""] +func ResolveCityCodes(ctx context.Context, cityInfo string) [4]string { + var codes [4]string + cityInfo = strings.TrimSpace(cityInfo) + if cityInfo == "" { + return codes + } + names := make([]string, 0, 4) + for _, part := range strings.Split(cityInfo, ",") { + if p := strings.TrimSpace(part); p != "" { + names = append(names, p) + } + } + if len(names) == 0 { + return codes + } + // 第一级:省(pid = 0) + prov, err := dao.City.Ctx(ctx).Where("pid", 0).Where("name", names[0]).One() + if err != nil || len(prov) == 0 { + return codes + } + codes[0] = prov["area_code"].String() + // 第二级:市(parent_code = 省 area_code) + if len(names) > 1 { + if city, e := dao.City.Ctx(ctx).Where("parent_code", codes[0]).Where("name", names[1]).One(); e == nil && len(city) > 0 { + codes[1] = city["area_code"].String() + } + } + // 第三级:区(parent_code = 市 area_code) + if len(names) > 2 && codes[1] != "" { + if dist, e := dao.City.Ctx(ctx).Where("parent_code", codes[1]).Where("name", names[2]).One(); e == nil && len(dist) > 0 { + codes[2] = dist["area_code"].String() + } + } + // 第四级:街道(parent_code = 区 area_code) + if len(names) > 3 && codes[2] != "" { + if street, e := dao.City.Ctx(ctx).Where("parent_code", codes[2]).Where("name", names[3]).One(); e == nil && len(street) > 0 { + codes[3] = street["area_code"].String() + } + } + return codes +} + +// toItem 数据库记录转换为城市行数据 +// 枚举层级同步补全层级文本 +func (s *sCity) toItem(ctx context.Context, r gdb.Record) v1.CityItem { + return v1.CityItem{ + Id: r["id"].Int64(), + CityCode: r["city_code"].String(), + AreaCode: r["area_code"].String(), + ParentCode: r["parent_code"].String(), + ZipCode: r["zip_code"].String(), + Level: r["level"].Int(), + LevelText: DictGetText(ctx, citySerializeMaps["level"], r["level"].Int()), + Pid: r["pid"].Int64(), + Name: r["name"].String(), + ShortName: r["short_name"].String(), + FullName: r["full_name"].String(), + Pinyin: r["pinyin"].String(), + Lng: r["lng"].String(), + Lat: r["lat"].String(), + } +} diff --git a/internal/logic/config.go b/internal/logic/config.go new file mode 100644 index 0000000..090ba16 --- /dev/null +++ b/internal/logic/config.go @@ -0,0 +1,336 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + "fmt" + "strings" + + "easygoadmin/api/v1" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" + "easygoadmin/utility/upload" +) + +// ============================== 配置业务实现 ============================== + +// sConfig 配置业务实现 +// 实现 service.IConfig 接口,承载配置模块的业务逻辑 +type sConfig struct{} + +// configSpec 配置模块声明 +// +// List 含嵌套配置项,采用手写实现 +var configSpec = ModuleSpec[v1.ConfigItem, v1.ConfigItem, v1.ConfigDetailRes]{ + Model: dao.Config.Ctx, + LikeFields: []string{"name", "code"}, + OrderBy: "sort, id", +} + +func init() { + service.RegisterConfig(&sConfig{}) +} + +// Page 配置分页 +// 按查询条件分页返回配置列表 +func (s *sConfig) Page(ctx context.Context, req *v1.ConfigPageReq) (res *v1.ConfigPageRes, err error) { + list, total, err := configSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize()) + if err != nil { + return nil, err + } + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil +} + +// List 配置列表(含配置项) +// 返回配置列表,每项携带其下的配置项集合 +func (s *sConfig) List(ctx context.Context, req *v1.ConfigListReq) (res v1.ConfigListRes, err error) { + m := dao.Config.Ctx(ctx) + if req.Keyword != "" { + kw := "%" + req.Keyword + "%" + m = m.WhereLike("name", kw).WhereOrLike("code", kw) + } + rows, err := m.Order("sort, id").All() + if err != nil { + return nil, err + } + itemsByConfig, err := s.itemsGrouped(ctx) + if err != nil { + return nil, err + } + list := make([]v1.ConfigWithItems, 0, len(rows)) + for _, r := range rows { + cid := r["id"].Int64() + list = append(list, v1.ConfigWithItems{ + ConfigItem: v1.ConfigItem{ + Id: cid, + Name: r["name"].String(), + Code: r["code"].String(), + Sort: r["sort"].Int(), + Note: r["note"].String(), + CreateUser: r["create_user"].String(), + }, + Items: itemsByConfig[cid], + }) + } + return list, nil +} + +// Detail 配置详情 +// 按配置ID查询详情及其下的配置项集合 +func (s *sConfig) Detail(ctx context.Context, req *v1.ConfigDetailReq) (res *v1.ConfigDetailRes, err error) { + return configSpec.Detail(ctx, req.Id, req) +} + +// Add 新增配置 +// 校验配置编码唯一后新增,返回新记录ID +func (s *sConfig) Add(ctx context.Context, req *v1.ConfigAddReq) (res *v1.IdRes, err error) { + if err = CheckUnique(ctx, dao.Config.Ctx(ctx), "code", req.Code, 0, "配置编码"); err != nil { + return nil, err + } + result, err := dao.Config.Ctx(ctx).Data(map[string]any{ + "name": req.Name, + "code": req.Code, + "sort": req.Sort, + "note": req.Note, + }).Insert() + if err != nil { + return nil, err + } + idv, _ := result.LastInsertId() + return &v1.IdRes{Id: idv}, nil +} + +// Update 修改配置 +// 按配置ID更新记录 +func (s *sConfig) Update(ctx context.Context, req *v1.ConfigUpdateReq) (res *v1.ConfigUpdateRes, err error) { + if _, err = dao.Config.Ctx(ctx).Where("id", req.Id).Data(map[string]any{ + "name": req.Name, + "code": req.Code, + "sort": req.Sort, + "note": req.Note, + }).Update(); err != nil { + return nil, err + } + return &v1.ConfigUpdateRes{}, nil +} + +// Delete 删除配置 +// 按配置ID删除记录,并连带删除其下配置项 +func (s *sConfig) Delete(ctx context.Context, req *v1.ConfigDeleteReq) (res *v1.ConfigDeleteRes, err error) { + if err = configSpec.Delete(ctx, req.Id); err != nil { + return nil, err + } + if _, err = dao.ConfigItem.Ctx(ctx).Where("config_id", req.Id).Delete(); err != nil { + return nil, err + } + return &v1.ConfigDeleteRes{}, nil +} + +// BatchDelete 批量删除配置 +// 按配置ID列表批量删除记录,并连带删除其下配置项 +func (s *sConfig) BatchDelete(ctx context.Context, req *v1.ConfigBatchDeleteReq) (res *v1.ConfigBatchDeleteRes, err error) { + if err = configSpec.BatchDelete(ctx, req.Ids); err != nil { + return nil, err + } + if _, err = dao.ConfigItem.Ctx(ctx).WhereIn("config_id", req.Ids).Delete(); err != nil { + return nil, err + } + return &v1.ConfigBatchDeleteRes{}, nil +} + +// WebIndex 网站配置聚合 +// +// 按配置分组返回,供网站配置页渲染; +// 文件类字段拼接访问URL,options 解析为 param 映射 +func (s *sConfig) WebIndex(ctx context.Context, req *v1.ConfigWebIndexReq) (res *v1.ConfigWebIndexRes, err error) { + configs, err := dao.Config.Ctx(ctx).Order("sort, id").All() + if err != nil { + return nil, err + } + items, err := dao.ConfigItem.Ctx(ctx).Order("sort, id").All() + if err != nil { + return nil, err + } + byConfig := make(map[int64][]v1.ConfigWebItem, len(configs)) + for _, r := range items { + value := r["value"].String() + typ := r["type"].String() + options := r["options"].String() + item := v1.ConfigWebItem{ + Id: r["id"].Int64(), + Name: r["name"].String(), + Code: r["code"].String(), + Value: value, + Type: typ, + Options: options, + Param: parseConfigOptions(options), + } + switch typ { + case "image": + item.Value = upload.GetFileUrl(ctx, value) + case "file": + item.Value = configItemFileURL(ctx, value) + case "images": + item.ValueList = configItemListURLs(ctx, value, false) + case "files": + item.ValueList = configItemListURLs(ctx, value, true) + case "ueditor": + item.Value = ExpandContentFields(ctx, value) + } + cid := r["config_id"].Int64() + byConfig[cid] = append(byConfig[cid], item) + } + groups := make([]v1.ConfigWebGroup, 0, len(configs)) + for _, c := range configs { + cid := c["id"].Int64() + groups = append(groups, v1.ConfigWebGroup{ + ConfigName: c["name"].String(), + DataList: byConfig[cid], + }) + } + return &groups, nil +} + +// WebSave 批量保存网站配置 +// +// 接收配置项编码到值的映射; +// 文件类字段执行转正与剥离域名,富文本执行本地化 +func (s *sConfig) WebSave(ctx context.Context, req *v1.ConfigWebSaveReq) (res *v1.ConfigWebSaveRes, err error) { + for code, raw := range req.Data { + one, e := dao.ConfigItem.Ctx(ctx).Where("code", code).One() + if e != nil { + return nil, e + } + if len(one) == 0 { + continue // 未知编码跳过(前端可能携带非配置项字段) + } + value := configItemSaveValue(ctx, one["type"].String(), configValueToString(raw)) + if _, e = dao.ConfigItem.Ctx(ctx).Where("id", one["id"].Int64()).Data("value", value).Update(); e != nil { + return nil, e + } + } + return &v1.ConfigWebSaveRes{}, nil +} + +// parseConfigOptions 解析选项串 +// 将 "value=name,value=name" 格式解析为值与显示名的映射 +func parseConfigOptions(options string) map[string]string { + param := make(map[string]string) + if options == "" { + return param + } + for _, pair := range strings.Split(options, ",") { + pair = strings.TrimSpace(pair) + if pair == "" { + continue + } + value, name, found := strings.Cut(pair, "=") + if !found { + continue + } + param[strings.TrimSpace(value)] = strings.TrimSpace(name) + } + return param +} + +// configValueToString 前端表单值归一化为字符串 +// 多选或多图为数组时按逗号拼接 +func configValueToString(v any) string { + switch t := v.(type) { + case nil: + return "" + case string: + return t + case []any: + parts := make([]string, 0, len(t)) + for _, e := range t { + parts = append(parts, fmt.Sprintf("%v", e)) + } + return strings.Join(parts, ",") + default: + return fmt.Sprintf("%v", t) + } +} + +// itemsByConfigId 查询单个配置的配置项 +func (s *sConfig) itemsByConfigId(ctx context.Context, configId int64) ([]v1.ConfigItemRow, error) { + rows, err := dao.ConfigItem.Ctx(ctx).Where("config_id", configId).Order("sort, id").All() + if err != nil { + return nil, err + } + items := make([]v1.ConfigItemRow, 0, len(rows)) + for _, r := range rows { + items = append(items, v1.ConfigItemRow{ + Id: r["id"].Int64(), + Name: r["name"].String(), + Code: r["code"].String(), + Value: r["value"].String(), + Type: r["type"].String(), + Status: r["status"].Int(), + Sort: r["sort"].Int(), + }) + } + return items, nil +} + +// itemsGrouped 全量配置项按 config_id 分组 +func (s *sConfig) itemsGrouped(ctx context.Context) (map[int64][]v1.ConfigItemRow, error) { + result := make(map[int64][]v1.ConfigItemRow) + rows, err := dao.ConfigItem.Ctx(ctx).Order("sort, id").All() + if err != nil { + return nil, err + } + for _, r := range rows { + cid := r["config_id"].Int64() + result[cid] = append(result[cid], v1.ConfigItemRow{ + Id: r["id"].Int64(), + Name: r["name"].String(), + Code: r["code"].String(), + Value: r["value"].String(), + Type: r["type"].String(), + Status: r["status"].Int(), + Sort: r["sort"].Int(), + }) + } + return result, nil +} diff --git a/internal/logic/config_item.go b/internal/logic/config_item.go new file mode 100644 index 0000000..639d008 --- /dev/null +++ b/internal/logic/config_item.go @@ -0,0 +1,306 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + "strings" + + "easygoadmin/api/v1" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" + "easygoadmin/utility/upload" +) + +// ============================== 配置项业务实现 ============================== + +// sConfigItem 配置项业务实现 +// 实现 service.IConfigItem 接口,承载配置项模块的业务逻辑 +type sConfigItem struct{} + +// configItemSerializeMaps 枚举字段到字典编码的映射 +// {field}Text 由 dict_item 解析,字典管理页可修改文案 +var configItemSerializeMaps = map[string]string{ + "status": "config_item_status", +} + +// configItemSpec 配置项模块声明 +var configItemSpec = ModuleSpec[v1.ConfigItemItem, v1.ConfigItemItem, v1.ConfigItemDetailRes]{ + Model: dao.ConfigItem.Ctx, + LikeFields: []string{"name", "code"}, + EqFields: []string{"status", "config_id"}, + OrderBy: "sort, id", + SerializeMaps: configItemSerializeMaps, + AfterPageList: configItemExpandName, // 行内所属配置名批量补全 + AfterDetail: configItemAfterDetail, // 所属配置编码 + 文件类字段拼接访问URL +} + +// configItemExpandName 分页行所属配置名批量补全 +// 将配置ID映射为配置名称,避免逐行查询 +func configItemExpandName(ctx context.Context, list []*v1.ConfigItemItem, req any) error { + cfgNames, err := (&sConfigItem{}).configNames(ctx) + if err != nil { + return err + } + for _, item := range list { + item.ConfigName = cfgNames[item.ConfigId] + } + return nil +} + +// configItemAfterDetail 详情装配 +// +// 补充所属配置编码,并按控件类型拼接文件类字段的访问URL: +// - image:value 直接拼域名 +// - file:value 为 "原文件名|路径",路径部分拼域名 +// - images:value 保持相对路径逗号串,valueList 为逐项拼域名后的数组 +// - files:value 保持 "原文件名|路径" 逗号串,valueList 为逐项拼域名后的数组 +func configItemAfterDetail(ctx context.Context, item *v1.ConfigItemDetailRes, req any) error { + code, err := (&sConfigItem{}).configCode(ctx, item.ConfigId) + if err != nil { + return err + } + item.ConfigCode = code + switch item.Type { + case "image": + item.Value = upload.GetFileUrl(ctx, item.Value) + case "file": + item.Value = configItemFileURL(ctx, item.Value) + case "images": + item.ValueList = configItemListURLs(ctx, item.Value, false) + case "files": + item.ValueList = configItemListURLs(ctx, item.Value, true) + } + return nil +} + +// configItemSaveValue 保存前按控件类型归一化 value +// +// 文件类字段执行临时转正并剥离域名前缀,入库形态为相对路径: +// - image:单文件 URL +// - images:逗号分隔多文件 URL +// - file:值为 "原文件名|URL",仅处理 URL 部分 +// - files:逗号分隔 "原文件名|URL" 串,逐项处理 URL 部分 +// - ueditor:富文本,临时转正并将本站URL写为 [IMG_URL] 占位符 +// - 其余类型:原样返回 +func configItemSaveValue(ctx context.Context, typ, value string) string { + switch typ { + case "image": + return SaveFileField(ctx, value, "config") + case "images": + return SaveMultiFileField(ctx, value, "config") + case "file": + return configItemSaveFilePair(ctx, value) + case "files": + if value == "" { + return "" + } + parts := strings.Split(value, ",") + for i, p := range parts { + parts[i] = configItemSaveFilePair(ctx, strings.TrimSpace(p)) + } + return strings.Join(parts, ",") + case "ueditor": + return LocalizeContentFields(ctx, value, "config") + } + return value +} + +// configItemSaveFilePair 处理单个 "原文件名|URL" 对 +// URL 部分转正并剥离域名,文件名前缀保留 +func configItemSaveFilePair(ctx context.Context, value string) string { + name, url, found := strings.Cut(value, "|") + if !found { + return SaveFileField(ctx, value, "config") + } + return name + "|" + SaveFileField(ctx, url, "config") +} + +// configItemFileURL 单文件值转访问URL +// 将 "原文件名|相对路径" 中的路径部分拼接访问URL +func configItemFileURL(ctx context.Context, value string) string { + name, path, found := strings.Cut(value, "|") + if !found { + return upload.GetFileUrl(ctx, value) + } + return name + "|" + upload.GetFileUrl(ctx, path) +} + +// configItemListURLs 逗号串逐项拼接访问URL +// keepName 为 true 时保留 "原文件名|" 前缀 +func configItemListURLs(ctx context.Context, value string, keepName bool) []string { + if value == "" { + return nil + } + parts := strings.Split(value, ",") + list := make([]string, 0, len(parts)) + for _, p := range parts { + p = strings.TrimSpace(p) + if p == "" { + continue + } + if keepName { + list = append(list, configItemFileURL(ctx, p)) + continue + } + list = append(list, upload.GetFileUrl(ctx, p)) + } + return list +} + +func init() { + service.RegisterConfigItem(&sConfigItem{}) +} + +// Page 配置项分页 +// 按查询条件分页返回配置项列表 +func (s *sConfigItem) Page(ctx context.Context, req *v1.ConfigItemPageReq) (res *v1.ConfigItemPageRes, err error) { + list, total, err := configItemSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize()) + if err != nil { + return nil, err + } + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil +} + +// List 配置项全量列表 +// 返回配置项列表并补全所属配置名,精简列表不挂基座钩子,在委托层后处理 +func (s *sConfigItem) List(ctx context.Context, req *v1.ConfigItemListReq) (res v1.ConfigItemListRes, err error) { + list, err := configItemSpec.List(ctx, req) + if err != nil { + return nil, err + } + cfgNames, err := s.configNames(ctx) + if err != nil { + return nil, err + } + rows := derefSlice(list) + for i := range rows { + rows[i].ConfigName = cfgNames[rows[i].ConfigId] + } + return rows, nil +} + +// Detail 配置项详情 +// 按配置项ID查询详情及其值列表 +func (s *sConfigItem) Detail(ctx context.Context, req *v1.ConfigItemDetailReq) (res *v1.ConfigItemDetailRes, err error) { + return configItemSpec.Detail(ctx, req.Id, req) +} + +// Add 新增配置项 +// 按控件类型归一化值后落库,返回新记录ID +func (s *sConfigItem) Add(ctx context.Context, req *v1.ConfigItemAddReq) (res *v1.IdRes, err error) { + result, err := dao.ConfigItem.Ctx(ctx).Data(map[string]any{ + "config_id": req.ConfigId, + "name": req.Name, + "code": req.Code, + "value": configItemSaveValue(ctx, req.Type, req.Value), + "options": req.Options, + "type": req.Type, + "status": req.Status, + "sort": req.Sort, + "note": req.Note, + }).Insert() + if err != nil { + return nil, err + } + idv, _ := result.LastInsertId() + return &v1.IdRes{Id: idv}, nil +} + +// Update 修改配置项 +// 按配置项ID更新记录,值按控件类型归一化 +func (s *sConfigItem) Update(ctx context.Context, req *v1.ConfigItemUpdateReq) (res *v1.ConfigItemUpdateRes, err error) { + if _, err = dao.ConfigItem.Ctx(ctx).Where("id", req.Id).Data(map[string]any{ + "config_id": req.ConfigId, + "name": req.Name, + "code": req.Code, + "value": configItemSaveValue(ctx, req.Type, req.Value), + "options": req.Options, + "type": req.Type, + "status": req.Status, + "sort": req.Sort, + "note": req.Note, + }).Update(); err != nil { + return nil, err + } + return &v1.ConfigItemUpdateRes{}, nil +} + +// Delete 删除配置项 +// 按配置项ID删除单条记录 +func (s *sConfigItem) Delete(ctx context.Context, req *v1.ConfigItemDeleteReq) (res *v1.ConfigItemDeleteRes, err error) { + if err = configItemSpec.Delete(ctx, req.Id); err != nil { + return nil, err + } + return &v1.ConfigItemDeleteRes{}, nil +} + +// BatchDelete 批量删除配置项 +// 按配置项ID列表批量删除记录 +func (s *sConfigItem) BatchDelete(ctx context.Context, req *v1.ConfigItemBatchDeleteReq) (res *v1.ConfigItemBatchDeleteRes, err error) { + if err = configItemSpec.BatchDelete(ctx, req.Ids); err != nil { + return nil, err + } + return &v1.ConfigItemBatchDeleteRes{}, nil +} + +// configCode 查询配置ID对应的编码 +func (s *sConfigItem) configCode(ctx context.Context, configId int64) (string, error) { + code, err := dao.Config.Ctx(ctx).Where("id", configId).Fields("code").Value() + if err != nil { + return "", err + } + return code.String(), nil +} + +// configNames 查询配置ID到名称的映射 +func (s *sConfigItem) configNames(ctx context.Context) (map[int64]string, error) { + result := make(map[int64]string) + rows, err := dao.Config.Ctx(ctx).Fields("id, name").All() + if err != nil { + return nil, err + } + for _, r := range rows { + result[r["id"].Int64()] = r["name"].String() + } + return result, nil +} diff --git a/internal/logic/daemon.go b/internal/logic/daemon.go new file mode 100644 index 0000000..74131d2 --- /dev/null +++ b/internal/logic/daemon.go @@ -0,0 +1,277 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + "fmt" + "strings" + "sync" + "time" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/gclient" + "github.com/gogf/gf/v2/os/gcron" + + "easygoadmin/internal/dao" + "easygoadmin/utility/cache" +) + +// ============================================================================ +// 定时任务调度器:基于 gcron 的常驻调度,启动时加载 status=1 的任务, +// 按 cron_expression 触发 url+params(HTTP 调用本服务接口),结果写任务日志表。 +// +// 任务键:job:{id}(gcron 命名条目),暂停=Remove、恢复=重新 Add。 +// ============================================================================ + +var ( + daemonOnce sync.Once // 保证调度器仅初始化一次 + daemonStart time.Time // 调度器启动时间 +) + +// jobLockTTLSeconds 任务执行锁 TTL(秒) +// 兜底上限:执行实例崩溃未主动释放时锁自动过期;正常路径执行结束即释放 +const jobLockTTLSeconds = 300 + +// jobTryLock 抢占任务执行锁 +// +// 键 job:lock:{id} 经 utility/cache(Redis 优先、内存降级)SET NX 实现, +// 多实例部署共享同一把锁,防止同一任务被并发执行; +// 抢占成功返回释放函数,失败返回 nil(调用方放弃本次执行) +func jobTryLock(jobId int64) func() { + key := fmt.Sprintf("job:lock:%d", jobId) + ctx := context.Background() + if !cache.SetNX(ctx, key, "1", jobLockTTLSeconds) { + return nil + } + return func() { _ = cache.Del(ctx, key) } +} + +// jobKey 生成 gcron 条目键 +func jobKey(jobId int64) string { + return fmt.Sprintf("job:%d", jobId) +} + +// DaemonInit 启动时加载运行中的任务 +// 幂等,多次调用仅首次生效 +func DaemonInit(ctx context.Context) { + daemonOnce.Do(func() { + daemonStart = time.Now() + rows, err := dao.Job.Ctx(ctx).Where("status", 1).All() + if err != nil { + g.Log().Warningf(ctx, "定时任务加载失败: %v", err) + return + } + g.Log().Debugf(ctx, "定时任务待挂载: %d 条", len(rows)) + loaded := 0 + for _, r := range rows { + if err := addJobToScheduler(r["id"].Int64(), r["cron_expression"].String(), + r["execute_policy"].Int(), r["is_sync"].Int()); err != nil { + g.Log().Warningf(ctx, "任务挂载失败 job=%d cron=%q: %v", + r["id"].Int64(), r["cron_expression"].String(), err) + continue + } + loaded++ + } + g.Log().Infof(ctx, "定时任务调度器已启动,加载 %d 个任务", loaded) + }) +} + +// addJobToScheduler 挂载任务到调度器 +// +// 执行策略(execute_policy): +// - 1 立即执行:允许并发,tick 即启动新执行(gcron.Add) +// - 2 执行一次(默认):单例调度,上次未结束则跳过本次(gcron.AddSingleton) +// - 3 放弃执行:并发 tick 照常触发,但抢不到执行锁则放弃本次 +// +// 执行方式(is_sync):1 同步在调度协程内执行;0 异步 go 协程执行。 +// 策略 2/3 均叠加执行锁(多实例共享),策略 1 按并发语义不加锁。 +func addJobToScheduler(jobId int64, cronExpr string, executePolicy int, isSync int) error { + if cronExpr == "" { + return fmt.Errorf("cron表达式为空") + } + handler := func(c context.Context) { + run := func(ctx context.Context) { + if executePolicy != 1 { + unlock := jobTryLock(jobId) + if unlock == nil { + g.Log().Debugf(ctx, "任务 %d 存在执行中的实例,本次放弃", jobId) + return + } + defer unlock() + } + executeJob(ctx, jobId, false) + } + if isSync == 1 { + run(c) + } else { + go run(context.Background()) + } + } + var err error + if executePolicy == 1 { + _, err = gcron.Add(context.Background(), cronExpr, handler, jobKey(jobId)) + } else { + _, err = gcron.AddSingleton(context.Background(), cronExpr, handler, jobKey(jobId)) + } + return err +} + +// removeJobFromScheduler 从调度器摘除任务 +func removeJobFromScheduler(jobId int64) { + gcron.Remove(jobKey(jobId)) +} + +// hasJobInScheduler 判断调度器中是否包含指定任务 +func hasJobInScheduler(jobId int64) bool { + return gcron.Search(jobKey(jobId)) != nil +} + +// executeJob 执行任务 +// +// 流程:读取任务定义 → HTTP 调用目标地址 → 写入执行日志; +// 内部捕获 panic,避免异常外泄影响调度器。 +// force 为 true 时跳过状态检查,用于手动"立即执行"场景,未发布任务也可执行。 +func executeJob(ctx context.Context, jobId int64, force bool) { + defer func() { + if e := recover(); e != nil { + g.Log().Errorf(ctx, "定时任务 %d 执行 panic: %v", jobId, e) + } + }() + one, err := dao.Job.Ctx(ctx).Where("id", jobId).One() + if err != nil || len(one) == 0 { + removeJobFromScheduler(jobId) + return + } + if !force && one["status"].Int() != 1 { + removeJobFromScheduler(jobId) + return + } + + start := time.Now() + status := 0 + message := "执行成功" + exception := "" + + // 任务执行目标分派: + // http(s) 地址 → HTTP 调用;其余取值 → 按方法名查找本地处理方法执行 + target := one["url"].String() + if target == "" { + status = 1 + message = "执行失败" + exception = "任务URL为空" + } else if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") { + client := gclient.New() + client.SetContentType("application/json") + client.SetTimeout(30 * time.Second) + body := one["params"].String() + var response *gclient.Response + var rerr error + if body == "" { + response, rerr = client.Get(ctx, target) + } else { + response, rerr = client.Post(ctx, target, body) + } + if rerr != nil { + status = 1 + message = "执行失败" + exception = truncStr(rerr.Error(), 1000) + } else { + respBody := truncStr(response.ReadAllString(), 1000) + if response.StatusCode != 200 { + status = 1 + message = "执行失败" + exception = fmt.Sprintf("HTTP %d: %s", response.StatusCode, respBody) + } else { + message = truncStr("执行成功: "+respBody, 500) + } + } + } else { + handler, ok := jobHandlers[target] + if !ok { + status = 1 + message = "执行失败" + exception = fmt.Sprintf("未找到本地任务处理方法: %s", truncStr(target, 200)) + } else { + summary, herr := handler(ctx, one["params"].String()) + if herr != nil { + status = 1 + message = "执行失败" + exception = truncStr(herr.Error(), 1000) + } else if summary != "" { + message = truncStr("执行成功: "+summary, 500) + } + } + } + end := time.Now() + + // 执行日志落库(后台 ctx,独立于任务 ctx) + if _, werr := dao.JobLog.Ctx(ctx).Data(map[string]any{ + "job_id": jobId, + "job_name": one["job_name"].String(), + "job_group": one["job_group"].String(), + "job_trigger": one["job_trigger"].String(), + "job_message": message, + "cron_expression": one["cron_expression"].String(), + "status": status, + "start_time": start, + "end_time": end, + "consume_time": end.Sub(start).Milliseconds(), + "exception_info": exception, + "create_user": "daemon", + }).Insert(); werr != nil { + g.Log().Errorf(ctx, "任务日志写入失败 job=%d: %v", jobId, werr) + } +} + +// truncStr 按字符数截断字符串 +// 长度未超限时原样返回,超限时按 rune 截断,避免截断多字节字符 +func truncStr(s string, maxLen int) string { + if len(s) <= maxLen { + return s + } + runes := []rune(s) + if len(runes) <= maxLen { + return s + } + return string(runes[:maxLen]) +} diff --git a/internal/logic/data_scope.go b/internal/logic/data_scope.go new file mode 100644 index 0000000..d1b7cb9 --- /dev/null +++ b/internal/logic/data_scope.go @@ -0,0 +1,129 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/consts" + "easygoadmin/internal/dao" +) + +// ============================== 角色数据权限(行级过滤) ============================== +// +// 档位(role.data_scope,多角色在身份加载层取最宽松): +// 1-全部 租户隔离之内不过滤 +// 2-本部门及以下 dept_id IN (自身部门 + 全部子孙部门) +// 3-本部门 dept_id = 自身部门 +// 4-仅本人 id = 自身用户ID +// +// 生效范围:挂接本函数的模块其 Page/List/导出(applyFilters 链)自动生效; +// Detail/Update/Delete 不过滤,由 permission 节点与租户隔离兜底。 +// 租户隔离、软删过滤由 dao.Ctx(hooks.ApplyCtx)先于本链注入,两层独立叠加。 + +// ApplyDataScope 数据权限过滤工厂 +// +// 返回可挂到 ModuleSpec.DataScope 的过滤函数;deptColumn 为行上的部门列名, +// 无部门归属的行表(如内容模块)后续可按需提供按创建人过滤的变体。 +func ApplyDataScope(deptColumn string) DataScopeFunc { + return func(ctx context.Context, m *gdb.Model, req any) *gdb.Model { + if dataScopeBypass(ctx) { + return m + } + scope := consts.GetDataScope(ctx) + if scope <= consts.DataScopeAll { + return m + } + uid := consts.GetUserID(ctx) + deptId := consts.GetDeptID(ctx) + switch { + case scope >= consts.DataScopeSelf || deptId <= 0: + // 仅本人;未分配部门的用户挂部门档时降级为仅本人(安全兜底) + return m.Where("id", uid) + case scope == consts.DataScopeDept: + return m.Where(deptColumn, deptId) + case scope == consts.DataScopeDeptAndBelow: + return m.WhereIn(deptColumn, deptSubtreeIds(ctx, deptId)) + } + return m + } +} + +// dataScopeBypass 数据范围豁免判断 +// 平台级(超管)或持有超管角色编码时全量可见,与权限通配逻辑对齐 +func dataScopeBypass(ctx context.Context) bool { + if consts.IsPlatform(ctx) { + return true + } + for _, r := range consts.GetRoles(ctx) { + if r == consts.RoleCodeSuperAdmin { + return true + } + } + return false +} + +// deptSubtreeIds 收集部门自身及全部子孙部门ID +// +// 部门为 parent_id 树且体量小,直接全量拉取后 BFS; +// 查询经 dao.Ctx(租户隔离+软删),后续如成热点可加短时缓存 +func deptSubtreeIds(ctx context.Context, deptId int) []int { + rows, err := dao.Dept.Ctx(ctx).Fields("id, parent_id").All() + if err != nil || len(rows) == 0 { + return []int{deptId} + } + children := make(map[int][]int, len(rows)) + for _, r := range rows { + pid := r["parent_id"].Int() + children[pid] = append(children[pid], r["id"].Int()) + } + ids := []int{deptId} + for queue := []int{deptId}; len(queue) > 0; queue = queue[1:] { + for _, cid := range children[queue[0]] { + ids = append(ids, cid) + queue = append(queue, cid) + } + } + return ids +} diff --git a/internal/logic/dept.go b/internal/logic/dept.go new file mode 100644 index 0000000..d157333 --- /dev/null +++ b/internal/logic/dept.go @@ -0,0 +1,230 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + + "github.com/gogf/gf/v2/errors/gerror" + + "easygoadmin/api/v1" + "easygoadmin/internal/consts" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" +) + +// ============================== 部门业务实现 ============================== + +// sDept 部门业务实现 +// 实现 service.IDept 接口,接口定义见 api/v1/dept.go +type sDept struct{} + +func init() { + service.RegisterDept(&sDept{}) +} + +// deptSerializeMaps 枚举字段到字典编码的映射 +// {field}Text 由基座自动填充 +var deptSerializeMaps = map[string]string{ + "type": "dept_type", +} + +// deptSpec 部门模块声明 +// +// 平铺列表 + 上级循环校验 + 删除守卫;树形结构由前端 buildTree 构建 +var deptSpec = ModuleSpec[v1.DeptItem, v1.DeptItem, v1.DeptDetailRes]{ + Model: dao.Dept.Ctx, + LikeFields: []string{"name"}, + EqFields: []string{"type"}, + OrderBy: "sort, id", + SerializeMaps: deptSerializeMaps, + ToData: deptToData, + BeforeUpdate: deptParentGuard, // 禁止挂到自身/后代下 + BeforeDelete: deptDeleteGuard, // 存在性 + 有子部门/用户引用禁止 +} + +// deptToData 全量表单语义转换 +// 将新增或修改请求转换为落库字段,tenant_id 由 hooks.ApplyCtx 自动填充 +func deptToData(req any) map[string]any { + switch r := req.(type) { + case *v1.DeptAddReq: + return map[string]any{ + "name": r.Name, "type": r.Type, "parent_id": r.ParentId, + "sort": r.Sort, "note": r.Note, + } + case *v1.DeptUpdateReq: + return map[string]any{ + "name": r.Name, "type": r.Type, "parent_id": r.ParentId, + "sort": r.Sort, "note": r.Note, + } + } + panic("deptToData: 不支持的入参类型") +} + +// deptParentGuard 上级循环校验 +// 禁止将上级设置为自身或自己的后代节点 +func deptParentGuard(ctx context.Context, data map[string]any, id int64, req any) error { + r := mustAs[*v1.DeptUpdateReq](req) + if r.ParentId <= 0 { + return nil + } + if int64(r.ParentId) == id { + return gerror.NewCodef(consts.CodeParamError, "上级不能选择自己") + } + descIds, err := deptDescendantIds(ctx, id) + if err != nil { + return err + } + for _, did := range descIds { + if did == int64(r.ParentId) { + return gerror.NewCodef(consts.CodeParamError, "上级不能选择自己的下级") + } + } + return nil +} + +// deptDeleteGuard 删除守卫 +// 校验记录存在性,存在子部门或用户引用时禁止删除 +func deptDeleteGuard(ctx context.Context, id int64) error { + if n, err := dao.Dept.Ctx(ctx).Where("id", id).Count(); err != nil { + return err + } else if n == 0 { + return gerror.NewCode(consts.CodeRecordNotFound) + } + if n, err := dao.Dept.Ctx(ctx).Where("parent_id", id).Count(); err != nil { + return err + } else if n > 0 { + return gerror.NewCode(consts.CodeHasChildren) + } + if n, err := dao.User.Ctx(ctx).Where("dept_id", id).Count(); err != nil { + return err + } else if n > 0 { + return gerror.NewCode(consts.CodeInUse) + } + return nil +} + +// deptDescendantIds 递归获取全部后代ID +// 采用广度优先遍历,返回指定部门下所有层级的后代节点ID +func deptDescendantIds(ctx context.Context, deptId int64) ([]int64, error) { + result := make([]int64, 0) + queue := []int64{deptId} + for len(queue) > 0 { + current := queue[0] + queue = queue[1:] + rows, err := dao.Dept.Ctx(ctx).Where("parent_id", current).Fields("id").All() + if err != nil { + return nil, err + } + for _, r := range rows { + idv := r["id"].Int64() + result = append(result, idv) + queue = append(queue, idv) + } + } + return result, nil +} + +// ---------- 标准方法:3行委托 ---------- + +// Page 部门分页(平铺) +// 按查询条件分页返回平铺的部门列表 +func (s *sDept) Page(ctx context.Context, req *v1.DeptPageReq) (res *v1.DeptPageRes, err error) { + list, total, err := deptSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize()) + if err != nil { + return nil, err + } + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil +} + +// List 部门平铺列表 +// data 直接为数组,树形结构由前端 buildTree 构建 +func (s *sDept) List(ctx context.Context, req *v1.DeptListReq) (res v1.DeptListRes, err error) { + list, err := deptSpec.List(ctx, req) + if err != nil { + return nil, err + } + return list, nil +} + +// Detail 部门详情 +// 按部门ID查询详情 +func (s *sDept) Detail(ctx context.Context, req *v1.DeptDetailReq) (res *v1.DeptDetailRes, err error) { + return deptSpec.Detail(ctx, req.Id, req) +} + +// Add 新增部门 +// 新增成功后返回新记录ID +func (s *sDept) Add(ctx context.Context, req *v1.DeptAddReq) (res *v1.IdRes, err error) { + id, err := deptSpec.Add(ctx, req) + if err != nil { + return nil, err + } + return &v1.IdRes{Id: id}, nil +} + +// Update 修改部门 +// 按部门ID更新记录 +func (s *sDept) Update(ctx context.Context, req *v1.DeptUpdateReq) (res *v1.DeptUpdateRes, err error) { + if err = deptSpec.Update(ctx, req.Id, req); err != nil { + return nil, err + } + return &v1.DeptUpdateRes{}, nil +} + +// Delete 删除部门 +// 按部门ID删除单条记录 +func (s *sDept) Delete(ctx context.Context, req *v1.DeptDeleteReq) (res *v1.DeptDeleteRes, err error) { + if err = deptSpec.Delete(ctx, req.Id); err != nil { + return nil, err + } + return &v1.DeptDeleteRes{}, nil +} + +// BatchDelete 批量删除部门 +// 按部门ID列表批量删除记录 +func (s *sDept) BatchDelete(ctx context.Context, req *v1.DeptBatchDeleteReq) (res *v1.DeptBatchDeleteRes, err error) { + if err = deptSpec.BatchDelete(ctx, req.Ids); err != nil { + return nil, err + } + return &v1.DeptBatchDeleteRes{}, nil +} diff --git a/internal/logic/dict.go b/internal/logic/dict.go new file mode 100644 index 0000000..eba96f6 --- /dev/null +++ b/internal/logic/dict.go @@ -0,0 +1,282 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + "encoding/json" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gerror" + + "easygoadmin/api/v1" + "easygoadmin/internal/consts" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" + "easygoadmin/utility/cache" +) + +// ============================== 字典业务实现 ============================== + +// sDict 字典业务实现 +// 实现 service.IDict 接口,承载数据字典模块的业务逻辑 +type sDict struct{} + +// dictSpec 数据字典模块声明 +// +// Detail 含字典项组装,采用手写实现 +var dictSpec = ModuleSpec[v1.DictItem, v1.DictItem, v1.DictDetailRes]{ + Model: dao.Dict.Ctx, + LikeFields: []string{"name", "code"}, + OrderBy: "sort, id", +} + +func init() { + service.RegisterDict(&sDict{}) +} + +// Page 数据字典分页 +// 按查询条件分页返回字典列表 +func (s *sDict) Page(ctx context.Context, req *v1.DictPageReq) (res *v1.DictPageRes, err error) { + list, total, err := dictSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize()) + if err != nil { + return nil, err + } + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil +} + +// Detail 字典详情(含字典项) +// 按字典ID查询详情及其下的字典项集合 +func (s *sDict) Detail(ctx context.Context, req *v1.DictDetailReq) (res *v1.DictDetailRes, err error) { + one, err := dao.Dict.Ctx(ctx).Where("id", req.Id).One() + if err != nil { + return nil, err + } + if len(one) == 0 { + return nil, gerror.NewCode(consts.CodeRecordNotFound) + } + items, err := s.itemsByDictId(ctx, req.Id) + if err != nil { + return nil, err + } + return &v1.DictDetailRes{DictItem: s.toItem(one), Items: items}, nil +} + +// Add 新增字典 +// 校验字典编码唯一后新增,返回新记录ID +func (s *sDict) Add(ctx context.Context, req *v1.DictAddReq) (res *v1.IdRes, err error) { + if err = CheckUnique(ctx, dao.Dict.Ctx(ctx), "code", req.Code, 0, "字典编码"); err != nil { + return nil, err + } + result, err := dao.Dict.Ctx(ctx).Data(map[string]any{ + "name": req.Name, + "code": req.Code, + "sort": req.Sort, + "note": req.Note, + }).Insert() + if err != nil { + return nil, err + } + idv, _ := result.LastInsertId() + return &v1.IdRes{Id: idv}, nil +} + +// Update 修改字典 +// 按字典ID更新记录 +func (s *sDict) Update(ctx context.Context, req *v1.DictUpdateReq) (res *v1.DictUpdateRes, err error) { + if _, err = dao.Dict.Ctx(ctx).Where("id", req.Id).Data(map[string]any{ + "name": req.Name, + "code": req.Code, + "sort": req.Sort, + "note": req.Note, + }).Update(); err != nil { + return nil, err + } + return &v1.DictUpdateRes{}, nil +} + +// Delete 删除数据字典(连带字典项) +// 按字典ID删除记录,并连带删除其下字典项 +func (s *sDict) Delete(ctx context.Context, req *v1.DictDeleteReq) (res *v1.DictDeleteRes, err error) { + if err = dictSpec.Delete(ctx, req.Id); err != nil { + return nil, err + } + if _, err = dao.DictItem.Ctx(ctx).Where("dict_id", req.Id).Delete(); err != nil { + return nil, err + } + return &v1.DictDeleteRes{}, nil +} + +// BatchDelete 批量删除数据字典(连带字典项) +// 按字典ID列表批量删除记录,并连带删除其下字典项 +func (s *sDict) BatchDelete(ctx context.Context, req *v1.DictBatchDeleteReq) (res *v1.DictBatchDeleteRes, err error) { + if err = dictSpec.BatchDelete(ctx, req.Ids); err != nil { + return nil, err + } + if _, err = dao.DictItem.Ctx(ctx).WhereIn("dict_id", req.Ids).Delete(); err != nil { + return nil, err + } + return &v1.DictBatchDeleteRes{}, nil +} + +// RefreshCache 刷新字典缓存 +// 重写 Redis 缓存,编码为空时刷新全部字典 +func (s *sDict) RefreshCache(ctx context.Context, req *v1.DictRefreshCacheReq) (res *v1.DictRefreshCacheRes, err error) { + m := dao.DictItem.Ctx(ctx) + if req.Code != "" { + dict, e := dao.Dict.Ctx(ctx).Where("code", req.Code).One() + if e != nil || len(dict) == 0 { + return nil, gerror.NewCode(consts.CodeRecordNotFound) + } + m = m.Where("dict_id", dict["id"].Int64()) + } + rows, err := m.Order("sort, id").All() + if err != nil { + return nil, err + } + byCode := make(map[string][]v1.DictItemRow) + for _, r := range rows { + dictId := r["dict_id"].Int64() + code := s.dictCodeById(ctx, dictId) + byCode[code] = append(byCode[code], v1.DictItemRow{ + Id: r["id"].Int64(), + Name: r["name"].String(), + Value: r["value"].String(), + Sort: r["sort"].Int(), + }) + } + for code, items := range byCode { + s.setCache(ctx, code, items) + } + return &v1.DictRefreshCacheRes{}, nil +} + +// AliasList 字典别名列表 +// 登录用户即可访问,按关键词返回字典列表 +func (s *sDict) AliasList(ctx context.Context, req *v1.DictAliasListReq) (res v1.DictAliasListRes, err error) { + m := dao.Dict.Ctx(ctx) + if req.Keyword != "" { + kw := "%" + req.Keyword + "%" + m = m.WhereLike("name", kw).WhereOrLike("code", kw) + } + rows, err := m.Order("sort, id").All() + if err != nil { + return nil, err + } + list := make([]v1.DictItem, 0, len(rows)) + for _, r := range rows { + list = append(list, s.toItem(r)) + } + return list, nil +} + +// AliasInfo 字典别名详情 +// 登录用户即可访问,按字典编码查询字典及其字典项 +func (s *sDict) AliasInfo(ctx context.Context, req *v1.DictAliasInfoReq) (res *v1.DictAliasInfoRes, err error) { + one, err := dao.Dict.Ctx(ctx).Where("code", req.Code).One() + if err != nil { + return nil, err + } + if len(one) == 0 { + return nil, gerror.NewCode(consts.CodeRecordNotFound) + } + items, err := s.itemsByDictId(ctx, one["id"].Int64()) + if err != nil { + return nil, err + } + return &v1.DictAliasInfoRes{DictItem: s.toItem(one), Items: items}, nil +} + +// ---------- 私有辅助 ---------- + +// toItem 数据库记录转换为字典行数据 +func (s *sDict) toItem(r gdb.Record) v1.DictItem { + return v1.DictItem{ + Id: r["id"].Int64(), + Name: r["name"].String(), + Code: r["code"].String(), + Sort: r["sort"].Int(), + Note: r["note"].String(), + CreateUser: r["create_user"].String(), + } +} + +// itemsByDictId 查询单个字典的字典项 +func (s *sDict) itemsByDictId(ctx context.Context, dictId int64) ([]v1.DictItemRow, error) { + rows, err := dao.DictItem.Ctx(ctx).Where("dict_id", dictId).Order("sort, id").All() + if err != nil { + return nil, err + } + items := make([]v1.DictItemRow, 0, len(rows)) + for _, r := range rows { + items = append(items, v1.DictItemRow{ + Id: r["id"].Int64(), + Name: r["name"].String(), + Value: r["value"].String(), + Sort: r["sort"].Int(), + }) + } + return items, nil +} + +// dictCodeById 查询字典ID对应的编码 +func (s *sDict) dictCodeById(ctx context.Context, dictId int64) string { + one, err := dao.Dict.Ctx(ctx).Where("id", dictId).Fields("code").One() + if err != nil || len(one) == 0 { + return "" + } + return one["code"].String() +} + +// setCache 写入字典项缓存 +// 采用尽力而为策略,序列化或写入失败时静默跳过 +func (s *sDict) setCache(ctx context.Context, code string, items []v1.DictItemRow) { + if code == "" { + return + } + // 序列化为简单 JSON(避免额外依赖) + b, err := json.Marshal(items) + if err != nil { + return + } + _ = cache.Set(ctx, consts.CacheKeyDictItem(code), string(b), 3600) +} diff --git a/internal/logic/dict_item.go b/internal/logic/dict_item.go new file mode 100644 index 0000000..17fc43c --- /dev/null +++ b/internal/logic/dict_item.go @@ -0,0 +1,192 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" +) + +// ============================== 字典项业务实现 ============================== + +// sDictItem 字典项业务实现 +// 实现 service.IDictItem 接口,承载字典项模块的业务逻辑 +type sDictItem struct{} + +// dictItemSpec 字典项模块声明 +// 无标准 List 方法 +var dictItemSpec = ModuleSpec[v1.DictItemItemRow, v1.DictItemItemRow, v1.DictItemDetailRes]{ + Model: dao.DictItem.Ctx, + LikeFields: []string{"name", "value"}, + EqFields: []string{"dict_id"}, + OrderBy: "sort, id", + AfterPageList: dictItemExpandName, // 行内字典名/编码批量补全 +} + +// dictItemExpandName 分页行所属字典名与编码批量补全 +// 将字典ID映射为字典名称与编码,避免逐行查询 +func dictItemExpandName(ctx context.Context, list []*v1.DictItemItemRow, req any) error { + info, err := (&sDictItem{}).dictInfoMap(ctx) + if err != nil { + return err + } + for _, item := range list { + item.DictName = info[item.DictId].name + item.DictCode = info[item.DictId].code + } + return nil +} + +func init() { + service.RegisterDictItem(&sDictItem{}) +} + +// Page 字典项分页 +// 按查询条件分页返回字典项列表 +func (s *sDictItem) Page(ctx context.Context, req *v1.DictItemPageReq) (res *v1.DictItemPageRes, err error) { + list, total, err := dictItemSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize()) + if err != nil { + return nil, err + } + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil +} + +// Detail 字典项详情 +// 按字典项ID查询详情 +func (s *sDictItem) Detail(ctx context.Context, req *v1.DictItemDetailReq) (res *v1.DictItemDetailRes, err error) { + return dictItemSpec.Detail(ctx, req.Id, req) +} + +// Add 新增字典项 +// 新增成功后返回新记录ID +func (s *sDictItem) Add(ctx context.Context, req *v1.DictItemAddReq) (res *v1.IdRes, err error) { + result, err := dao.DictItem.Ctx(ctx).Data(map[string]any{ + "dict_id": req.DictId, + "name": req.Name, + "value": req.Value, + "sort": req.Sort, + "note": req.Note, + }).Insert() + if err != nil { + return nil, err + } + idv, _ := result.LastInsertId() + return &v1.IdRes{Id: idv}, nil +} + +// Update 修改字典项 +// 按字典项ID更新记录 +func (s *sDictItem) Update(ctx context.Context, req *v1.DictItemUpdateReq) (res *v1.DictItemUpdateRes, err error) { + if _, err = dao.DictItem.Ctx(ctx).Where("id", req.Id).Data(map[string]any{ + "dict_id": req.DictId, + "name": req.Name, + "value": req.Value, + "sort": req.Sort, + "note": req.Note, + }).Update(); err != nil { + return nil, err + } + return &v1.DictItemUpdateRes{}, nil +} + +// Delete 删除字典项 +// 按字典项ID删除单条记录 +func (s *sDictItem) Delete(ctx context.Context, req *v1.DictItemDeleteReq) (res *v1.DictItemDeleteRes, err error) { + if err = dictItemSpec.Delete(ctx, req.Id); err != nil { + return nil, err + } + return &v1.DictItemDeleteRes{}, nil +} + +// BatchDelete 批量删除字典项 +// 按字典项ID列表批量删除记录 +func (s *sDictItem) BatchDelete(ctx context.Context, req *v1.DictItemBatchDeleteReq) (res *v1.DictItemBatchDeleteRes, err error) { + if err = dictItemSpec.BatchDelete(ctx, req.Ids); err != nil { + return nil, err + } + return &v1.DictItemBatchDeleteRes{}, nil +} + +// QuickList 快捷字典项列表 +// 登录用户即可访问,按字典编码查询该字典下的字典项 +func (s *sDictItem) QuickList(ctx context.Context, req *v1.DictItemQuickListReq) (res v1.DictItemQuickListRes, err error) { + dict, err := dao.Dict.Ctx(ctx).Where("code", req.Code).One() + if err != nil { + return nil, err + } + if len(dict) == 0 { + return []v1.DictItemQuickRow{}, nil + } + rows, err := dao.DictItem.Ctx(ctx).Where("dict_id", dict["id"].Int64()).Order("sort, id").All() + if err != nil { + return nil, err + } + list := make([]v1.DictItemQuickRow, 0, len(rows)) + for _, r := range rows { + list = append(list, v1.DictItemQuickRow{ + Id: r["id"].Int64(), + Name: r["name"].String(), + Value: r["value"].String(), + }) + } + return list, nil +} + +// dictInfo 字典摘要 +// 仅包含字典名称与编码 +type dictInfo struct{ name, code string } + +// dictInfoMap 查询字典ID到字典摘要的映射 +func (s *sDictItem) dictInfoMap(ctx context.Context) (map[int64]dictInfo, error) { + result := make(map[int64]dictInfo) + rows, err := dao.Dict.Ctx(ctx).Fields("id, name, code").All() + if err != nil { + return nil, err + } + for _, r := range rows { + result[r["id"].Int64()] = dictInfo{name: r["name"].String(), code: r["code"].String()} + } + return result, nil +} diff --git a/internal/logic/example.go b/internal/logic/example.go new file mode 100644 index 0000000..2ad86ab --- /dev/null +++ b/internal/logic/example.go @@ -0,0 +1,233 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + "fmt" + + v1 "easygoadmin/api/v1" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" + "easygoadmin/utility/excel" + "easygoadmin/utility/upload" +) + +// ============================== 案例演示业务实现 ============================== + +// sExample 案例演示业务实现 +// 实现 service.IExample 接口,接口定义见 api/v1/example.go +type sExample struct{} + +func init() { + service.RegisterExample(&sExample{}) +} + +// exampleSerializeMaps 枚举字段到字典编码的映射 +// {field}Text 由基座自动填充 +var exampleSerializeMaps = map[string]string{ + "status": "example_status", +} + +// exampleSpec 案例演示模块声明 +// +// 采用声明式实现:查询、输出与规则均在声明中定义,方法仅做委托 +var exampleSpec = ModuleSpec[v1.ExampleItem, v1.ExampleItem, v1.ExampleDetailRes]{ + Model: dao.Example.Ctx, + LikeFields: []string{"name"}, + EqFields: []string{"type", "status"}, + OrderBy: "sort, id", + SerializeMaps: exampleSerializeMaps, + FileFields: []string{ + "avatar", + }, + FileSaveDir: "example", + ToData: exampleToData, +} + +// exampleToData 全量表单语义转换 +// 新增与修改均提交全部字段,兼容两种入参类型 +func exampleToData(req any) map[string]any { + switch r := req.(type) { + case *v1.ExampleAddReq: + return map[string]any{ + "name": r.Name, + "avatar": r.Avatar, + "type": r.Type, + "status": r.Status, + "sort": r.Sort, + } + case *v1.ExampleUpdateReq: + return map[string]any{ + "name": r.Name, + "avatar": r.Avatar, + "type": r.Type, + "status": r.Status, + "sort": r.Sort, + } + } + panic(fmt.Sprintf("exampleToData: 不支持的入参类型 %T", req)) +} + +// ---------- 标准方法:委托基座 ---------- + +// Page 案例演示分页 +// 按查询条件分页返回案例演示列表 +func (s *sExample) Page(ctx context.Context, req *v1.ExamplePageReq) (res *v1.ExamplePageRes, err error) { + list, total, err := exampleSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize()) + if err != nil { + return nil, err + } + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil +} + +// List 案例演示平铺列表 +// data 直接为数组,树形结构由前端 buildTree 构建 +func (s *sExample) List(ctx context.Context, req *v1.ExampleListReq) (res v1.ExampleListRes, err error) { + list, err := exampleSpec.List(ctx, req) + if err != nil { + return nil, err + } + return derefSlice(list), nil +} + +// Detail 案例演示详情 +// 按主键ID查询详情 +func (s *sExample) Detail(ctx context.Context, req *v1.ExampleDetailReq) (res *v1.ExampleDetailRes, err error) { + return exampleSpec.Detail(ctx, req.Id, req) +} + +// Add 新增案例演示 +// 新增成功后返回新记录ID +func (s *sExample) Add(ctx context.Context, req *v1.ExampleAddReq) (res *v1.ExampleIdRes, err error) { + id, err := exampleSpec.Add(ctx, req) + if err != nil { + return nil, err + } + return &v1.ExampleIdRes{Id: id}, nil +} + +// Update 修改案例演示 +// 按主键ID更新记录 +func (s *sExample) Update(ctx context.Context, req *v1.ExampleUpdateReq) (res *v1.ExampleUpdateRes, err error) { + if err = exampleSpec.Update(ctx, req.Id, req); err != nil { + return nil, err + } + return &v1.ExampleUpdateRes{}, nil +} + +// Status 案例演示状态 +// 非标准方法,保留手写实现,设置记录的状态字段 +func (s *sExample) Status(ctx context.Context, req *v1.ExampleStatusReq) (res *v1.ExampleStatusRes, err error) { + if _, err = dao.Example.Ctx(ctx).Where("id", req.Id).Data(map[string]any{"status": req.Status}).Update(); err != nil { + return nil, err + } + return &v1.ExampleStatusRes{}, nil +} + +// Delete 删除案例演示 +// 按主键ID删除单条记录 +func (s *sExample) Delete(ctx context.Context, req *v1.ExampleDeleteReq) (res *v1.ExampleDeleteRes, err error) { + if err = exampleSpec.Delete(ctx, req.Id); err != nil { + return nil, err + } + return &v1.ExampleDeleteRes{}, nil +} + +// BatchDelete 批量删除案例演示 +// 按主键ID列表批量删除记录 +func (s *sExample) BatchDelete(ctx context.Context, req *v1.ExampleBatchDeleteReq) (res *v1.ExampleBatchDeleteRes, err error) { + if err = exampleSpec.BatchDelete(ctx, req.Ids); err != nil { + return nil, err + } + return &v1.ExampleBatchDeleteRes{}, nil +} + +// Import 导入案例演示 +// 表头为表单字段名,按列序写入 +func (s *sExample) Import(ctx context.Context, req *v1.ExampleImportReq) (res *v1.ExampleImportRes, err error) { + _, rows, err := excel.ReadSheet(req.File) + if err != nil { + return nil, err + } + res = &v1.ExampleImportRes{Total: len(rows)} + for i, row := range rows { + if cellAt(row, 0) == "" { + res.Failed++ + res.Reasons = append(res.Reasons, fmt.Sprintf("第%d行:案例名称为空", i+2)) + continue + } + if _, e := dao.Example.Ctx(ctx).Data(map[string]any{ + "name": cellAt(row, 0), + "avatar": cellAt(row, 1), + "type": cellInt(row, 2, 0), + "status": cellInt(row, 3, 0), + "sort": cellInt(row, 4, 0), + }).Insert(); e != nil { + res.Failed++ + res.Reasons = append(res.Reasons, fmt.Sprintf("第%d行:写入失败 %v", i+2, e)) + continue + } + res.Success++ + } + return res, nil +} + +// Export 导出案例演示 +// 与列表同套筛选条件,枚举列输出字典文本 +func (s *sExample) Export(ctx context.Context, req *v1.ExampleExportReq) (res *v1.ExampleExportRes, err error) { + rows, err := exampleSpec.applyFilters(ctx, exampleSpec.Model(ctx), req).Order("sort, id").All() + if err != nil { + return nil, err + } + headers := []string{"案例名称", "案例图片", "案例类型", "案例状态", "案例排序"} + data := make([][]any, 0, len(rows)) + for _, r := range rows { + data = append(data, []any{r["name"].String(), r["avatar"].String(), DictGetText(ctx, "example_type", r["type"]), DictGetText(ctx, "example_status", r["status"]), r["sort"].String()}) + } + fileName, _, err := excel.WriteSheet(upload.BaseDir(ctx), "案例演示数据", headers, data) + if err != nil { + return nil, err + } + url := upload.GetFileUrl(ctx, fileName) + return &url, nil +} diff --git a/internal/logic/file_template.go b/internal/logic/file_template.go new file mode 100644 index 0000000..6c7d031 --- /dev/null +++ b/internal/logic/file_template.go @@ -0,0 +1,185 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gerror" + + "easygoadmin/api/v1" + "easygoadmin/internal/consts" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" + "easygoadmin/utility/upload" +) + +// ============================== 文件模板业务实现 ============================== + +// sFileTemplate 文件模板业务实现 +// 实现 service.IFileTemplate 接口,承载文件模板模块的业务逻辑 +type sFileTemplate struct{} + +// fileTemplateSpec 文件模板模块声明 +// ByCode 为特有方法,保留手写实现 +var fileTemplateSpec = ModuleSpec[v1.FileTemplateItem, v1.FileTemplateItem, v1.FileTemplateDetailRes]{ + Model: dao.FileTemplate.Ctx, + LikeFields: []string{"name", "code"}, + OrderBy: "sort, id", + FileFields: []string{"filePath"}, // 读侧自动补域名(file.domainUrl) + FileSaveDir: "file_template", +} + +func init() { + service.RegisterFileTemplate(&sFileTemplate{}) +} + +// Page 文件模板分页 +// 按查询条件分页返回文件模板列表 +func (s *sFileTemplate) Page(ctx context.Context, req *v1.FileTemplatePageReq) (res *v1.FileTemplatePageRes, err error) { + list, total, err := fileTemplateSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize()) + if err != nil { + return nil, err + } + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil +} + +// List 文件模板全量列表 +// 按关键词返回文件模板列表 +func (s *sFileTemplate) List(ctx context.Context, req *v1.FileTemplateListReq) (res v1.FileTemplateListRes, err error) { + list, err := fileTemplateSpec.List(ctx, req) + if err != nil { + return nil, err + } + return derefSlice(list), nil +} + +// Detail 文件模板详情 +// 按模板ID查询详情 +func (s *sFileTemplate) Detail(ctx context.Context, req *v1.FileTemplateDetailReq) (res *v1.FileTemplateDetailRes, err error) { + return fileTemplateSpec.Detail(ctx, req.Id, req) +} + +// Add 新增文件模板 +// 校验模板编码唯一后新增,文件路径由临时目录转正,返回新记录ID +func (s *sFileTemplate) Add(ctx context.Context, req *v1.FileTemplateAddReq) (res *v1.IdRes, err error) { + if err = CheckUnique(ctx, dao.FileTemplate.Ctx(ctx), "code", req.Code, 0, "模板编码"); err != nil { + return nil, err + } + result, err := dao.FileTemplate.Ctx(ctx).Data(map[string]any{ + "name": req.Name, + "code": req.Code, + "file_name": req.FileName, + "file_path": SaveFileField(ctx, req.FilePath, "file_template"), + "note": req.Note, + "sort": req.Sort, + }).Insert() + if err != nil { + return nil, err + } + idv, _ := result.LastInsertId() + return &v1.IdRes{Id: idv}, nil +} + +// Update 修改文件模板 +// 按模板ID更新记录,文件路径由临时目录转正 +func (s *sFileTemplate) Update(ctx context.Context, req *v1.FileTemplateUpdateReq) (res *v1.FileTemplateUpdateRes, err error) { + if _, err = dao.FileTemplate.Ctx(ctx).Where("id", req.Id).Data(map[string]any{ + "name": req.Name, + "code": req.Code, + "file_name": req.FileName, + "file_path": SaveFileField(ctx, req.FilePath, "file_template"), + "note": req.Note, + "sort": req.Sort, + }).Update(); err != nil { + return nil, err + } + return &v1.FileTemplateUpdateRes{}, nil +} + +// Delete 删除文件模板 +// 按模板ID删除单条记录 +func (s *sFileTemplate) Delete(ctx context.Context, req *v1.FileTemplateDeleteReq) (res *v1.FileTemplateDeleteRes, err error) { + if err = fileTemplateSpec.Delete(ctx, req.Id); err != nil { + return nil, err + } + return &v1.FileTemplateDeleteRes{}, nil +} + +// BatchDelete 批量删除文件模板 +// 按模板ID列表批量删除记录 +func (s *sFileTemplate) BatchDelete(ctx context.Context, req *v1.FileTemplateBatchDeleteReq) (res *v1.FileTemplateBatchDeleteRes, err error) { + if err = fileTemplateSpec.BatchDelete(ctx, req.Ids); err != nil { + return nil, err + } + return &v1.FileTemplateBatchDeleteRes{}, nil +} + +// ByCode 按编码查询模板 +// 登录用户即可访问,按模板编码查询模板详情 +func (s *sFileTemplate) ByCode(ctx context.Context, req *v1.FileTemplateByCodeReq) (res *v1.FileTemplateByCodeRes, err error) { + one, err := dao.FileTemplate.Ctx(ctx).Where("code", req.Code).One() + if err != nil { + return nil, err + } + if len(one) == 0 { + return nil, gerror.NewCode(consts.CodeRecordNotFound) + } + return &v1.FileTemplateByCodeRes{FileTemplateItem: s.toItem(ctx, one)}, nil +} + +// toItem 数据库记录转换为文件模板行数据 +// 文件路径补全为可访问的URL +func (s *sFileTemplate) toItem(ctx context.Context, r gdb.Record) v1.FileTemplateItem { + return v1.FileTemplateItem{ + Id: r["id"].Int64(), + Name: r["name"].String(), + Code: r["code"].String(), + FileName: r["file_name"].String(), + FilePath: upload.GetFileUrl(ctx, r["file_path"].String()), + Note: r["note"].String(), + Sort: r["sort"].Int(), + CreateUser: r["create_user"].String(), + CreateTime: r["create_time"].GTime(), + } +} diff --git a/internal/logic/generator.go b/internal/logic/generator.go new file mode 100644 index 0000000..9ad5d7d --- /dev/null +++ b/internal/logic/generator.go @@ -0,0 +1,1001 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + "fmt" + "go/format" + "os" + "path/filepath" + "regexp" + "strings" + "text/template" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + + "easygoadmin/api/v1" + "easygoadmin/internal/consts" + "easygoadmin/internal/service" +) + +// ============================== 代码生成业务实现 ============================== + +// sGenerator 代码生成业务实现 +// 实现 service.IGenerator 接口,承载代码生成模块的业务逻辑 +type sGenerator struct{} + +func init() { + service.RegisterGenerator(&sGenerator{}) +} + +// ============================================================================ +// 模板数据模型:后端三模板(api/controller/logic)与前端 ui/ui2 模板共用。 +// 模板语法:Go text/template({{.Var}} / {{range}} / {{if}})。 +// ============================================================================ + +// genChoice 枚举选项 +// 从字段注释解析,例如 "状态:1-正常 2-禁用" +type genChoice struct { + Label string `json:"label"` // 选项显示名 + Value string `json:"value"` // 选项值 + Type string `json:"type"` // 标签类型:number / string + ValueType string `json:"valueType"` // TS 类型注释 +} + +// genField 单字段 +// 驱动前后端模板的字段循环 +type genField struct { + Name string // 数据库列名(snake) + CamelName string // JS 驼峰(parentId) + CamelExport string // Go 导出名(ParentId) + CamelLower string // 首字母小写驼峰(供 filtered 参数) + Label string // 中文名(注释清洗) + Comment string // 原始注释 + Type string // 伪 Eloquent 类型(CharField/IntegerField/...) + GoType string // Go 类型(string/int/float64) + EntityGoType string // 实体与 dto 的 Go 类型(int64/*gtime.Time 等) + IsGtime bool // 时间列(entity 使用 *gtime.Time,do 同步) + RowMethod string // 行取值方法(.String()/.Int()/.Float64()) + IsNumber bool // 数值类(驱动 if 判断) + Required bool // 表单必填 + Default string // 表单默认值 + IsImage bool // 图片字段 + IsRichText bool // 富文本字段 + IsTextarea bool // 多行文本字段 + MinValue string // 数值控件最小值 + MaxValue string // 数值控件最大值 + Choices []genChoice // 枚举选项集合 + HasChoices bool // 是否包含枚举选项 + IsPk bool // 主键列 + IsAudit bool // 审计列(不进表单与列表) +} + +// genVars 模板顶层数据 +type genVars struct { + AppName string // 前端 DOM 与表单名(等于模块驼峰) + ApiPath string // 前端 API 导入路径(domain/moduleCamel) + Domain string // 路由域(system/data/...) + DomainPkg string // Go 包名 + DomainPkgExport string // 包名导出形(System/Data) + Module string // 模块名(snake:config_item) + ModuleCamel string // 模块驼峰(configItem) + ModelClass string // 结构体类名(ConfigItem) + TableNameFull string // 原表名(goframe_config_item) + ModuleComment string // 中文名 + RoutePrefix string // 路由前缀(由模块名直接承担) + PermissionPrefix string // 权限前缀(sys:{module}) + PrimaryPk string // 主键列名 + DaoName string // dao 变量名(ConfigItem) + DisplayField string // 模糊搜索列 + DisplayLabel string // 模糊搜索列的中文名 + DisplayFieldCamel string // 模糊搜索列的驼峰名 + ParentIdField string // 上级字段列名 + ParentIdCamel string // 上级字段驼峰名 + ParentIdCamelExport string // 上级字段导出名 + OrderField string // 排序字段 + OrderType string // 排序方向 + TemplateType string // 模板类型:ui / ui2 + IsTree bool // 是否树形结构 + HasStatus bool // 是否含状态字段 + HasSort bool // 是否含排序字段 + HasImport bool // 是否支持导入 + HasExport bool // 是否支持导出 + HasCreateUser bool // 是否含创建人字段 + HasCreateTime bool // 是否含创建时间字段 + NeedGtime bool // 是否需要引入 gtime + HasGtime bool // 是否存在时间列 + Fields []genField // 全部字段 + ListFields []genField // 列表字段 + FormFields []genField // 表单字段 + FilterFields []genField // 筛选字段 + HasImageField bool // 是否含图片字段 + HasRichTextField bool // 是否含富文本字段 +} + +// genDomainMap 表到域的映射 +// 架构文档约定的域划分,未匹配时默认 tool +var genDomainMap = map[string]string{ + "user": "system", "role": "system", "menu": "system", "dept": "system", + "level": "system", "position": "system", "tenant": "system", + "config": "data", "config_item": "data", "param": "data", "dict": "data", + "dict_item": "data", "notice": "data", "city": "data", + "article": "content", "category": "content", "link": "content", + "operation_log": "monitor", "login_log": "monitor", "job": "monitor", "job_log": "monitor", + "example": "tool", "file_template": "tool", "generator": "tool", "upload": "tool", +} + +// Page 数据表分页 +// MySQL 查询 information_schema,其他方言使用 gdb 元数据降级 +func (s *sGenerator) Page(ctx context.Context, req *v1.GeneratorPageReq) (res *v1.GeneratorPageRes, err error) { + tables, err := s.tableList(ctx) + if err != nil { + return nil, err + } + // 过滤 + filtered := make([]v1.GeneratorTableItem, 0, len(tables)) + for _, t := range tables { + if req.TableName != "" && !strings.Contains(strings.ToLower(t.TableName), strings.ToLower(req.TableName)) { + continue + } + if req.TableComment != "" && !strings.Contains(t.TableComment, req.TableComment) { + continue + } + filtered = append(filtered, t) + } + total := len(filtered) + start := req.Offset() + if start > total { + start = total + } + end := start + req.GetPageSize() + if end > total { + end = total + } + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, filtered[start:end]), nil +} + +// Preview 预览代码 +// 渲染模板但不落盘,返回文件清单 +func (s *sGenerator) Preview(ctx context.Context, req *v1.GeneratorPreviewReq) (res *v1.GeneratorPreviewRes, err error) { + res = &v1.GeneratorPreviewRes{List: []v1.GeneratorFileItem{}} + for _, raw := range req.TableNames { + tableName := parseTableArg(raw) + files, _, e := s.renderTable(ctx, tableName) + if e != nil { + return nil, gerror.NewCodef(consts.CodeParamError, "渲染 %s 失败: %v", tableName, e) + } + for _, f := range files { + res.List = append(res.List, v1.GeneratorFileItem{Path: f.path, Content: f.content}) + } + } + return res, nil +} + +// Generate 一键生成 +// +// 缺失文件补生成;已存在文件跳过; +// 开启 generator.overwrite 时覆盖,并同步创建菜单、字典与路由 +func (s *sGenerator) Generate(ctx context.Context, req *v1.GeneratorGenerateReq) (res *v1.GeneratorGenerateRes, err error) { + res = &v1.GeneratorGenerateRes{Generated: []string{}, Skipped: []string{}, Failed: []string{}, Overwritten: []string{}, Dict: []v1.GeneratorDictResult{}} + overwrite := s.overwriteEnabled(ctx) + for _, raw := range req.TableNames { + tableName := parseTableArg(raw) + skipFiles := skipTables(tableName) + var vars *genVars + var files []genFile + var e error + if skipFiles { + // 关联表:跳过文件写入,但幂等副作用(菜单/字典/路由)照常执行 + vars, e = s.buildVars(ctx, tableName) + } else { + files, vars, e = s.renderTable(ctx, tableName) + } + if e != nil { + res.Failed = append(res.Failed, fmt.Sprintf("%s: %v", tableName, e)) + continue + } + if skipFiles { + res.Skipped = append(res.Skipped, tableName+"(关联表,跳过文件)") + } + for _, f := range files { + switch s.writeFile(f.path, f.content, overwrite) { + case fileWritten: + res.Generated = append(res.Generated, f.path) + case fileExists: + res.Skipped = append(res.Skipped, f.path) + case fileOverwritten: + res.Overwritten = append(res.Overwritten, f.path) + default: + res.Failed = append(res.Failed, f.path) + } + } + // 同步创建:菜单 + 字典 + 路由注册(多表时以最后一张为准合并提示) + res.Menu = s.createMenu(ctx, vars) + res.Dict = s.createDict(ctx, vars) + res.Route = s.registerRouter(vars) + } + return res, nil +} + +// BatchGenerate 批量生成 +// 逐表渲染并统计生成、跳过、覆盖、失败数量 +func (s *sGenerator) BatchGenerate(ctx context.Context, req *v1.GeneratorBatchGenerateReq) (res *v1.GeneratorBatchGenerateRes, err error) { + res = &v1.GeneratorBatchGenerateRes{List: []v1.GeneratorBatchItem{}} + overwrite := s.overwriteEnabled(ctx) + for _, raw := range req.TableNames { + tableName := parseTableArg(raw) + item := v1.GeneratorBatchItem{Table: tableName, Success: true, Dict: []v1.GeneratorDictResult{}} + skipFiles := skipTables(tableName) + var vars *genVars + var files []genFile + var e error + if skipFiles { + vars, e = s.buildVars(ctx, tableName) + } else { + files, vars, e = s.renderTable(ctx, tableName) + } + if e != nil { + item.Success = false + item.Message = e.Error() + res.List = append(res.List, item) + continue + } + if skipFiles { + item.Message = "关联表(跳过文件)" + } + for _, f := range files { + switch s.writeFile(f.path, f.content, overwrite) { + case fileWritten: + item.Generated++ + case fileExists: + item.Skipped++ + case fileOverwritten: + item.Overwritten++ + default: + item.Failed++ + } + } + item.Menu = s.createMenu(ctx, vars) + item.Dict = s.createDict(ctx, vars) + item.Route = s.registerRouter(vars) + item.Success = item.Failed == 0 + if !skipFiles { + item.Message = fmt.Sprintf("生成 %d,跳过 %d,覆盖 %d,失败 %d", item.Generated, item.Skipped, item.Overwritten, item.Failed) + } + res.List = append(res.List, item) + } + return res, nil +} + +// ============================================================================ +// 表结构与模板数据 +// ============================================================================ + +// fileOutcome 文件写入结果 +type fileOutcome int + +const ( + fileWritten fileOutcome = iota // 新写入 + fileExists // 已存在并跳过 + fileFailed // 写入失败 + fileOverwritten // 已存在并覆盖 +) + +// overwriteEnabled 读取覆盖开关 +// +// 唯一入口为配置 generator.overwrite; +// true 时生成一律覆盖已存在文件,false 或未配置时缺失补生成、存在跳过。 +func (s *sGenerator) overwriteEnabled(ctx context.Context) bool { + return g.Cfg().MustGet(ctx, "generator.overwrite").Bool() +} + +// skipTables 生成保护 +// +// 仅多对多关联表整表跳过,这类表本就不应有模块文件; +// 其余表一律渲染,落盘由 writeFile 按文件粒度处理,不存在则补生成、存在则跳过、开启覆盖开关时覆盖。 +func skipTables(tableName string) bool { + switch stripPrefix(tableName) { + case "role_menu", "user_role": + return true + } + return false +} + +// parseTableArg 解析表参数 +// 将 "表名|注释" 解析为表名 +func parseTableArg(arg string) string { + if i := strings.Index(arg, "|"); i >= 0 { + return strings.TrimSpace(arg[:i]) + } + return strings.TrimSpace(arg) +} + +// tableList 查询数据表清单 +// MySQL 走 information_schema,其他方言走 gdb 元数据降级 +func (s *sGenerator) tableList(ctx context.Context) ([]v1.GeneratorTableItem, error) { + dbTypeVar, _ := g.Cfg().GetWithEnv(ctx, "database.default.type") + dbType := dbTypeVar.String() + if dbType != "mysql" { + // 非 MySQL:gdb 元数据仅提供表名(注释留空,模块名回退表名) + names, err := g.DB().Tables(ctx) + if err != nil { + return nil, err + } + items := make([]v1.GeneratorTableItem, 0, len(names)) + for _, n := range names { + items = append(items, v1.GeneratorTableItem{TableName: n}) + } + return items, nil + } + linkVar, _ := g.Cfg().GetWithEnv(ctx, "database.default.link") + dbName := linkVar.String() + // link 形如 mysql:user:pass@tcp(host:port)/dbname?charset=... + if i := strings.Index(dbName, "/"); i >= 0 { + dbName = dbName[i+1:] + } + if i := strings.IndexAny(dbName, "?"); i >= 0 { + dbName = dbName[:i] + } + rows, err := g.DB().GetAll(ctx, + "SELECT TABLE_NAME AS table_name, TABLE_COMMENT AS table_comment, ENGINE AS engine,"+ + " TABLE_ROWS AS table_rows, DATA_LENGTH AS data_length, AUTO_INCREMENT AS auto_increment,"+ + " TABLE_COLLATION AS table_collation, CREATE_TIME AS create_time"+ + " FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME", + dbName, + ) + if err != nil { + return nil, err + } + items := make([]v1.GeneratorTableItem, 0, len(rows)) + for _, r := range rows { + items = append(items, v1.GeneratorTableItem{ + TableName: r["table_name"].String(), + TableComment: r["table_comment"].String(), + Engine: r["engine"].String(), + TableRows: r["table_rows"].Int64(), + DataLength: r["data_length"].Int64(), + AutoIncrement: r["auto_increment"].Int64(), + TableCollation: r["table_collation"].String(), + CreateTime: r["create_time"].GTime().String(), + }) + } + return items, nil +} + +// renderTable 渲染单张表的全部目标文件 +// 返回渲染结果与模板变量,供副作用同步使用 +type genFile struct{ path, content string } + +func (s *sGenerator) renderTable(ctx context.Context, tableName string) ([]genFile, *genVars, error) { + vars, err := s.buildVars(ctx, tableName) + if err != nil { + return nil, nil, err + } + // 目标文件规划:后端三件套 + 前端(ui 普通列表 / ui2 树形) + type tplTarget struct { + tpl string + path string + } + // 后端平铺:api/v1、controller、logic、service 均不分域目录 + // dao 四件套(dao/index + dao/internal + entity + do)由生成器直接落盘 + targets := []tplTarget{ + {"templates/api.tpl", filepath.Join("api", "v1", vars.Module+".go")}, + {"templates/controller.tpl", filepath.Join("internal", "controller", vars.Module+".go")}, + {"templates/logic.tpl", filepath.Join("internal", "logic", vars.Module+".go")}, + {"templates/service.tpl", filepath.Join("internal", "service", vars.Module+".go")}, + {"templates/dao_index.tpl", filepath.Join("internal", "dao", vars.Module+".go")}, + {"templates/dao_internal.tpl", filepath.Join("internal", "dao", "internal", vars.Module+".go")}, + {"templates/entity.tpl", filepath.Join("internal", "model", "entity", vars.Module+".go")}, + {"templates/do.tpl", filepath.Join("internal", "model", "do", vars.Module+".go")}, + } + if vars.TemplateType == "ui2" { + targets = append(targets, + tplTarget{"templates/ui2/api.ts.tpl", filepath.Join("ui", "src", "api", vars.Domain, vars.ModuleCamel+".ts")}, + tplTarget{"templates/ui2/index.vue.tpl", filepath.Join("ui", "src", "views", vars.Domain, vars.ModuleCamel, "index.vue")}, + tplTarget{"templates/ui2/edit.vue.tpl", filepath.Join("ui", "src", "views", vars.Domain, vars.ModuleCamel, "edit.vue")}, + tplTarget{"templates/ui2/detail.vue.tpl", filepath.Join("ui", "src", "views", vars.Domain, vars.ModuleCamel, "detail.vue")}, + ) + } else { + targets = append(targets, + tplTarget{"templates/ui/api.ts.tpl", filepath.Join("ui", "src", "api", vars.Domain, vars.ModuleCamel+".ts")}, + tplTarget{"templates/ui/index.vue.tpl", filepath.Join("ui", "src", "views", vars.Domain, vars.ModuleCamel, "index.vue")}, + tplTarget{"templates/ui/edit.vue.tpl", filepath.Join("ui", "src", "views", vars.Domain, vars.ModuleCamel, "edit.vue")}, + tplTarget{"templates/ui/detail.vue.tpl", filepath.Join("ui", "src", "views", vars.Domain, vars.ModuleCamel, "detail.vue")}, + tplTarget{"templates/ui/columns.ts.tpl", filepath.Join("ui", "src", "views", vars.Domain, vars.ModuleCamel, "columns.ts")}, + tplTarget{"templates/ui/querySchemas.ts.tpl", filepath.Join("ui", "src", "views", vars.Domain, vars.ModuleCamel, "querySchemas.ts")}, + ) + } + files := make([]genFile, 0, len(targets)) + for _, t := range targets { + content, e := renderTemplate(t.tpl, vars) + if e != nil { + return nil, nil, fmt.Errorf("%s: %v", t.tpl, e) + } + // Go 产物统一 gofmt(结构体标签与注释对齐) + if strings.HasSuffix(t.path, ".go") { + if formatted, fe := format.Source([]byte(content)); fe == nil { + content = string(formatted) + } else { + return nil, nil, fmt.Errorf("%s: gofmt: %v", t.tpl, fe) + } + } + files = append(files, genFile{path: t.path, content: content}) + } + return files, vars, nil +} + +// renderTemplate 解析并渲染单个模板 +// +// 后端模板使用 Go 默认 {{ }} 定界; +// 前端模板因与 Vue 插值 {{ }} 冲突,改用 [[ ]] 定界 +func renderTemplate(tplPath string, vars *genVars) (string, error) { + raw, err := os.ReadFile(tplPath) + if err != nil { + return "", err + } + name := filepath.Base(tplPath) + tpl := template.New(name).Funcs(template.FuncMap{ + // UI 模板条件辅助 + "containsAny": func(s string, subs ...string) bool { + for _, sub := range subs { + if strings.Contains(s, sub) { + return true + } + } + return false + }, + "isUrlField": func(name string) bool { + switch strings.ToLower(name) { + case "website", "site", "url", "link": + return true + } + return false + }, + }) + if strings.HasPrefix(filepath.ToSlash(tplPath), "templates/ui") { + tpl = tpl.Delims("[[", "]]") + } + tpl, err = tpl.Parse(string(raw)) + if err != nil { + return "", err + } + var sb strings.Builder + if err = tpl.ExecuteTemplate(&sb, name, vars); err != nil { + return "", err + } + return sb.String(), nil +} + +// writeFile 写入文件 +// 缺失时补写,已存在默认跳过以保留手工代码,overwrite 为 true 时覆盖 +func (s *sGenerator) writeFile(path, content string, overwrite bool) fileOutcome { + _, statErr := os.Stat(path) + exists := statErr == nil + if exists && !overwrite { + return fileExists + } + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return fileFailed + } + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + return fileFailed + } + if exists { + return fileOverwritten + } + return fileWritten +} + +// buildVars 组装模板变量 +// 读取表结构元数据,构建字段集合与命名变量 +func (s *sGenerator) buildVars(ctx context.Context, tableName string) (*genVars, error) { + fieldsMap, err := g.DB().TableFields(ctx, tableName) + if err != nil { + return nil, fmt.Errorf("读取表结构失败: %v", err) + } + // 按元数据 Index 还原列顺序(map 无序) + ordered := make([]*genField, 0, len(fieldsMap)) + used := make(map[string]bool, len(fieldsMap)) + for i := 0; i < len(fieldsMap); i++ { + for name, f := range fieldsMap { + if !used[name] && f.Index == i { + used[name] = true + ordered = append(ordered, buildField(f)) + break + } + } + } + // 兜底:Index 不连续时按名称补齐 + if len(ordered) != len(fieldsMap) { + ordered = ordered[:0] + for name, f := range fieldsMap { + _ = name + ordered = append(ordered, buildField(f)) + } + } + fields := ordered + + moduleName := stripPrefix(tableName) + comment := tableCommentOf(ctx, tableName) + comment = cleanModuleComment(comment, moduleName) + + // 字段集合 + all := make([]genField, 0, len(fields)) + listF := make([]genField, 0, len(fields)) + formF := make([]genField, 0, len(fields)) + filterF := make([]genField, 0, len(fields)) + var displayField = "name" + displayCandidates := []string{"name", "title", "label", "username", "realname", "nickname", + "category_name", "job_name", "dept_name", "role_name", "position_name", "level_name"} + foundDisplay := false + hasStatus, hasSort, hasParent := false, false, false + hasImage, hasRich := false, false + hasCreateUser, hasCreateTime := false, false + hasGtime := false + + for _, f := range fields { + all = append(all, *f) + if f.IsGtime { + hasGtime = true + } + switch f.Name { + case "id": + // 主键进列表(树形/分页需要 item.Id),不进表单与筛选 + listF = append(listF, *f) + continue + case "create_user": + hasCreateUser = true + continue + case "create_time": + hasCreateTime = true + continue + case "update_user", "update_time", "del_flag", "tenant_id": + continue // 审计/租户列不进前后端字段 + case "status": + hasStatus = true + case "sort": + hasSort = true + case "parent_id", "pid": + hasParent = true + } + // 显示字段(优先级) + if !foundDisplay { + for _, c := range displayCandidates { + if f.Name == c { + displayField = f.Name + foundDisplay = true + break + } + } + } + if f.IsRichText { + hasRich = true + } + if f.IsImage { + hasImage = true + } + // 列表:排除长文本/密码类 + if !f.IsRichText && f.Name != "password" && f.Name != "salt" && f.Type != "BinaryField" { + listF = append(listF, *f) + } + // 表单:全部业务列 + if f.Type != "BinaryField" { + formF = append(formF, *f) + } + // 筛选:字符串非图片/非URL → Input 逐列模糊;带枚举 → Select;外键 *_id → InputNumber; + // 排序/统计等普通数值列无查询意义,不进筛选 + switch { + case f.Type == "CharField" && !f.IsImage && !isUrlFieldName(f.Name): + filterF = append(filterF, *f) + case f.HasChoices && (f.Type == "IntegerField" || f.Type == "BooleanField"): + filterF = append(filterF, *f) + case f.Type == "IntegerField" && strings.HasSuffix(f.Name, "_id"): + filterF = append(filterF, *f) + } + } + // display 必须在 list 中展示 + if !foundDisplay && len(listF) > 0 { + displayField = listF[0].Name + } + + // 域与命名 + domain := genDomainMap[moduleName] + if domain == "" { + domain = "tool" + } + modelClass := pascal(moduleName) + moduleCamel := camelLower(moduleName) + + parentIdField := "parent_id" + if hasParentField(fields, "pid") { + parentIdField = "pid" + } + + vars := &genVars{ + AppName: moduleCamel, + ApiPath: domain + "/" + moduleCamel, + Domain: domain, + DomainPkg: strings.ReplaceAll(domain, "_", ""), + DomainPkgExport: pascal(strings.ReplaceAll(domain, "_", "")), + Module: moduleName, + ModuleCamel: moduleCamel, + ModelClass: modelClass, + TableNameFull: tableName, + ModuleComment: comment, + RoutePrefix: moduleName, + PermissionPrefix: "sys:" + camelLower(moduleName), + PrimaryPk: "id", + DaoName: pascal(moduleName), + DisplayField: displayField, + DisplayLabel: labelOf(displayField, fields), + DisplayFieldCamel: camelLower(displayField), + ParentIdField: parentIdField, + ParentIdCamel: camelLower(parentIdField), + ParentIdCamelExport: pascal(camelLower(parentIdField)), + OrderField: orderField(hasSort), + OrderType: orderType(hasSort), + TemplateType: templateType(hasParent), + IsTree: hasParent, + HasStatus: hasStatus, + HasSort: hasSort, + HasImport: true, + HasExport: true, + HasCreateUser: hasCreateUser, + HasCreateTime: hasCreateTime, + NeedGtime: hasCreateTime, + HasGtime: hasGtime, + Fields: ptrDeref(fields), + ListFields: listF, + FormFields: formF, + FilterFields: filterF, + HasImageField: hasImage, + HasRichTextField: hasRich, + } + return vars, nil +} + +// ---------- 字段构建 ---------- + +var ( + reTinyInt1 = regexp.MustCompile(`^tinyint\(1\)`) + reInt = regexp.MustCompile(`^(tinyint|smallint|mediumint|int|bigint)\b`) + reDec = regexp.MustCompile(`^(decimal|numeric)\b`) + reFloat = regexp.MustCompile(`^(float|double|real)\b`) + reVarchar = regexp.MustCompile(`^(varchar|char|enum|set)\b`) + reText = regexp.MustCompile(`^(tinytext|text|mediumtext|longtext)\b`) + reDate = regexp.MustCompile(`^date\b`) + reDateTime = regexp.MustCompile(`^(datetime|timestamp|time)\b`) + reBlob = regexp.MustCompile(`^(tinyblob|blob|mediumblob|longblob|binary|varbinary)\b`) + reBigint = regexp.MustCompile(`^bigint\b`) + reChoice = regexp.MustCompile(`(\d+)\s*[-—-=]\s*([^\s,,;;]+)`) +) + +// buildField 数据库元数据转换为模板字段 +// 依据列类型推断伪 Eloquent 类型、Go 类型与行取值方法 +func buildField(f *gdb.TableField) *genField { + field := &genField{ + Name: f.Name, + Comment: f.Comment, + CamelName: camelLower(f.Name), + CamelExport: pascal(camelLower(f.Name)), + CamelLower: camelLower(f.Name), + Label: cleanLabel(f.Comment, f.Name), + } + switch { + case reTinyInt1.MatchString(f.Type): + field.Type = "BooleanField" + field.GoType = "int" + field.RowMethod = ".Int()" + field.IsNumber = true + case reInt.MatchString(f.Type): + field.Type = "IntegerField" + field.GoType = "int" + field.RowMethod = ".Int()" + field.IsNumber = true + case reDec.MatchString(f.Type): + field.Type = "DecimalField" + field.GoType = "float64" + field.RowMethod = ".Float64()" + field.IsNumber = true + case reFloat.MatchString(f.Type): + field.Type = "FloatField" + field.GoType = "float64" + field.RowMethod = ".Float64()" + field.IsNumber = true + case reDate.MatchString(f.Type): + field.Type = "DateField" + field.GoType = "string" + field.RowMethod = ".String()" + case reDateTime.MatchString(f.Type): + field.Type = "DateTimeField" + field.GoType = "string" + field.RowMethod = ".String()" + case reBlob.MatchString(f.Type): + field.Type = "BinaryField" + field.GoType = "string" + field.RowMethod = ".String()" + case reText.MatchString(f.Type): + field.Type = "TextField" + field.GoType = "string" + field.RowMethod = ".String()" + field.IsTextarea = true + case reVarchar.MatchString(f.Type): + field.Type = "CharField" + field.GoType = "string" + field.RowMethod = ".String()" + default: + field.Type = "CharField" + field.GoType = "string" + field.RowMethod = ".String()" + } + field.IsPk = f.Key == "PRI" || f.Name == "id" + field.IsAudit = isAuditColumn(f.Name) + field.Required = !f.Null && f.Default == nil && f.Extra != "auto_increment" && !field.IsPk + + // 实体与 dto 类型映射:bigint 转 int64,时间列转 *gtime.Time,其余值类型为非指针 + switch { + case reBigint.MatchString(f.Type): + field.EntityGoType = "int64" + case reDec.MatchString(f.Type), reFloat.MatchString(f.Type): + field.EntityGoType = "float64" + case reDate.MatchString(f.Type), reDateTime.MatchString(f.Type): + field.EntityGoType = "*gtime.Time" + field.IsGtime = true + case reBlob.MatchString(f.Type): + field.EntityGoType = "[]byte" + case reInt.MatchString(f.Type): + field.EntityGoType = "int" + default: + field.EntityGoType = "string" + } + + // 注释解析:枚举、图片、富文本 + field.Choices = parseChoices(f.Comment) + field.HasChoices = len(field.Choices) > 0 + lower := strings.ToLower(f.Name) + field.IsImage = strings.Contains(f.Comment, "图片") || strings.Contains(f.Comment, "头像") || + strings.Contains(f.Comment, "封面") || containsAny(lower, "avatar", "cover", "image", "icon", "photo") + field.IsRichText = strings.Contains(f.Comment, "富文本") || + (field.Type == "TextField" && containsAny(lower, "content", "body", "editor")) + if field.Type == "TextField" { + field.IsTextarea = true + } + // 表单默认值与数字控件边界:数值字段默认 0,输入框 min=0 max=99999 + field.Default = "" + field.MinValue = "0" + field.MaxValue = "99999" + switch field.Type { + case "IntegerField", "BooleanField", "FloatField", "DecimalField": + field.Default = "0" + } + return field +} + +// isAuditColumn 判断是否为审计列 +func isAuditColumn(name string) bool { + switch name { + case "id", "create_user", "create_time", "update_user", "update_time", "del_flag", "tenant_id", "salt": + return true + } + return false +} + +// parseChoices 从注释解析枚举 +// 例如从 "状态:1-正常 2-禁用" 解析出选项集合 +func parseChoices(comment string) []genChoice { + matches := reChoice.FindAllStringSubmatch(comment, -1) + if len(matches) == 0 { + return nil + } + choices := make([]genChoice, 0, len(matches)) + for _, m := range matches { + choices = append(choices, genChoice{ + Label: m[2], + Value: m[1], + Type: choiceTagType(m[1]), + ValueType: "number", + }) + } + return choices +} + +// choiceTagType 枚举值转换为标签类型 +// 1 对应 success,2 对应 danger,3 对应 warning,其余为 info +func choiceTagType(value string) string { + switch value { + case "1": + return "success" + case "2": + return "danger" + case "3": + return "warning" + default: + return "info" + } +} + +// cleanLabel 字段注释转换为表单标签 +// 去除枚举尾巴,保留可读的中文名 +func cleanLabel(comment, name string) string { + if comment == "" { + return camelLower(name) + } + // 去掉 "状态:1-正常 2-禁用" 的枚举尾巴 + if i := strings.IndexAny(comment, "::"); i > 0 { + comment = comment[:i] + } + return strings.TrimSpace(comment) +} + +// cleanModuleComment 表注释转换为模块中文名 +// 去除常见的表、管理、信息、数据等后缀 +func cleanModuleComment(comment, fallback string) string { + comment = strings.TrimSpace(comment) + if comment == "" { + return fallback + } + comment = strings.TrimSuffix(comment, "表") + for _, suffix := range []string{"管理", "信息", "数据"} { + comment = strings.TrimSuffix(comment, suffix) + } + return comment +} + +// stripPrefix 去除表名前缀 +// 将 goframe_x 转换为 x +func stripPrefix(tableName string) string { + return strings.TrimPrefix(tableName, "goframe_") +} + +// tableCommentOf 查询表注释 +// MySQL 查询 information_schema,其他方言回退表名 +func tableCommentOf(ctx context.Context, tableName string) string { + dbTypeVar, _ := g.Cfg().GetWithEnv(ctx, "database.default.type") + if dbTypeVar.String() != "mysql" { + return "" + } + row, err := g.DB().GetOne(ctx, + "SELECT TABLE_COMMENT AS c FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?", tableName) + if err != nil || len(row) == 0 { + return "" + } + return row["c"].String() +} + +// templateType 根据是否树形选择模板类型 +func templateType(isTree bool) string { + if isTree { + return "ui2" + } + return "ui" +} + +// orderField 返回排序字段 +func orderField(hasSort bool) string { + if hasSort { + return "sort" + } + return "id" +} + +// orderType 返回排序方向 +func orderType(hasSort bool) string { + if hasSort { + return "asc" + } + return "desc" +} + +// hasParentField 判断字段集合中是否包含指定字段 +func hasParentField(fields []*genField, name string) bool { + for _, f := range fields { + if f.Name == name { + return true + } + } + return false +} + +// labelOf 查询字段的中文名 +func labelOf(name string, fields []*genField) string { + for _, f := range fields { + if f.Name == name { + return f.Label + } + } + return name +} + +// ptrDeref 指针切片转为值切片 +func ptrDeref(fields []*genField) []genField { + out := make([]genField, 0, len(fields)) + for _, f := range fields { + out = append(out, *f) + } + return out +} + +// pascal 命名转换 +// 将 snake 或 kebab 命名转换为 Pascal,例如 config_item 转换为 ConfigItem +func pascal(s string) string { + parts := strings.FieldsFunc(s, func(r rune) bool { return r == '_' || r == '-' || r == ' ' }) + for i, p := range parts { + if p == "" { + continue + } + parts[i] = strings.ToUpper(p[:1]) + p[1:] + } + return strings.Join(parts, "") +} + +// camelLower 命名转换 +// 将 snake 命名转换为小驼峰,例如 config_item 转换为 configItem +func camelLower(s string) string { + p := pascal(s) + if p == "" { + return p + } + return strings.ToLower(p[:1]) + p[1:] +} + +// containsAny 判断字符串是否包含任一子串 +func containsAny(s string, subs ...string) bool { + for _, sub := range subs { + if strings.Contains(s, sub) { + return true + } + } + return false +} + +// isUrlFieldName 判断是否为 URL 或链接类列名 +// 这类列存储的是地址而非可读文本,不作为模糊筛选条件 +func isUrlFieldName(name string) bool { + switch strings.ToLower(name) { + case "website", "site", "url", "link": + return true + } + return false +} diff --git a/internal/logic/generator_side.go b/internal/logic/generator_side.go new file mode 100644 index 0000000..0ac4221 --- /dev/null +++ b/internal/logic/generator_side.go @@ -0,0 +1,227 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/gogf/gf/v2/frame/g" + + "easygoadmin/api/v1" + "easygoadmin/internal/dao" +) + +// ============================================================ +// 代码生成器副作用同步:模块菜单与权限节点、字典数据、router.go 路由注册 +// 所有副作用均保证幂等 +// ============================================================ + +// createMenu 生成模块主菜单与权限节点 +// +// 菜单挂在 generator.parentMenuId 指定的父菜单下; +// 按 path 查重保证幂等,已存在时直接返回。 +func (s *sGenerator) createMenu(ctx context.Context, vars *genVars) v1.GeneratorMenuResult { + path := "/" + vars.Domain + "/" + vars.ModuleCamel + component := path + "/index" + + exists, err := dao.Menu.Ctx(ctx).Where("path", path).Where("type", 0).One() + if err == nil && len(exists) > 0 { + return v1.GeneratorMenuResult{Success: false, MenuId: exists["id"].Int64(), Message: "菜单已存在"} + } + + parentId := g.Cfg().MustGet(ctx, "generator.parentMenuId").Int() + // 同级最大 sort + 5 + sortBase := 5 + if one, e := dao.Menu.Ctx(ctx).Where("parent_id", parentId).Fields("MAX(sort) AS max_sort").One(); e == nil && !one["max_sort"].IsNil() { + sortBase = one["max_sort"].Int() + 5 + } + + result, err := dao.Menu.Ctx(ctx).Data(map[string]any{ + "parent_id": parentId, + "name": vars.ModuleComment, + "icon": "el-icon-Document", + "icon2": "GlobalOutlined", + "path": path, + "component": component, + "permission": "", + "target": 0, + "type": 0, + "status": 0, + "hide": 0, + "sort": sortBase, + "note": "代码生成器自动创建", + }).Insert() + if err != nil { + return v1.GeneratorMenuResult{Success: false, Message: "菜单创建失败: " + err.Error()} + } + menuId, _ := result.LastInsertId() + + // 权限节点(与生成模块实际路由一一对应) + type perm struct{ name, code string } + perms := []perm{ + {vars.ModuleComment + "-分页查询", ":page"}, + {vars.ModuleComment + "-列表查询", ":list"}, + {vars.ModuleComment + "-详情查询", ":detail"}, + {vars.ModuleComment + "-新增", ":add"}, + {vars.ModuleComment + "-编辑", ":update"}, + {vars.ModuleComment + "-删除", ":delete"}, + {vars.ModuleComment + "-批量删除", ":batchDelete"}, + } + if vars.HasStatus { + perms = append(perms, perm{vars.ModuleComment + "-状态管理", ":status"}) + } + if vars.HasImport { + perms = append(perms, perm{vars.ModuleComment + "-导入", ":import"}) + } + if vars.HasExport { + perms = append(perms, perm{vars.ModuleComment + "-导出", ":export"}) + } + nodeSort := 0 + for _, pm := range perms { + nodeSort += 5 + if _, e := dao.Menu.Ctx(ctx).Data(map[string]any{ + "parent_id": menuId, + "name": pm.name, + "icon": "", + "path": "", + "component": "", + "permission": vars.PermissionPrefix + pm.code, + "target": 0, + "type": 1, + "status": 0, + "hide": 0, + "sort": nodeSort, + "note": "代码生成器自动创建", + }).Insert(); e != nil { + return v1.GeneratorMenuResult{Success: false, MenuId: menuId, Message: "权限节点创建失败: " + e.Error()} + } + } + return v1.GeneratorMenuResult{Success: true, MenuId: menuId, + Message: fmt.Sprintf("菜单创建成功(含 %d 个权限节点)", len(perms))} +} + +// createDict 为带枚举注释的表单字段创建字典 +// +// 字典编码形如 {module_snake}_{field_snake},按编码查重保证幂等 +func (s *sGenerator) createDict(ctx context.Context, vars *genVars) []v1.GeneratorDictResult { + results := []v1.GeneratorDictResult{} + for _, f := range vars.FormFields { + if !f.HasChoices { + continue + } + code := vars.Module + "_" + f.Name + exists, err := dao.Dict.Ctx(ctx).Where("code", code).One() + if err == nil && len(exists) > 0 { + results = append(results, v1.GeneratorDictResult{Dict: code, Status: "已存在"}) + continue + } + dResult, err := dao.Dict.Ctx(ctx).Data(map[string]any{ + "name": vars.ModuleComment + "-" + f.Label, + "code": code, + "sort": 0, + "note": "代码生成器自动创建", + }).Insert() + if err != nil { + results = append(results, v1.GeneratorDictResult{Dict: code, Status: "创建失败: " + err.Error()}) + continue + } + dictId, _ := dResult.LastInsertId() + itemSort := 0 + for _, c := range f.Choices { + itemSort += 5 + if _, e := dao.DictItem.Ctx(ctx).Data(map[string]any{ + "dict_id": dictId, + "name": c.Label, + "value": c.Value, + "sort": itemSort, + "note": "", + }).Insert(); e != nil { + results = append(results, v1.GeneratorDictResult{Dict: code, Status: "字典项失败: " + e.Error()}) + break + } + } + results = append(results, v1.GeneratorDictResult{ + Dict: code, Name: vars.ModuleComment + "-" + f.Label, Items: len(f.Choices), Status: "创建成功", + }) + } + return results +} + +// registerRouter 将路由注册行写入 internal/router/router.go +// +// 按控制器是否已存在保证幂等,已注册时直接返回; +// 在最后一个 Bind 行之后插入新注册行。 +func (s *sGenerator) registerRouter(vars *genVars) v1.GeneratorRouteResult { + const routerFile = "internal/router/router.go" + content, err := os.ReadFile(routerFile) + if err != nil { + return v1.GeneratorRouteResult{Success: false, Message: "路由文件不存在: " + err.Error()} + } + text := string(content) + + if strings.Contains(text, "new(controller.C"+vars.ModelClass+")") { + return v1.GeneratorRouteResult{Success: false, Message: "路由已存在: " + vars.ModelClass} + } + + // 1) 控制器为单一 controller 包(扁平结构),确认已导入 + if !strings.Contains(text, `"easygoadmin/internal/controller"`) { + return v1.GeneratorRouteResult{Success: false, Message: "路由缺少 controller 导入"} + } + + // 2) 在最后一个 Bind 行之后插入注册行 + idx := strings.LastIndex(text, "\n\tBind(s, new(") + if idx < 0 { + return v1.GeneratorRouteResult{Success: false, Message: "未找到路由注册锚点"} + } + lineEnd := strings.Index(text[idx:], "\n") + insertAt := idx + lineEnd + newLine := "\n\tBind(s, new(controller.C" + vars.ModelClass + ")) // 代码生成器注册" + text = text[:insertAt] + newLine + text[insertAt:] + + if err = os.WriteFile(routerFile, []byte(text), 0o644); err != nil { + return v1.GeneratorRouteResult{Success: false, Message: "路由写入失败: " + err.Error()} + } + return v1.GeneratorRouteResult{Success: true, Message: "路由注册成功: " + vars.ModelClass} +} diff --git a/internal/logic/index.go b/internal/logic/index.go new file mode 100644 index 0000000..c96ce44 --- /dev/null +++ b/internal/logic/index.go @@ -0,0 +1,327 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + "sort" + + "github.com/gogf/gf/v2/errors/gerror" + + "easygoadmin/api/v1" + "easygoadmin/internal/consts" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" + "easygoadmin/utility/crypto" + "easygoadmin/utility/upload" +) + +// ============================== 个人中心与菜单聚合业务实现 ============================== + +// sIndex 个人中心与菜单聚合业务实现 +// 实现 service.IIndex 接口,承载当前用户信息、菜单树与个人资料相关业务 +type sIndex struct{} + +func init() { + service.RegisterIndex(&sIndex{}) +} + +// GetUser 获取当前登录用户信息 +// +// 敏感字段永不返回;角色与权限来自身份加载器, +// 超管返回通配权限码,供前端放开全部按钮权限 +func (s *sIndex) GetUser(ctx context.Context, req *v1.GetUserReq) (res *v1.GetUserRes, err error) { + uid := consts.GetUserID(ctx) + if uid <= 0 { + return nil, gerror.NewCode(consts.CodeUnauthorized) + } + one, err := dao.User.Ctx(ctx).Where("id", uid).One() + if err != nil { + return nil, err + } + if len(one) == 0 { + return nil, gerror.NewCode(consts.CodeUserNotFound) + } + + // 角色编码与权限节点(与 JWT 中间件同源:user_role → role / role_menu → menu.permission) + roles := consts.GetRoles(ctx) + perms := consts.GetPerms(ctx) + if len(roles) == 0 { + if idn, lerr := LoadIdentity(ctx, uid); lerr == nil && idn != nil { + roles, perms = idn.Roles, idn.Perms + } + } + if roles == nil { + roles = []string{} + } + // 超管返回通配权限码 *:*:*(前端据此放开全部按钮权限) + if s.isSuperAdmin(ctx, uid) { + perms = []string{consts.PermWildcard} + } else if perms == nil { + perms = []string{} + } + + // 城市编码数组(省市区街道,空编码跳过);city_info 为空时按编码回填名称 + city := make([]string, 0, 4) + for _, col := range []string{"province_code", "city_code", "district_code", "street_code"} { + if v := one[col].String(); v != "" { + city = append(city, v) + } + } + cityInfo := one["city_info"].String() + if cityInfo == "" && len(city) > 0 { + cityInfo = ResolveCityNames(ctx, city, ",") + } + return &v1.GetUserRes{ + Id: one["id"].Int64(), + Username: one["username"].String(), + Realname: one["realname"].String(), + Gender: one["gender"].Int(), + Avatar: upload.GetFileUrl(ctx, one["avatar"].String()), + Mobile: one["mobile"].String(), + Email: one["email"].String(), + DeptId: one["dept_id"].Int(), + LevelId: one["level_id"].Int(), + PositionId: one["position_id"].Int(), + City: city, + CityInfo: cityInfo, + Address: one["address"].String(), + Intro: one["intro"].String(), + Status: one["status"].Int(), + Note: one["note"].String(), + TenantId: one["tenant_id"].Int(), + Roles: roles, + Permissions: perms, + }, nil +} + +// GetMenus 获取当前用户菜单树 +// +// 关联路径:user → user_role → role → role_menu → menu,超管返回全量 +func (s *sIndex) GetMenus(ctx context.Context, req *v1.GetMenusReq) (res *v1.GetMenusRes, err error) { + return s.getMenuTree(ctx) +} + +// GetMenusById 获取当前用户菜单树(带路径参数) +// +// 与无路径参数的菜单路由共用同一处理逻辑 +func (s *sIndex) GetMenusById(ctx context.Context, req *v1.GetMenusByIdReq) (res *v1.GetMenusRes, err error) { + return s.getMenuTree(ctx) +} + +// getMenuTree 组装菜单树 +// +// 仅取目录与菜单节点,按钮权限节点不进菜单树 +func (s *sIndex) getMenuTree(ctx context.Context) (*v1.GetMenusRes, error) { + uid := consts.GetUserID(ctx) + if uid <= 0 { + return nil, gerror.NewCode(consts.CodeUnauthorized) + } + + // 1. 可见菜单集合:超管全量,其余按角色关联 + // 仅取目录/菜单(type=0)、启用(status=0)、非隐藏(hide=0);按钮权限节点不进菜单树(由 GetUser.permissions 下发) + query := dao.Menu.Ctx(ctx).Where("type", 0).Where("status", 0).Where("hide", 0) + if !s.isSuperAdmin(ctx, uid) { + roleRows, err := dao.UserRole.Ctx(ctx).Where("user_id", uid).All() + if err != nil { + return nil, err + } + if len(roleRows) == 0 { + empty := []*v1.MenuTreeItem{} + return &empty, nil + } + roleIds := make([]int64, 0, len(roleRows)) + for _, r := range roleRows { + roleIds = append(roleIds, r["role_id"].Int64()) + } + menuRows, err := dao.RoleMenu.Ctx(ctx).WhereIn("role_id", roleIds).All() + if err != nil { + return nil, err + } + if len(menuRows) == 0 { + empty := []*v1.MenuTreeItem{} + return &empty, nil + } + menuIds := make([]int64, 0, len(menuRows)) + for _, r := range menuRows { + menuIds = append(menuIds, r["menu_id"].Int64()) + } + query = query.WhereIn("id", menuIds) + } + rows, err := query.Order("sort, id").All() + if err != nil { + return nil, err + } + + // 2. 平铺节点 + nodes := make(map[int64]*v1.MenuTreeItem, len(rows)) + ids := make([]int64, 0, len(rows)) + for _, row := range rows { + item := &v1.MenuTreeItem{ + Id: row["id"].Int64(), + ParentId: row["parent_id"].Int(), + Name: row["name"].String(), + Icon: row["icon"].String(), + Icon2: row["icon2"].String(), + Path: row["path"].String(), + Component: row["component"].String(), + Permission: row["permission"].String(), + Target: row["target"].Int(), + Type: row["type"].Int(), + Status: row["status"].Int(), + Hide: row["hide"].Int(), + Sort: row["sort"].Int(), + Children: []*v1.MenuTreeItem{}, + } + nodes[item.Id] = item + ids = append(ids, item.Id) + } + + // 3. 挂树:父节点不在集合内(未授权/顶级)一律挂根,避免菜单丢失 + roots := make([]*v1.MenuTreeItem, 0, len(ids)) + for _, id := range ids { + node := nodes[id] + if node.ParentId != 0 { + if parent, ok := nodes[int64(node.ParentId)]; ok { + parent.Children = append(parent.Children, node) + continue + } + } + roots = append(roots, node) + } + // 父子均已按 sort,id 查询,子级顺序天然有序;根级再兜底一次 + sort.SliceStable(roots, func(i, j int) bool { + if roots[i].Sort == roots[j].Sort { + return roots[i].Id < roots[j].Id + } + return roots[i].Sort < roots[j].Sort + }) + return &roots, nil +} + +// isSuperAdmin 判断是否超级管理员 +// 优先读取上下文角色,未命中时回源用户角色表 +func (s *sIndex) isSuperAdmin(ctx context.Context, uid int) bool { + for _, r := range consts.GetRoles(ctx) { + if r == consts.RoleCodeSuperAdmin { + return true + } + } + // 角色缓存未命中时回源 + roleRows, err := dao.UserRole.Ctx(ctx).Where("user_id", uid).All() + if err != nil || len(roleRows) == 0 { + return false + } + roleIds := make([]int64, 0, len(roleRows)) + for _, r := range roleRows { + roleIds = append(roleIds, r["role_id"].Int64()) + } + count, err := dao.Role.Ctx(ctx).WhereIn("id", roleIds).Where("code", consts.RoleCodeSuperAdmin).Count() + return err == nil && count > 0 +} + +// UpdatePassword 修改个人密码 +// +// 校验旧密码后使用 bcrypt 重写密码与盐 +func (s *sIndex) UpdatePassword(ctx context.Context, req *v1.UpdatePasswordReq) (res *v1.UpdatePasswordRes, err error) { + uid := consts.GetUserID(ctx) + if uid <= 0 { + return nil, gerror.NewCode(consts.CodeUnauthorized) + } + one, err := dao.User.Ctx(ctx).Where("id", uid).Fields("id, password, salt").One() + if err != nil { + return nil, err + } + if len(one) == 0 { + return nil, gerror.NewCode(consts.CodeUserNotFound) + } + // 旧密码校验(外部 salt + bcrypt(password+salt);salt 为空兼容旧数据) + if !crypto.IsBcrypt(one["password"].String()) || !crypto.VerifyPassword(one["password"].String(), req.Password, one["salt"].String()) { + return nil, gerror.NewCode(consts.CodeOldPwdWrong) + } + salt := crypto.GenerateSalt() + hashed, err := crypto.HashPassword(req.NewPassword, salt) + if err != nil { + return nil, err + } + if _, err = dao.User.Ctx(ctx).Where("id", uid).Data(map[string]any{ + "password": hashed, + "salt": salt, + }).Update(); err != nil { + return nil, err + } + return &v1.UpdatePasswordRes{}, nil +} + +// UpdateProfile 更新个人信息 +// +// 头像执行临时转正,城市信息优先由编码数组解析名称,审计字段由全局钩子盖章 +func (s *sIndex) UpdateProfile(ctx context.Context, req *v1.UpdateProfileReq) (res *v1.UpdateProfileRes, err error) { + uid := consts.GetUserID(ctx) + if uid <= 0 { + return nil, gerror.NewCode(consts.CodeUnauthorized) + } + data := map[string]any{ + "realname": req.Realname, + "gender": req.Gender, + "avatar": SaveFileField(ctx, req.Avatar, "user"), // 头像 temp 转正 + 剥离域名 + "mobile": req.Mobile, + "email": req.Email, + "city_info": req.CityInfo, + "province_code": cityCodeAt(req.City, 0), + "city_code": cityCodeAt(req.City, 1), + "district_code": cityCodeAt(req.City, 2), + "street_code": cityCodeAt(req.City, 3), + "address": req.Address, + "intro": req.Intro, + } + // 城市信息:优先由编码数组解析名称 + if len(req.City) > 0 { + if info := ResolveCityNames(ctx, req.City, ","); info != "" { + data["city_info"] = info + } + } + if _, err = dao.User.Ctx(ctx).Where("id", uid).Data(data).Update(); err != nil { + return nil, err + } + return &v1.UpdateProfileRes{}, nil +} diff --git a/internal/logic/job.go b/internal/logic/job.go new file mode 100644 index 0000000..c50ed8d --- /dev/null +++ b/internal/logic/job.go @@ -0,0 +1,333 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gcron" + + "easygoadmin/api/v1" + "easygoadmin/internal/consts" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" +) + +// ============================== 定时任务业务实现 ============================== + +// sJob 定时任务业务实现 +// 实现 service.IJob 接口,接口定义见 api/v1/job.go +type sJob struct{} + +// jobSerializeMaps 枚举字段到字典编码的映射 +// {field}Text 由基座自动填充 +var jobSerializeMaps = map[string]string{ + "status": "job_status", + "execute_policy": "job_execute_policy", + "is_sync": "job_is_sync", +} + +func init() { + service.RegisterJob(&sJob{}) + // 调度器启动加载(cmd 启动流程会调用 DaemonInit,此处保证 init 顺序无关) +} + +// jobSpec 定时任务模块声明 +// 调度器挂载与摘除通过钩子承载 +var jobSpec = ModuleSpec[v1.JobItem, v1.JobItem, v1.JobDetailRes]{ + Model: dao.Job.Ctx, + LikeFields: []string{"job_name", "job_alias"}, + EqFields: []string{"status"}, + OrderBy: "id DESC", + ListOrderBy: "id", + SerializeMaps: jobSerializeMaps, + ToData: jobToData, + BeforeAdd: jobDefaultStatus, // 新增固定 status=0(未发布) + BeforeUpdate: jobExistGuard, // 存在性检查 + AfterUpdate: jobRemount, // 运行中任务重挂新配置 + BeforeDelete: jobDeleteGuard, // 存在性 + 摘除调度 +} + +// jobToData 全量表单语义转换 +// 新增与修改均提交全部字段,status 不在表单内 +func jobToData(req any) map[string]any { + switch r := req.(type) { + case *v1.JobAddReq: + return map[string]any{ + "job_name": r.JobName, "job_alias": r.JobAlias, "job_group": r.JobGroup, + "job_trigger": r.JobTrigger, "cron_expression": r.CronExpression, + "execute_policy": r.ExecutePolicy, "is_sync": r.IsSync, + "url": r.Url, "params": r.Params, "note": r.Note, + } + case *v1.JobUpdateReq: + return map[string]any{ + "job_name": r.JobName, "job_alias": r.JobAlias, "job_group": r.JobGroup, + "job_trigger": r.JobTrigger, "cron_expression": r.CronExpression, + "execute_policy": r.ExecutePolicy, "is_sync": r.IsSync, + "url": r.Url, "params": r.Params, "note": r.Note, + } + } + panic("jobToData: 不支持的入参类型") +} + +// jobDefaultStatus 新增时固定状态为未发布 +// 发布操作走 Status 或 Resume 方法 +func jobDefaultStatus(ctx context.Context, data map[string]any, req any) error { + data["status"] = 0 + return nil +} + +// jobExistGuard 更新前存在性检查 +func jobExistGuard(ctx context.Context, data map[string]any, id int64, req any) error { + if n, err := dao.Job.Ctx(ctx).Where("id", id).Count(); err != nil { + return err + } else if n == 0 { + return gerror.NewCode(consts.CodeRecordNotFound) + } + return nil +} + +// jobRemount 更新后重挂调度 +// 运行中任务先摘除再按新配置挂载,挂载失败时降级为暂停 +func jobRemount(ctx context.Context, id int64, req any) error { + r := mustAs[*v1.JobUpdateReq](req) + one, err := dao.Job.Ctx(ctx).Where("id", id).One() + if err != nil { + return err + } + if len(one) == 0 || one["status"].Int() != 1 { + return nil + } + removeJobFromScheduler(id) + if err = addJobToScheduler(id, r.CronExpression, r.ExecutePolicy, r.IsSync); err != nil { + // 挂载失败降级为暂停 + _, _ = dao.Job.Ctx(ctx).Where("id", id).Data(map[string]any{"status": 2}).Update() + return gerror.NewCodef(consts.CodeParamError, "Cron表达式无效: %v", err) + } + return nil +} + +// jobDeleteGuard 删除守卫 +// 校验存在性并摘除调度 +func jobDeleteGuard(ctx context.Context, id int64) error { + if n, err := dao.Job.Ctx(ctx).Where("id", id).Count(); err != nil { + return err + } else if n == 0 { + return gerror.NewCode(consts.CodeRecordNotFound) + } + removeJobFromScheduler(id) + return nil +} + +// ---------- 标准方法:3行委托 ---------- + +// Page 任务分页 +// 按查询条件分页返回定时任务列表 +func (s *sJob) Page(ctx context.Context, req *v1.JobPageReq) (res *v1.JobPageRes, err error) { + list, total, err := jobSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize()) + if err != nil { + return nil, err + } + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil +} + +// List 任务列表 +// 按关键词、状态等条件返回定时任务列表 +func (s *sJob) List(ctx context.Context, req *v1.JobListReq) (res v1.JobListRes, err error) { + list, err := jobSpec.List(ctx, req) + if err != nil { + return nil, err + } + return derefSlice(list), nil +} + +// Detail 任务详情 +// +// 因 JobDetailRes 为嵌入结构,基座 FillTextFields 的反射计划要求顶层字段,故手写实现 +func (s *sJob) Detail(ctx context.Context, req *v1.JobDetailReq) (res *v1.JobDetailRes, err error) { + one, err := dao.Job.Ctx(ctx).Where("id", req.Id).One() + if err != nil { + return nil, err + } + if len(one) == 0 { + return nil, gerror.NewCode(consts.CodeRecordNotFound) + } + var item v1.JobItem + if err = one.Struct(&item); err != nil { + return nil, err + } + FillTextFields(ctx, &item, jobSerializeMaps) + return &v1.JobDetailRes{JobItem: item}, nil +} + +// Add 新增任务 +// 默认未发布状态,发布操作走 Status 或 Resume 方法 +func (s *sJob) Add(ctx context.Context, req *v1.JobAddReq) (res *v1.IdRes, err error) { + id, err := jobSpec.Add(ctx, req) + if err != nil { + return nil, err + } + return &v1.IdRes{Id: id}, nil +} + +// Update 修改任务 +// 运行中任务先摘除再重挂 +func (s *sJob) Update(ctx context.Context, req *v1.JobUpdateReq) (res *v1.JobUpdateRes, err error) { + if err = jobSpec.Update(ctx, req.Id, req); err != nil { + return nil, err + } + return &v1.JobUpdateRes{}, nil +} + +// Delete 删除任务 +// 软删记录并摘除调度 +func (s *sJob) Delete(ctx context.Context, req *v1.JobDeleteReq) (res *v1.JobDeleteRes, err error) { + if err = jobSpec.Delete(ctx, req.Id); err != nil { + return nil, err + } + return &v1.JobDeleteRes{}, nil +} + +// BatchDelete 批量删除任务 +// 按任务ID列表批量删除记录 +func (s *sJob) BatchDelete(ctx context.Context, req *v1.JobBatchDeleteReq) (res *v1.JobBatchDeleteRes, err error) { + if err = jobSpec.BatchDelete(ctx, req.Ids); err != nil { + return nil, err + } + return &v1.JobBatchDeleteRes{}, nil +} + +// ---------- 非标准方法(手写实现) ---------- + +// RunOnce 立即执行一次 +// 异步触发指定任务,未发布任务也可执行 +func (s *sJob) RunOnce(ctx context.Context, req *v1.JobRunOnceReq) (res *v1.JobRunOnceRes, err error) { + one, err := dao.Job.Ctx(ctx).Where("id", req.Id).One() + if err != nil { + return nil, err + } + if len(one) == 0 { + return nil, gerror.NewCode(consts.CodeRecordNotFound) + } + go executeJob(context.Background(), req.Id, true) + return &v1.JobRunOnceRes{}, nil +} + +// Pause 暂停任务 +// 摘除调度并将状态置为暂停 +func (s *sJob) Pause(ctx context.Context, req *v1.JobPauseReq) (res *v1.JobPauseRes, err error) { + one, err := dao.Job.Ctx(ctx).Where("id", req.Id).One() + if err != nil { + return nil, err + } + if len(one) == 0 { + return nil, gerror.NewCode(consts.CodeRecordNotFound) + } + removeJobFromScheduler(req.Id) + if _, err = dao.Job.Ctx(ctx).Where("id", req.Id).Data(map[string]any{"status": 2}).Update(); err != nil { + return nil, err + } + return &v1.JobPauseRes{}, nil +} + +// Resume 恢复任务 +// 重新挂载调度并将状态置为运行中 +func (s *sJob) Resume(ctx context.Context, req *v1.JobResumeReq) (res *v1.JobResumeRes, err error) { + one, err := dao.Job.Ctx(ctx).Where("id", req.Id).One() + if err != nil { + return nil, err + } + if len(one) == 0 { + return nil, gerror.NewCode(consts.CodeRecordNotFound) + } + if err = addJobToScheduler(req.Id, one["cron_expression"].String(), + one["execute_policy"].Int(), one["is_sync"].Int()); err != nil { + return nil, gerror.NewCodef(consts.CodeParamError, "Cron表达式无效: %v", err) + } + if _, err = dao.Job.Ctx(ctx).Where("id", req.Id).Data(map[string]any{"status": 1}).Update(); err != nil { + return nil, err + } + return &v1.JobResumeRes{}, nil +} + +// Status 设置任务状态 +// 运行中状态挂载调度,暂停与未发布状态摘除调度 +func (s *sJob) Status(ctx context.Context, req *v1.JobStatusReq) (res *v1.JobStatusRes, err error) { + one, err := dao.Job.Ctx(ctx).Where("id", req.Id).One() + if err != nil { + return nil, err + } + if len(one) == 0 { + return nil, gerror.NewCode(consts.CodeRecordNotFound) + } + removeJobFromScheduler(req.Id) + if req.Status == 1 { + if err = addJobToScheduler(req.Id, one["cron_expression"].String(), + one["execute_policy"].Int(), one["is_sync"].Int()); err != nil { + return nil, gerror.NewCodef(consts.CodeParamError, "Cron表达式无效: %v", err) + } + } + if _, err = dao.Job.Ctx(ctx).Where("id", req.Id).Data(map[string]any{"status": req.Status}).Update(); err != nil { + return nil, err + } + return &v1.JobStatusRes{}, nil +} + +// DaemonStatus 查询调度器状态 +// 返回调度器中运行的任务数、任务键与启动时间 +func (s *sJob) DaemonStatus(ctx context.Context, req *v1.JobDaemonStatusReq) (res *v1.JobDaemonStatusRes, err error) { + entries := gcron.Entries() + ids := make([]string, 0, len(entries)) + for _, e := range entries { + ids = append(ids, e.Name) + } + startText := "" + if !daemonStart.IsZero() { + startText = daemonStart.Format("2006-01-02 15:04:05") + } + return &v1.JobDaemonStatusRes{ + Running: len(entries), + JobIds: ids, + StartTime: startText, + }, nil +} diff --git a/internal/logic/job_handler.go b/internal/logic/job_handler.go new file mode 100644 index 0000000..3a9e091 --- /dev/null +++ b/internal/logic/job_handler.go @@ -0,0 +1,75 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + "fmt" +) + +// ============================================================================ +// 定时任务本地处理方法注册表 +// +// 任务执行目标两种形态(见 executeJob): +// - url 以 http:// 或 https:// 开头:HTTP 调用目标地址 +// - 其余取值:视为本地处理方法名,按方法名从注册表查找执行 +// +// 具体任务实现在 internal/task 包,于 init 中调用 RegisterJobHandler 注册。 +// 依赖方向单向:task → logic,logic 不得反向依赖 task。 +// ============================================================================ + +// jobHandler 本地任务处理方法 +// 入参为任务参数(JSON 字符串,可为空),返回值为可写入任务日志的执行摘要 +type jobHandler func(ctx context.Context, params string) (string, error) + +// jobHandlers 本地处理方法注册表 +// 键为方法名,与任务记录的 url 字段值一一对应 +var jobHandlers = map[string]jobHandler{} + +// RegisterJobHandler 注册本地任务处理方法 +// 方法名需全局唯一,重复注册时进程直接 panic 暴露配置错误 +func RegisterJobHandler(name string, h jobHandler) { + if _, exists := jobHandlers[name]; exists { + panic(fmt.Sprintf("定时任务处理方法重复注册: %s", name)) + } + jobHandlers[name] = h +} diff --git a/internal/logic/job_log.go b/internal/logic/job_log.go new file mode 100644 index 0000000..e08a5bd --- /dev/null +++ b/internal/logic/job_log.go @@ -0,0 +1,147 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/api/v1" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" +) + +// ============================== 任务日志业务实现 ============================== + +// sJobLog 任务日志业务实现 +// 实现 service.IJobLog 接口,承载任务日志模块的业务逻辑 +type sJobLog struct{} + +// jobLogSerializeMaps 枚举字段到字典编码的映射 +// {field}Text 由 dict_item 解析,字典管理页可修改文案 +var jobLogSerializeMaps = map[string]string{ + "status": "job_log_status", +} + +// jobLogSpec 任务日志模块声明 +var jobLogSpec = ModuleSpec[v1.JobLogItem, v1.JobLogItem, v1.JobLogDetailRes]{ + Model: dao.JobLog.Ctx, + LikeFields: []string{"job_name"}, + EqFields: []string{"status", "job_id"}, + OrderBy: "id DESC", + SerializeMaps: jobLogSerializeMaps, +} + +func init() { + service.RegisterJobLog(&sJobLog{}) +} + +// Page 任务日志分页 +// 按查询条件分页返回任务日志列表 +func (s *sJobLog) Page(ctx context.Context, req *v1.JobLogPageReq) (res *v1.JobLogPageRes, err error) { + list, total, err := jobLogSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize()) + if err != nil { + return nil, err + } + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil +} + +// List 任务日志全量列表 +// 按关键词、任务ID、状态等条件返回任务日志列表 +func (s *sJobLog) List(ctx context.Context, req *v1.JobLogListReq) (res v1.JobLogListRes, err error) { + list, err := jobLogSpec.List(ctx, req) + if err != nil { + return nil, err + } + return derefSlice(list), nil +} + +// Detail 任务日志详情 +// 按日志ID查询详情 +func (s *sJobLog) Detail(ctx context.Context, req *v1.JobLogDetailReq) (res *v1.JobLogDetailRes, err error) { + return jobLogSpec.Detail(ctx, req.Id, req) +} + +// Delete 删除任务日志 +// 按日志ID删除单条记录 +func (s *sJobLog) Delete(ctx context.Context, req *v1.JobLogDeleteReq) (res *v1.JobLogDeleteRes, err error) { + if err = jobLogSpec.Delete(ctx, req.Id); err != nil { + return nil, err + } + return &v1.JobLogDeleteRes{}, nil +} + +// BatchDelete 批量删除任务日志 +// 按日志ID列表批量删除记录 +func (s *sJobLog) BatchDelete(ctx context.Context, req *v1.JobLogBatchDeleteReq) (res *v1.JobLogBatchDeleteRes, err error) { + if err = jobLogSpec.BatchDelete(ctx, req.Ids); err != nil { + return nil, err + } + return &v1.JobLogBatchDeleteRes{}, nil +} + +// toItem 数据库记录转换为任务日志行数据 +// 时间字段按可空处理,枚举状态同步补全状态文本 +func (s *sJobLog) toItem(ctx context.Context, r gdb.Record) v1.JobLogItem { + item := v1.JobLogItem{ + Id: r["id"].Int64(), + JobId: r["job_id"].Int64(), + JobName: r["job_name"].String(), + JobGroup: r["job_group"].String(), + JobTrigger: r["job_trigger"].String(), + JobMessage: r["job_message"].String(), + CronExpression: r["cron_expression"].String(), + Status: r["status"].Int(), + StatusText: DictGetText(ctx, jobLogSerializeMaps["status"], r["status"].Int()), + ConsumeTime: r["consume_time"].Int64(), + ExceptionInfo: r["exception_info"].String(), + CreateTime: r["create_time"].GTime(), + } + if v := r["start_time"]; !v.IsNil() { + item.StartTime = v.GTime() + } + if v := r["end_time"]; !v.IsNil() { + item.EndTime = v.GTime() + } + return item +} diff --git a/internal/logic/level.go b/internal/logic/level.go new file mode 100644 index 0000000..f0774c7 --- /dev/null +++ b/internal/logic/level.go @@ -0,0 +1,268 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + "fmt" + + "github.com/gogf/gf/v2/errors/gerror" + + "easygoadmin/api/v1" + "easygoadmin/internal/consts" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" + "easygoadmin/utility/excel" + "easygoadmin/utility/upload" +) + +// ============================== 职级业务实现 ============================== + +// sLevel 职级业务实现 +// 实现 service.ILevel 接口,接口定义见 api/v1/level.go +type sLevel struct{} + +func init() { + service.RegisterLevel(&sLevel{}) +} + +// levelSerializeMaps 枚举字段到字典编码的映射 +// {field}Text 由基座自动填充 +var levelSerializeMaps = map[string]string{ + "status": "level_status", +} + +// levelSpec 职级模块声明 +// +// 采用声明式实现:查询、输出与规则均在声明中定义,方法仅做委托 +var levelSpec = ModuleSpec[v1.LevelItem, v1.LevelListItem, v1.LevelDetailRes]{ + Model: dao.Level.Ctx, + LikeFields: []string{"name"}, // 名称模糊(querySchemas 输入) + EqFields: []string{"status"}, // status=0 视为未筛选(零值跳过) + OrderBy: "sort, id", + SerializeMaps: levelSerializeMaps, + ToData: levelToData, + BeforeAdd: levelDefaultStatus, // status 缺省置 1 + // Delete 守卫(存在性 + 用户引用检查) + BeforeDelete: levelDeleteGuard, +} + +// levelToData 全量表单语义转换 +// 新增与修改均提交全部字段,兼容两种入参类型 +func levelToData(req any) map[string]any { + switch r := req.(type) { + case *v1.LevelAddReq: + return map[string]any{"name": r.Name, "status": r.Status, "sort": r.Sort} + case *v1.LevelUpdateReq: + return map[string]any{"name": r.Name, "status": r.Status, "sort": r.Sort} + } + panic(fmt.Sprintf("levelToData: 不支持的入参类型 %T", req)) +} + +// levelDefaultStatus 状态缺省处理 +// 状态为零值时置为 1(正常) +func levelDefaultStatus(ctx context.Context, data map[string]any, req any) error { + if v, ok := data["status"].(int); ok && v == 0 { + data["status"] = 1 + } + return nil +} + +// levelDeleteGuard 删除守卫 +// 校验记录存在性,存在用户引用时禁止删除 +func levelDeleteGuard(ctx context.Context, id int64) error { + if n, err := dao.Level.Ctx(ctx).Where("id", id).Count(); err != nil { + return err + } else if n == 0 { + return gerror.NewCode(consts.CodeRecordNotFound) + } + if n, err := dao.User.Ctx(ctx).Where("level_id", id).Count(); err != nil { + return err + } else if n > 0 { + return gerror.NewCode(consts.CodeInUse) + } + return nil +} + +// ---------- 标准方法:3行委托 ---------- + +// Page 职级分页 +// 分页返回职级列表,并补充关联用户数与状态文本 +func (s *sLevel) Page(ctx context.Context, req *v1.LevelPageReq) (res *v1.LevelPageRes, err error) { + list, total, err := levelSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize()) + if err != nil { + return nil, err + } + fillUserCount(ctx, "level_id", &list) + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil +} + +// List 职级全量列表 +// 返回精简结构的职级列表 +func (s *sLevel) List(ctx context.Context, req *v1.LevelListReq) (res v1.LevelListRes, err error) { + list, err := levelSpec.List(ctx, req) + if err != nil { + return nil, err + } + return derefSlice(list), nil +} + +// Detail 职级详情 +// 按职级ID查询详情 +func (s *sLevel) Detail(ctx context.Context, req *v1.LevelDetailReq) (res *v1.LevelDetailRes, err error) { + return levelSpec.Detail(ctx, req.Id, req) +} + +// Add 新增职级 +// 新增成功后返回新记录ID +func (s *sLevel) Add(ctx context.Context, req *v1.LevelAddReq) (res *v1.IdRes, err error) { + id, err := levelSpec.Add(ctx, req) + if err != nil { + return nil, err + } + return &v1.IdRes{Id: id}, nil +} + +// Update 修改职级 +// 按职级ID更新记录 +func (s *sLevel) Update(ctx context.Context, req *v1.LevelUpdateReq) (res *v1.LevelUpdateRes, err error) { + err = levelSpec.Update(ctx, req.Id, req) + if err != nil { + return nil, err + } + return &v1.LevelUpdateRes{}, nil +} + +// Status 职级状态 +// 非标准方法,手写实现,设置职级的正常或禁用状态 +func (s *sLevel) Status(ctx context.Context, req *v1.LevelStatusReq) (res *v1.LevelStatusRes, err error) { + if _, err = dao.Level.Ctx(ctx).Where("id", req.Id).Data(map[string]any{"status": req.Status}).Update(); err != nil { + return nil, err + } + return &v1.LevelStatusRes{}, nil +} + +// Delete 删除职级 +// 按职级ID删除单条记录 +func (s *sLevel) Delete(ctx context.Context, req *v1.LevelDeleteReq) (res *v1.LevelDeleteRes, err error) { + if err = levelSpec.Delete(ctx, req.Id); err != nil { + return nil, err + } + return &v1.LevelDeleteRes{}, nil +} + +// BatchDelete 批量删除职级 +// 按职级ID列表批量删除记录 +func (s *sLevel) BatchDelete(ctx context.Context, req *v1.LevelBatchDeleteReq) (res *v1.LevelBatchDeleteRes, err error) { + if err = levelSpec.BatchDelete(ctx, req.Ids); err != nil { + return nil, err + } + return &v1.LevelBatchDeleteRes{}, nil +} + +// ---------- 非标准方法(手写实现)(Import/Export)---------- + +// Import 导入职级 +// +// 表格列依次为名称、状态、排序,名称重复时跳过并记录原因 +func (s *sLevel) Import(ctx context.Context, req *v1.LevelImportReq) (res *v1.LevelImportRes, err error) { + _, rows, err := excel.ReadSheet(req.File) + if err != nil { + return nil, err + } + res = &v1.LevelImportRes{Total: len(rows)} + tenantId := consts.GetTenantID(ctx) + for i, row := range rows { + name := cellAt(row, 0) + if name == "" { + res.Failed++ + res.Reasons = append(res.Reasons, fmt.Sprintf("第%d行:名称为空", i+2)) + continue + } + count, e := dao.Level.Ctx(ctx).Where("name", name).Count() + if e != nil { + return nil, e + } + if count > 0 { + res.Failed++ + res.Reasons = append(res.Reasons, fmt.Sprintf("第%d行:名称已存在", i+2)) + continue + } + status := cellInt(row, 1, 1) + if status <= 0 { + status = 1 + } + if _, e = dao.Level.Ctx(ctx).Data(map[string]any{ + "name": name, + "status": status, + "sort": cellInt(row, 2, 0), + "tenant_id": tenantId, + }).Insert(); e != nil { + res.Failed++ + res.Reasons = append(res.Reasons, fmt.Sprintf("第%d行:写入失败 %v", i+2, e)) + continue + } + res.Success++ + } + return res, nil +} + +// Export 导出职级 +// 按查询条件导出职级数据并返回文件访问地址 +func (s *sLevel) Export(ctx context.Context, req *v1.LevelExportReq) (res *v1.LevelExportRes, err error) { + m := levelSpec.applyFilters(ctx, levelSpec.Model(ctx), req) + rows, err := m.Order("sort, id").All() + if err != nil { + return nil, err + } + headers := []string{"名称", "状态", "排序"} + data := make([][]any, 0, len(rows)) + for _, r := range rows { + data = append(data, []any{r["name"].String(), userStatusText(r["status"].Int()), r["sort"].Int()}) + } + fileName, _, err := excel.WriteSheet(upload.BaseDir(ctx), "职级数据", headers, data) + if err != nil { + return nil, err + } + url := upload.GetFileUrl(ctx, fileName) + return &url, nil +} diff --git a/internal/logic/link.go b/internal/logic/link.go new file mode 100644 index 0000000..a00d331 --- /dev/null +++ b/internal/logic/link.go @@ -0,0 +1,147 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" +) + +// ============================== 友情链接业务实现 ============================== + +// sLink 友情链接业务实现 +// 实现 service.ILink 接口,接口定义见 api/v1/link.go +type sLink struct{} + +func init() { + service.RegisterLink(&sLink{}) +} + +// linkSerializeMaps 枚举字段到字典编码的映射 +// {field}Text 由基座自动填充 +var linkSerializeMaps = map[string]string{ + "type": "link_type", + "form": "link_form", + "status": "link_status", +} + +// linkSpec 友链模块声明 +var linkSpec = ModuleSpec[v1.LinkItem, v1.LinkItem, v1.LinkDetailRes]{ + Model: dao.Link.Ctx, + LikeFields: []string{"name"}, + EqFields: []string{"type", "status"}, + OrderBy: "sort, id", + SerializeMaps: linkSerializeMaps, + FileFields: []string{"image"}, + FileSaveDir: "link", + ToData: linkToData, +} + +// linkToData 全量表单语义转换 +// 兼容新增与修改请求,将入参转换为落库字段 +func linkToData(req any) map[string]any { + switch r := req.(type) { + case *v1.LinkAddReq: + return map[string]any{"name": r.Name, "type": r.Type, "url": r.Url, "form": r.Form, + "image": r.Image, "status": r.Status, "sort": r.Sort, "note": r.Note} + case *v1.LinkUpdateReq: + return map[string]any{"name": r.Name, "type": r.Type, "url": r.Url, "form": r.Form, + "image": r.Image, "status": r.Status, "sort": r.Sort, "note": r.Note} + } + panic("linkToData: 不支持的入参类型") +} + +// Page 友链分页 +// 按查询条件分页返回友链列表 +func (s *sLink) Page(ctx context.Context, req *v1.LinkPageReq) (res *v1.LinkPageRes, err error) { + list, total, err := linkSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize()) + if err != nil { + return nil, err + } + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil +} + +// Detail 友链详情 +// 按友链ID查询详情 +func (s *sLink) Detail(ctx context.Context, req *v1.LinkDetailReq) (res *v1.LinkDetailRes, err error) { + return linkSpec.Detail(ctx, req.Id, req) +} + +// Add 新增友链 +// 新增成功后返回新记录ID +func (s *sLink) Add(ctx context.Context, req *v1.LinkAddReq) (res *v1.IdRes, err error) { + id, err := linkSpec.Add(ctx, req) + if err != nil { + return nil, err + } + return &v1.IdRes{Id: id}, nil +} + +// Update 修改友链 +// 按友链ID更新记录 +func (s *sLink) Update(ctx context.Context, req *v1.LinkUpdateReq) (res *v1.LinkUpdateRes, err error) { + if err = linkSpec.Update(ctx, req.Id, req); err != nil { + return nil, err + } + return &v1.LinkUpdateRes{}, nil +} + +// Delete 删除友链 +// 按友链ID删除单条记录 +func (s *sLink) Delete(ctx context.Context, req *v1.LinkDeleteReq) (res *v1.LinkDeleteRes, err error) { + if err = linkSpec.Delete(ctx, req.Id); err != nil { + return nil, err + } + return &v1.LinkDeleteRes{}, nil +} + +// BatchDelete 批量删除友链 +// 按友链ID列表批量删除记录 +func (s *sLink) BatchDelete(ctx context.Context, req *v1.LinkBatchDeleteReq) (res *v1.LinkBatchDeleteRes, err error) { + if err = linkSpec.BatchDelete(ctx, req.Ids); err != nil { + return nil, err + } + return &v1.LinkBatchDeleteRes{}, nil +} diff --git a/internal/logic/logic.go b/internal/logic/logic.go new file mode 100644 index 0000000..6f997db --- /dev/null +++ b/internal/logic/logic.go @@ -0,0 +1,48 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// Package logic 业务逻辑层根包(扁平结构:每模块一个文件,同包 init() 注册服务)。 +// +// 目录约定(每个数据表一个模块文件): +// - internal/logic/{module}.go —— 业务实现,结构体命名 sUser/sRole…(供 internal/service/{module}.go 接口引用) +// - 各模块通过 init() 将服务实例注册到 internal/service 包级变量(依赖接线) +// - 中间件加载器接线见 internal/cmd/wire.go 与各模块 init(如 auth 的 Identity/Tenant 加载器) +package logic diff --git a/internal/logic/login_log.go b/internal/logic/login_log.go new file mode 100644 index 0000000..7a92ce9 --- /dev/null +++ b/internal/logic/login_log.go @@ -0,0 +1,151 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/api/v1" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" +) + +// ============================== 登录日志业务实现 ============================== + +// sLoginLog 登录日志业务实现 +// 实现 service.ILoginLog 接口,承载登录日志模块的业务逻辑 +type sLoginLog struct{} + +// loginLogSerializeMaps 枚举字段到字典编码的映射 +// {field}Text 由 dict_item 解析,字典管理页可修改文案 +var loginLogSerializeMaps = map[string]string{ + "log_type": "login_log_log_type", + "source": "login_log_source", + "status": "login_log_status", +} + +// loginLogSpec 登录日志模块声明 +var loginLogSpec = ModuleSpec[v1.LoginLogItem, v1.LoginLogItem, v1.LoginLogDetailRes]{ + Model: dao.LoginLog.Ctx, + LikeFields: []string{"username"}, + EqFields: []string{"log_type", "status"}, + OrderBy: "id DESC", + SerializeMaps: loginLogSerializeMaps, +} + +func init() { + service.RegisterLoginLog(&sLoginLog{}) +} + +// Page 登录日志分页 +// 按查询条件分页返回登录日志列表 +func (s *sLoginLog) Page(ctx context.Context, req *v1.LoginLogPageReq) (res *v1.LoginLogPageRes, err error) { + list, total, err := loginLogSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize()) + if err != nil { + return nil, err + } + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil +} + +// List 登录日志全量列表 +// 按关键词、类型、状态等条件返回登录日志列表 +func (s *sLoginLog) List(ctx context.Context, req *v1.LoginLogListReq) (res v1.LoginLogListRes, err error) { + list, err := loginLogSpec.List(ctx, req) + if err != nil { + return nil, err + } + return derefSlice(list), nil +} + +// Detail 登录日志详情 +// 按日志ID查询详情 +func (s *sLoginLog) Detail(ctx context.Context, req *v1.LoginLogDetailReq) (res *v1.LoginLogDetailRes, err error) { + return loginLogSpec.Detail(ctx, req.Id, req) +} + +// Delete 删除登录日志 +// 按日志ID删除单条记录 +func (s *sLoginLog) Delete(ctx context.Context, req *v1.LoginLogDeleteReq) (res *v1.LoginLogDeleteRes, err error) { + if err = loginLogSpec.Delete(ctx, req.Id); err != nil { + return nil, err + } + return &v1.LoginLogDeleteRes{}, nil +} + +// BatchDelete 批量删除登录日志 +// 按日志ID列表批量删除记录 +func (s *sLoginLog) BatchDelete(ctx context.Context, req *v1.LoginLogBatchDeleteReq) (res *v1.LoginLogBatchDeleteRes, err error) { + if err = loginLogSpec.BatchDelete(ctx, req.Ids); err != nil { + return nil, err + } + return &v1.LoginLogBatchDeleteRes{}, nil +} + +// toItem 数据库记录转换为登录日志行数据 +// 枚举字段同步补全对应文本 +func (s *sLoginLog) toItem(ctx context.Context, r gdb.Record) v1.LoginLogItem { + return v1.LoginLogItem{ + Id: r["id"].Int64(), + Username: r["username"].String(), + LogType: r["log_type"].Int(), + LogTypeText: DictGetText(ctx, loginLogSerializeMaps["log_type"], r["log_type"].Int()), + Source: r["source"].Int(), + SourceText: DictGetText(ctx, loginLogSerializeMaps["source"], r["source"].Int()), + Method: r["method"].String(), + RequestMethod: r["request_method"].String(), + Url: r["url"].String(), + Param: r["param"].String(), + Result: r["result"].String(), + Ip: r["ip"].String(), + Location: r["location"].String(), + Os: r["os"].String(), + Browser: r["browser"].String(), + ConsumeTime: r["consume_time"].Int64(), + Status: r["status"].Int(), + StatusText: DictGetText(ctx, loginLogSerializeMaps["status"], r["status"].Int()), + Error: r["error"].String(), + CreateTime: r["create_time"].GTime(), + UpdateTime: r["update_time"].GTime(), + } +} diff --git a/internal/logic/menu.go b/internal/logic/menu.go new file mode 100644 index 0000000..e948577 --- /dev/null +++ b/internal/logic/menu.go @@ -0,0 +1,305 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gerror" + + "easygoadmin/api/v1" + "easygoadmin/internal/consts" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" + "easygoadmin/utility/cache" +) + +// ============================== 菜单业务实现 ============================== + +// sMenu 菜单业务实现 +// 实现 service.IMenu 接口,接口定义见 api/v1/menu.go +type sMenu struct{} + +func init() { + service.RegisterMenu(&sMenu{}) +} + +// menuSerializeMaps 枚举字段到字典编码的映射 +// {field}Text 由基座自动填充 +var menuSerializeMaps = map[string]string{ + "type": "menu_type", + "status": "menu_status", +} + +// menuSpec 菜单模块声明 +// +// 平铺列表 + 上级循环校验 + 删除守卫,并连带清理 role_menu 与缓存; +// 树形结构由前端 buildTree 构建 +var menuSpec = ModuleSpec[v1.MenuItem, v1.MenuItem, v1.MenuDetailRes]{ + Model: dao.Menu.Ctx, + LikeFields: []string{"name", "path"}, + OrderBy: "sort, id", + SerializeMaps: menuSerializeMaps, + Filter: menuFilter, // Type/Status 为 *int 指针(nil=不筛,指向0也筛),EqFields 表达不了 + ToData: menuToData, + BeforeUpdate: menuParentGuard, // 禁止挂到自身/后代下 + BeforeDelete: menuDeleteGuard, // 存在性 + 有子菜单禁止 + AfterDelete: menuCleanRelations, // 清理 role_menu + 失效受影响用户缓存 + AfterBatchDelete: func(ctx context.Context, ids []int64) error { + return menuCleanRelationsBatch(ctx, ids) + }, +} + +// menuFilter 复杂筛选 +// Type 与 Status 为指针字段,nil 时跳过,指向 0 时同样生效 +func menuFilter(ctx context.Context, m *gdb.Model, req any) *gdb.Model { + switch r := req.(type) { + case *v1.MenuPageReq: + if r.Type != nil { + m = m.Where("type", *r.Type) + } + if r.Status != nil { + m = m.Where("status", *r.Status) + } + case *v1.MenuListReq: + if r.Type != nil { + m = m.Where("type", *r.Type) + } + if r.Status != nil { + m = m.Where("status", *r.Status) + } + } + return m +} + +// menuToData 全量表单语义转换 +// 新增与修改均提交全部字段;修改时 icon/icon2/note 未传值则不落键,保留库中原值 +func menuToData(req any) map[string]any { + switch r := req.(type) { + case *v1.MenuAddReq: + return map[string]any{ + "parent_id": r.ParentId, "name": r.Name, "icon": r.Icon, "icon2": r.Icon2, "path": r.Path, + "component": r.Component, "permission": r.Permission, "target": r.Target, + "type": r.Type, "status": r.Status, "hide": r.Hide, "sort": r.Sort, "note": r.Note, + } + case *v1.MenuUpdateReq: + data := map[string]any{ + "parent_id": r.ParentId, "name": r.Name, "path": r.Path, + "component": r.Component, "permission": r.Permission, "target": r.Target, + "type": r.Type, "status": r.Status, "hide": r.Hide, "sort": r.Sort, + } + if r.Icon != nil { + data["icon"] = *r.Icon + } + if r.Icon2 != nil { + data["icon2"] = *r.Icon2 + } + if r.Note != nil { + data["note"] = *r.Note + } + return data + } + panic("menuToData: 不支持的入参类型") +} + +// menuParentGuard 上级循环校验 +// 禁止将上级设置为自身或自己的后代节点 +func menuParentGuard(ctx context.Context, data map[string]any, id int64, req any) error { + r := mustAs[*v1.MenuUpdateReq](req) + if r.ParentId <= 0 { + return nil + } + if int64(r.ParentId) == id { + return gerror.NewCodef(consts.CodeParamError, "上级不能选择自己") + } + descIds, err := menuDescendantIds(ctx, id) + if err != nil { + return err + } + for _, did := range descIds { + if did == int64(r.ParentId) { + return gerror.NewCodef(consts.CodeParamError, "上级不能选择自己的下级") + } + } + return nil +} + +// menuDeleteGuard 删除守卫 +// 校验记录存在性,存在子菜单时禁止删除 +func menuDeleteGuard(ctx context.Context, id int64) error { + if n, err := dao.Menu.Ctx(ctx).Where("id", id).Count(); err != nil { + return err + } else if n == 0 { + return gerror.NewCode(consts.CodeRecordNotFound) + } + if n, err := dao.Menu.Ctx(ctx).Where("parent_id", id).Count(); err != nil { + return err + } else if n > 0 { + return gerror.NewCode(consts.CodeHasChildren) + } + return nil +} + +// menuCleanRelations 单条删除后清理 +// 清理 role_menu 关联并失效受影响用户的缓存 +func menuCleanRelations(ctx context.Context, id int64) error { + return menuCleanRelationsBatch(ctx, []int64{id}) +} + +// menuCleanRelationsBatch 批量删除后清理 +// 清理 role_menu 关联并失效受影响用户的缓存 +func menuCleanRelationsBatch(ctx context.Context, ids []int64) error { + roleMenus, err := dao.RoleMenu.Ctx(ctx).WhereIn("menu_id", ids).All() + if err != nil { + return err + } + if _, err = dao.RoleMenu.Ctx(ctx).WhereIn("menu_id", ids).Delete(); err != nil { + return err + } + if len(roleMenus) == 0 { + return nil + } + roleIds := make([]int64, 0, len(roleMenus)) + for _, rm := range roleMenus { + roleIds = append(roleIds, rm["role_id"].Int64()) + } + urRows, err := dao.UserRole.Ctx(ctx).WhereIn("role_id", roleIds).All() + if err != nil { + return err + } + for _, ur := range urRows { + uid := int(ur["user_id"].Int64()) + cache.Del(ctx, + consts.CacheKeyUserIdentity(uid), + consts.CacheKeyUserPerms(uid), + consts.CacheKeyUserRoles(uid), + ) + } + return nil +} + +// menuDescendantIds 递归获取全部后代ID +// 采用广度优先遍历,返回指定菜单下所有层级的后代节点ID +func menuDescendantIds(ctx context.Context, menuId int64) ([]int64, error) { + result := make([]int64, 0) + queue := []int64{menuId} + for len(queue) > 0 { + current := queue[0] + queue = queue[1:] + rows, err := dao.Menu.Ctx(ctx).Where("parent_id", current).Fields("id").All() + if err != nil { + return nil, err + } + for _, r := range rows { + idv := r["id"].Int64() + result = append(result, idv) + queue = append(queue, idv) + } + } + return result, nil +} + +// ---------- 标准方法:3行委托 ---------- + +// Page 菜单分页(平铺) +// 按查询条件分页返回平铺的菜单列表 +func (s *sMenu) Page(ctx context.Context, req *v1.MenuPageReq) (res *v1.MenuPageRes, err error) { + list, total, err := menuSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize()) + if err != nil { + return nil, err + } + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil +} + +// List 菜单平铺列表 +// data 直接为数组,树形结构由前端 buildTree 构建 +func (s *sMenu) List(ctx context.Context, req *v1.MenuListReq) (res v1.MenuListRes, err error) { + list, err := menuSpec.List(ctx, req) + if err != nil { + return nil, err + } + return list, nil +} + +// Detail 菜单详情 +// 按菜单ID查询详情 +func (s *sMenu) Detail(ctx context.Context, req *v1.MenuDetailReq) (res *v1.MenuDetailRes, err error) { + return menuSpec.Detail(ctx, req.Id, req) +} + +// Add 新增菜单 +// 新增成功后返回新记录ID +func (s *sMenu) Add(ctx context.Context, req *v1.MenuAddReq) (res *v1.IdRes, err error) { + id, err := menuSpec.Add(ctx, req) + if err != nil { + return nil, err + } + return &v1.IdRes{Id: id}, nil +} + +// Update 修改菜单 +// 按菜单ID更新记录 +func (s *sMenu) Update(ctx context.Context, req *v1.MenuUpdateReq) (res *v1.MenuUpdateRes, err error) { + if err = menuSpec.Update(ctx, req.Id, req); err != nil { + return nil, err + } + return &v1.MenuUpdateRes{}, nil +} + +// Delete 删除菜单 +// 按菜单ID删除单条记录,并清理关联关系与缓存 +func (s *sMenu) Delete(ctx context.Context, req *v1.MenuDeleteReq) (res *v1.MenuDeleteRes, err error) { + if err = menuSpec.Delete(ctx, req.Id); err != nil { + return nil, err + } + return &v1.MenuDeleteRes{}, nil +} + +// BatchDelete 批量删除菜单 +// 按菜单ID列表批量删除记录,并清理关联关系与缓存 +func (s *sMenu) BatchDelete(ctx context.Context, req *v1.MenuBatchDeleteReq) (res *v1.MenuBatchDeleteRes, err error) { + if err = menuSpec.BatchDelete(ctx, req.Ids); err != nil { + return nil, err + } + return &v1.MenuBatchDeleteRes{}, nil +} diff --git a/internal/logic/module_spec.go b/internal/logic/module_spec.go new file mode 100644 index 0000000..8ab890c --- /dev/null +++ b/internal/logic/module_spec.go @@ -0,0 +1,865 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +// ============================================================================ +// ModuleSpec 泛型 CRUD 基座(声明式) +// +// 设计原则(纪律): +// 1. 基座禁止出现"按模块分支"——差异只进声明字段与钩子;写不进钩子 → 该方法逃逸手写(合法逃生舱) +// 2. 钩子/Filter 使用具体函数类型;req any 一律经 mustAs[T] 断言(失败=编程错误,快速 panic) +// 3. 钩子内:可修改元素/data,禁止 append、禁止 HTTP/文件 IO(文件迁移统一在 saveTransform) +// 4. applyFilters 顺序固定:LikeFields → EqFields → KeywordFields → Filter → DataScope; +// 租户/软删隔离由 dao.Ctx(hooks.ApplyCtx)先于本链注入 +// 5. ToData:Add=全量必填;Update=全量表单语义(前端 Detail 回显 → 全量 PUT,零值即用户所见) +// 6. 错误:钩子错误记 stage 日志后原样上抛(保留 gerror 语义码,不包装) +// ============================================================================ + +import ( + "context" + "fmt" + "reflect" + "strings" + "sync" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + + "easygoadmin/internal/consts" + "easygoadmin/utility/upload" +) + +// ============================== 声明类型 ============================== + +// UniqueRule 唯一性规则 +// Group 为空表示全局唯一;非空表示分组内唯一,例如按租户分组 +type UniqueRule struct { + Field string // 唯一字段 + Group string // 分组字段 +} + +// DataScopeFunc 角色数据权限 +// 共享工厂为 ApplyDataScope(data_scope.go);为 nil 时不做过滤 +type DataScopeFunc func(ctx context.Context, m *gdb.Model, req any) *gdb.Model + +// ModuleSpec 模块声明 +// +// Item 为分页行类型,LItem 为列表行类型(可与 Item 相同),DRes 为详情类型。 +// 裁剪说明:List 不挂 AfterPageList,因精简列表结构无冗余列与 Text 字段,树形构建在 LItem 上。 +type ModuleSpec[Item any, LItem any, DRes any] struct { + // ─── 数据源与查询声明(applyFilters 按字段顺序执行 1→5)─── + Model func(ctx context.Context) *gdb.Model // 1 数据源(租户/软删已由 dao.Ctx 注入) + LikeFields []string // 2 逐列 LIKE(值取 req 同名列,空跳过) + EqFields []string // 3 等值(列名→驼峰 json 取值,零值跳过) + KeywordFields []string // 4 关键词列(req.keyword,多列 OR 组内配对) + Filter func(ctx context.Context, m *gdb.Model, req any) *gdb.Model // 5 复杂条件(禁重复 2-4 已覆盖者) + OrderBy string // 默认排序(硬编码,如 "sort, id") + ListOrderBy string // List 排序(空=沿用 OrderBy) + OrderableFields []string // 前端排序白名单(空=忽略 orderField/orderType 入参) + + // ─── 输出声明 ─── + IsTree bool // 树形(仅影响 List) + ParentOf func(*LItem) int64 // IsTree 必填(缺失 fail-fast) + ChildSetter func(*LItem, []*LItem) // 可选;缺省反射写 LItem 的 Children + SerializeMaps map[string]string // {field}Text 来自 dict_item + FileFields []string // 单文件字段名单(保存 temp→正式、读侧补域名) + MultiFileFields []string // 逗号多文件字段名单 + ContentFields []string // 富文本字段(存 [IMG_URL] 占位,读自动还原) + FileSaveDir string // 本模块文件统一转正目录(单文件/多文件/富文本三类共用) + FieldLabels map[string]string // 唯一校验提示的显式覆盖 + + // ─── 业务规则 ─── + UniqueFields []UniqueRule + DataScope DataScopeFunc // 角色数据权限(nil 跳过;见 ApplyDataScope) + + // ─── 钩子(具体签名;错误原样上抛)─── + BeforeAdd func(ctx context.Context, data map[string]any, req any) error + AfterAdd func(ctx context.Context, id int64, req any) error + BeforeUpdate func(ctx context.Context, data map[string]any, id int64, req any) error + AfterUpdate func(ctx context.Context, id int64, req any) error + BeforeDelete func(ctx context.Context, id int64) error + AfterDelete func(ctx context.Context, id int64) error + BeforeBatchDelete func(ctx context.Context, ids []int64) error + AfterBatchDelete func(ctx context.Context, ids []int64) error + AfterPageList func(ctx context.Context, list []*Item, req any) error // 可改元素或就地排序,禁 append + AfterDetail func(ctx context.Context, item *DRes, req any) error + + // ─── 数据装配 ─── + ToData func(req any) map[string]any // Add=全量必填;Update=全量表单语义 +} + +// ============================================================================ +// 通用助手 +// ============================================================================ + +// mustAs 请求类型断言 +// +// 支持指针与值互转;配置错误属于编程错误,直接 panic 暴露 +func mustAs[T any](req any) T { + var zero T + if v, ok := req.(T); ok { + return v + } + rv := reflect.ValueOf(req) + tv := reflect.TypeOf(zero) + if rv.IsValid() { + // T=值类型、req=*T:解引用转换 + if tv.Kind() != reflect.Ptr && rv.Kind() == reflect.Ptr && !rv.IsNil() && rv.Type().Elem() == tv { + return rv.Elem().Interface().(T) + } + // T=*T指针、req=值:装箱新指针 + if tv.Kind() == reflect.Ptr && rv.Kind() != reflect.Ptr && rv.Type() == tv.Elem() { + p := reflect.New(tv.Elem()) + p.Elem().Set(rv) + return p.Interface().(T) + } + } + panic(fmt.Sprintf("ModuleSpec: req 类型断言失败,期望 %T,实际 %T", zero, req)) +} + +// reqFieldCache 请求结构字段索引缓存 +var reqFieldCache sync.Map // reflect.Type → map[jsonName]fieldIndex + +// reqValueByColumn 按列名从请求对象取值 +// +// 列名 dept_id 映射到 json 标签 deptId,再通过反射读取字段,找不到时返回 nil +func reqValueByColumn(req any, column string) any { + if req == nil { + return nil + } + rv := reflect.ValueOf(req) + for rv.Kind() == reflect.Ptr { + if rv.IsNil() { + return nil + } + rv = rv.Elem() + } + rt := rv.Type() + idxMap, _ := reqFieldCache.LoadOrStore(rt, buildReqFieldIndex(rt)) + fields, _ := idxMap.(map[string]int) + jsonName := camelLower(column) + idx, ok := fields[jsonName] + if !ok { + return nil + } + return rv.Field(idx).Interface() +} + +// buildReqFieldIndex 构建 json 标签名到字段索引的映射 +func buildReqFieldIndex(rt reflect.Type) map[string]int { + m := make(map[string]int) + for i := 0; i < rt.NumField(); i++ { + tag := rt.Field(i).Tag.Get("json") + if tag == "" { + continue + } + name := strings.Split(tag, ",")[0] + if name == "" || name == "-" { + continue + } + m[name] = i + } + return m +} + +// builtinLabels 常见列名的内置中文标签 +var ( + builtinLabels = map[string]string{ + "username": "用户名", "realname": "姓名", "name": "名称", "code": "编码", + "mobile": "手机号", "email": "邮箱", "password": "密码", "title": "标题", + "status": "状态", "sort": "排序", "note": "备注", "remark": "备注", + "address": "地址", "avatar": "头像", "type": "类型", "content": "内容", + } +) + +// fieldLabel 字段标签三级回退 +// +// 依次取 FieldLabels、内置常见列名、字段名原文; +// 凡声明 UniqueFields 的模块必须显式写 FieldLabels,提示语以此为准,不依赖数据库列注释 +func (s *ModuleSpec[Item, LItem, DRes]) fieldLabel(field string) string { + if s.FieldLabels != nil { + if v, ok := s.FieldLabels[field]; ok && v != "" { + return v + } + } + if v, ok := builtinLabels[field]; ok { + return v + } + return field +} + +// textPlanCache 文本填充计划缓存 +var textPlanCache sync.Map // reflect.Type → []textFillPlan + +// textFillPlan 文本填充计划 +// 描述源字段索引、目标文本字段索引与字典编码 +type textFillPlan struct { + srcIdx, dstIdx int + code string +} + +// FillTextFields 批量填充结构体中已声明的 {Field}Text 字段 +// +// 声明即生效:结构里存在 Text 字段就一定被填充;反射计划按类型缓存。 +// target 支持 *[]*Item、*Item、*DRes。 +func FillTextFields(ctx context.Context, target any, maps map[string]string) { + if len(maps) == 0 || target == nil { + return + } + rv := reflect.ValueOf(target) + if rv.Kind() != reflect.Ptr || rv.IsNil() { + return + } + switch rv.Elem().Kind() { + case reflect.Slice: + s := rv.Elem() + for i := 0; i < s.Len(); i++ { + fillOneText(ctx, s.Index(i), maps) + } + default: + fillOneText(ctx, rv.Elem(), maps) + } +} + +// fillOneText 填充单个结构体的文本字段 +func fillOneText(ctx context.Context, v reflect.Value, maps map[string]string) { + // 解引用 + for v.Kind() == reflect.Ptr { + if v.IsNil() { + return + } + v = v.Elem() + } + if v.Kind() != reflect.Struct { + return + } + plans := plansForText(v.Type(), maps) + for _, p := range plans { + src := v.Field(p.srcIdx) + dst := v.Field(p.dstIdx) + if !dst.CanSet() || dst.Type().Kind() != reflect.String { + continue + } + dst.SetString(DictGetText(ctx, p.code, src.Interface())) + } +} + +// plansForText 生成或读取文本填充计划 +func plansForText(rt reflect.Type, maps map[string]string) []textFillPlan { + key := rt.String() + if v, ok := textPlanCache.Load(key); ok { + if plans, ok2 := v.([]textFillPlan); ok2 { + return plans + } + } + var plans []textFillPlan + for field, code := range maps { + base := pascal(field) + dst, ok := rt.FieldByName(base + "Text") + if !ok || dst.Type.Kind() != reflect.String || len(dst.Index) != 1 { + continue + } + src, ok := rt.FieldByName(base) + if !ok || len(src.Index) != 1 { + continue + } + plans = append(plans, textFillPlan{srcIdx: src.Index[0], dstIdx: dst.Index[0], code: code}) + } + textPlanCache.Store(key, plans) + return plans +} + +// hookWarn 钩子错误仅记日志 +// 原样返回错误,保留 gerror 语义码 +func hookWarn(ctx context.Context, stage string, err error) error { + if err != nil { + g.Log().Warningf(ctx, "ModuleSpec %s: %v", stage, err) + } + return err +} + +// ============================================================================ +// 查询流水线 +// ============================================================================ + +// applyFilters 应用查询条件 +// +// 固定顺序:LikeFields → EqFields → KeywordFields → Filter → DataScope +func (s *ModuleSpec[Item, LItem, DRes]) applyFilters(ctx context.Context, m *gdb.Model, req any) *gdb.Model { + // 2) 逐列 LIKE(req 同名或驼峰列取值) + for _, col := range s.LikeFields { + if v := reqValueByColumn(req, col); v != nil { + if str, ok := v.(string); ok && str != "" { + m = m.WhereLike(col, "%"+str+"%") + } + } + } + // 3) 等值(零值跳过) + for _, col := range s.EqFields { + if v := reqValueByColumn(req, col); v != nil { + rv := reflect.ValueOf(v) + if rv.IsValid() && !rv.IsZero() { + m = m.Where(col, v) + } + } + } + // 4) 关键词多列 OR(参数化条件,整体作为一个 AND 项,避免 OR 优先级问题) + if len(s.KeywordFields) > 0 { + if kw, ok := reqValueByColumn(req, "keyword").(string); ok && kw != "" { + likes := make([]string, 0, len(s.KeywordFields)) + args := make([]any, 0, len(s.KeywordFields)) + for _, col := range s.KeywordFields { + likes = append(likes, col+" LIKE ?") + args = append(args, "%"+kw+"%") + } + m = m.Where("("+strings.Join(likes, " OR ")+")", args...) + } + } + // 5) 复杂条件 + if s.Filter != nil { + m = s.Filter(ctx, m, req) + } + // 6) 数据权限(nil 跳过) + if s.DataScope != nil { + m = s.DataScope(ctx, m, req) + } + return m +} + +// applyOrder 应用排序 +// 入参 orderField 与 orderType 仅在 OrderableFields 白名单内生效 +func (s *ModuleSpec[Item, LItem, DRes]) applyOrder(m *gdb.Model, req any) *gdb.Model { + if len(s.OrderableFields) > 0 { + field, _ := reqValueByColumn(req, "order_field").(string) + if field != "" { + for _, allowed := range s.OrderableFields { + if allowed == field { + dir := "desc" + if t, ok := reqValueByColumn(req, "order_type").(string); ok && strings.EqualFold(t, "asc") { + dir = "asc" + } + return m.Order(field, dir) + } + } + } + } + if s.OrderBy != "" { + return m.Order(s.OrderBy) + } + return m +} + +// readTransform 读侧转换 +// +// 依次执行文件字段补域名、富文本还原、枚举文本填充; +// FileFields 与 MultiFileFields 声明后读侧自动补可访问 URL +func (s *ModuleSpec[Item, LItem, DRes]) readTransform(ctx context.Context, target any) { + for _, field := range s.FileFields { + fillStringField(target, field, func(v string) string { return upload.GetFileUrl(ctx, v) }) + } + for _, field := range s.MultiFileFields { + fillStringField(target, field, func(v string) string { + if v == "" { + return v + } + parts := strings.Split(v, ",") + for i, p := range parts { + parts[i] = upload.GetFileUrl(ctx, strings.TrimSpace(p)) + } + return strings.Join(parts, ",") + }) + } + for _, f := range s.ContentFields { + fillStringField(target, f, func(v string) string { return ExpandContentFields(ctx, v) }) + } + FillTextFields(ctx, target, s.SerializeMaps) +} + +// saveTransform 写侧转换 +// +// 单文件、多文件与富文本字段由临时目录转正至统一目录 +func (s *ModuleSpec[Item, LItem, DRes]) saveTransform(ctx context.Context, data map[string]any) { + for _, field := range s.FileFields { + if str, ok := data[field].(string); ok && str != "" { + data[field] = upload.SaveFromTemp(ctx, str, s.FileSaveDir) + } + } + for _, field := range s.MultiFileFields { + if str, ok := data[field].(string); ok && str != "" { + data[field] = upload.SaveFromTempMulti(ctx, str, s.FileSaveDir) + } + } + for _, field := range s.ContentFields { + if str, ok := data[field].(string); ok && str != "" { + data[field] = LocalizeContentFields(ctx, str, s.FileSaveDir) + } + } +} + +// checkUniqueRules 唯一性批量校验 +// 提示语按字段标签四级回退 +func (s *ModuleSpec[Item, LItem, DRes]) checkUniqueRules(ctx context.Context, data map[string]any, excludeId int64) error { + for _, rule := range s.UniqueFields { + v, ok := data[rule.Field] + if !ok || v == nil { + continue + } + if str, isStr := v.(string); isStr && str == "" { + continue + } + m := s.Model(ctx).Where(rule.Field, v) + if rule.Group != "" { + if gv, ok := data[rule.Group]; ok { + m = m.Where(rule.Group, gv) + } + } + if excludeId > 0 { + m = m.WhereNot("id", excludeId) + } + n, err := m.Count() + if err != nil { + return err + } + if n > 0 { + return gerror.NewCodef(consts.CodeRecordExists, "%s已存在", s.fieldLabel(rule.Field)) + } + } + return nil +} + +// fillStringField 按 json 字段名对结构或指针结构应用字符串变换 +func fillStringField(target any, jsonName string, fn func(string) string) { + rv := reflect.ValueOf(target) + for rv.Kind() == reflect.Ptr { + if rv.IsNil() { + return + } + rv = rv.Elem() + } + applyOnStruct := func(sv reflect.Value) { + if sv.Kind() != reflect.Struct { + return + } + rt := sv.Type() + for i := 0; i < rt.NumField(); i++ { + tag := strings.Split(rt.Field(i).Tag.Get("json"), ",")[0] + if tag == jsonName && rt.Field(i).Type.Kind() == reflect.String && sv.Field(i).CanSet() { + sv.Field(i).SetString(fn(sv.Field(i).String())) + } + } + } + switch rv.Kind() { + case reflect.Slice: + for i := 0; i < rv.Len(); i++ { + elem := rv.Index(i) + for elem.Kind() == reflect.Ptr { + elem = elem.Elem() + } + applyOnStruct(elem) + } + default: + applyOnStruct(rv) + } +} + +// ============================================================================ +// CRUD 流水线 +// ============================================================================ + +// Page 分页查询 +// 流水线:过滤 → 计数 → 分页 → 映射 → 读转换 → 钩子 +func (s *ModuleSpec[Item, LItem, DRes]) Page(ctx context.Context, req any, pageNo, pageSize int) ([]*Item, int, error) { + m := s.applyFilters(ctx, s.Model(ctx), req) + total, err := m.Count() + if err != nil { + return nil, 0, err + } + rows, err := s.applyOrder(m, req).Page(pageNo, pageSize).All() + if err != nil { + return nil, 0, err + } + list, err := structRows[Item](rows) + if err != nil { + return nil, 0, err + } + s.readTransform(ctx, &list) + if err = hookWarn(ctx, "AfterPageList", s.callAfterPageList(ctx, list, req)); err != nil { + return nil, 0, err + } + return list, total, nil +} + +// List 全量列表 +// +// 使用 LItem 类型;IsTree 为真时在末尾建树; +// 不挂 AfterPageList,因精简结构无冗余列 +func (s *ModuleSpec[Item, LItem, DRes]) List(ctx context.Context, req any) ([]*LItem, error) { + m := s.applyFilters(ctx, s.Model(ctx), req) + if s.ListOrderBy != "" { + m = m.Order(s.ListOrderBy) + } else { + m = s.applyOrder(m, req) + } + // 条数上限(req.limit,零值表示不限) + if v := reqValueByColumn(req, "limit"); v != nil { + if n, ok := v.(int); ok && n > 0 { + m = m.Limit(n) + } + } + rows, err := m.All() + if err != nil { + return nil, err + } + list, err := structRows[LItem](rows) + if err != nil { + return nil, err + } + s.readTransform(ctx, &list) + if s.IsTree { + s.checkTreeDeclared() + return s.buildTree(list), nil + } + return list, nil +} + +// Detail 详情查询 +// 流水线:查询 → 映射 → 读转换 → 钩子 +func (s *ModuleSpec[Item, LItem, DRes]) Detail(ctx context.Context, id int64, req any) (*DRes, error) { + one, err := s.Model(ctx).Where("id", id).One() + if err != nil { + return nil, err + } + if len(one) == 0 { + return nil, gerror.NewCode(consts.CodeRecordNotFound) + } + var item DRes + if err = one.Struct(&item); err != nil { + return nil, err + } + s.readTransform(ctx, &item) + if err = hookWarn(ctx, "AfterDetail", s.callAfterDetail(ctx, &item, req)); err != nil { + return nil, err + } + return &item, nil +} + +// Add 新增 +// 流水线:ToData → BeforeAdd → 文件三类 → 唯一校验 → Insert → AfterAdd +func (s *ModuleSpec[Item, LItem, DRes]) Add(ctx context.Context, req any) (int64, error) { + data := s.ToData(req) + if err := hookWarn(ctx, "BeforeAdd", callBeforeAdd(s.BeforeAdd, ctx, data, req)); err != nil { + return 0, err + } + s.saveTransform(ctx, data) + if err := s.checkUniqueRules(ctx, data, 0); err != nil { + return 0, err + } + result, err := s.Model(ctx).Data(data).Insert() + if err != nil { + return 0, err + } + id, _ := result.LastInsertId() + if err = hookWarn(ctx, "AfterAdd", callAfterAdd(s.AfterAdd, ctx, id, req)); err != nil { + return 0, err + } + return id, nil +} + +// Update 更新 +// 全量表单语义,仅更新 data 中出现的键 +func (s *ModuleSpec[Item, LItem, DRes]) Update(ctx context.Context, id int64, req any) error { + data := s.ToData(req) + if err := hookWarn(ctx, "BeforeUpdate", callBeforeUpdate(s.BeforeUpdate, ctx, data, id, req)); err != nil { + return err + } + s.saveTransform(ctx, data) + if err := s.checkUniqueRules(ctx, data, id); err != nil { + return err + } + if _, err := s.Model(ctx).Where("id", id).Data(data).Update(); err != nil { + return err + } + return hookWarn(ctx, "AfterUpdate", callAfterUpdate(s.AfterUpdate, ctx, id, req)) +} + +// Delete 删除 +// 存在性与守卫由模块 BeforeDelete 决定,随后软删并触发 AfterDelete +func (s *ModuleSpec[Item, LItem, DRes]) Delete(ctx context.Context, id int64) error { + if err := hookWarn(ctx, "BeforeDelete", callBeforeDelete(s.BeforeDelete, ctx, id)); err != nil { + return err + } + if _, err := s.Model(ctx).Where("id", id).Delete(); err != nil { + return err + } + return hookWarn(ctx, "AfterDelete", callAfterDelete(s.AfterDelete, ctx, id)) +} + +// BatchDelete 批量删除 +// 流程:BeforeBatchDelete → 逐条执行 BeforeDelete 与软删 → AfterBatchDelete +func (s *ModuleSpec[Item, LItem, DRes]) BatchDelete(ctx context.Context, ids []int64) error { + if err := hookWarn(ctx, "BeforeBatchDelete", callBeforeBatch(s.BeforeBatchDelete, ctx, ids)); err != nil { + return err + } + for _, id := range ids { + if err := hookWarn(ctx, "BeforeDelete", callBeforeDelete(s.BeforeDelete, ctx, id)); err != nil { + return err + } + if _, err := s.Model(ctx).Where("id", id).Delete(); err != nil { + return err + } + } + return hookWarn(ctx, "AfterBatchDelete", callAfterBatch(s.AfterBatchDelete, ctx, ids)) +} + +// MergeOld 全量覆盖辅助 +// +// 将 data 中缺失或零值的键用库中旧值补齐,供 BeforeUpdate 内使用 +func (s *ModuleSpec[Item, LItem, DRes]) MergeOld(ctx context.Context, data map[string]any, id int64) { + one, err := s.Model(ctx).Where("id", id).One() + if err != nil || len(one) == 0 { + return + } + for k, oldV := range one { + if v, ok := data[k]; !ok || v == nil || v == "" || v == 0 { + data[k] = oldV + } + } +} + +// ─── 内部:结构映射与树 ──────────────────────────────────────────── + +// structRows 数据库行转换为指针切片 +// 通过 orm 与 json 标签映射 +func structRows[Item any](rows gdb.Result) ([]*Item, error) { + list := make([]*Item, 0, len(rows)) + for _, row := range rows { + var item Item + if err := row.Struct(&item); err != nil { + return nil, err + } + list = append(list, &item) + } + return list, nil +} + +// callAfterPageList 调用分页后钩子 +func (s *ModuleSpec[Item, LItem, DRes]) callAfterPageList(ctx context.Context, list []*Item, req any) error { + if s.AfterPageList == nil { + return nil + } + return s.AfterPageList(ctx, list, req) +} + +// callAfterDetail 调用详情后钩子 +func (s *ModuleSpec[Item, LItem, DRes]) callAfterDetail(ctx context.Context, item *DRes, req any) error { + if s.AfterDetail == nil { + return nil + } + return s.AfterDetail(ctx, item, req) +} + +// 钩子 nil 安全包装(字段已是具体类型,直接判空调用) + +// callBeforeAdd 调用新增前钩子 +func callBeforeAdd(f func(context.Context, map[string]any, any) error, ctx context.Context, data map[string]any, req any) error { + if f == nil { + return nil + } + return f(ctx, data, req) +} + +// callAfterAdd 调用新增后钩子 +func callAfterAdd(f func(context.Context, int64, any) error, ctx context.Context, id int64, req any) error { + if f == nil { + return nil + } + return f(ctx, id, req) +} + +// callBeforeUpdate 调用更新前钩子 +func callBeforeUpdate(f func(context.Context, map[string]any, int64, any) error, ctx context.Context, data map[string]any, id int64, req any) error { + if f == nil { + return nil + } + return f(ctx, data, id, req) +} + +// callAfterUpdate 调用更新后钩子 +func callAfterUpdate(f func(context.Context, int64, any) error, ctx context.Context, id int64, req any) error { + if f == nil { + return nil + } + return f(ctx, id, req) +} + +// callBeforeDelete 调用删除前钩子 +func callBeforeDelete(f func(context.Context, int64) error, ctx context.Context, id int64) error { + if f == nil { + return nil + } + return f(ctx, id) +} + +// callAfterDelete 调用删除后钩子 +func callAfterDelete(f func(context.Context, int64) error, ctx context.Context, id int64) error { + if f == nil { + return nil + } + return f(ctx, id) +} + +// callBeforeBatch 调用批量删除前钩子 +func callBeforeBatch(f func(context.Context, []int64) error, ctx context.Context, ids []int64) error { + if f == nil { + return nil + } + return f(ctx, ids) +} + +// callAfterBatch 调用批量删除后钩子 +func callAfterBatch(f func(context.Context, []int64) error, ctx context.Context, ids []int64) error { + if f == nil { + return nil + } + return f(ctx, ids) +} + +// ─── 树 ────────────────────────────────────────────────────────── + +// checkTreeDeclared 校验树形声明的完整性 +func (s *ModuleSpec[Item, LItem, DRes]) checkTreeDeclared() { + if s.ParentOf == nil { + // 泛型类型名作调试标识 + var zero LItem + panic(fmt.Sprintf("ModuleSpec: IsTree=true 但未声明 ParentOf(%s)", reflect.TypeOf(&zero).Elem().Name())) + } +} + +// childrenIdxCache 子节点字段索引缓存 +var childrenIdxCache sync.Map // reflect.Type → children field index(-1 表示无) + +// childrenSetter 获取子节点写入函数 +// +// 优先使用声明的 ChildSetter,缺省时反射写入 DTO 的 Children 字段 +func (s *ModuleSpec[Item, LItem, DRes]) childrenSetter() func(*LItem, []*LItem) { + if s.ChildSetter != nil { + return s.ChildSetter + } + // 缺省:反射写 DTO 的 Children 字段(命名约定) + var sample LItem + rt := reflect.TypeOf(sample) + v, _ := childrenIdxCache.LoadOrStore(rt, func() any { + f, ok := rt.FieldByName("Children") + if !ok || f.Type.Kind() != reflect.Slice || len(f.Index) != 1 { + return -1 + } + return f.Index[0] + }()) // 注意:LoadOrStore 存值不存函数,需立即调用 + idx, ok := v.(int) + if !ok { + idx = -1 + } + if idx < 0 { + panic(fmt.Sprintf("ModuleSpec: IsTree 需要 ChildSetter 或 DTO 的 Children 字段(%s)", rt.Name())) + } + return func(it *LItem, children []*LItem) { + rv := reflect.ValueOf(it).Elem().Field(idx) + rv.Set(reflect.ValueOf(children)) + } +} + +// buildTree 平铺列表转换为树 +// +// 先按 ParentOf 分组,再写入各父节点的 Children; +// 输入顺序稳定,缺少父节点的元素挂到根 +func (s *ModuleSpec[Item, LItem, DRes]) buildTree(list []*LItem) []*LItem { + return buildTreeBy(list, s.ParentOf, s.childrenSetter()) +} + +// buildTreeBy 通用树构建 +// idOf 约定 DTO 包含 Id 字段,父节点不在集合内时挂根 +func buildTreeBy[T any](list []*T, parentOf func(*T) int64, setChildren func(*T, []*T)) []*T { + // idOf:约定 DTO 含 Id 字段(int64/int 均可,reflect.Int 读取) + idOf := func(it *T) int64 { + f := reflect.ValueOf(it).Elem().FieldByName("Id") + if !f.IsValid() { + panic("ModuleSpec: 树形 DTO 必须包含 Id 字段") + } + return f.Int() + } + nodes := make(map[int64]*T, len(list)) + children := make(map[int64][]*T, len(list)) + ids := make([]int64, 0, len(list)) + for _, it := range list { + setChildren(it, []*T{}) // 预置空(叶子 JSON 输出 [] 而非 null) + idv := idOf(it) + nodes[idv] = it + ids = append(ids, idv) + } + roots := make([]*T, 0, len(ids)) + for _, idv := range ids { + node := nodes[idv] + pid := parentOf(node) + if pid != 0 { + if _, ok := nodes[pid]; ok { + children[pid] = append(children[pid], node) + continue + } + } + roots = append(roots, node) // 顶级或缺父挂根 + } + for pid, ch := range children { + setChildren(nodes[pid], ch) + } + return roots +} + +// derefSlice 指针切片转换为值切片 +// 统一响应值语义:PageData[T] 的 T 为值类型 +func derefSlice[T any](in []*T) []T { + out := make([]T, 0, len(in)) + for _, p := range in { + out = append(out, *p) + } + return out +} diff --git a/internal/logic/notice.go b/internal/logic/notice.go new file mode 100644 index 0000000..9cda054 --- /dev/null +++ b/internal/logic/notice.go @@ -0,0 +1,181 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" +) + +// ============================== 通知公告业务实现 ============================== + +// sNotice 通知公告业务实现 +// 实现 service.INotice 接口,接口定义见 api/v1/notice.go +type sNotice struct{} + +func init() { + service.RegisterNotice(&sNotice{}) +} + +// noticeSerializeMaps 枚举字段到字典编码的映射 +var noticeSerializeMaps = map[string]string{ + "type": "notice_type", + "status": "notice_status", +} + +// noticeSpec 通知公告模块声明 +var noticeSpec = ModuleSpec[v1.NoticeItem, v1.NoticeListItem, v1.NoticeDetailRes]{ + Model: dao.Notice.Ctx, + LikeFields: []string{"title"}, + EqFields: []string{"type", "status"}, + OrderBy: "id DESC", + SerializeMaps: noticeSerializeMaps, + FileFields: []string{"cover"}, // 保存 temp→正式;读侧补域名 + ContentFields: []string{"content"}, // 富文本:存 [IMG_URL] 占位,读还原 + FileSaveDir: "notice", + ToData: noticeToData, + // 详情读取后点击数+1(读后更新,返回原值) + AfterDetail: noticeCountClick, +} + +// noticeToData 全量表单语义转换 +// 兼容新增与修改请求,将入参转换为落库字段 +func noticeToData(req any) map[string]any { + switch r := req.(type) { + case *v1.NoticeAddReq: + return map[string]any{"title": r.Title, "cover": r.Cover, "type": r.Type, + "status": r.Status, "content": r.Content} + case *v1.NoticeUpdateReq: + return map[string]any{"title": r.Title, "cover": r.Cover, "type": r.Type, + "status": r.Status, "content": r.Content} + } + panic("noticeToData: 不支持的入参类型") +} + +// noticeCountClick 详情点击数自增 +// +// 先读取原值再更新,响应保持查询时的数值 +func noticeCountClick(ctx context.Context, item *v1.NoticeDetailRes, req any) error { + one, err := dao.Notice.Ctx(ctx).Where("id", item.Id).Fields("click_num").One() + if err != nil || len(one) == 0 { + return err + } + _, err = dao.Notice.Ctx(ctx).Where("id", item.Id).Data(map[string]any{ + "click_num": one["click_num"].Int() + 1, + }).Update() + return err +} + +// Page 通知公告分页 +// 按查询条件分页返回通知公告列表 +func (s *sNotice) Page(ctx context.Context, req *v1.NoticePageReq) (res *v1.NoticePageRes, err error) { + list, total, err := noticeSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize()) + if err != nil { + return nil, err + } + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil +} + +// List 通知公告全量列表 +// 按关键词、类型、状态等条件返回通知公告列表 +func (s *sNotice) List(ctx context.Context, req *v1.NoticeListReq) (res v1.NoticeListRes, err error) { + list, err := noticeSpec.List(ctx, req) + if err != nil { + return nil, err + } + return derefSlice(list), nil +} + +// Detail 通知公告详情 +// 按公告ID查询详情,并触发点击数自增 +func (s *sNotice) Detail(ctx context.Context, req *v1.NoticeDetailReq) (res *v1.NoticeDetailRes, err error) { + return noticeSpec.Detail(ctx, req.Id, req) +} + +// Add 新增通知公告 +// 新增成功后返回新记录ID +func (s *sNotice) Add(ctx context.Context, req *v1.NoticeAddReq) (res *v1.IdRes, err error) { + id, err := noticeSpec.Add(ctx, req) + if err != nil { + return nil, err + } + return &v1.IdRes{Id: id}, nil +} + +// Update 修改通知公告 +// 按公告ID更新记录 +func (s *sNotice) Update(ctx context.Context, req *v1.NoticeUpdateReq) (res *v1.NoticeUpdateRes, err error) { + if err = noticeSpec.Update(ctx, req.Id, req); err != nil { + return nil, err + } + return &v1.NoticeUpdateRes{}, nil +} + +// Status 通知公告状态 +// 非标准方法,手写实现,设置公告的草稿或发布状态 +func (s *sNotice) Status(ctx context.Context, req *v1.NoticeStatusReq) (res *v1.NoticeStatusRes, err error) { + if _, err = dao.Notice.Ctx(ctx).Where("id", req.Id).Data(map[string]any{"status": req.Status}).Update(); err != nil { + return nil, err + } + return &v1.NoticeStatusRes{}, nil +} + +// Delete 删除通知公告 +// 按公告ID删除单条记录 +func (s *sNotice) Delete(ctx context.Context, req *v1.NoticeDeleteReq) (res *v1.NoticeDeleteRes, err error) { + if err = noticeSpec.Delete(ctx, req.Id); err != nil { + return nil, err + } + return &v1.NoticeDeleteRes{}, nil +} + +// BatchDelete 批量删除通知公告 +// 按公告ID列表批量删除记录 +func (s *sNotice) BatchDelete(ctx context.Context, req *v1.NoticeBatchDeleteReq) (res *v1.NoticeBatchDeleteRes, err error) { + if err = noticeSpec.BatchDelete(ctx, req.Ids); err != nil { + return nil, err + } + return &v1.NoticeBatchDeleteRes{}, nil +} diff --git a/internal/logic/online_user.go b/internal/logic/online_user.go new file mode 100644 index 0000000..4507bdf --- /dev/null +++ b/internal/logic/online_user.go @@ -0,0 +1,375 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + "encoding/json" + "math" + "sort" + "strings" + "time" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + + "easygoadmin/api/v1" + "easygoadmin/internal/consts" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" + "easygoadmin/utility/cache" + "easygoadmin/utility/ip2region" + jwtutil "easygoadmin/utility/jwt" + "easygoadmin/utility/ua" + "easygoadmin/utility/upload" +) + +// ============================== 在线用户业务实现 ============================== + +// sOnlineUser 在线用户业务实现 +// 数据源为在线令牌集合(登录登记、登出/强退移除),非数据库表 +type sOnlineUser struct{} + +func init() { + service.RegisterOnlineUser(&sOnlineUser{}) +} + +// onlineInfo 在线令牌详情(JSON 缓存) +type onlineInfo struct { + Jti string `json:"jti"` + UserId int `json:"userId"` + Username string `json:"username"` + Realname string `json:"realname"` + Ip string `json:"ip"` + Location string `json:"location"` + Oss string `json:"oss"` // 操作系统 + Browser string `json:"browser"` // 浏览器 + LoginTime string `json:"loginTime"` + IssuedAt int64 `json:"issuedAt"` // 访问令牌颁发时间戳(秒) + ExpireAt int64 `json:"expireAt"` // 访问令牌过期时间戳(秒) + RefreshJti string `json:"refreshJti"` // 刷新令牌 jti + RefreshIssuedAt int64 `json:"refreshIssuedAt"` // 刷新令牌颁发时间戳(秒) + RefreshExpireAt int64 `json:"refreshExpireAt"` // 刷新令牌过期时间戳(秒) + AccessToken string `json:"accessToken"` // 访问令牌原文(详情查看) + RefreshToken string `json:"refreshToken"` // 刷新令牌原文(详情查看) + TokenType string `json:"tokenType"` // 令牌类型(Bearer) + ExpiresIn int64 `json:"expiresIn"` // 访问令牌有效期(秒) +} + +// MarkOnline 登录成功后登记在线令牌 +// zset score=访问令牌过期时间戳用于区间过滤,详情键携带展示字段; +// 刷新令牌仅登记标识与有效期(不入 zset,列表以访问令牌为准) +func MarkOnline(ctx context.Context, access, refresh *jwtutil.Claims, accessToken, refreshToken string, userId int, username, realname, ip, userAgent string) { + if access == nil || access.ID == "" { + return + } + now := time.Now() + expireAt := now.Add(jwtutil.ExpireDuration(ctx, jwtutil.TokenTypeAccess)) + if access.ExpiresAt != nil { + expireAt = access.ExpiresAt.Time + } + info := onlineInfo{ + Jti: access.ID, + UserId: userId, + Username: username, + Realname: realname, + Ip: ip, + Location: ip2region.Location(ip), + Oss: ua.ParseOS(userAgent), + Browser: ua.ParseBrowser(userAgent), + LoginTime: now.Format("2006-01-02 15:04:05"), + IssuedAt: now.Unix(), + ExpireAt: expireAt.Unix(), + RefreshJti: func() string { + if refresh != nil { + return refresh.ID + } + return "" + }(), + RefreshIssuedAt: now.Unix(), + RefreshExpireAt: func() int64 { + if refresh != nil && refresh.ExpiresAt != nil { + return refresh.ExpiresAt.Time.Unix() + } + return 0 + }(), + AccessToken: accessToken, + RefreshToken: refreshToken, + TokenType: "Bearer", + ExpiresIn: int64(jwtutil.ExpireDuration(ctx, jwtutil.TokenTypeAccess).Seconds()), + } + if b, err := json.Marshal(info); err == nil { + ttl := int(expireAt.Sub(now).Seconds()) + 60 + _ = cache.Set(ctx, consts.CacheKeyOnlineInfo(access.ID), string(b), ttl) + } + _ = cache.ZAdd(ctx, consts.CacheKeyOnlineJtiSet(), access.ID, float64(expireAt.Unix())) +} + +// MarkOffline 登出或强退后移除在线登记 +func MarkOffline(ctx context.Context, jti string) { + if jti == "" { + return + } + _ = cache.ZRem(ctx, consts.CacheKeyOnlineJtiSet(), jti) + _ = cache.Del(ctx, consts.CacheKeyOnlineInfo(jti)) +} + +// Page 在线用户分页 +// 取未过期令牌后按用户名/IP 过滤,内存分页,按登录时间倒序 +func (s *sOnlineUser) Page(ctx context.Context, req *v1.OnlineUserPageReq) (res *v1.OnlineUserPageRes, err error) { + jtis, err := cache.ZRangeByScore(ctx, consts.CacheKeyOnlineJtiSet(), float64(time.Now().Unix()), math.Inf(1)) + if err != nil { + return nil, err + } + items := make([]v1.OnlineUserItem, 0, len(jtis)) + for _, jti := range jtis { + raw, ok := cache.Get(ctx, consts.CacheKeyOnlineInfo(jti)) + if !ok { + continue + } + var info onlineInfo + if json.Unmarshal([]byte(raw), &info) != nil { + continue + } + if req.Username != "" && !strings.Contains(info.Username, req.Username) { + continue + } + if req.Ip != "" && !strings.Contains(info.Ip, req.Ip) { + continue + } + items = append(items, v1.OnlineUserItem{ + Id: info.Jti, + UserId: info.UserId, + Username: info.Username, + Realname: info.Realname, + Ip: info.Ip, + Location: info.Location, + Oss: info.Oss, + Browser: info.Browser, + LoginTime: info.LoginTime, + IssuedTime: time.Unix(info.IssuedAt, 0).Format("2006-01-02 15:04:05"), + ExpireTime: time.Unix(info.ExpireAt, 0).Format("2006-01-02 15:04:05"), + RefreshIssuedTime: func() string { + if info.RefreshIssuedAt <= 0 { + return "" + } + return time.Unix(info.RefreshIssuedAt, 0).Format("2006-01-02 15:04:05") + }(), + RefreshExpireTime: func() string { + if info.RefreshExpireAt <= 0 { + return "" + } + return time.Unix(info.RefreshExpireAt, 0).Format("2006-01-02 15:04:05") + }(), + TokenType: info.TokenType, + ExpiresIn: info.ExpiresIn, + }) + } + sort.Slice(items, func(i, j int) bool { return items[i].LoginTime > items[j].LoginTime }) + + // 同用户多会话标注(序号/总数,按登录时间倒序内的序号) + if len(items) > 1 { + totalByUser := map[int]int{} + seqByUser := map[int]int{} + for _, it := range items { + totalByUser[it.UserId]++ + } + for i := range items { + seqByUser[items[i].UserId]++ + items[i].SessionNo = seqByUser[items[i].UserId] + items[i].Sessions = totalByUser[items[i].UserId] + } + } + + // 按用户ID批量补齐头像(与用户列表同源,头像变更即时生效) + if len(items) > 0 { + if err = fillOnlineAvatars(ctx, items); err != nil { + return nil, err + } + } + + total := int64(len(items)) + pageNo, pageSize := req.GetPageNo(), req.GetPageSize() + start := int((pageNo - 1) * pageSize) + if start > len(items) { + start = len(items) + } + end := start + int(pageSize) + if end > len(items) { + end = len(items) + } + return v1.NewPageData(pageNo, pageSize, int(total), items[start:end]), nil +} + +// fillOnlineAvatars 按用户ID批量查询并回填头像 +// 查询用户表 avatar 相对路径,经 upload.GetFileUrl 补全访问域名; +// 单个用户多会话只查一次 +func fillOnlineAvatars(ctx context.Context, items []v1.OnlineUserItem) error { + idMap := map[int]bool{} + ids := make([]int, 0, len(items)) + for _, it := range items { + if it.UserId > 0 && !idMap[it.UserId] { + idMap[it.UserId] = true + ids = append(ids, it.UserId) + } + } + if len(ids) == 0 { + return nil + } + rows, err := dao.User.Ctx(ctx).WhereIn("id", ids).Fields("id, avatar").All() + if err != nil { + return err + } + avatarMap := make(map[int]string, len(rows)) + for _, r := range rows { + avatarMap[r["id"].Int()] = upload.GetFileUrl(ctx, r["avatar"].String()) + } + for i := range items { + items[i].Avatar = avatarMap[items[i].UserId] + } + return nil +} + +// Detail 在线会话详情 +// 按访问令牌 jti 读取在线登记,返回含令牌原文的完整信息 +func (s *sOnlineUser) Detail(ctx context.Context, req *v1.OnlineUserDetailReq) (res *v1.OnlineUserDetailRes, err error) { + raw, ok := cache.Get(ctx, consts.CacheKeyOnlineInfo(req.Id)) + if !ok { + return nil, gerror.NewCode(consts.CodeRecordNotFound) + } + var info onlineInfo + if err = json.Unmarshal([]byte(raw), &info); err != nil { + return nil, gerror.NewCode(consts.CodeRecordNotFound) + } + item := v1.OnlineUserItem{ + Id: info.Jti, + UserId: info.UserId, + Username: info.Username, + Realname: info.Realname, + Ip: info.Ip, + Location: info.Location, + Oss: info.Oss, + Browser: info.Browser, + LoginTime: info.LoginTime, + IssuedTime: time.Unix(info.IssuedAt, 0).Format("2006-01-02 15:04:05"), + ExpireTime: time.Unix(info.ExpireAt, 0).Format("2006-01-02 15:04:05"), + RefreshIssuedTime: func() string { + if info.RefreshIssuedAt <= 0 { + return "" + } + return time.Unix(info.RefreshIssuedAt, 0).Format("2006-01-02 15:04:05") + }(), + RefreshExpireTime: func() string { + if info.RefreshExpireAt <= 0 { + return "" + } + return time.Unix(info.RefreshExpireAt, 0).Format("2006-01-02 15:04:05") + }(), + TokenType: info.TokenType, + ExpiresIn: info.ExpiresIn, + AccessToken: info.AccessToken, + RefreshToken: info.RefreshToken, + } + // 切片字面量为副本,经列表回填后再取回 + list := []v1.OnlineUserItem{item} + if err = fillOnlineAvatars(ctx, list); err != nil { + return nil, err + } + return &v1.OnlineUserDetailRes{OnlineUserItem: list[0]}, nil +} + +// ForceLogout 强制下线 +// 将访问令牌拉黑并移除在线登记 +func (s *sOnlineUser) ForceLogout(ctx context.Context, req *v1.OnlineUserForceLogoutReq) (res *v1.OnlineUserForceLogoutRes, err error) { + raw, ok := cache.Get(ctx, consts.CacheKeyOnlineInfo(req.Id)) + if !ok { + return nil, gerror.NewCode(consts.CodeRecordNotFound) + } + var info onlineInfo + if err = json.Unmarshal([]byte(raw), &info); err != nil { + return nil, gerror.NewCode(consts.CodeRecordNotFound) + } + ttl := int(time.Until(time.Unix(info.ExpireAt, 0)).Seconds()) + 60 + if ttl > 0 { + _ = cache.Set(ctx, consts.CacheKeyTokenBlacklist(info.Jti), "1", ttl) + } + // 刷新令牌一并拉黑,防止被踢会话经 refresh 换新令牌重新上线 + if info.RefreshJti != "" && info.RefreshExpireAt > 0 { + if rttl := int(time.Until(time.Unix(info.RefreshExpireAt, 0)).Seconds()) + 60; rttl > 0 { + _ = cache.Set(ctx, consts.CacheKeyTokenBlacklist(info.RefreshJti), "1", rttl) + } + } + MarkOffline(ctx, info.Jti) + return &v1.OnlineUserForceLogoutRes{}, nil +} + +// ForceLogoutUser 踢出该用户全部设备 +// 遍历在线集合,将该用户所有会话的访问/刷新令牌拉黑并移除登记 +func (s *sOnlineUser) ForceLogoutUser(ctx context.Context, req *v1.OnlineUserForceLogoutUserReq) (res *v1.OnlineUserForceLogoutUserRes, err error) { + jtis, err := cache.ZRangeByScore(ctx, consts.CacheKeyOnlineJtiSet(), float64(time.Now().Unix()), math.Inf(1)) + if err != nil { + return nil, err + } + kicked := 0 + for _, jti := range jtis { + raw, ok := cache.Get(ctx, consts.CacheKeyOnlineInfo(jti)) + if !ok { + continue + } + var info onlineInfo + if json.Unmarshal([]byte(raw), &info) != nil || info.UserId != req.UserId { + continue + } + if ttl := int(time.Until(time.Unix(info.ExpireAt, 0)).Seconds()) + 60; ttl > 0 { + _ = cache.Set(ctx, consts.CacheKeyTokenBlacklist(info.Jti), "1", ttl) + } + if info.RefreshJti != "" && info.RefreshExpireAt > 0 { + if rttl := int(time.Until(time.Unix(info.RefreshExpireAt, 0)).Seconds()) + 60; rttl > 0 { + _ = cache.Set(ctx, consts.CacheKeyTokenBlacklist(info.RefreshJti), "1", rttl) + } + } + MarkOffline(ctx, info.Jti) + kicked++ + } + g.Log().Infof(ctx, "踢出用户 %d 全部设备,共 %d 个会话", req.UserId, kicked) + return &v1.OnlineUserForceLogoutUserRes{}, nil +} diff --git a/internal/logic/operation_log.go b/internal/logic/operation_log.go new file mode 100644 index 0000000..e9b6236 --- /dev/null +++ b/internal/logic/operation_log.go @@ -0,0 +1,162 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/dao" + "easygoadmin/internal/middleware" + "easygoadmin/internal/service" +) + +// ============================== 操作日志业务实现 ============================== + +// sOperLog 操作日志业务实现 +// 实现 service.IOperLog 接口,承载操作日志模块的业务逻辑 +type sOperLog struct{} + +// operLogSerializeMaps 枚举字段到字典编码的映射 +// {field}Text 由 dict_item 解析,字典管理页可修改文案 +var operLogSerializeMaps = map[string]string{ + "source": "operation_log_source", + "status": "operation_log_status", +} + +// operLogSpec 操作日志模块声明 +var operLogSpec = ModuleSpec[v1.OperLogItem, v1.OperLogItem, v1.OperLogDetailRes]{ + Model: dao.OperationLog.Ctx, + LikeFields: []string{"title", "url"}, + EqFields: []string{"type", "status"}, + OrderBy: "id DESC", + SerializeMaps: operLogSerializeMaps, + // typeText 走静态映射(注解常量值,与字典体系无关) + AfterPageList: func(ctx context.Context, list []*v1.OperLogItem, req any) error { + for _, item := range list { + item.TypeText = LogTypeText(item.Type) + } + return nil + }, + AfterDetail: func(ctx context.Context, item *v1.OperLogDetailRes, req any) error { + item.TypeText = LogTypeText(item.Type) + return nil + }, +} + +func init() { + service.RegisterOperLog(&sOperLog{}) + // 操作日志中间件落库接线(异步批量,见 middleware/operation_log.go) + middleware.SetLogSink(SaveOperationLog) +} + +// SaveOperationLog 操作日志落库 +// +// 作为 LogSink 实现,ctx 为后台上下文 +func SaveOperationLog(ctx context.Context, entry *middleware.OperationLogEntry) error { + _, err := dao.OperationLog.Ctx(ctx).Data(map[string]any{ + "title": entry.Title, + "type": entry.Type, + "source": entry.Source, + "method": entry.Method, + "request_method": entry.RequestMethod, + "url": entry.URL, + "param": entry.Param, + "result": entry.Result, + "ip": entry.IP, + "location": entry.Location, + "os": entry.OS, + "browser": entry.Browser, + "consume_time": entry.ConsumeTime, + "status": entry.Status, + "error": entry.Error, + "create_user": entry.CreateUser, + }).Insert() + return err +} + +// Page 操作日志分页 +// 按查询条件分页返回操作日志列表 +func (s *sOperLog) Page(ctx context.Context, req *v1.OperLogPageReq) (res *v1.OperLogPageRes, err error) { + list, total, err := operLogSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize()) + if err != nil { + return nil, err + } + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil +} + +// List 操作日志全量列表 +// 按关键词、类型、状态等条件返回操作日志列表,并补全类型文本 +func (s *sOperLog) List(ctx context.Context, req *v1.OperLogListReq) (res v1.OperLogListRes, err error) { + list, err := operLogSpec.List(ctx, req) + if err != nil { + return nil, err + } + for _, item := range list { + item.TypeText = LogTypeText(item.Type) + } + return derefSlice(list), nil +} + +// Detail 操作日志详情 +// 按日志ID查询详情 +func (s *sOperLog) Detail(ctx context.Context, req *v1.OperLogDetailReq) (res *v1.OperLogDetailRes, err error) { + return operLogSpec.Detail(ctx, req.Id, req) +} + +// Delete 删除操作日志 +// 按日志ID删除单条记录 +func (s *sOperLog) Delete(ctx context.Context, req *v1.OperLogDeleteReq) (res *v1.OperLogDeleteRes, err error) { + if err = operLogSpec.Delete(ctx, req.Id); err != nil { + return nil, err + } + return &v1.OperLogDeleteRes{}, nil +} + +// BatchDelete 批量删除操作日志 +// 按日志ID列表批量删除记录 +func (s *sOperLog) BatchDelete(ctx context.Context, req *v1.OperLogBatchDeleteReq) (res *v1.OperLogBatchDeleteRes, err error) { + if err = operLogSpec.BatchDelete(ctx, req.Ids); err != nil { + return nil, err + } + return &v1.OperLogBatchDeleteRes{}, nil +} diff --git a/internal/logic/param.go b/internal/logic/param.go new file mode 100644 index 0000000..6a86cce --- /dev/null +++ b/internal/logic/param.go @@ -0,0 +1,174 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + + "easygoadmin/api/v1" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" +) + +// ============================== 系统参数业务实现 ============================== + +// sParam 系统参数业务实现 +// 实现 service.IParam 接口,接口定义见 api/v1/param.go +type sParam struct{} + +func init() { + service.RegisterParam(&sParam{}) +} + +// paramSerializeMaps 枚举字段到字典编码的映射 +var paramSerializeMaps = map[string]string{ + "type": "param_type", + "status": "param_status", +} + +// paramSpec 系统参数模块声明 +var paramSpec = ModuleSpec[v1.ParamItem, v1.ParamItem, v1.ParamDetailRes]{ + Model: dao.Param.Ctx, + LikeFields: []string{"name", "code"}, + EqFields: []string{"type", "status"}, + OrderBy: "sort, id", + SerializeMaps: paramSerializeMaps, + UniqueFields: []UniqueRule{{Field: "code"}}, // 参数编码唯一 + FieldLabels: map[string]string{"code": "参数编码"}, // 唯一提示显式声明(原取自列注释) + ToData: paramToData, +} + +// paramToData 全量表单语义转换 +// 兼容新增与修改请求,将入参转换为落库字段 +func paramToData(req any) map[string]any { + switch r := req.(type) { + case *v1.ParamAddReq: + return map[string]any{"name": r.Name, "code": r.Code, "value": r.Value, + "type": r.Type, "status": r.Status, "sort": r.Sort, "note": r.Note} + case *v1.ParamUpdateReq: + return map[string]any{"name": r.Name, "code": r.Code, "value": r.Value, + "type": r.Type, "status": r.Status, "sort": r.Sort, "note": r.Note} + } + panic("paramToData: 不支持的入参类型") +} + +// Page 系统参数分页 +// 按查询条件分页返回系统参数列表 +func (s *sParam) Page(ctx context.Context, req *v1.ParamPageReq) (res *v1.ParamPageRes, err error) { + list, total, err := paramSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize()) + if err != nil { + return nil, err + } + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil +} + +// List 系统参数全量列表 +// 按关键词、类型、状态等条件返回系统参数列表 +func (s *sParam) List(ctx context.Context, req *v1.ParamListReq) (res v1.ParamListRes, err error) { + list, err := paramSpec.List(ctx, req) + if err != nil { + return nil, err + } + return derefSlice(list), nil +} + +// Detail 系统参数详情 +// 按参数ID查询详情 +func (s *sParam) Detail(ctx context.Context, req *v1.ParamDetailReq) (res *v1.ParamDetailRes, err error) { + return paramSpec.Detail(ctx, req.Id, req) +} + +// Add 新增系统参数 +// 校验参数编码唯一后新增,返回新记录ID +func (s *sParam) Add(ctx context.Context, req *v1.ParamAddReq) (res *v1.IdRes, err error) { + id, err := paramSpec.Add(ctx, req) + if err != nil { + return nil, err + } + return &v1.IdRes{Id: id}, nil +} + +// Update 修改系统参数 +// 按参数ID更新记录 +func (s *sParam) Update(ctx context.Context, req *v1.ParamUpdateReq) (res *v1.ParamUpdateRes, err error) { + if err = paramSpec.Update(ctx, req.Id, req); err != nil { + return nil, err + } + return &v1.ParamUpdateRes{}, nil +} + +// Delete 删除系统参数 +// 按参数ID删除单条记录 +func (s *sParam) Delete(ctx context.Context, req *v1.ParamDeleteReq) (res *v1.ParamDeleteRes, err error) { + if err = paramSpec.Delete(ctx, req.Id); err != nil { + return nil, err + } + return &v1.ParamDeleteRes{}, nil +} + +// BatchDelete 批量删除系统参数 +// 按参数ID列表批量删除记录 +func (s *sParam) BatchDelete(ctx context.Context, req *v1.ParamBatchDeleteReq) (res *v1.ParamBatchDeleteRes, err error) { + if err = paramSpec.BatchDelete(ctx, req.Ids); err != nil { + return nil, err + } + return &v1.ParamBatchDeleteRes{}, nil +} + +// paramGetString 按编码读取启用状态的系统参数值 +// +// 软删除由全局钩子过滤,未命中时返回 fallback +func paramGetString(ctx context.Context, code, fallback string) string { + v, err := dao.Param.Ctx(ctx).Where("code", code).Where("status", 1).Fields("value").Value() + if err != nil || v.IsNil() || v.String() == "" { + return fallback + } + return v.String() +} + +// getDefaultPassword 获取默认密码明文 +// +// 取系统参数 DEFAULT_PASSWORD,未配置时回退 123456; +// 仅用于初始化密码的中间环节,禁止写入日志 +func getDefaultPassword(ctx context.Context) string { + return paramGetString(ctx, "DEFAULT_PASSWORD", "123456") +} diff --git a/internal/logic/position.go b/internal/logic/position.go new file mode 100644 index 0000000..1769d4e --- /dev/null +++ b/internal/logic/position.go @@ -0,0 +1,191 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + + "github.com/gogf/gf/v2/errors/gerror" + + "easygoadmin/api/v1" + "easygoadmin/internal/consts" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" +) + +// ============================== 岗位业务实现 ============================== + +// sPosition 岗位业务实现 +// 实现 service.IPosition 接口,接口定义见 api/v1/position.go +type sPosition struct{} + +func init() { + service.RegisterPosition(&sPosition{}) +} + +// positionSerializeMaps 枚举字段到字典编码的映射 +// {field}Text 由基座自动填充 +var positionSerializeMaps = map[string]string{ + "status": "position_status", +} + +// positionSpec 岗位模块声明 +var positionSpec = ModuleSpec[v1.PositionItem, v1.PositionListItem, v1.PositionDetailRes]{ + Model: dao.Position.Ctx, + LikeFields: []string{"name"}, + EqFields: []string{"status"}, + OrderBy: "sort, id", + SerializeMaps: positionSerializeMaps, + ToData: positionToData, + BeforeAdd: positionDefaultStatus, // status 缺省置 1 + BeforeDelete: positionDeleteGuard, // 存在性 + 用户引用检查 +} + +// positionToData 全量表单语义转换 +// 兼容新增与修改请求,将入参转换为落库字段 +func positionToData(req any) map[string]any { + switch r := req.(type) { + case *v1.PositionAddReq: + return map[string]any{"name": r.Name, "status": r.Status, "sort": r.Sort} + case *v1.PositionUpdateReq: + return map[string]any{"name": r.Name, "status": r.Status, "sort": r.Sort} + } + panic("positionToData: 不支持的入参类型") +} + +// positionDefaultStatus 状态缺省处理 +// 状态为零值时置为 1(正常) +func positionDefaultStatus(ctx context.Context, data map[string]any, req any) error { + if v, ok := data["status"].(int); ok && v == 0 { + data["status"] = 1 + } + return nil +} + +// positionDeleteGuard 删除守卫 +// 校验记录存在性,存在用户引用时禁止删除 +func positionDeleteGuard(ctx context.Context, id int64) error { + if n, err := dao.Position.Ctx(ctx).Where("id", id).Count(); err != nil { + return err + } else if n == 0 { + return gerror.NewCode(consts.CodeRecordNotFound) + } + if n, err := dao.User.Ctx(ctx).Where("position_id", id).Count(); err != nil { + return err + } else if n > 0 { + return gerror.NewCode(consts.CodeInUse) + } + return nil +} + +// Page 岗位分页 +// 分页返回岗位列表,并补充关联用户数与状态文本 +func (s *sPosition) Page(ctx context.Context, req *v1.PositionPageReq) (res *v1.PositionPageRes, err error) { + list, total, err := positionSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize()) + if err != nil { + return nil, err + } + fillUserCount(ctx, "position_id", &list) + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil +} + +// List 岗位全量列表 +// 按关键词、状态等条件返回岗位列表 +func (s *sPosition) List(ctx context.Context, req *v1.PositionListReq) (res v1.PositionListRes, err error) { + list, err := positionSpec.List(ctx, req) + if err != nil { + return nil, err + } + return derefSlice(list), nil +} + +// Detail 岗位详情 +// 按岗位ID查询详情 +func (s *sPosition) Detail(ctx context.Context, req *v1.PositionDetailReq) (res *v1.PositionDetailRes, err error) { + return positionSpec.Detail(ctx, req.Id, req) +} + +// Add 新增岗位 +// 新增成功后返回新记录ID +func (s *sPosition) Add(ctx context.Context, req *v1.PositionAddReq) (res *v1.IdRes, err error) { + id, err := positionSpec.Add(ctx, req) + if err != nil { + return nil, err + } + return &v1.IdRes{Id: id}, nil +} + +// Update 修改岗位 +// 按岗位ID更新记录 +func (s *sPosition) Update(ctx context.Context, req *v1.PositionUpdateReq) (res *v1.PositionUpdateRes, err error) { + if err = positionSpec.Update(ctx, req.Id, req); err != nil { + return nil, err + } + return &v1.PositionUpdateRes{}, nil +} + +// Status 岗位状态 +// 非标准方法,手写实现,设置岗位的正常或禁用状态 +func (s *sPosition) Status(ctx context.Context, req *v1.PositionStatusReq) (res *v1.PositionStatusRes, err error) { + if _, err = dao.Position.Ctx(ctx).Where("id", req.Id).Data(map[string]any{"status": req.Status}).Update(); err != nil { + return nil, err + } + return &v1.PositionStatusRes{}, nil +} + +// Delete 删除岗位 +// 按岗位ID删除单条记录 +func (s *sPosition) Delete(ctx context.Context, req *v1.PositionDeleteReq) (res *v1.PositionDeleteRes, err error) { + if err = positionSpec.Delete(ctx, req.Id); err != nil { + return nil, err + } + return &v1.PositionDeleteRes{}, nil +} + +// BatchDelete 批量删除岗位 +// 按岗位ID列表批量删除记录 +func (s *sPosition) BatchDelete(ctx context.Context, req *v1.PositionBatchDeleteReq) (res *v1.PositionBatchDeleteRes, err error) { + if err = positionSpec.BatchDelete(ctx, req.Ids); err != nil { + return nil, err + } + return &v1.PositionBatchDeleteRes{}, nil +} diff --git a/internal/logic/role.go b/internal/logic/role.go new file mode 100644 index 0000000..d9d8a88 --- /dev/null +++ b/internal/logic/role.go @@ -0,0 +1,322 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + + "github.com/gogf/gf/v2/errors/gerror" + + "easygoadmin/api/v1" + "easygoadmin/internal/consts" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" + "easygoadmin/utility/cache" +) + +// ============================== 角色业务实现 ============================== + +// sRole 角色业务实现 +// 实现 service.IRole 接口,接口定义见 api/v1/role.go +type sRole struct{} + +func init() { + service.RegisterRole(&sRole{}) +} + +// roleSerializeMaps 枚举字段到字典编码的映射 +var roleSerializeMaps = map[string]string{ + "data_scope": "role_data_scope", +} + +// roleSpec 角色模块声明 +// +// 编码唯一 + 删除守卫 + 关联清理 +var roleSpec = ModuleSpec[v1.RoleItem, v1.RoleListItem, v1.RoleDetailRes]{ + Model: dao.Role.Ctx, + LikeFields: []string{"name", "code"}, + OrderBy: "sort, id", + SerializeMaps: roleSerializeMaps, + UniqueFields: []UniqueRule{{Field: "code"}}, + FieldLabels: map[string]string{"code": "角色编码"}, // 唯一提示文案 + ToData: roleToData, + AfterUpdate: roleClearCache, // 身份缓存失效 + BeforeDelete: roleDeleteGuard, // 存在性 + 关联用户禁止 + AfterDelete: roleCleanRelations, // 清理 role_menu + AfterBatchDelete: func(ctx context.Context, ids []int64) error { + return roleCleanRelationsBatch(ctx, ids) + }, + AfterPageList: roleFillUserCount, // 行内用户数批量补全 +} + +// roleToData 全量表单语义转换 +// +// 修改请求不包含编码,因编码不允许变更;tenant_id 由 hooks 自动填充; +// 数据范围缺省归一为"全部"(1) +func roleToData(req any) map[string]any { + switch r := req.(type) { + case *v1.RoleAddReq: + scope := r.DataScope + if scope <= 0 { + scope = 1 + } + return map[string]any{"name": r.Name, "code": r.Code, "data_scope": scope, "sort": r.Sort, "note": r.Note} + case *v1.RoleUpdateReq: + scope := r.DataScope + if scope <= 0 { + scope = 1 + } + return map[string]any{"name": r.Name, "data_scope": scope, "sort": r.Sort, "note": r.Note} + } + panic("roleToData: 不支持的入参类型") +} + +// roleFillUserCount 分页行用户数批量补全 +// 依据 user_role 关联统计 +func roleFillUserCount(ctx context.Context, list []*v1.RoleItem, req any) error { + counts, err := roleUserCounts(ctx) + if err != nil { + return err + } + for _, item := range list { + item.UserCount = counts[item.Id] + } + return nil +} + +// roleClearCache 角色变更后清理身份缓存 +// 失效该角色下所有用户的身份缓存 +func roleClearCache(ctx context.Context, id int64, req any) error { + roleClearIdentityCache(ctx, id) + return nil +} + +// roleDeleteGuard 删除守卫 +// 校验记录存在性,存在关联用户时禁止删除 +func roleDeleteGuard(ctx context.Context, id int64) error { + if n, err := dao.Role.Ctx(ctx).Where("id", id).Count(); err != nil { + return err + } else if n == 0 { + return gerror.NewCode(consts.CodeRecordNotFound) + } + if n, err := dao.UserRole.Ctx(ctx).Where("role_id", id).Count(); err != nil { + return err + } else if n > 0 { + return gerror.NewCode(consts.CodeInUse) + } + return nil +} + +// roleCleanRelations 单条删除后清理角色菜单关联 +func roleCleanRelations(ctx context.Context, id int64) error { + _, err := dao.RoleMenu.Ctx(ctx).Where("role_id", id).Delete() + return err +} + +// roleCleanRelationsBatch 批量删除后清理角色菜单关联 +func roleCleanRelationsBatch(ctx context.Context, ids []int64) error { + _, err := dao.RoleMenu.Ctx(ctx).WhereIn("role_id", ids).Delete() + return err +} + +// ---------- 标准方法:3行委托 ---------- + +// Page 角色分页 +// 分页返回角色列表,并补充关联用户数 +func (s *sRole) Page(ctx context.Context, req *v1.RolePageReq) (res *v1.RolePageRes, err error) { + list, total, err := roleSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize()) + if err != nil { + return nil, err + } + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil +} + +// List 角色全量列表 +// 按关键词返回角色精简列表 +func (s *sRole) List(ctx context.Context, req *v1.RoleListReq) (res v1.RoleListRes, err error) { + list, err := roleSpec.List(ctx, req) + if err != nil { + return nil, err + } + return derefSlice(list), nil +} + +// Detail 角色详情 +// 按角色ID查询详情 +func (s *sRole) Detail(ctx context.Context, req *v1.RoleDetailReq) (res *v1.RoleDetailRes, err error) { + return roleSpec.Detail(ctx, req.Id, req) +} + +// Add 新增角色 +// 校验角色编码唯一后新增,返回新记录ID +func (s *sRole) Add(ctx context.Context, req *v1.RoleAddReq) (res *v1.IdRes, err error) { + id, err := roleSpec.Add(ctx, req) + if err != nil { + return nil, err + } + return &v1.IdRes{Id: id}, nil +} + +// Update 修改角色 +// 按角色ID更新记录,编码不允许变更 +func (s *sRole) Update(ctx context.Context, req *v1.RoleUpdateReq) (res *v1.RoleUpdateRes, err error) { + if err = roleSpec.Update(ctx, req.Id, req); err != nil { + return nil, err + } + return &v1.RoleUpdateRes{}, nil +} + +// Delete 删除角色 +// 按角色ID删除单条记录,存在关联用户时禁止 +func (s *sRole) Delete(ctx context.Context, req *v1.RoleDeleteReq) (res *v1.RoleDeleteRes, err error) { + if err = roleSpec.Delete(ctx, req.Id); err != nil { + return nil, err + } + return &v1.RoleDeleteRes{}, nil +} + +// BatchDelete 批量删除角色 +// 按角色ID列表批量删除记录 +func (s *sRole) BatchDelete(ctx context.Context, req *v1.RoleBatchDeleteReq) (res *v1.RoleBatchDeleteRes, err error) { + if err = roleSpec.BatchDelete(ctx, req.Ids); err != nil { + return nil, err + } + return &v1.RoleBatchDeleteRes{}, nil +} + +// ---------- 非标准方法(手写实现) ---------- + +// MenuList 角色菜单勾选列表 +// 返回菜单平铺列表并附带勾选状态,供权限配置使用 +func (s *sRole) MenuList(ctx context.Context, req *v1.RoleMenuListReq) (res v1.RoleMenuListRes, err error) { + roleMenus, err := dao.RoleMenu.Ctx(ctx).Where("role_id", req.RoleId).All() + if err != nil { + return nil, err + } + checkedSet := make(map[int64]bool, len(roleMenus)) + for _, r := range roleMenus { + checkedSet[r["menu_id"].Int64()] = true + } + rows, err := dao.Menu.Ctx(ctx).Order("sort, id").All() + if err != nil { + return nil, err + } + list := make([]v1.RoleMenuItem, 0, len(rows)) + for _, r := range rows { + list = append(list, v1.RoleMenuItem{ + Id: r["id"].Int64(), + ParentId: r["parent_id"].Int(), + Name: r["name"].String(), + Icon: r["icon"].String(), + Icon2: r["icon2"].String(), + Path: r["path"].String(), + Component: r["component"].String(), + Permission: r["permission"].String(), + Target: r["target"].Int(), + Type: r["type"].Int(), + Status: r["status"].Int(), + Hide: r["hide"].Int(), + Sort: r["sort"].Int(), + Checked: checkedSet[r["id"].Int64()], + }) + } + return list, nil +} + +// SavePermission 保存角色菜单权限 +// +// 全量替换角色菜单关联,并失效相关用户的身份缓存 +func (s *sRole) SavePermission(ctx context.Context, req *v1.RoleSavePermissionReq) (res *v1.RoleSavePermissionRes, err error) { + if _, err = dao.RoleMenu.Ctx(ctx).Where("role_id", req.RoleId).Delete(); err != nil { + return nil, err + } + for _, mid := range req.MenuIds { + if mid <= 0 { + continue + } + if _, err = dao.RoleMenu.Ctx(ctx).Data(map[string]any{ + "role_id": req.RoleId, + "menu_id": mid, + }).Insert(); err != nil { + return nil, err + } + } + s.clearRoleIdentityCache(ctx, req.RoleId) + return &v1.RoleSavePermissionRes{}, nil +} + +// ---------- 私有辅助 ---------- + +// roleUserCounts 统计角色ID到用户数的映射 +func roleUserCounts(ctx context.Context) (map[int64]int, error) { + result := make(map[int64]int) + rows, err := dao.UserRole.Ctx(ctx).All() + if err != nil { + return nil, err + } + for _, r := range rows { + result[r["role_id"].Int64()]++ + } + return result, nil +} + +// roleClearIdentityCache 失效角色下所有用户的身份缓存 +func roleClearIdentityCache(ctx context.Context, roleId int64) { + urRows, err := dao.UserRole.Ctx(ctx).Where("role_id", roleId).All() + if err != nil { + return + } + for _, r := range urRows { + uid := int(r["user_id"].Int64()) + cache.Del(ctx, + consts.CacheKeyUserIdentity(uid), + consts.CacheKeyUserPerms(uid), + consts.CacheKeyUserRoles(uid), + ) + } +} + +// clearRoleIdentityCache 身份缓存失效(方法版) +// 供 SavePermission 调用 +func (s *sRole) clearRoleIdentityCache(ctx context.Context, roleId int64) { + roleClearIdentityCache(ctx, roleId) +} diff --git a/internal/logic/server_monitor.go b/internal/logic/server_monitor.go new file mode 100644 index 0000000..dc2b034 --- /dev/null +++ b/internal/logic/server_monitor.go @@ -0,0 +1,157 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + "math" + "os" + "runtime" + "strings" + "time" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gcron" + + "easygoadmin/api/v1" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" + "easygoadmin/utility/cache" +) + +// ============================== 服务器监控业务实现 ============================== + +// sServerMonitor 服务器监控业务实现 +// 汇集进程运行时、磁盘、数据库与缓存连通信息 +type sServerMonitor struct{} + +// procStart 进程启动时间(包加载时刻) +var procStart = time.Now() + +func init() { + service.RegisterServerMonitor(&sServerMonitor{}) +} + +// Info 查询服务器监控信息 +func (s *sServerMonitor) Info(ctx context.Context, req *v1.ServerInfoReq) (res *v1.ServerInfoRes, err error) { + var m runtime.MemStats + runtime.ReadMemStats(&m) + hostname, _ := os.Hostname() + + res = &v1.ServerInfoRes{ + Server: v1.ServerOsInfo{ + Hostname: hostname, + GoVersion: runtime.Version(), + Arch: runtime.GOARCH, + NumCPU: runtime.NumCPU(), + Goroutines: runtime.NumGoroutine(), + Uptime: time.Since(procStart).Round(time.Second).String(), + }, + Mem: v1.ServerMemInfo{ + AllocMB: roundMB(m.Alloc), + TotalAllocMB: roundMB(m.TotalAlloc), + SysMB: roundMB(m.Sys), + HeapAllocMB: roundMB(m.HeapAlloc), + HeapSysMB: roundMB(m.HeapSys), + HeapObjects: m.HeapObjects, + GCCycles: m.NumGC, + }, + Disk: diskUsage(), + } + + // 数据库连通与版本 + start := time.Now() + _, err = g.DB().Ctx(ctx).Exec(ctx, "SELECT VERSION()") + pingMs := time.Since(start).Milliseconds() + version := "" + if err == nil { + if v, verr := g.DB().Ctx(ctx).GetValue(ctx, "SELECT VERSION()"); verr == nil { + version = v.String() + } + res.Db = v1.ServerDbInfo{Version: version, PingMs: pingMs} + } else { + res.Db = v1.ServerDbInfo{PingMs: pingMs} + } + + // 缓存连通 + if cache.UsingMemory() { + res.Cache = v1.ServerCacheBrief{Mode: "memory", Connected: false} + } else if _, cerr := g.Redis().Do(ctx, "PING"); cerr == nil { + res.Cache = v1.ServerCacheBrief{Mode: "redis", Connected: true} + } else { + res.Cache = v1.ServerCacheBrief{Mode: "redis", Connected: false} + } + + // 定时任务调度器状态 + // 挂载数统计 gcron 中 job: 前缀条目(排除上传清理等非任务条目), + // 应挂载数取任务表 status=1(运行中)的记录数,挂载失败或数据被外部改动会出现差值 + mounted := 0 + for _, e := range gcron.Entries() { + if strings.HasPrefix(e.Name, "job:") { + mounted++ + } + } + expected, _ := dao.Job.Ctx(ctx).Where("status", 1).Count() + res.Scheduler = v1.ServerSchedulerInfo{ + Enabled: !daemonStart.IsZero(), + Healthy: !daemonStart.IsZero() && mounted >= int(expected), + Mounted: mounted, + Expected: int(expected), + StartTime: func() string { + if daemonStart.IsZero() { + return "" + } + return daemonStart.Format("2006-01-02 15:04:05") + }(), + } + + if err != nil { + // 数据库不通不视为整体失败,仅版本留空;返回详情供页面展示 + err = nil + } + return res, nil +} + +// roundMB 字节转 MB(保留两位小数) +func roundMB(v uint64) float64 { + return math.Round(float64(v)/1048576*100) / 100 +} diff --git a/internal/logic/server_monitor_other.go b/internal/logic/server_monitor_other.go new file mode 100644 index 0000000..7aea9a2 --- /dev/null +++ b/internal/logic/server_monitor_other.go @@ -0,0 +1,77 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +//go:build !windows + +package logic + +import ( + "math" + "os" + "syscall" + + "easygoadmin/api/v1" +) + +// diskUsage 读取工作目录所在磁盘分区容量(Unix) +func diskUsage() []v1.ServerDiskInfo { + wd, err := os.Getwd() + if err != nil { + return nil + } + var st syscall.Statfs_t + if err = syscall.Statfs(wd, &st); err != nil { + return nil + } + bs := float64(st.Bsize) + total := float64(st.Blocks) * bs / 1073741824 + free := float64(st.Bavail) * bs / 1073741824 + usedPercent := 0.0 + if total > 0 { + usedPercent = math.Round((total-free)/total*10000) / 100 + } + return []v1.ServerDiskInfo{{ + Path: wd, + TotalGB: math.Round(total*100) / 100, + FreeGB: math.Round(free*100) / 100, + UsedPercent: usedPercent, + }} +} diff --git a/internal/logic/server_monitor_windows.go b/internal/logic/server_monitor_windows.go new file mode 100644 index 0000000..45b6169 --- /dev/null +++ b/internal/logic/server_monitor_windows.go @@ -0,0 +1,82 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +//go:build windows + +package logic + +import ( + "math" + "os" + + "golang.org/x/sys/windows" + + "easygoadmin/api/v1" +) + +// diskUsage 读取工作目录所在磁盘分区容量(Windows) +func diskUsage() []v1.ServerDiskInfo { + wd, err := os.Getwd() + if err != nil || len(wd) < 3 { + return nil + } + root := wd[:3] // 形如 "E:\" + handle, err := windows.UTF16PtrFromString(root) + if err != nil { + return nil + } + var freeBytes, totalBytes, totalFreeBytes uint64 + if err = windows.GetDiskFreeSpaceEx(handle, &freeBytes, &totalBytes, &totalFreeBytes); err != nil { + return nil + } + total := float64(totalBytes) / 1073741824 + free := float64(freeBytes) / 1073741824 + usedPercent := 0.0 + if total > 0 { + usedPercent = math.Round((total-free)/total*10000) / 100 + } + return []v1.ServerDiskInfo{{ + Path: root, + TotalGB: math.Round(total*100) / 100, + FreeGB: math.Round(free*100) / 100, + UsedPercent: usedPercent, + }} +} diff --git a/internal/logic/tenant.go b/internal/logic/tenant.go new file mode 100644 index 0000000..d7a7cfe --- /dev/null +++ b/internal/logic/tenant.go @@ -0,0 +1,424 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + "fmt" + "time" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gtime" + + "easygoadmin/api/v1" + "easygoadmin/internal/consts" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" + "easygoadmin/utility/cache" + "easygoadmin/utility/crypto" +) + +// ============================== 租户业务实现 ============================== + +// sTenant 租户业务实现 +// 实现 service.ITenant 接口,接口定义见 api/v1/tenant.go +type sTenant struct{} + +func init() { + service.RegisterTenant(&sTenant{}) +} + +// tenantSerializeMaps 枚举字段到字典编码的映射 +// {field}Text 由基座自动填充 +var tenantSerializeMaps = map[string]string{ + "status": "tenant_status", +} + +// tenantSpec 租户模块声明 +// +// Add 含临时编码占位回填,采用手写实现 +var tenantSpec = ModuleSpec[v1.TenantItem, v1.TenantListItem, v1.TenantDetailRes]{ + Model: dao.Tenant.Ctx, + LikeFields: []string{"name", "code"}, // querySchemas 字符串输入 → 逐列模糊 + EqFields: []string{"status"}, + OrderBy: "id DESC", + ListOrderBy: "id", + SerializeMaps: tenantSerializeMaps, + Filter: tenantFilter, // keyword 按入参分支(Page 含 contact) + FileFields: []string{"logo"}, + FileSaveDir: "tenant", + ToData: tenantUpdateToData, + AfterUpdate: tenantClearCache, // 状态等变更 → 租户信息缓存失效 + BeforeDelete: tenantDeleteGuard, + AfterDelete: func(ctx context.Context, id int64) error { + cache.Del(ctx, consts.CacheKeyTenantInfo(int(id))) + return nil + }, + AfterBatchDelete: func(ctx context.Context, ids []int64) error { + for _, id := range ids { + cache.Del(ctx, consts.CacheKeyTenantInfo(int(id))) + } + return nil + }, + AfterPageList: tenantFillUserCount, // 行内用户数批量补全 +} + +// tenantFilter 关键词筛选 +// +// 分页请求匹配名称、编码与联系人,列表请求匹配名称与编码 +func tenantFilter(ctx context.Context, m *gdb.Model, req any) *gdb.Model { + switch r := req.(type) { + case *v1.TenantPageReq: + if r.Keyword != "" { + kw := "%" + r.Keyword + "%" + m = m.Where("(name LIKE ? OR code LIKE ? OR contact LIKE ?)", kw, kw, kw) + } + case *v1.TenantListReq: + if r.Keyword != "" { + kw := "%" + r.Keyword + "%" + m = m.Where("(name LIKE ? OR code LIKE ?)", kw, kw) + } + } + return m +} + +// tenantUpdateToData 修改请求的表单语义转换 +// 编码不可变更,过期时间为空时表示清空 +func tenantUpdateToData(req any) map[string]any { + r, ok := req.(*v1.TenantUpdateReq) + if !ok { + panic("tenantUpdateToData: 不支持的入参类型") + } + data := map[string]any{ + "name": r.Name, + "contact": r.Contact, + "mobile": r.Mobile, + "email": r.Email, + "address": r.Address, + "logo": r.Logo, + "status": r.Status, + "max_user_num": r.MaxUserNum, + "note": r.Note, + } + if r.ExpireTime != "" { + if t := gtime.New(r.ExpireTime); t != nil { + data["expire_time"] = t + } + } else { + data["expire_time"] = nil + } + return data +} + +// tenantFillUserCount 分页行用户数批量补全 +func tenantFillUserCount(ctx context.Context, list []*v1.TenantItem, req any) error { + counts, err := tenantUserCounts(ctx) + if err != nil { + return err + } + for _, item := range list { + item.UserCount = counts[item.Id] + } + return nil +} + +// tenantClearCache 租户变更后清理缓存 +// 失效对应租户的信息缓存 +func tenantClearCache(ctx context.Context, id int64, req any) error { + cache.Del(ctx, consts.CacheKeyTenantInfo(int(id))) + return nil +} + +// tenantDeleteGuard 删除守卫 +// 校验记录存在性,存在关联用户时禁止删除 +func tenantDeleteGuard(ctx context.Context, id int64) error { + if n, err := dao.Tenant.Ctx(ctx).Where("id", id).Count(); err != nil { + return err + } else if n == 0 { + return gerror.NewCode(consts.CodeRecordNotFound) + } + if n, err := dao.User.Ctx(ctx).Where("tenant_id", id).Count(); err != nil { + return err + } else if n > 0 { + return gerror.NewCode(consts.CodeInUse) + } + return nil +} + +// ---------- 标准方法:3行委托 ---------- + +// Page 租户分页 +// 分页返回租户列表,并补充关联用户数 +func (s *sTenant) Page(ctx context.Context, req *v1.TenantPageReq) (res *v1.TenantPageRes, err error) { + list, total, err := tenantSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize()) + if err != nil { + return nil, err + } + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil +} + +// List 租户全量列表 +// 按关键词、状态等条件返回租户精简列表 +func (s *sTenant) List(ctx context.Context, req *v1.TenantListReq) (res v1.TenantListRes, err error) { + list, err := tenantSpec.List(ctx, req) + if err != nil { + return nil, err + } + return derefSlice(list), nil +} + +// Detail 租户详情 +// 按租户ID查询详情 +func (s *sTenant) Detail(ctx context.Context, req *v1.TenantDetailReq) (res *v1.TenantDetailRes, err error) { + return tenantSpec.Detail(ctx, req.Id, req) +} + +// Update 修改租户 +// 按租户ID更新记录,编码不允许变更 +func (s *sTenant) Update(ctx context.Context, req *v1.TenantUpdateReq) (res *v1.TenantUpdateRes, err error) { + if err = tenantSpec.Update(ctx, req.Id, req); err != nil { + return nil, err + } + return &v1.TenantUpdateRes{}, nil +} + +// Status 租户状态 +// 设置租户的正常或禁用状态,并失效租户信息缓存 +func (s *sTenant) Status(ctx context.Context, req *v1.TenantStatusReq) (res *v1.TenantStatusRes, err error) { + if _, err = dao.Tenant.Ctx(ctx).Where("id", req.Id).Data(map[string]any{"status": req.Status}).Update(); err != nil { + return nil, err + } + cache.Del(ctx, consts.CacheKeyTenantInfo(int(req.Id))) + return &v1.TenantStatusRes{}, nil +} + +// Delete 删除租户 +// 按租户ID删除单条记录,存在关联用户时禁止 +func (s *sTenant) Delete(ctx context.Context, req *v1.TenantDeleteReq) (res *v1.TenantDeleteRes, err error) { + if err = tenantSpec.Delete(ctx, req.Id); err != nil { + return nil, err + } + return &v1.TenantDeleteRes{}, nil +} + +// BatchDelete 批量删除租户 +// 按租户ID列表批量删除记录 +func (s *sTenant) BatchDelete(ctx context.Context, req *v1.TenantBatchDeleteReq) (res *v1.TenantBatchDeleteRes, err error) { + if err = tenantSpec.BatchDelete(ctx, req.Ids); err != nil { + return nil, err + } + return &v1.TenantBatchDeleteRes{}, nil +} + +// ---------- 非标准方法(手写实现)---------- + +// Add 新增租户 +// +// 编码留空时自动生成,自增回填前先用临时编码占位 +func (s *sTenant) Add(ctx context.Context, req *v1.TenantAddReq) (res *v1.IdRes, err error) { + code := req.Code + if code != "" { + count, e := dao.Tenant.Ctx(ctx).Where("code", code).Count() + if e != nil { + return nil, e + } + if count > 0 { + return nil, gerror.NewCodef(consts.CodeRecordExists, "租户编码已存在") + } + } + status := req.Status + if status == 0 { + status = 1 + } + // 自增回填前先用临时编码占位(code NOT NULL + 唯一键),插入后修正为 T%04d(id) + req.Logo = SaveFileField(ctx, req.Logo, "tenant") + tempCode := code + if tempCode == "" { + tempCode = fmt.Sprintf("T%010d", time.Now().UnixNano()%10000000000) + } + data := map[string]any{ + "name": req.Name, + "code": tempCode, + "contact": req.Contact, + "mobile": req.Mobile, + "email": req.Email, + "address": req.Address, + "logo": req.Logo, + "status": status, + "max_user_num": req.MaxUserNum, + "note": req.Note, + } + if req.ExpireTime != "" { + if t := gtime.New(req.ExpireTime); t != nil { + data["expire_time"] = t + } + } + result, err := dao.Tenant.Ctx(ctx).Data(data).Insert() + if err != nil { + return nil, err + } + idv, _ := result.LastInsertId() + if code == "" { + finalCode := fmt.Sprintf("T%04d", idv) + if _, err = dao.Tenant.Ctx(ctx).Where("id", idv).Data(map[string]any{"code": finalCode}).Update(); err != nil { + return nil, err + } + } + return &v1.IdRes{Id: idv}, nil +} + +// Account 创建租户账号 +// +// 同时创建租户及其初始管理员账号; +// bcrypt 属 CPU 密集操作,放在事务外预计算,保持短事务 +func (s *sTenant) Account(ctx context.Context, req *v1.TenantAccountReq) (res *v1.TenantAccountRes, err error) { + // 租户:自增回填(临时编码占位 → T%04d 修正) + tempCode := fmt.Sprintf("T%010d", time.Now().UnixNano()%10000000000) + tData := map[string]any{ + "name": req.Name, + "code": tempCode, + "contact": req.Contact, + "mobile": req.Mobile, + "email": req.Email, + "address": req.Address, + "status": 1, + "max_user_num": req.MaxUserNum, + "note": req.Note, + } + if req.ExpireTime != "" { + if t := gtime.New(req.ExpireTime); t != nil { + tData["expire_time"] = t + } + } + // bcrypt 属 CPU 密集操作,放在事务外预计算(短事务纪律) + salt := crypto.GenerateSalt() + hashed, err := crypto.HashPassword(req.Password, salt) + if err != nil { + return nil, err + } + realname := req.Realname + if realname == "" { + realname = req.Username + } + + // 租户 + 初始管理员三段写入保持原子性(全部经 txCtx) + var tenantId, uid int64 + var code string + if err = WithTrans(ctx, func(txCtx context.Context) error { + tResult, e := dao.Tenant.Ctx(txCtx).Data(tData).Insert() + if e != nil { + return e + } + tenantId, _ = tResult.LastInsertId() + code = fmt.Sprintf("T%04d", tenantId) + if _, e = dao.Tenant.Ctx(txCtx).Where("id", tenantId).Data(map[string]any{"code": code}).Update(); e != nil { + return e + } + // 初始管理员(tenant_id 由 hooks 自动填充) + tenantCtx := consts.WithTenantID(txCtx, int(tenantId)) + uResult, e := dao.User.Ctx(tenantCtx).Data(map[string]any{ + "username": req.Username, + "code": fmt.Sprintf("%s-000001", code), // 首个用户序号 000001 + "password": hashed, + "salt": salt, + "realname": realname, + "mobile": req.Mobile, + "email": req.Email, + "province_code": "", + "city_code": "", + "district_code": "", + "street_code": "", + "address": "", + "status": 1, + "tenant_id": int(tenantId), + }).Insert() + if e != nil { + return e + } + uid, _ = uResult.LastInsertId() + return nil + }); err != nil { + return nil, err + } + return &v1.TenantAccountRes{TenantId: tenantId, Code: code, UserId: uid}, nil +} + +// Users 查询租户用户列表 +// +// 平台上下文可跨租户查看,按 tenant_id 精确查询 +func (s *sTenant) Users(ctx context.Context, req *v1.TenantUsersReq) (res *v1.TenantUsersRes, err error) { + // 平台上下文可跨租户查看;直接按 tenant_id 精确查询(绕过 ApplyCtx 的租户收敛,显式指定) + rows, err := dao.User.Ctx(ctx).Where("tenant_id", req.TenantId).Order("id").All() + if err != nil { + return nil, err + } + list := make([]v1.TenantUserItem, 0, len(rows)) + for _, r := range rows { + list = append(list, v1.TenantUserItem{ + Id: r["id"].Int64(), + Username: r["username"].String(), + Realname: r["realname"].String(), + Mobile: r["mobile"].String(), + Email: r["email"].String(), + Status: r["status"].Int(), + }) + } + return &v1.TenantUsersRes{List: list}, nil +} + +// ---------- 私有辅助 ---------- + +// tenantUserCounts 统计租户ID到用户数的映射 +func tenantUserCounts(ctx context.Context) (map[int64]int, error) { + result := make(map[int64]int) + rows, err := dao.User.Ctx(ctx).Fields("tenant_id").All() + if err != nil { + return nil, err + } + for _, r := range rows { + if v := r["tenant_id"].Int64(); v > 0 { + result[v]++ + } + } + return result, nil +} diff --git a/internal/logic/upload.go b/internal/logic/upload.go new file mode 100644 index 0000000..07f6f18 --- /dev/null +++ b/internal/logic/upload.go @@ -0,0 +1,102 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + "path" + + "easygoadmin/api/v1" + "easygoadmin/internal/service" + "easygoadmin/utility/upload" +) + +// ============================== 文件上传业务实现 ============================== + +// sUpload 文件上传业务实现 +// 实现 service.IUpload 接口,承载文件上传模块的业务逻辑 +type sUpload struct{} + +func init() { + service.RegisterUpload(&sUpload{}) +} + +// UploadFile 上传单文件 +// +// 文件先落入临时目录,表单保存时由 SaveFromTemp 转正至正式目录 +func (s *sUpload) UploadFile(ctx context.Context, req *v1.UploadFileReq) (res *v1.UploadFileRes, err error) { + typ := req.Type + if typ != "image" { + typ = "file" + } + r, err := upload.SaveTemp(ctx, req.File, typ) + if err != nil { + return nil, err + } + return &v1.UploadFileRes{ + OriginalName: r.Name, + FileExtension: r.Ext, + FileType: r.Mime, + FileSize: r.Size, + FileName: path.Base(r.Path), + FilePath: r.Path, + FileUrl: r.Url, + }, nil +} + +// UploadFiles 批量上传文件 +// +// 逐个上传文件,任一文件上传失败时返回错误 +func (s *sUpload) UploadFiles(ctx context.Context, req *v1.UploadFilesReq) (res *v1.UploadFilesRes, err error) { + res = &v1.UploadFilesRes{List: []v1.UploadFileRes{}} + for _, f := range req.Files { + if f == nil { + continue + } + one, e := s.UploadFile(ctx, &v1.UploadFileReq{File: f}) + if e != nil { + return nil, e + } + res.List = append(res.List, *one) + } + return res, nil +} diff --git a/internal/logic/user.go b/internal/logic/user.go new file mode 100644 index 0000000..4d89fd6 --- /dev/null +++ b/internal/logic/user.go @@ -0,0 +1,987 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package logic + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gerror" + + "easygoadmin/api/v1" + "easygoadmin/internal/consts" + "easygoadmin/internal/dao" + "easygoadmin/internal/service" + "easygoadmin/utility/cache" + "easygoadmin/utility/crypto" + "easygoadmin/utility/excel" + "easygoadmin/utility/upload" +) + +// ============================== 用户业务实现 ============================== + +// sUser 用户业务实现 +// 实现 service.IUser 接口,承载用户模块的业务逻辑 +type sUser struct{} + +func init() { + service.RegisterUser(&sUser{}) +} + +// userSerializeMaps 枚举字段到字典编码的映射 +// {field}Text 由 dict_item 解析,字典管理页可修改文案 +var userSerializeMaps = map[string]string{ + "gender": "user_gender", + "status": "user_status", +} + +// userSpec 用户模块声明 +// +// Add、Update、Detail 涉及密码、城市、头像与角色装配,采用手写实现 +var userSpec = ModuleSpec[v1.UserItem, v1.UserListItem, v1.UserDetailRes]{ + Model: dao.User.Ctx, + LikeFields: []string{"realname"}, // querySchemas 姓名输入 → 模糊 + EqFields: []string{"status", "dept_id", "level_id", "position_id", "tenant_id"}, + OrderBy: "id DESC", + ListOrderBy: "sort, id", + SerializeMaps: userSerializeMaps, + FileFields: []string{"avatar"}, // 读侧自动补域名(file.domainUrl) + FileSaveDir: "user", + Filter: userFilter, // keyword 列 Page/List 有别 + role_id 子查询 + DataScope: ApplyDataScope("dept_id"), // 角色数据权限(Page/List/导出生效) + BeforeDelete: userDeleteGuard, + AfterDelete: userCleanRelations, // 清理 user_role + 身份缓存 + AfterBatchDelete: func(ctx context.Context, ids []int64) error { + return userCleanRelationsBatch(ctx, ids) + }, + AfterPageList: userExpandNames, // 行内角色/部门/职级/岗位名批量补全 +} + +// userFilter 复杂筛选 +// +// 关键词按入参分支,分页请求额外匹配手机号,并支持按角色ID过滤 +func userFilter(ctx context.Context, m *gdb.Model, req any) *gdb.Model { + switch r := req.(type) { + case *v1.UserPageReq: + if r.Keyword != "" { + kw := "%" + r.Keyword + "%" + m = m.Where("(username LIKE ? OR realname LIKE ? OR mobile LIKE ?)", kw, kw, kw) + } + roleId := r.RoleId + if roleId == 0 { + roleId = r.Role // 前端传参名兼容(role) + } + if roleId > 0 { + m = m.WhereIn("id", dao.UserRole.Ctx(ctx).Where("role_id", roleId).Fields("user_id")) + } + case *v1.UserListReq: + if r.Keyword != "" { + kw := "%" + r.Keyword + "%" + m = m.Where("(username LIKE ? OR realname LIKE ?)", kw, kw) + } + } + return m +} + +// userDeleteGuard 删除守卫 +// 校验记录存在性 +func userDeleteGuard(ctx context.Context, id int64) error { + if n, err := dao.User.Ctx(ctx).Where("id", id).Count(); err != nil { + return err + } else if n == 0 { + return gerror.NewCode(consts.CodeRecordNotFound) + } + return nil +} + +// userCleanRelations 单条删除后清理 +// 清理 user_role 关联并失效身份缓存 +func userCleanRelations(ctx context.Context, id int64) error { + return userCleanRelationsBatch(ctx, []int64{id}) +} + +// userCleanRelationsBatch 批量删除后清理 +// 清理 user_role 关联并失效身份缓存 +func userCleanRelationsBatch(ctx context.Context, ids []int64) error { + if _, err := dao.UserRole.Ctx(ctx).WhereIn("user_id", ids).Delete(); err != nil { + return err + } + for _, id := range ids { + cache.Del(ctx, + consts.CacheKeyUserIdentity(int(id)), + consts.CacheKeyUserPerms(int(id)), + consts.CacheKeyUserRoles(int(id)), + ) + } + return nil +} + +// userExpandNames 分页行冗余装配 +// +// 补全角色引用数组、城市编码数组、部门、职级、岗位名称及头像域名 +func userExpandNames(ctx context.Context, list []*v1.UserItem, req any) error { + if len(list) == 0 { + return nil + } + ids := make([]int64, 0, len(list)) + for _, item := range list { + ids = append(ids, item.Id) + } + // UserItem 无 dept/level/position/city id 列 → 按主键回查装配所需外键 + rows, err := dao.User.Ctx(ctx).WhereIn("id", ids). + Fields("id, dept_id, level_id, position_id, province_code, city_code, district_code, street_code").All() + if err != nil { + return err + } + u := &sUser{} + levelIds := make([]int64, 0) + positionIds := make([]int64, 0) + deptIds := make([]int64, 0) + fkOf := make(map[int64]gdb.Record, len(rows)) + for _, r := range rows { + fkOf[r["id"].Int64()] = r + if v := r["level_id"].Int64(); v > 0 { + levelIds = append(levelIds, v) + } + if v := r["position_id"].Int64(); v > 0 { + positionIds = append(positionIds, v) + } + if v := r["dept_id"].Int64(); v > 0 { + deptIds = append(deptIds, v) + } + } + nameOfLevel := u.namesById(ctx, dao.Level.Ctx(ctx), levelIds) + nameOfPosition := u.namesById(ctx, dao.Position.Ctx(ctx), positionIds) + nameOfDept := u.namesById(ctx, dao.Dept.Ctx(ctx), deptIds) + roleRefMap, err := u.userRoleRefs(ctx, ids) + if err != nil { + return err + } + for _, item := range list { + item.Roles = roleRefMap[item.Id] + if item.Roles == nil { + item.Roles = []v1.UserRoleRef{} + } + if fk, ok := fkOf[item.Id]; ok { + item.LevelName = nameOfLevel[fk["level_id"].Int64()] + item.PositionName = nameOfPosition[fk["position_id"].Int64()] + item.DeptName = nameOfDept[fk["dept_id"].Int64()] + city := make([]string, 0, 4) + for _, col := range []string{"province_code", "city_code", "district_code", "street_code"} { + if v := fk[col].String(); v != "" { + city = append(city, v) + } + } + item.City = city + } + if item.City == nil { + item.City = []string{} + } + } + return nil +} + +// Page 用户分页 +// +// 支持关键词、状态、部门、角色、职级、岗位等条件,行内冗余名称批量补全 +func (s *sUser) Page(ctx context.Context, req *v1.UserPageReq) (res *v1.UserPageRes, err error) { + list, total, err := userSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize()) + if err != nil { + return nil, err + } + return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil +} + +// List 用户全量列表 +// +// 供下拉选择使用,角色名在委托层后处理 +func (s *sUser) List(ctx context.Context, req *v1.UserListReq) (res v1.UserListRes, err error) { + list, err := userSpec.List(ctx, req) + if err != nil { + return nil, err + } + // 精简列表不挂基座钩子 → 角色名在委托层后处理 + roleMap, err := s.userRoleNames(ctx) + if err != nil { + return nil, err + } + for _, item := range list { + item.Role = roleMap[item.Id] + } + return derefSlice(list), nil +} + +// Detail 用户详情 +// +// 含角色ID数组与四级区划编码,城市信息为空时按编码回填名称 +func (s *sUser) Detail(ctx context.Context, req *v1.UserDetailReq) (res *v1.UserDetailRes, err error) { + one, err := dao.User.Ctx(ctx).Where("id", req.Id).One() + if err != nil { + return nil, err + } + if len(one) == 0 { + return nil, gerror.NewCode(consts.CodeRecordNotFound) + } + roleIds, err := s.userRoleIds(ctx, req.Id) + if err != nil { + return nil, err + } + detail := s.toDetail(ctx, one, roleIds) + if detail.CityInfo == "" && len(detail.City) > 0 { + detail.CityInfo = ResolveCityNames(ctx, detail.City, ",") + } + return detail, nil +} + +// Add 新增用户 +// +// 用户名与手机号租户内唯一,校验租户用户上限,自动生成用户编码并写入 user_role +func (s *sUser) Add(ctx context.Context, req *v1.UserAddReq) (res *v1.IdRes, err error) { + tenantId := consts.GetTenantID(ctx) + if exists, e := s.usernameExists(ctx, req.Username, tenantId, 0); e != nil { + return nil, e + } else if exists { + return nil, gerror.NewCodef(consts.CodeRecordExists, "用户名已存在") + } + if req.Mobile != "" { + if exists, e := s.mobileExists(ctx, req.Mobile, tenantId, 0); e != nil { + return nil, e + } else if exists { + return nil, gerror.NewCodef(consts.CodeRecordExists, "手机号已被使用") + } + } + if err = s.checkTenantUserMax(ctx, tenantId); err != nil { + return nil, err + } + // 自动生成用户编码:{租户编码}-{租户内6位序号}(如 T0001-000020;平台租户用 SYS) + code, err := genUserCode(ctx, tenantId) + if err != nil { + return nil, err + } + // 双重加盐:外部 salt + bcrypt(password+salt),两值分别入 password/salt 列 + salt := crypto.GenerateSalt() + hashed, err := crypto.HashPassword(req.Password, salt) + if err != nil { + return nil, err + } + status := req.Status + if status == 0 { + status = 1 + } + data := map[string]any{ + "tenant_id": tenantId, + "code": code, + "username": req.Username, + "password": hashed, + "salt": salt, + "realname": req.Realname, + "gender": req.Gender, + "mobile": req.Mobile, + "email": req.Email, + "dept_id": req.DeptId, + "level_id": req.LevelId, + "position_id": req.PositionId, + "city_info": req.CityInfo, + "address": req.Address, + "avatar": req.Avatar, + "intro": req.Intro, + "status": status, + "sort": req.Sort, + "note": req.Note, + "province_code": cityCodeAt(req.City, 0), + "city_code": cityCodeAt(req.City, 1), + "district_code": cityCodeAt(req.City, 2), + "street_code": cityCodeAt(req.City, 3), + } + // 头像 temp → 正式目录(入库形态为相对 uploadDir 的路径,剥离域名) + data["avatar"] = SaveFileField(ctx, req.Avatar, "user") + // 城市信息:优先由编码数组解析名称 + if len(req.City) > 0 { + if info := ResolveCityNames(ctx, req.City, ","); info != "" { + data["city_info"] = info + } + } + result, err := dao.User.Ctx(ctx).Data(data).Insert() + if err != nil { + return nil, err + } + idv, _ := result.LastInsertId() + if err = s.AssignRoles(ctx, idv, req.Roles); err != nil { + return nil, err + } + return &v1.IdRes{Id: idv}, nil +} + +// Update 修改用户 +// +// 密码留空时不做修改,角色关联全量重写 +func (s *sUser) Update(ctx context.Context, req *v1.UserUpdateReq) (res *v1.UserUpdateRes, err error) { + if exists, e := s.usernameExists(ctx, req.Username, consts.GetTenantID(ctx), req.Id); e != nil { + return nil, e + } else if exists { + return nil, gerror.NewCodef(consts.CodeRecordExists, "用户名已存在") + } + if req.Mobile != "" { + if exists, e := s.mobileExists(ctx, req.Mobile, consts.GetTenantID(ctx), req.Id); e != nil { + return nil, e + } else if exists { + return nil, gerror.NewCodef(consts.CodeRecordExists, "手机号已被使用") + } + } + data := map[string]any{ + "username": req.Username, + "realname": req.Realname, + "gender": req.Gender, + "mobile": req.Mobile, + "email": req.Email, + "dept_id": req.DeptId, + "level_id": req.LevelId, + "position_id": req.PositionId, + "city_info": req.CityInfo, + "address": req.Address, + "avatar": req.Avatar, + "intro": req.Intro, + "status": req.Status, + "sort": req.Sort, + "note": req.Note, + "province_code": cityCodeAt(req.City, 0), + "city_code": cityCodeAt(req.City, 1), + "district_code": cityCodeAt(req.City, 2), + "street_code": cityCodeAt(req.City, 3), + } + if len(req.City) > 0 { + if info := ResolveCityNames(ctx, req.City, ","); info != "" { + data["city_info"] = info + } + } + data["avatar"] = SaveFileField(ctx, req.Avatar, "user") + if req.Password != "" { + salt := crypto.GenerateSalt() + hashed, e := crypto.HashPassword(req.Password, salt) + if e != nil { + return nil, e + } + data["password"] = hashed + data["salt"] = salt + } + if _, err = dao.User.Ctx(ctx).Where("id", req.Id).Data(data).Update(); err != nil { + return nil, err + } + if err = s.AssignRoles(ctx, req.Id, req.Roles); err != nil { + return nil, err + } + return &v1.UserUpdateRes{}, nil +} + +// Status 用户状态 +// 设置用户的正常或禁用状态 +func (s *sUser) Status(ctx context.Context, req *v1.UserStatusReq) (res *v1.UserStatusRes, err error) { + if _, err = dao.User.Ctx(ctx).Where("id", req.Id).Data(map[string]any{"status": req.Status}).Update(); err != nil { + return nil, err + } + return &v1.UserStatusRes{}, nil +} + +// ResetPwd 重置用户密码 +// 未传新密码时重置为系统默认密码 +func (s *sUser) ResetPwd(ctx context.Context, req *v1.UserResetPwdReq) (res *v1.UserResetPwdRes, err error) { + if n, e := dao.User.Ctx(ctx).Where("id", req.UserId).Count(); e != nil { + return nil, e + } else if n == 0 { + return nil, gerror.New("用户不存在") + } + password := req.Password + if password == "" { + password = getDefaultPassword(ctx) + } + salt := crypto.GenerateSalt() + hashed, err := crypto.HashPassword(password, salt) + if err != nil { + return nil, err + } + if _, err = dao.User.Ctx(ctx).Where("id", req.UserId).Data(map[string]any{ + "password": hashed, + "salt": salt, + }).Update(); err != nil { + return nil, err + } + return &v1.UserResetPwdRes{}, nil +} + +// Delete 删除用户 +// 软删记录并清理 user_role 关联 +func (s *sUser) Delete(ctx context.Context, req *v1.UserDeleteReq) (res *v1.UserDeleteRes, err error) { + if err = userSpec.Delete(ctx, req.Id); err != nil { + return nil, err + } + return &v1.UserDeleteRes{}, nil +} + +// BatchDelete 批量删除用户 +// 按用户ID列表批量删除记录 +func (s *sUser) BatchDelete(ctx context.Context, req *v1.UserBatchDeleteReq) (res *v1.UserBatchDeleteRes, err error) { + if err = userSpec.BatchDelete(ctx, req.Ids); err != nil { + return nil, err + } + return &v1.UserBatchDeleteRes{}, nil +} + +// Import 导入用户 +// +// 表格列序:姓名、性别、手机号、邮箱、部门、职级、岗位、城市、地址、用户名、简介、状态、备注; +// 密码统一取系统默认密码,用户编码自动生成; +// 性别与状态支持文本或数字,部门、职级、岗位支持名称或ID,城市为逗号分隔的名称串 +func (s *sUser) Import(ctx context.Context, req *v1.UserImportReq) (res *v1.UserImportRes, err error) { + _, rows, err := excel.ReadSheet(req.File) + if err != nil { + return nil, err + } + res = &v1.UserImportRes{Total: len(rows)} + tenantId := consts.GetTenantID(ctx) + for i, row := range rows { + username := cellAt(row, 9) + if username == "" { + res.Failed++ + res.Reasons = append(res.Reasons, fmt.Sprintf("第%d行:用户名为空", i+2)) + continue + } + if exists, e := s.usernameExists(ctx, username, tenantId, 0); e != nil { + return nil, e + } else if exists { + res.Failed++ + res.Reasons = append(res.Reasons, fmt.Sprintf("第%d行:用户名已存在", i+2)) + continue + } + salt := crypto.GenerateSalt() + hashed, e := crypto.HashPassword(getDefaultPassword(ctx), salt) + if e != nil { + res.Failed++ + res.Reasons = append(res.Reasons, fmt.Sprintf("第%d行:密码加密失败", i+2)) + continue + } + code, e := genUserCode(ctx, tenantId) + if e != nil { + return nil, e + } + cityInfo := cellAt(row, 7) + codes := ResolveCityCodes(ctx, cityInfo) + if _, e = dao.User.Ctx(ctx).Data(map[string]any{ + "username": username, + "realname": cellAt(row, 0), + "code": code, + "password": hashed, + "salt": salt, + "gender": parseGenderCell(cellAt(row, 1)), + "mobile": cellAt(row, 2), + "email": cellAt(row, 3), + "dept_id": resolveRefID(ctx, dao.Dept.Ctx(ctx), cellAt(row, 4)), + "level_id": resolveRefID(ctx, dao.Level.Ctx(ctx), cellAt(row, 5)), + "position_id": resolveRefID(ctx, dao.Position.Ctx(ctx), cellAt(row, 6)), + "city_info": cityInfo, + "address": cellAt(row, 8), + "intro": cellAt(row, 10), + "status": parseStatusCell(cellAt(row, 11)), + "note": cellAt(row, 12), + "tenant_id": tenantId, + "province_code": codes[0], + "city_code": codes[1], + "district_code": codes[2], + "street_code": codes[3], + }).Insert(); e != nil { + res.Failed++ + res.Reasons = append(res.Reasons, fmt.Sprintf("第%d行:写入失败 %v", i+2, e)) + continue + } + res.Success++ + } + return res, nil +} + +// Export 导出用户 +// +// 列序对齐导出模板,每批 500 条防 OOM,写出 xlsx 并返回访问地址 +func (s *sUser) Export(ctx context.Context, req *v1.UserExportReq) (res *v1.UserExportRes, err error) { + // 部门/职级/岗位 ID→名称(小维表全量加载,行内引用) + deptNames, err := idNameMap(ctx, dao.Dept.Ctx(ctx)) + if err != nil { + return nil, err + } + levelNames, err := idNameMap(ctx, dao.Level.Ctx(ctx)) + if err != nil { + return nil, err + } + positionNames, err := idNameMap(ctx, dao.Position.Ctx(ctx)) + if err != nil { + return nil, err + } + headers := []string{"用户编码", "姓名", "性别", "手机号", "邮箱", "部门", "职级", "岗位", "城市", "地址", "用户名", "简介", "状态", "备注"} + var rows [][]any + // 分块遍历导出(每页重建查询防 OOM;并发写入下可能有翻页漂移,导出场景可接受) + if err = ChunkQuery(ctx, func(page, size int) *gdb.Model { + m := userSpec.applyFilters(ctx, dao.User.Ctx(ctx), req) + return m.Page(page, size).Order("id DESC") + }, 500, func(part gdb.Result) error { + for _, r := range part { + codes := make([]string, 0, 4) + for _, col := range []string{"province_code", "city_code", "district_code", "street_code"} { + if v := r[col].String(); v != "" { + codes = append(codes, v) + } + } + // 城市优先由四级编码解析名称,编码缺失时回退 city_info 存量值 + city := ResolveCityNames(ctx, codes, ",") + if city == "" { + city = r["city_info"].String() + } + rows = append(rows, []any{ + r["code"].String(), r["realname"].String(), genderText(r["gender"].Int()), + r["mobile"].String(), r["email"].String(), + deptNames[r["dept_id"].Int64()], levelNames[r["level_id"].Int64()], + positionNames[r["position_id"].Int64()], city, r["address"].String(), + r["username"].String(), r["intro"].String(), + userStatusText(r["status"].Int()), r["note"].String(), + }) + } + return nil + }); err != nil { + return nil, err + } + fileName, _, err := excel.WriteSheet(upload.BaseDir(ctx), "用户数据", headers, rows) + if err != nil { + return nil, err + } + url := upload.GetFileUrl(ctx, fileName) + return &url, nil +} + +// Document 用户打印信息 +// +// 在用户详情基础上追加角色、部门、职级、岗位名称 +func (s *sUser) Document(ctx context.Context, req *v1.UserDocumentReq) (res *v1.UserDocumentRes, err error) { + one, err := dao.User.Ctx(ctx).Where("id", req.Id).One() + if err != nil { + return nil, err + } + if len(one) == 0 { + return nil, gerror.NewCode(consts.CodeRecordNotFound) + } + roleIds, err := s.userRoleIds(ctx, req.Id) + if err != nil { + return nil, err + } + detail := s.toDetail(ctx, one, roleIds) + res = &v1.UserDocumentRes{UserDetailRes: *detail, RoleNames: []string{}} + if len(roleIds) > 0 { + roleRows, e := dao.Role.Ctx(ctx).WhereIn("id", roleIds).All() + if e == nil { + for _, r := range roleRows { + res.RoleNames = append(res.RoleNames, r["name"].String()) + } + } + } + res.DeptName = s.dictName(ctx, dao.Dept.Ctx(ctx), one["dept_id"].Int()) + res.LevelName = s.dictName(ctx, dao.Level.Ctx(ctx), one["level_id"].Int()) + res.PositionName = s.dictName(ctx, dao.Position.Ctx(ctx), one["position_id"].Int()) + return res, nil +} + +// AssignRoles 重写用户角色关联 +// +// 供新增与修改调用,完成后失效该用户的身份缓存 +func (s *sUser) AssignRoles(ctx context.Context, userId int64, roleIds []int64) error { + if _, err := dao.UserRole.Ctx(ctx).Where("user_id", userId).Delete(); err != nil { + return err + } + for _, rid := range roleIds { + if rid <= 0 { + continue + } + if _, err := dao.UserRole.Ctx(ctx).Data(map[string]any{ + "user_id": userId, + "role_id": rid, + }).Insert(); err != nil { + return err + } + } + s.clearIdentityCache(ctx, userId) + return nil +} + +// ---------- 私有辅助 ---------- + +// clearIdentityCache 权限相关缓存失效 +// 采用尽力而为策略 +func (s *sUser) clearIdentityCache(ctx context.Context, userId int64) { + cache.Del(ctx, + consts.CacheKeyUserIdentity(int(userId)), + consts.CacheKeyUserPerms(int(userId)), + consts.CacheKeyUserRoles(int(userId)), + ) +} + +// usernameExists 判断用户名是否已存在 +// tenantId 限定租户,excludeId 排除自身 +func (s *sUser) usernameExists(ctx context.Context, username string, tenantId int, excludeId int64) (bool, error) { + m := dao.User.Ctx(ctx).Where("username", username).Where("tenant_id", tenantId) + if excludeId > 0 { + m = m.WhereNot("id", excludeId) + } + count, err := m.Count() + return count > 0, err +} + +// mobileExists 判断手机号是否已存在 +func (s *sUser) mobileExists(ctx context.Context, mobile string, tenantId int, excludeId int64) (bool, error) { + m := dao.User.Ctx(ctx).Where("mobile", mobile).Where("tenant_id", tenantId) + if excludeId > 0 { + m = m.WhereNot("id", excludeId) + } + count, err := m.Count() + return count > 0, err +} + +// checkTenantUserMax 校验租户用户数上限 +func (s *sUser) checkTenantUserMax(ctx context.Context, tenantId int) error { + if tenantId == 0 { + return nil + } + one, err := dao.Tenant.Ctx(ctx).Where("id", tenantId).One() + if err != nil || len(one) == 0 { + return nil + } + maxNum := one["max_user_num"].Int() + if maxNum <= 0 { + return nil + } + count, err := dao.User.Ctx(ctx).Where("tenant_id", tenantId).Count() + if err != nil { + return err + } + if count >= maxNum { + return gerror.NewCode(consts.CodeTenantUserLimit) + } + return nil +} + +// genUserCode 生成用户编码 +// +// 格式为 {租户编码}-{租户内6位序号},如 T0001-000020;平台租户前缀为 SYS。 +// 序号取租户内未删除用户数加一,与现有编码顺延 +func genUserCode(ctx context.Context, tenantId int) (string, error) { + prefix := "SYS" + if tenantId > 0 { + one, err := dao.Tenant.Ctx(ctx).Where("id", tenantId).Fields("code").One() + if err != nil { + return "", err + } + if len(one) == 0 { + return "", gerror.NewCode(consts.CodeTenantNotFound) + } + prefix = one["code"].String() + } + n, err := dao.User.Ctx(ctx).Where("tenant_id", tenantId).Count() + if err != nil { + return "", err + } + return fmt.Sprintf("%s-%06d", prefix, n+1), nil +} + +// userRoleIds 查询用户的角色ID列表 +func (s *sUser) userRoleIds(ctx context.Context, userId int64) ([]int64, error) { + rows, err := dao.UserRole.Ctx(ctx).Where("user_id", userId).All() + if err != nil { + return nil, err + } + ids := make([]int64, 0, len(rows)) + for _, r := range rows { + ids = append(ids, r["role_id"].Int64()) + } + return ids, nil +} + +// userRoleRefs 批量查询用户角色引用数组 +// +// 返回用户ID到角色引用的映射,一次查询避免 N+1 +func (s *sUser) userRoleRefs(ctx context.Context, userIds []int64) (map[int64][]v1.UserRoleRef, error) { + result := make(map[int64][]v1.UserRoleRef) + if len(userIds) == 0 { + return result, nil + } + // 注意:不可给主表起别名——dao.Ctx 注入的软删条件使用完整表名,带别名时 MySQL 无法解析 + urTable := dao.UserRole.Table() + roleData, err := dao.UserRole.Ctx(ctx). + LeftJoin(dao.Role.Table()+" r", urTable+".role_id = r.id AND r.del_flag=0"). + WhereIn(urTable+".user_id", userIds). + Fields(urTable + ".user_id, r.id, r.name"). + Order(urTable + ".role_id"). + All() + if err != nil { + return nil, err + } + for _, rd := range roleData { + uid := rd["user_id"].Int64() + result[uid] = append(result[uid], v1.UserRoleRef{ + Id: rd["id"].Int64(), + Name: rd["name"].String(), + }) + } + return result, nil +} + +// userRoleNames 查询用户ID到角色名的映射 +// 多个角色名以逗号分隔 +func (s *sUser) userRoleNames(ctx context.Context) (map[int64]string, error) { + result := make(map[int64]string) + urRows, err := dao.UserRole.Ctx(ctx).All() + if err != nil { + return nil, err + } + if len(urRows) == 0 { + return result, nil + } + byUser := make(map[int64][]int64) + roleIds := make([]int64, 0, len(urRows)) + for _, r := range urRows { + uid, rid := r["user_id"].Int64(), r["role_id"].Int64() + byUser[uid] = append(byUser[uid], rid) + roleIds = append(roleIds, rid) + } + nameById := make(map[int64]string) + roleRows, err := dao.Role.Ctx(ctx).WhereIn("id", roleIds).All() + if err != nil { + return nil, err + } + for _, r := range roleRows { + nameById[r["id"].Int64()] = r["name"].String() + } + for uid, ids := range byUser { + names := "" + for i, rid := range ids { + if i > 0 { + names += "," + } + names += nameById[rid] + } + result[uid] = names + } + return result, nil +} + +// toDetail 数据库记录转换为用户详情 +func (s *sUser) toDetail(ctx context.Context, row gdb.Record, roleIds []int64) *v1.UserDetailRes { + if roleIds == nil { + roleIds = []int64{} + } + city := make([]string, 0, 4) + for _, col := range []string{"province_code", "city_code", "district_code", "street_code"} { + if v := row[col].String(); v != "" { + city = append(city, v) + } + } + return &v1.UserDetailRes{ + Id: row["id"].Int64(), + Username: row["username"].String(), + Realname: row["realname"].String(), + Avatar: upload.GetFileUrl(ctx, row["avatar"].String()), // 逃生舱手拼 DTO,直接调底层公共助手补域名 + Gender: row["gender"].Int(), + GenderText: DictGetText(ctx, userSerializeMaps["gender"], row["gender"].Int()), + Mobile: row["mobile"].String(), + Email: row["email"].String(), + Roles: roleIds, + DeptId: row["dept_id"].Int(), + LevelId: row["level_id"].Int(), + PositionId: row["position_id"].Int(), + City: city, + CityInfo: row["city_info"].String(), + Address: row["address"].String(), + Intro: row["intro"].String(), + Status: row["status"].Int(), + StatusText: DictGetText(ctx, userSerializeMaps["status"], row["status"].Int()), + Sort: row["sort"].Int(), + Note: row["note"].String(), + } +} + +// namesById 查询ID到名称的映射 +// 传入的模型需已构造好上下文 +func (s *sUser) namesById(ctx context.Context, m *gdb.Model, ids []int64) map[int64]string { + result := make(map[int64]string) + if len(ids) == 0 { + return result + } + rows, err := m.WhereIn("id", ids).Fields("id, name").All() + if err != nil { + return result + } + for _, r := range rows { + result[r["id"].Int64()] = r["name"].String() + } + return result +} + +// dictName 按ID查询名称 +// 适用于部门、职级、岗位等通用维表 +func (s *sUser) dictName(ctx context.Context, m *gdb.Model, idv int) string { + if idv <= 0 { + return "" + } + one, err := m.Where("id", idv).Fields("name").One() + if err != nil || len(one) == 0 { + return "" + } + return one["name"].String() +} + +// cityCodeAt 按下标取城市编码 +// 下标越界时返回空串 +func cityCodeAt(cities []string, i int) string { + if i < len(cities) { + return cities[i] + } + return "" +} + +// cellAt 按下标取单元格文本 +// 下标越界时返回空串 +func cellAt(row []string, i int) string { + if i < len(row) { + return row[i] + } + return "" +} + +// cellInt 按下标取单元格整数 +// 解析失败时返回默认值 +func cellInt(row []string, i, def int) int { + if v := cellAt(row, i); v != "" { + if n, err := strconv.Atoi(v); err == nil { + return n + } + } + return def +} + +// idNameMap 加载维表的ID到名称映射 +// 供部门、职级、岗位导出人读化使用 +func idNameMap(ctx context.Context, m *gdb.Model) (map[int64]string, error) { + rows, err := m.Fields("id, name").All() + if err != nil { + return nil, err + } + result := make(map[int64]string, len(rows)) + for _, r := range rows { + result[r["id"].Int64()] = r["name"].String() + } + return result, nil +} + +// resolveRefID 解析引用列 +// 数字按ID处理,非数字按名称反查,未命中时返回 0 +func resolveRefID(ctx context.Context, m *gdb.Model, v string) int64 { + v = strings.TrimSpace(v) + if v == "" { + return 0 + } + if n, err := strconv.ParseInt(v, 10, 64); err == nil { + return n + } + id, err := m.Where("name", v).Fields("id").Value() + if err != nil { + return 0 + } + return id.Int64() +} + +// parseGenderCell 解析性别单元格 +// 支持男、女、保密文本或 1、2、3 数字,其余取保密 +func parseGenderCell(v string) int { + switch strings.TrimSpace(v) { + case "男": + return 1 + case "女": + return 2 + case "保密": + return 3 + } + if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil && n >= 1 && n <= 3 { + return n + } + return 3 +} + +// parseStatusCell 解析状态单元格 +// 支持正常、禁用文本或 1、2 数字,其余取正常 +func parseStatusCell(v string) int { + switch strings.TrimSpace(v) { + case "正常": + return 1 + case "禁用": + return 2 + } + if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil && n > 0 { + return n + } + return 1 +} + +// genderText 性别值转文本 +func genderText(g int) string { + switch g { + case 1: + return "男" + case 2: + return "女" + default: + return "保密" + } +} + +// userStatusText 用户状态值转文本 +func userStatusText(st int) string { + if st == 1 { + return "正常" + } + return "禁用" +} diff --git a/internal/middleware/cors.go b/internal/middleware/cors.go new file mode 100644 index 0000000..3ce8f64 --- /dev/null +++ b/internal/middleware/cors.go @@ -0,0 +1,70 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package middleware + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" +) + +// ============================== 跨域中间件 ============================== + +// CORS 跨域处理 +// +// 读取配置 cors.allowOrigin:为空或为 * 时使用框架内置默认策略; +// 配置为具体来源时显式回显,并对预检请求直接返回 204 +func CORS(r *ghttp.Request) { + origin := g.Cfg().MustGet(r.Context(), "cors.allowOrigin").String() + if origin == "" || origin == "*" { + r.Response.CORSDefault() + } else { + r.Response.Header().Set("Access-Control-Allow-Origin", origin) + r.Response.Header().Set("Access-Control-Allow-Credentials", "true") + r.Response.Header().Set("Access-Control-Allow-Headers", "Origin,Authorization,Content-Type,Accept,Token,X-Requested-With") + r.Response.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") + if r.Method == "OPTIONS" { + r.Response.WriteStatus(204) + return + } + } + r.Middleware.Next() +} diff --git a/internal/middleware/demo.go b/internal/middleware/demo.go new file mode 100644 index 0000000..58b85a0 --- /dev/null +++ b/internal/middleware/demo.go @@ -0,0 +1,78 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package middleware + +import ( + "net/http" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + + "easygoadmin/internal/consts" + "easygoadmin/utility/response" +) + +// ============================== 演示模式中间件 ============================== + +// Demo 演示模式拦截 +// +// 当配置 app.demo 为 true 时启用,规则如下: +// - 读操作(GET/HEAD/OPTIONS)放行,演示站可正常浏览 +// - 写操作(POST/PUT/PATCH/DELETE)一律拒绝并提示"演示环境,禁止操作!" +// - 公开白名单路径(登录/注册/验证码/令牌兑换)放行,保证演示站可登录 +func Demo(r *ghttp.Request) { + if !g.Cfg().MustGet(r.Context(), "app.demo").Bool() { + r.Middleware.Next() + return + } + switch r.Method { + case http.MethodGet, http.MethodHead, http.MethodOptions: + r.Middleware.Next() + return + } + if IsPublicPath(r.URL.Path) { + r.Middleware.Next() + return + } + response.Write(r, response.Fail(gerror.NewCode(consts.CodeDemoMode))) +} diff --git a/internal/middleware/jwt.go b/internal/middleware/jwt.go new file mode 100644 index 0000000..6ab7d24 --- /dev/null +++ b/internal/middleware/jwt.go @@ -0,0 +1,158 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package middleware + +import ( + "encoding/json" + "strings" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/net/ghttp" + + "easygoadmin/internal/consts" + "easygoadmin/utility/cache" + jwtutil "easygoadmin/utility/jwt" + "easygoadmin/utility/response" +) + +// ============================== 身份认证中间件 ============================== + +// JWT 身份认证中间件 +// +// 处理流程: +// 1. 免登录路由直接放行 +// 2. 解析 Bearer Token,校验签名、过期与签发者 +// 3. 校验 Redis 黑名单,登出或刷新后的旧令牌失效 +// 4. 加载角色与权限(缓存 300 秒)并注入请求上下文 +func JWT(r *ghttp.Request) { + if IsPublicPath(r.URL.Path) { + r.Middleware.Next() + return + } + token := parseBearer(r.Header.Get("Authorization")) + if token == "" { + token = r.Get("token").String() // 兼容个别 URL 直传场景 + } + if token == "" { + writeUnauthorized(r) + return + } + claims, err := jwtutil.Parse(r.Context(), token) + if err != nil { + writeUnauthorized(r) + return + } + // 黑名单(登出/轮换后的旧令牌) + if cache.Exists(r.Context(), consts.CacheKeyTokenBlacklist(claims.ID)) { + writeUnauthorized(r) + return + } + + ctx := r.Context() + ctx = consts.WithUserID(ctx, claims.UserID) + ctx = consts.WithTenantID(ctx, claims.TenantID) + ctx = consts.WithUsername(ctx, claims.Username) + ctx = consts.WithRealname(ctx, claims.Realname) + ctx = consts.WithTokenJTI(ctx, claims.ID) + if claims.ExpiresAt != nil { + ctx = consts.WithTokenExpire(ctx, claims.ExpiresAt.Time) + } + + // 身份扩展(角色/权限/数据范围):缓存优先 + if idn := loadIdentity(r, claims.UserID); idn != nil { + ctx = consts.WithRoles(ctx, idn.Roles) + ctx = consts.WithPerms(ctx, idn.Perms) + ctx = consts.WithDataScope(ctx, idn.DataScope) + ctx = consts.WithDeptID(ctx, idn.DeptId) + } + r.SetCtx(ctx) + r.Middleware.Next() +} + +// loadIdentity 读取用户身份 +// +// 优先读取身份缓存,未命中时回源 IdentityLoader 并写入缓存 +func loadIdentity(r *ghttp.Request, userID int) *Identity { + if userID <= 0 { + return nil + } + ck := consts.CacheKeyUserIdentity(userID) + if v, ok := cache.Get(r.Context(), ck); ok { + idn := &Identity{} + if err := json.Unmarshal([]byte(v), idn); err == nil { + return idn + } + } + if IdentityLoader == nil { + return &Identity{} + } + idn, err := IdentityLoader(r.Context(), userID) + if err != nil || idn == nil { + glogWarn(r.Context(), "加载用户身份失败 userID=%d err=%v", userID, err) + return &Identity{} + } + if b, err := json.Marshal(idn); err == nil { + _ = cache.Set(r.Context(), ck, string(b), 300) + } + return idn +} + +// parseBearer 解析 Bearer 令牌 +// +// 兼容带前缀与直接传值两种形式 +func parseBearer(h string) string { + if h == "" { + return "" + } + if strings.HasPrefix(h, "Bearer ") || strings.HasPrefix(h, "bearer ") { + return strings.TrimSpace(h[7:]) + } + return strings.TrimSpace(h) +} + +// writeUnauthorized 输出未认证响应 +// +// 状态码为 401,供前端识别并跳转登录页 +func writeUnauthorized(r *ghttp.Request) { + res := response.Fail(gerror.NewCode(consts.CodeUnauthorized)) + response.WriteWithCode(r, res, 401) +} diff --git a/internal/middleware/log.go b/internal/middleware/log.go new file mode 100644 index 0000000..2c4c71d --- /dev/null +++ b/internal/middleware/log.go @@ -0,0 +1,56 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package middleware + +import ( + "context" + + "github.com/gogf/gf/v2/frame/g" +) + +// ============================== 日志工具 ============================== + +// glogWarn 中间件统一告警输出 +// 以警告级别记录中间件运行时的异常或降级信息 +func glogWarn(ctx context.Context, format string, args ...any) { + g.Log().Warningf(ctx, format, args...) +} diff --git a/internal/middleware/operation_log.go b/internal/middleware/operation_log.go new file mode 100644 index 0000000..ea3a3aa --- /dev/null +++ b/internal/middleware/operation_log.go @@ -0,0 +1,209 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package middleware + +import ( + "context" + "sync" + "time" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + + "easygoadmin/internal/consts" + "easygoadmin/internal/router/meta" + "easygoadmin/utility/response" + "easygoadmin/utility/ua" +) + +// ============================== 操作日志中间件 ============================== +// +// 请求结束后根据路由元数据(gmeta log/logType 标签)组装记录, +// 经有界通道异步批量落库,不阻塞业务请求。 +// +// 开关:operationLog.enable 为总开关;operationLog.logQuery 控制查询类是否记录。 + +const ( + logChanSize = 2048 // 日志通道容量 + logBatchSize = 100 // 单批落库条数 + logFlushEvery = time.Second // 定时刷新间隔 + logMaxLen = 8000 // param/result 截断长度 +) + +var ( + logOnce sync.Once // 保证日志工作协程仅启动一次 + logChan = make(chan *OperationLogEntry, logChanSize) // 日志缓冲通道 + logDrop int64 // 累计丢弃条数 + logMu sync.Mutex // 丢弃计数互斥锁 +) + +// OperationLog 操作日志采集 +// +// 依据路由元数据判断是否记录,组装日志记录后投入异步通道 +func OperationLog(r *ghttp.Request) { + if !cfgBool(r.Context(), "operationLog.enable", true) { + r.Middleware.Next() + return + } + start := time.Now() + r.Middleware.Next() + + rm, ok := r.Context().Value(meta.CtxKey).(meta.RouteMeta) + if !ok || rm.Log == "" { + return + } + logType := consts.LogTypeMap[rm.LogType] + if logType == consts.LogTypeQuery && !cfgBool(r.Context(), "operationLog.logQuery", true) { + return + } + + code, _ := response.GetCode(r) + entry := &OperationLogEntry{ + Title: rm.Log, + Type: logType, + Source: 0, + Method: rm.Method + " " + rm.Path, + RequestMethod: r.Method, + URL: r.URL.String(), + Param: trunc(collectParam(r), logMaxLen), + Result: trunc(collectResult(r), logMaxLen), + IP: realClientIP(r), + Location: requestLocation(r.Context(), realClientIP(r)), + OS: ua.ParseOS(r.Header.Get("User-Agent")), + Browser: ua.ParseBrowser(r.Header.Get("User-Agent")), + ConsumeTime: time.Since(start).Milliseconds(), + Status: 0, + Error: "", + CreateUser: consts.GetUsername(r.Context()), + } + if code != 0 { + entry.Status = 1 + if body, ok := response.GetBody(r); ok && body.Msg != "" { + entry.Error = trunc(body.Msg, 500) + } + } + + logOnce.Do(startLogWorker) + select { + case logChan <- entry: + default: + logMu.Lock() + logDrop++ + n := logDrop + logMu.Unlock() + if n%100 == 1 { + g.Log().Warningf(r.Context(), "操作日志通道已满,累计丢弃 %d 条", n) + } + } +} + +// collectParam 组装脱敏后的请求参数 +// +// GET 与 DELETE 请求取 query 参数,写操作优先取请求体 +func collectParam(r *ghttp.Request) string { + if r.Method == "GET" || r.Method == "DELETE" { + m := r.GetMap() + if len(m) == 0 { + return "" + } + return trunc(gjsonEncode(m), logMaxLen) + } + body := r.GetBodyString() + if body != "" { + return maskSecret(body) + } + return trunc(gjsonEncode(r.GetMap()), logMaxLen) +} + +// collectResult 组装响应信封内容 +func collectResult(r *ghttp.Request) string { + body, ok := response.GetBody(r) + if !ok { + return "" + } + return trunc(gjsonEncode(body), logMaxLen) +} + +// maskSecret 脱敏敏感字段 +// +// 覆盖密码、盐与令牌等字段 +func maskSecret(s string) string { + for _, k := range []string{"password", "Password", "oldPassword", "newPassword", "salt", "access_token", "refresh_token"} { + s = maskJSONField(s, k) + } + return s +} + +// startLogWorker 启动异步落库工作协程 +// +// 按批量大小或定时触发刷新,将缓冲日志写入 LogSink +func startLogWorker() { + go func() { + batch := make([]*OperationLogEntry, 0, logBatchSize) + flush := func() { + if len(batch) == 0 || LogSink == nil { + batch = batch[:0] + return + } + ctx := context.Background() + for _, e := range batch { + if err := LogSink(ctx, e); err != nil { + g.Log().Errorf(ctx, "操作日志落库失败: %v", err) + } + } + batch = batch[:0] + } + ticker := time.NewTicker(logFlushEvery) + defer ticker.Stop() + for { + select { + case e := <-logChan: + batch = append(batch, e) + if len(batch) >= logBatchSize { + flush() + } + case <-ticker.C: + flush() + } + } + }() +} diff --git a/internal/middleware/public.go b/internal/middleware/public.go new file mode 100644 index 0000000..b723b4d --- /dev/null +++ b/internal/middleware/public.go @@ -0,0 +1,74 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package middleware + +import "strings" + +// ============================== 免登录路由白名单 ============================== + +// publicPaths 免登录路由白名单 +// +// 精确匹配,公开路由一律不带版本前缀; +// 须与各注册处保持一致:auth.go 的 gmeta 标注(登录、验证码、令牌兑换) +// 以及 router.go 中的健康检查 +var publicPaths = map[string]bool{ + "/api/login": true, + "/api/register": true, + "/api/captcha": true, + "/api/oauth2/token": true, // refresh_token 兑换 + "/api/health": true, // 健康检查 +} + +// IsPublicPath 判断是否为免登录路由 +// +// 供身份认证与租户中间件据此放行; +// 同时放行 OpenAPI 与 Swagger 文档路径 +func IsPublicPath(path string) bool { + if publicPaths[path] { + return true + } + // OpenAPI / Swagger 文档 + if path == "/api.json" || strings.HasPrefix(path, "/swagger") { + return true + } + return false +} diff --git a/internal/middleware/ratelimit.go b/internal/middleware/ratelimit.go new file mode 100644 index 0000000..99b63c5 --- /dev/null +++ b/internal/middleware/ratelimit.go @@ -0,0 +1,143 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package middleware + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/net/ghttp" + + "easygoadmin/internal/consts" + "easygoadmin/utility/cache" + "easygoadmin/utility/response" +) + +// ============================== API 限流中间件 ============================== +// +// 固定窗口计数:键按窗口起点对齐(ratelimit:{ip}:{path}:{窗口起点}), +// 计数存 utility/cache(Redis 优先、内存降级),多实例部署时共享计数。 +// +// 规则: +// - 仅限 /api/ 前缀接口;文档(/api.json)、静态资源、健康检查不受限 +// - 认证入口(登录/验证码/令牌兑换)按 rateLimit.authLimit 单独更严阈值 +// - 计数存储异常时放行(限流不阻断业务) + +// RateLimit API 限流 +func RateLimit(r *ghttp.Request) { + if !cfgBool(r.Context(), "rateLimit.enable", true) { + r.Middleware.Next() + return + } + path := r.URL.Path + if !isRateLimitedPath(path) { + r.Middleware.Next() + return + } + window := cfgInt(r.Context(), "rateLimit.windowSeconds", 60) + limit := cfgInt(r.Context(), "rateLimit.limit", 120) + if isAuthPath(path) { + limit = cfgInt(r.Context(), "rateLimit.authLimit", 10) + } + if limit <= 0 || window <= 0 { + r.Middleware.Next() + return + } + + // 窗口起点对齐:同一窗口内共享计数键 + windowStart := time.Now().Unix() / int64(window) * int64(window) + key := fmt.Sprintf("ratelimit:%s:%s:%d", realClientIP(r), path, windowStart) + n, err := incrCounter(r, key, window) + if err != nil { + r.Middleware.Next() + return + } + if n > int64(limit) { + response.Write(r, response.Fail(gerror.NewCode(consts.CodeTooManyRequests))) + return + } + r.Middleware.Next() +} + +// isRateLimitedPath 判断是否为参与限流的接口路径 +// /api.json 与 /swagger 不带 /api/ 前缀,自然排除;健康检查显式豁免 +func isRateLimitedPath(path string) bool { + if path == "/api/health" { + return false + } + return path == "/api" || strings.HasPrefix(path, "/api/") +} + +// isAuthPath 判断是否为认证入口(适用更严格的独立阈值) +func isAuthPath(path string) bool { + switch path { + case "/api/login", "/api/captcha", "/api/oauth2/token": + return true + } + return false +} + +// incrCounter 固定窗口计数自增 +// 键已含窗口起点,TTL 仅用于回收存储,续期不影响窗口归属 +func incrCounter(r *ghttp.Request, key string, windowSeconds int) (int64, error) { + ttl := windowSeconds + 5 + if raw, ok := cache.Get(r.Context(), key); ok { + n, _ := strconv.ParseInt(raw, 10, 64) + n++ + _ = cache.Set(r.Context(), key, strconv.FormatInt(n, 10), ttl) + return n, nil + } + if cache.SetNX(r.Context(), key, "1", ttl) { + return 1, nil + } + // 并发下 SetNX 未抢到:读现值再加一 + if raw, ok := cache.Get(r.Context(), key); ok { + n, _ := strconv.ParseInt(raw, 10, 64) + n++ + _ = cache.Set(r.Context(), key, strconv.FormatInt(n, 10), ttl) + return n, nil + } + return 1, nil +} diff --git a/internal/middleware/response.go b/internal/middleware/response.go new file mode 100644 index 0000000..40f1921 --- /dev/null +++ b/internal/middleware/response.go @@ -0,0 +1,89 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package middleware + +import ( + "fmt" + "runtime/debug" + "strings" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + + "easygoadmin/internal/consts" + "easygoadmin/utility/response" +) + +// ============================== 统一响应兜底中间件 ============================== + +// Response 统一响应兜底 +// +// 负责 panic 恢复并输出统一 JSON 错误(前端契约 {code,msg,data}), +// 置于跨域之后、认证之前,保证任何环节的 panic 都不会泄漏堆栈到前端。 +// 同时将请求元信息(IP、UA、方法、URL)写入上下文,供登录日志与操作日志采集。 +func Response(r *ghttp.Request) { + // SVG 静态文件加固:强制附件下载 + nosniff(防内嵌脚本同源执行) + if strings.HasSuffix(strings.ToLower(r.URL.Path), ".svg") { + r.Response.Header().Set("Content-Disposition", "attachment") + r.Response.Header().Set("X-Content-Type-Options", "nosniff") + } + // 请求元信息(所有路由均需,登录日志依赖) + r.SetCtx(consts.WithRequestMeta(r.Context(), + realClientIP(r), + r.Header.Get("User-Agent"), + r.Method, + r.URL.String(), + )) + defer func() { + if e := recover(); e != nil { + g.Log().Errorf(r.Context(), "panic recovered: %v\n%s", e, debug.Stack()) + if _, written := response.GetCode(r); written { + return // 已写出响应,仅记录 + } + response.WriteWithCode(r, + response.Fail(gerror.NewCode(consts.CodeServerError, fmt.Sprintf("服务器内部错误: %v", e))), + 500) + } + }() + r.Middleware.Next() +} diff --git a/internal/middleware/tenant.go b/internal/middleware/tenant.go new file mode 100644 index 0000000..abd00a4 --- /dev/null +++ b/internal/middleware/tenant.go @@ -0,0 +1,111 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package middleware + +import ( + "encoding/json" + "time" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/net/ghttp" + + "easygoadmin/internal/consts" + "easygoadmin/utility/cache" + "easygoadmin/utility/response" +) + +// ============================== 租户校验中间件 ============================== + +// Tenant 租户状态校验 +// +// 位于身份认证中间件之后,处理规则如下: +// - 超管(上下文租户ID=0)直接放行 +// - 租户级校验状态与过期时间,摘要缓存 60 秒 +func Tenant(r *ghttp.Request) { + if IsPublicPath(r.URL.Path) { + r.Middleware.Next() + return + } + tenantID := consts.CtxTenantID(r.Context()) + if tenantID == 0 { + r.Middleware.Next() + return + } + + info := loadTenantInfo(r, tenantID) + if info != nil { + if info.Status != 1 { + response.WriteWithCode(r, response.Fail(gerror.NewCode(consts.CodeTenantDisabled)), 1) + return + } + if info.ExpireTime != nil && info.ExpireTime.Time.Before(time.Now()) { + response.WriteWithCode(r, response.Fail(gerror.NewCode(consts.CodeTenantExpired)), 1) + return + } + } + r.Middleware.Next() +} + +// loadTenantInfo 加载租户摘要 +// +// 优先读取缓存(60 秒),未命中时回源 TenantLoader 并写缓存 +func loadTenantInfo(r *ghttp.Request, tenantID int) *TenantInfo { + ck := consts.CacheKeyTenantInfo(tenantID) + if v, ok := cache.Get(r.Context(), ck); ok { + info := &TenantInfo{} + if err := json.Unmarshal([]byte(v), info); err == nil { + return info + } + } + if TenantLoader == nil { + return nil + } + info, err := TenantLoader(r.Context(), tenantID) + if err != nil || info == nil { + glogWarn(r.Context(), "加载租户信息失败 tenantID=%d err=%v", tenantID, err) + return nil + } + if b, err := json.Marshal(info); err == nil { + _ = cache.Set(r.Context(), ck, string(b), 60) + } + return info +} diff --git a/internal/middleware/util.go b/internal/middleware/util.go new file mode 100644 index 0000000..d011be7 --- /dev/null +++ b/internal/middleware/util.go @@ -0,0 +1,168 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package middleware + +import ( + "context" + "encoding/json" + "net" + "regexp" + "strings" + "unicode/utf8" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + + "easygoadmin/utility/ip2region" +) + +// ============================== 中间件通用工具 ============================== + +// cfgBool 读取布尔配置 +// 配置缺失时返回默认值 +func cfgBool(ctx context.Context, key string, def bool) bool { + v := g.Cfg().MustGet(ctx, key) + if v.IsNil() { + return def + } + return v.Bool() +} + +// cfgInt 读取整数配置 +// 配置缺失时返回默认值(显式配置 0 表示业务含义上的关闭,原样返回) +func cfgInt(ctx context.Context, key string, def int) int { + v := g.Cfg().MustGet(ctx, key) + if v.IsNil() { + return def + } + return v.Int() +} + +// gjsonEncode JSON 序列化 +// 序列化失败时返回空串 +func gjsonEncode(v any) string { + b, err := json.Marshal(v) + if err != nil { + return "" + } + return string(b) +} + +// trunc 按字节截断且保证 UTF-8 完整 +// +// 截断点回退到完整的字符边界,避免 MySQL 严格模式拒绝非法字节 +func trunc(s string, n int) string { + if len(s) <= n { + return s + } + for n > 0 && !utf8.RuneStart(s[n]) { + n-- + } + return s[:n] + "..." +} + +// maskJSONField 脱敏 JSON 文本中的指定字段 +// 将该字段的值替换为 *** +var maskReCache = map[string]*regexp.Regexp{} + +func maskJSONField(s, field string) string { + re, ok := maskReCache[field] + if !ok { + re = regexp.MustCompile(`("` + regexp.QuoteMeta(field) + `"\s*:\s*)"?[^",}]*"?`) + maskReCache[field] = re + } + return re.ReplaceAllString(s, `${1}***`) +} + +// realClientIP 获取客户端真实IP +// +// 读取配置 api.proxyIps,直连来源为可信代理时, +// 依次取 X-Forwarded-For 首段或 X-Real-IP +func realClientIP(r *ghttp.Request) string { + remote := r.GetRemoteIp() + if !isTrustedProxy(r.Context(), remote) { + return remote + } + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { + if ip := strings.TrimSpace(strings.Split(xff, ",")[0]); ip != "" { + return ip + } + } + if real := strings.TrimSpace(r.Header.Get("X-Real-IP")); real != "" { + return real + } + return remote +} + +// isTrustedProxy 判断来源是否为可信代理 +// 支持精确 IP 与 CIDR 两种配置形式 +func isTrustedProxy(ctx context.Context, ip string) bool { + v := g.Cfg().MustGet(ctx, "api.proxyIps") + if v.IsNil() || v.String() == "" { + return false + } + parsed := net.ParseIP(ip) + if parsed == nil { + return false + } + for _, item := range strings.Split(v.String(), ",") { + item = strings.TrimSpace(item) + if item == "" { + continue + } + if item == ip { + return true + } + if _, cidr, err := net.ParseCIDR(item); err == nil && cidr.Contains(parsed) { + return true + } + } + return false +} + +// requestLocation 按请求IP解析归属地 +// +// 基于 ip2region 实现,解析失败时返回 "-" +func requestLocation(ctx context.Context, ip string) string { + _ = ctx + return ip2region.Location(ip) +} diff --git a/internal/middleware/wire.go b/internal/middleware/wire.go new file mode 100644 index 0000000..85f9789 --- /dev/null +++ b/internal/middleware/wire.go @@ -0,0 +1,128 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package middleware + +import ( + "context" + "time" + + "github.com/gogf/gf/v2/os/gtime" +) + +// ============================================================================ +// 依赖接线:中间件不直接依赖业务层,由 cmd 在启动时注册加载器与落地器。 +// ============================================================================ + +// ============================== 依赖类型 ============================== + +// Identity 用户身份扩展信息 +// 包含角色编码与权限节点 +type Identity struct { + Roles []string `json:"roles"` // 角色编码集合 + Perms []string `json:"perms"` // 权限节点集合 + DataScope int `json:"dataScope"` // 数据范围(多角色取最宽松;0=旧缓存缺字段,按全部处理) + DeptId int `json:"deptId"` // 所属部门ID(0=未分配) +} + +// TenantInfo 租户摘要 +// 仅保留状态与过期校验热路径所需的最小字段 +type TenantInfo struct { + ID int `json:"id"` // 租户ID + Status int `json:"status"` // 状态:1-正常 2-禁用 + ExpireTime *gtime.Time `json:"expireTime"` // 过期时间,nil 表示永不过期 +} + +// OperationLogEntry 操作日志记录 +// 字段覆盖操作日志表全部列 +type OperationLogEntry struct { + Title string `json:"title"` // 日志标题 + Type int `json:"type"` // 操作类型 + Source int `json:"source"` // 来源 + Method string `json:"method"` // 路由模板 + RequestMethod string `json:"requestMethod"` // HTTP 方法 + URL string `json:"url"` // 请求地址 + Param string `json:"param"` // 脱敏后的请求参数 + Result string `json:"result"` // 响应报文(截断) + IP string `json:"ip"` // 客户端IP + Location string `json:"location"` // 归属地 + OS string `json:"os"` // 操作系统 + Browser string `json:"browser"` // 浏览器 + ConsumeTime int64 `json:"consumeTime"` // 耗时(毫秒) + Status int `json:"status"` // 状态:0-正常 1-异常 + Error string `json:"error"` // 错误信息 + CreateUser string `json:"createUser"` // 操作人 +} + +// ============================== 依赖注册 ============================== + +var ( + // IdentityLoader 加载用户角色与权限 + // 为 nil 时跳过加载,仅认证身份 + IdentityLoader func(ctx context.Context, userID int) (*Identity, error) + + // TenantLoader 加载租户状态摘要 + // 为 nil 时跳过租户校验 + TenantLoader func(ctx context.Context, tenantID int) (*TenantInfo, error) + + // LogSink 操作日志异步落库 + // 为 nil 时降级为调试日志 + LogSink func(ctx context.Context, entry *OperationLogEntry) error +) + +// SetIdentityLoader 注册身份加载器 +func SetIdentityLoader(f func(ctx context.Context, userID int) (*Identity, error)) { + IdentityLoader = f +} + +// SetTenantLoader 注册租户加载器 +func SetTenantLoader(f func(ctx context.Context, tenantID int) (*TenantInfo, error)) { + TenantLoader = f +} + +// SetLogSink 注册操作日志落地器 +func SetLogSink(f func(ctx context.Context, entry *OperationLogEntry) error) { + LogSink = f +} + +// Now 返回当前时间 +// 供各中间件统一使用 +func Now() time.Time { return time.Now() } diff --git a/internal/migrate/conn.go b/internal/migrate/conn.go new file mode 100644 index 0000000..96af2fc --- /dev/null +++ b/internal/migrate/conn.go @@ -0,0 +1,155 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package migrate + +import ( + "context" + "fmt" + + "github.com/gogf/gf/v2/database/gdb" +) + +// ============================== 数据库连接 ============================== + +// openConn 按 DSN 动态建立连接并探活 +// +// 通过 gdb.New 注册独立命名节点,不触碰 config.yaml 中的默认连接 +func openConn(ctx context.Context, node string, dsn DSN) (gdb.DB, error) { + link, err := buildLink(dsn) + if err != nil { + return nil, err + } + db, err := gdb.New(gdb.ConfigNode{ + Name: node, + Type: dsn.Type, + Link: link, + }) + if err != nil { + return nil, fmt.Errorf("连接 %s 库失败: %v", node, err) + } + if _, err = db.Query(ctx, "SELECT 1"); err != nil { + return nil, fmt.Errorf("%s 库探活失败: %v", node, err) + } + return db, nil +} + +// ensureDatabase 目标库不存在时先创建 +// +// MySQL、PostgreSQL、SQL Server 经维护库建库;Oracle 与 SQLite 无需预建 +func ensureDatabase(ctx context.Context, dsn DSN) error { + var maintainDB, createSQL, checkSQL string + switch dsn.Type { + case "mysql": + maintainDB, checkSQL = "", "SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = ?" + createSQL = "CREATE DATABASE `" + dsn.Database + "` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci" + case "pgsql": + maintainDB, checkSQL = "postgres", "SELECT 1 FROM pg_database WHERE datname = ?" + createSQL = `CREATE DATABASE "` + dsn.Database + `"` + case "mssql": + maintainDB, checkSQL = "master", "SELECT 1 FROM sys.databases WHERE name = ?" + createSQL = "CREATE DATABASE [" + dsn.Database + "]" + default: + return nil // oracle/sqlite 无需预建 + } + checkDSN := dsn + checkDSN.Database = maintainDB + link, err := buildLink(checkDSN) + if err != nil { + return err + } + maintain, err := gdb.New(gdb.ConfigNode{Name: "migrate_maintain", Type: dsn.Type, Link: link}) + if err != nil { + return fmt.Errorf("连接 %s 维护库失败: %v", dsn.Type, err) + } + defer maintain.Close(ctx) + row, err := maintain.Query(ctx, checkSQL, dsn.Database) + if err != nil { + return fmt.Errorf("检查目标库是否存在失败: %v", err) + } + if len(row) > 0 { + return nil // 已存在 + } + if _, err = maintain.Exec(ctx, createSQL); err != nil { + return fmt.Errorf("创建目标库 %s 失败: %v", dsn.Database, err) + } + return nil +} + +// buildLink 将 DSN 转换为 gdb link 串 +// +// 各方言按驱动约定格式组装,对齐配置示例 +func buildLink(dsn DSN) (string, error) { + switch dsn.Type { + case "mysql": + return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true&loc=Local", + dsn.Username, dsn.Password, dsn.Host, dsn.Port, dsn.Database), nil + case "pgsql": + return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?sslmode=disable", + dsn.Username, dsn.Password, dsn.Host, dsn.Port, dsn.Database), nil + case "mssql": + return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?encrypt=disable", + dsn.Username, dsn.Password, dsn.Host, dsn.Port, dsn.Database), nil + case "oracle": + return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", + dsn.Username, dsn.Password, dsn.Host, dsn.Port, dsn.Database), nil + case "sqlite": + if dsn.Database == "" { + return "", fmt.Errorf("sqlite 需以 -src-database/-dst-database 指定数据库文件路径") + } + return fmt.Sprintf("sqlite::@file(%s)", dsn.Database), nil + } + return "", fmt.Errorf("不支持的数据库类型: %s", dsn.Type) +} + +// tableExists 判断目标库中同名表是否已存在 +func tableExists(ctx context.Context, db gdb.DB, table string) (bool, error) { + tables, err := db.Tables(ctx) + if err != nil { + return false, err + } + for _, t := range tables { + if t == table { + return true, nil + } + } + return false, nil +} diff --git a/internal/migrate/copy.go b/internal/migrate/copy.go new file mode 100644 index 0000000..e817ca5 --- /dev/null +++ b/internal/migrate/copy.go @@ -0,0 +1,262 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package migrate + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/gogf/gf/v2/database/gdb" +) + +// ============================== 数据搬运 ============================== + +// copyTable 分批搬运单表数据 +// +// 源端按主键排序做快照分页,目标端使用多行 INSERT。 +// onBatch 为每批完成回调(本批行数、累计行数),为 nil 时静默。 +// 返回搬运行数;无主键表直接报错。 +// SQL Server 目标使用内联字面量 INSERT 绕过参数上限,含自增列时以事务包裹切换 IDENTITY_INSERT。 +func copyTable(ctx context.Context, src, dst gdb.DB, r Renderer, t *TableMeta, batchSize int, onBatch func(batch, total int64)) (int64, error) { + pk := pkColumns(t) + if len(pk) == 0 { + return 0, errNoPK(t.Name) + } + pkCol := pk[0] + if batchSize <= 0 { + batchSize = 1000 + } + + // 目标端列清单(以源表列序组装 INSERT,map 键名驱动目标方言语句生成) + colNames := make([]string, 0, len(t.Columns)) + for _, c := range t.Columns { + colNames = append(colNames, c.Name) + } + + // mssql:内联字面量 INSERT(无参数占位符,不受 2100 上限约束);自增列事务钉连接 + if dst.GetConfig().Type == "mssql" { + return copyMssql(ctx, src, dst, r, t, pkCol, colNames, batchSize, onBatch) + } + + // 其余方言:参数化多行 INSERT + return doCopy(ctx, src, dst.Ctx(ctx), t.Name, pkCol, colNames, batchSize, onBatch, + func(ins inserter, batch []map[string]any) error { + _, err := ins.Model(t.Name).Data(batch).Insert() + return err + }) +} + +// copyMssql SQL Server 数据搬运 +// +// 使用内联字面量 INSERT 绕过参数上限; +// 自增列每批独立小事务切换 IDENTITY_INSERT, +// 既钉住连接,又避免整表单事务撑爆事务日志 +func copyMssql(ctx context.Context, src, dst gdb.DB, r Renderer, t *TableMeta, pkCol string, colNames []string, batchSize int, onBatch func(batch, total int64)) (int64, error) { + quoted := r.Quote(t.Name) + + // 自增列:每批 SET ON → INSERT → SET OFF → COMMIT(事务粒度=单批,日志占用受批大小约束) + if hasAutoIncCol(t) { + return doCopy(ctx, src, dst.Ctx(ctx), t.Name, pkCol, colNames, batchSize, onBatch, + func(ins inserter, batch []map[string]any) error { + tx, err := dst.Begin(ctx) + if err != nil { + return err + } + defer tx.Rollback() + if _, err = tx.Exec("SET IDENTITY_INSERT " + quoted + " ON"); err != nil { + return err + } + if _, err = tx.Exec(buildInsertSQL(r, t.Name, colNames, batch)); err != nil { + return err + } + if _, err = tx.Exec("SET IDENTITY_INSERT " + quoted + " OFF"); err != nil { + return err + } + return tx.Commit() + }) + } + + return doCopy(ctx, src, dst.Ctx(ctx), t.Name, pkCol, colNames, batchSize, onBatch, + func(ins inserter, batch []map[string]any) error { + _, err := dst.Exec(ctx, buildInsertSQL(r, t.Name, colNames, batch)) + return err + }) +} + +// inserter 目标端写入器 +// gdb.DB 与 gdb.TX 均满足该接口 +type inserter interface { + Model(tableNameOrStruct ...any) *gdb.Model +} + +// doCopy 核心搬运循环 +// +// insertBatch 为每批写入实现,方言差异由此注入 +func doCopy(ctx context.Context, src gdb.DB, ins inserter, table, pkCol string, colNames []string, batchSize int, onBatch func(batch, total int64), insertBatch func(ins inserter, batch []map[string]any) error) (int64, error) { + var ( + total int64 + lastPK any + ) + for { + m := src.Ctx(ctx).Model(table).Fields(strings.Join(quoteAll(src, colNames), ",")).Order(pkCol).Limit(batchSize) + if lastPK != nil { + m = m.Where(pkCol+" > ?", lastPK) + } + rows, err := m.All() + if err != nil { + return total, err + } + if len(rows) == 0 { + return total, nil + } + batch := make([]map[string]any, 0, len(rows)) + for _, row := range rows { + item := make(map[string]any, len(colNames)) + for _, c := range colNames { + item[c] = convertValue(row[c]) + } + batch = append(batch, item) + lastPK = row[pkCol].Val() + } + if err = insertBatch(ins, batch); err != nil { + return total, err + } + total += int64(len(batch)) + if onBatch != nil { + onBatch(int64(len(batch)), total) + } + if len(rows) < batchSize { + return total, nil + } + } +} + +// buildInsertSQL 构建带内联字面量的多行 INSERT +// +// SQL Server 专用,绕过 2100 参数占位符上限 +func buildInsertSQL(r Renderer, table string, colNames []string, batch []map[string]any) string { + cols := make([]string, len(colNames)) + for i, c := range colNames { + cols[i] = r.Quote(c) + } + valueRows := make([]string, 0, len(batch)) + for _, row := range batch { + vals := make([]string, len(colNames)) + for i, c := range colNames { + vals[i] = inlineValue(row[c]) + } + valueRows = append(valueRows, "("+strings.Join(vals, ",")+")") + } + return "INSERT INTO " + r.Quote(table) + " (" + strings.Join(cols, ",") + ") VALUES " + strings.Join(valueRows, ",") +} + +// inlineValue 将值转换为内联字面量 +// +// nil 转 NULL,字符串与字节切片转义单引号, +// 数值与布尔原样,时间格式化输出 +func inlineValue(v any) string { + if v == nil { + return "NULL" + } + switch t := v.(type) { + case string: + return "'" + strings.ReplaceAll(t, "'", "''") + "'" + case []byte: + if len(t) == 0 { + return "NULL" + } + return "'" + strings.ReplaceAll(string(t), "'", "''") + "'" + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: + return fmt.Sprintf("%v", t) + case bool: + if t { + return "1" + } + return "0" + case time.Time: + return "'" + t.Format("2006-01-02 15:04:05") + "'" + default: + return "'" + strings.ReplaceAll(fmt.Sprintf("%v", t), "'", "''") + "'" + } +} + +// quoteAll 源端标识符引用 +// +// 源方言与目标方言可能不同,按源方言的引号规则处理 +func quoteAll(db gdb.DB, cols []string) []string { + out := make([]string, len(cols)) + for i, c := range cols { + switch db.GetConfig().Type { + case "mysql": + out[i] = "`" + c + "`" + case "mssql": + out[i] = "[" + c + "]" + default: + out[i] = `"` + c + `"` + } + } + return out +} + +// convertValue 值转换 +// +// 字节切片文本转为字符串,便于目标驱动绑定,其余原样返回 +func convertValue(v gdb.Value) any { + val := v.Val() + if b, ok := val.([]byte); ok { + return string(b) + } + return val +} + +// errNoPK 无主键表错误 +func errNoPK(table string) error { + return &migrateError{"表 " + table + " 无主键,无法分页搬运(请先补主键)"} +} + +// migrateError 迁移错误类型 +type migrateError struct{ msg string } + +// Error 返回错误信息 +func (e *migrateError) Error() string { return e.msg } diff --git a/internal/migrate/ddl.go b/internal/migrate/ddl.go new file mode 100644 index 0000000..45af801 --- /dev/null +++ b/internal/migrate/ddl.go @@ -0,0 +1,846 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package migrate + +import ( + "context" + "fmt" + "strings" + + "github.com/gogf/gf/v2/database/gdb" +) + +// ============================== 目标方言 DDL 渲染 ============================== + +// Renderer 目标方言 DDL 渲染器 +// 负责标识符引用、列定义、建表、建索引、清表与自增序列修正 +type Renderer interface { + Quote(name string) string + Column(col ColumnMeta) string + CreateTable(t *TableMeta) string + CreateIndex(t *TableMeta, idx IndexMeta) string + DropAllTables(ctx context.Context, db gdb.DB) error + FixSequence(ctx context.Context, db gdb.DB, t *TableMeta) error +} + +// NewRenderer 按目标方言创建渲染器 +func NewRenderer(dialect string) (Renderer, error) { + switch dialect { + case "mysql": + return &mysqlRenderer{dialect: "mysql"}, nil + case "pgsql": + return &pgsqlRenderer{dialect: "pgsql"}, nil + case "sqlite": + return &sqliteRenderer{dialect: "sqlite"}, nil + case "mssql": + return &mssqlRenderer{dialect: "mssql"}, nil + case "oracle": + return &oracleRenderer{dialect: "oracle"}, nil + } + return nil, fmt.Errorf("不支持的目标数据库类型: %s", dialect) +} + +// quoteStd 双引号引用 +// 适用于 PostgreSQL、Oracle、SQLite +func quoteStd(name string) string { + return `"` + strings.ReplaceAll(name, `"`, `""`) + `"` +} + +// quoteTick 反引号引用 +// 适用于 MySQL +func quoteTick(name string) string { + return "`" + strings.ReplaceAll(name, "`", "``") + "`" +} + +// quoteBracket 方括号引用 +// 适用于 SQL Server +func quoteBracket(name string) string { + return "[" + strings.ReplaceAll(name, "]", "]]") + "]" +} + +// defaultLiteral 渲染默认值字面量 +// +// 数值原样输出,其余加引号; +// 时间函数按目标方言归一转换 +func defaultLiteral(dialect, v string) string { + if v == "" { + return "" + } + u := strings.ToUpper(v) + // 时间函数按目标方言归一(NOW/CURRENT_TIMESTAMP → 各库原生) + if u == "CURRENT_TIMESTAMP" || u == "NOW()" || u == "NOW" { + switch dialect { + case "mssql": + return "GETDATE()" + case "oracle": + return "SYSDATE" + default: + return "CURRENT_TIMESTAMP" + } + } + if u == "NULL" { + return "NULL" + } + // 纯数值/负数原样 + if _, err := fmt.Sscanf(v, "%f", new(float64)); err == nil { + return v + } + return "'" + strings.ReplaceAll(v, "'", "''") + "'" +} + +// hasAutoIncCol 判断表中是否存在自增列 +func hasAutoIncCol(t *TableMeta) bool { + for _, c := range t.Columns { + if c.AutoInc { + return true + } + } + return false +} + +// pkColumns 返回表的主键列清单 +func pkColumns(t *TableMeta) []string { + var cols []string + for _, c := range t.Columns { + if c.IsPK { + cols = append(cols, c.Name) + } + } + return cols +} + +// ============================================================ +// MySQL +// ============================================================ + +// mysqlRenderer MySQL 方言渲染器 +type mysqlRenderer struct{ dialect string } + +// Quote 引用标识符 +func (r *mysqlRenderer) Quote(name string) string { return quoteTick(name) } + +// Column 渲染列定义 +func (r *mysqlRenderer) Column(col ColumnMeta) string { + var b strings.Builder + b.WriteString(quoteTick(col.Name) + " " + r.colType(col)) + if col.AutoInc { + b.WriteString(" NOT NULL AUTO_INCREMENT") + } else { + if !col.Nullable { + b.WriteString(" NOT NULL") + } + if d := defaultLiteral(r.dialect, col.Default); d != "" { + b.WriteString(" DEFAULT " + d) + } + } + if col.Comment != "" { + b.WriteString(" COMMENT '" + strings.ReplaceAll(col.Comment, "'", "''") + "'") + } + return b.String() +} + +// colType 列类型映射 +func (r *mysqlRenderer) colType(col ColumnMeta) string { + switch col.Type { + case TypeBool: + return "tinyint(1)" + case TypeTinyint: + return "tinyint" + case TypeSmallint: + return "smallint" + case TypeInt: + return "int" + case TypeBigint: + return "bigint" + case TypeFloat: + return "float" + case TypeDouble: + return "double" + case TypeDecimal: + if col.Length > 0 { + return fmt.Sprintf("decimal(%d,%d)", col.Length, col.Scale) + } + return "decimal" + case TypeVarchar: + if col.Length > 0 { + return fmt.Sprintf("varchar(%d)", col.Length) + } + return "varchar(255)" + case TypeChar: + if col.Length > 0 { + return fmt.Sprintf("char(%d)", col.Length) + } + return "char(36)" + case TypeTinytext: + return "tinytext" + case TypeMediumText: + return "mediumtext" + case TypeLongText: + return "longtext" + case TypeText: + return "text" + case TypeDate: + return "date" + case TypeTime: + return "time" + case TypeDateTime: + return "datetime" + case TypeBlob: + return "longblob" + case TypeJson: + return "json" + case TypeEnum: + return "enum('" + strings.Join(col.EnumVals, "','") + "')" + case TypeSet: + return "set('" + strings.Join(col.EnumVals, "','") + "')" + } + return "text" +} + +// CreateTable 渲染建表语句 +func (r *mysqlRenderer) CreateTable(t *TableMeta) string { + var lines []string + for _, c := range t.Columns { + lines = append(lines, " "+r.Column(c)) + } + if pk := pkColumns(t); len(pk) > 0 { + quoted := make([]string, len(pk)) + for i, c := range pk { + quoted[i] = quoteTick(c) + } + lines = append(lines, " PRIMARY KEY ("+strings.Join(quoted, ",")+")") + } + sql := "CREATE TABLE IF NOT EXISTS " + quoteTick(t.Name) + " (\n" + strings.Join(lines, ",\n") + "\n)" + if t.Comment != "" { + sql += " COMMENT='" + strings.ReplaceAll(t.Comment, "'", "''") + "'" + } + return sql + ";" +} + +// CreateIndex 渲染索引语句 +func (r *mysqlRenderer) CreateIndex(t *TableMeta, idx IndexMeta) string { + unique := "" + if idx.Unique { + unique = "UNIQUE " + } + cols := make([]string, len(idx.Columns)) + for i, c := range idx.Columns { + cols[i] = quoteTick(c) + } + return fmt.Sprintf("CREATE %sINDEX %s ON %s (%s);", + unique, quoteTick(idx.Name), quoteTick(t.Name), strings.Join(cols, ",")) +} + +// DropAllTables 清空全部表 +func (r *mysqlRenderer) DropAllTables(ctx context.Context, db gdb.DB) error { + if _, err := db.Exec(ctx, "SET FOREIGN_KEY_CHECKS=0"); err != nil { + return err + } + defer db.Exec(ctx, "SET FOREIGN_KEY_CHECKS=1") + tables, err := db.Tables(ctx) + if err != nil { + return err + } + for _, t := range tables { + if _, err = db.Exec(ctx, "DROP TABLE IF EXISTS "+quoteTick(t)); err != nil { + return err + } + } + return nil +} + +// FixSequence 修正自增序列 +// MySQL 自增自动续接,无需处理 +func (r *mysqlRenderer) FixSequence(ctx context.Context, db gdb.DB, t *TableMeta) error { return nil } + +// ============================================================ +// PostgreSQL +// ============================================================ + +// pgsqlRenderer PostgreSQL 方言渲染器 +type pgsqlRenderer struct{ dialect string } + +// Quote 引用标识符 +func (r *pgsqlRenderer) Quote(name string) string { return quoteStd(name) } + +// Column 渲染列定义 +func (r *pgsqlRenderer) Column(col ColumnMeta) string { + var b strings.Builder + b.WriteString(quoteStd(col.Name) + " " + r.colType(col)) + if col.AutoInc { + b.WriteString(" GENERATED BY DEFAULT AS IDENTITY") + } + if !col.Nullable && !col.AutoInc { + b.WriteString(" NOT NULL") + } + if !col.AutoInc { + if d := defaultLiteral(r.dialect, col.Default); d != "" { + b.WriteString(" DEFAULT " + d) + } + } + return b.String() +} + +// colType 列类型映射 +func (r *pgsqlRenderer) colType(col ColumnMeta) string { + switch col.Type { + case TypeBool: + return "BOOLEAN" + case TypeTinyint, TypeSmallint: + return "SMALLINT" + case TypeInt: + return "INTEGER" + case TypeBigint: + return "BIGINT" + case TypeFloat: + return "REAL" + case TypeDouble: + return "DOUBLE PRECISION" + case TypeDecimal: + if col.Length > 0 { + return fmt.Sprintf("NUMERIC(%d,%d)", col.Length, col.Scale) + } + return "NUMERIC" + case TypeVarchar: + if col.Length > 0 { + return fmt.Sprintf("VARCHAR(%d)", col.Length) + } + return "VARCHAR" + case TypeChar: + if col.Length > 0 { + return fmt.Sprintf("CHAR(%d)", col.Length) + } + return "CHAR(36)" + case TypeText, TypeTinytext, TypeMediumText, TypeLongText: + return "TEXT" + case TypeDate: + return "DATE" + case TypeTime: + return "TIME" + case TypeDateTime: + return "TIMESTAMP" + case TypeBlob: + return "BYTEA" + case TypeJson: + return "JSONB" + case TypeEnum, TypeSet: + return "VARCHAR(255)" // pgsql 无内建 enum/set,降级为 varchar(值域由应用层保证) + } + return "TEXT" +} + +// CreateTable 渲染建表语句 +func (r *pgsqlRenderer) CreateTable(t *TableMeta) string { + var lines []string + for _, c := range t.Columns { + lines = append(lines, " "+r.Column(c)) + } + if pk := pkColumns(t); len(pk) > 0 { + quoted := make([]string, len(pk)) + for i, c := range pk { + quoted[i] = quoteStd(c) + } + lines = append(lines, " PRIMARY KEY ("+strings.Join(quoted, ",")+")") + } + sql := "CREATE TABLE IF NOT EXISTS " + quoteStd(t.Name) + " (\n" + strings.Join(lines, ",\n") + "\n);" + if t.Comment != "" { + sql += "\nCOMMENT ON TABLE " + quoteStd(t.Name) + " IS '" + strings.ReplaceAll(t.Comment, "'", "''") + "';" + } + // 列注释随建表语句一并输出 + for _, c := range t.Columns { + if c.Comment != "" { + sql += "\nCOMMENT ON COLUMN " + quoteStd(t.Name) + "." + quoteStd(c.Name) + + " IS '" + strings.ReplaceAll(c.Comment, "'", "''") + "';" + } + } + return sql +} + +// CreateIndex 渲染索引语句 +func (r *pgsqlRenderer) CreateIndex(t *TableMeta, idx IndexMeta) string { + unique := "" + if idx.Unique { + unique = "UNIQUE " + } + cols := make([]string, len(idx.Columns)) + for i, c := range idx.Columns { + cols[i] = quoteStd(c) + } + return fmt.Sprintf("CREATE %sINDEX IF NOT EXISTS %s ON %s (%s);", + unique, quoteStd(idx.Name), quoteStd(t.Name), strings.Join(cols, ",")) +} + +// DropAllTables 清空全部表 +func (r *pgsqlRenderer) DropAllTables(ctx context.Context, db gdb.DB) error { + rows, err := db.Ctx(ctx).Raw( + "SELECT tablename FROM pg_tables WHERE schemaname = current_schema()").All() + if err != nil { + return err + } + for _, row := range rows { + if _, err = db.Exec(ctx, "DROP TABLE IF EXISTS "+quoteStd(row["tablename"].String())+" CASCADE"); err != nil { + return err + } + } + return nil +} + +// FixSequence 修正自增序列 +// +// 将序列设置到当前 max(id),下次插入取 max+1; +// 空表不处理,IDENTITY 从 1 开始 +func (r *pgsqlRenderer) FixSequence(ctx context.Context, db gdb.DB, t *TableMeta) error { + for _, c := range t.Columns { + if !c.AutoInc { + continue + } + // setval 到当前 max(id),下次插入取 max+1;空表不动(IDENTITY 从1开始) + _, err := db.Exec(ctx, fmt.Sprintf( + `SELECT setval(pg_get_serial_sequence('%s', '%s'), COALESCE((SELECT MAX(%s) FROM %s), 0) + 1, false)`, + t.Name, c.Name, quoteStd(c.Name), quoteStd(t.Name))) + if err != nil { + return err + } + } + return nil +} + +// ============================================================ +// SQLite +// ============================================================ + +// sqliteRenderer SQLite 方言渲染器 +type sqliteRenderer struct{ dialect string } + +// Quote 引用标识符 +func (r *sqliteRenderer) Quote(name string) string { return quoteStd(name) } + +// Column 渲染列定义 +func (r *sqliteRenderer) Column(col ColumnMeta) string { + var b strings.Builder + b.WriteString(quoteStd(col.Name) + " " + r.colType(col)) + if col.IsPK && col.AutoInc { + b.WriteString(" PRIMARY KEY AUTOINCREMENT") + } else { + if col.IsPK { + b.WriteString(" PRIMARY KEY") + } + if !col.Nullable { + b.WriteString(" NOT NULL") + } + if d := defaultLiteral(r.dialect, col.Default); d != "" { + b.WriteString(" DEFAULT " + d) + } + } + return b.String() +} + +// colType 列类型映射 +func (r *sqliteRenderer) colType(col ColumnMeta) string { + switch col.Type { + case TypeBool, TypeTinyint, TypeSmallint, TypeInt, TypeBigint, TypeDate, TypeTime, TypeDateTime: + return "INTEGER" + case TypeFloat, TypeDouble, TypeDecimal: + return "REAL" + case TypeBlob: + return "BLOB" + default: + return "TEXT" + } +} + +// CreateTable 渲染建表语句 +func (r *sqliteRenderer) CreateTable(t *TableMeta) string { + var lines []string + for _, c := range t.Columns { + lines = append(lines, " "+r.Column(c)) + } + // 复合主键走表级约束(列级 PRIMARY KEY 仅单列生效) + if pk := pkColumns(t); len(pk) > 1 || !hasAutoIncCol(t) { + if pk := pkColumns(t); len(pk) > 0 { + quoted := make([]string, len(pk)) + for i, c := range pk { + quoted[i] = quoteStd(c) + } + lines = append(lines, " PRIMARY KEY ("+strings.Join(quoted, ",")+")") + } + } + // sqlite 无表/列注释 + return "CREATE TABLE IF NOT EXISTS " + quoteStd(t.Name) + " (\n" + strings.Join(lines, ",\n") + "\n);" +} + +// CreateIndex 渲染索引语句 +func (r *sqliteRenderer) CreateIndex(t *TableMeta, idx IndexMeta) string { + unique := "" + if idx.Unique { + unique = "UNIQUE " + } + cols := make([]string, len(idx.Columns)) + for i, c := range idx.Columns { + cols[i] = quoteStd(c) + } + return fmt.Sprintf("CREATE %sINDEX IF NOT EXISTS %s ON %s (%s);", + unique, quoteStd(idx.Name), quoteStd(t.Name), strings.Join(cols, ",")) +} + +// DropAllTables 清空全部表 +func (r *sqliteRenderer) DropAllTables(ctx context.Context, db gdb.DB) error { + rows, err := db.Ctx(ctx).Raw( + "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").All() + if err != nil { + return err + } + if _, err = db.Exec(ctx, "PRAGMA foreign_keys = OFF"); err != nil { + return err + } + defer db.Exec(ctx, "PRAGMA foreign_keys = ON") + for _, row := range rows { + if _, err = db.Exec(ctx, "DROP TABLE IF EXISTS "+quoteStd(row["name"].String())); err != nil { + return err + } + } + return nil +} + +// FixSequence 修正自增序列 +// AUTOINCREMENT 自动续接,无需处理 +func (r *sqliteRenderer) FixSequence(ctx context.Context, db gdb.DB, t *TableMeta) error { return nil } + +// ============================================================ +// SQL Server(首期实测链路为 mysql→pgsql,mssql/oracle 按定稿方言约定实现,待人工校对) +// ============================================================ + +// mssqlRenderer SQL Server 方言渲染器 +type mssqlRenderer struct{ dialect string } + +// Quote 引用标识符 +func (r *mssqlRenderer) Quote(name string) string { return quoteBracket(name) } + +// Column 渲染列定义 +func (r *mssqlRenderer) Column(col ColumnMeta) string { + var b strings.Builder + b.WriteString(quoteBracket(col.Name) + " " + r.colType(col)) + if col.AutoInc { + b.WriteString(" IDENTITY(1,1)") + } + if !col.Nullable { + b.WriteString(" NOT NULL") + } + if !col.AutoInc { + if d := defaultLiteral(r.dialect, col.Default); d != "" { + b.WriteString(" DEFAULT " + d) + } + } + return b.String() +} + +// colType 列类型映射 +func (r *mssqlRenderer) colType(col ColumnMeta) string { + switch col.Type { + case TypeBool: + return "BIT" + case TypeTinyint: + return "TINYINT" + case TypeSmallint: + return "SMALLINT" + case TypeInt: + return "INT" + case TypeBigint: + return "BIGINT" + case TypeFloat: + return "FLOAT" + case TypeDouble: + return "FLOAT" + case TypeDecimal: + if col.Length > 0 { + return fmt.Sprintf("DECIMAL(%d,%d)", col.Length, col.Scale) + } + return "DECIMAL" + case TypeVarchar: + if col.Length > 0 { + return fmt.Sprintf("NVARCHAR(%d)", col.Length) + } + return "NVARCHAR(255)" + case TypeChar: + if col.Length > 0 { + return fmt.Sprintf("NCHAR(%d)", col.Length) + } + return "NCHAR(36)" + case TypeText, TypeTinytext, TypeMediumText, TypeLongText, TypeJson, TypeEnum, TypeSet: + return "NVARCHAR(MAX)" + case TypeDate: + return "DATE" + case TypeTime: + return "TIME" + case TypeDateTime: + return "DATETIME2" + case TypeBlob: + return "VARBINARY(MAX)" + } + return "NVARCHAR(MAX)" +} + +// CreateTable 渲染建表语句 +func (r *mssqlRenderer) CreateTable(t *TableMeta) string { + var lines []string + for _, c := range t.Columns { + lines = append(lines, " "+r.Column(c)) + } + if pk := pkColumns(t); len(pk) > 0 { + quoted := make([]string, len(pk)) + for i, c := range pk { + quoted[i] = quoteBracket(c) + } + lines = append(lines, " PRIMARY KEY ("+strings.Join(quoted, ",")+")") + } + sql := "IF OBJECT_ID(N'" + t.Name + "', N'U') IS NULL CREATE TABLE " + quoteBracket(t.Name) + + " (\n" + strings.Join(lines, ",\n") + "\n);" + // 表注释(扩展属性 MS_Description) + if t.Comment != "" { + sql += "\nEXEC sp_addextendedproperty N'MS_Description', N'" + + strings.ReplaceAll(t.Comment, "'", "''") + + "', N'SCHEMA', N'dbo', N'TABLE', N'" + t.Name + "';" + } + // 列注释(扩展属性 MS_Description) + for _, c := range t.Columns { + if c.Comment != "" { + sql += "\nEXEC sp_addextendedproperty N'MS_Description', N'" + + strings.ReplaceAll(c.Comment, "'", "''") + + "', N'SCHEMA', N'dbo', N'TABLE', N'" + t.Name + "', N'COLUMN', N'" + c.Name + "';" + } + } + return sql +} + +// CreateIndex 渲染索引语句 +func (r *mssqlRenderer) CreateIndex(t *TableMeta, idx IndexMeta) string { + unique := "" + if idx.Unique { + unique = "UNIQUE " + } + cols := make([]string, len(idx.Columns)) + for i, c := range idx.Columns { + cols[i] = quoteBracket(c) + } + // IF NOT EXISTS 保护:重复建索引跳过 + return fmt.Sprintf("IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = N'%s' AND object_id = OBJECT_ID(N'%s')) "+ + "CREATE %sINDEX %s ON %s (%s);", + idx.Name, t.Name, unique, quoteBracket(idx.Name), quoteBracket(t.Name), strings.Join(cols, ",")) +} + +// DropAllTables 清空全部表 +func (r *mssqlRenderer) DropAllTables(ctx context.Context, db gdb.DB) error { + // 先禁用全部外键约束,再逐表 DROP + fks, err := db.Ctx(ctx).Raw( + `SELECT OBJECT_NAME(parent_object_id) AS tbl, name AS fk FROM sys.foreign_keys`).All() + if err != nil { + return err + } + for _, fk := range fks { + if _, err = db.Exec(ctx, fmt.Sprintf("ALTER TABLE %s DROP CONSTRAINT %s", + quoteBracket(fk["tbl"].String()), quoteBracket(fk["fk"].String()))); err != nil { + return err + } + } + tables, err := db.Tables(ctx) + if err != nil { + return err + } + for _, t := range tables { + if _, err = db.Exec(ctx, "DROP TABLE "+quoteBracket(t)); err != nil { + return err + } + } + return nil +} + +// FixSequence 修正自增序列 +func (r *mssqlRenderer) FixSequence(ctx context.Context, db gdb.DB, t *TableMeta) error { + for _, c := range t.Columns { + if !c.AutoInc { + continue + } + if _, err := db.Exec(ctx, fmt.Sprintf( + "DBCC CHECKIDENT ('%s', RESEED)", t.Name)); err != nil { + return err + } + } + return nil +} + +// ============================================================ +// Oracle +// ============================================================ + +// oracleRenderer Oracle 方言渲染器 +type oracleRenderer struct{ dialect string } + +// Quote 引用标识符 +func (r *oracleRenderer) Quote(name string) string { return quoteStd(name) } + +// Column 渲染列定义 +func (r *oracleRenderer) Column(col ColumnMeta) string { + var b strings.Builder + b.WriteString(quoteStd(col.Name) + " " + r.colType(col)) + if col.AutoInc { + b.WriteString(" GENERATED BY DEFAULT AS IDENTITY") + } + if !col.Nullable && !col.AutoInc { + b.WriteString(" NOT NULL") + } + if !col.AutoInc { + if d := defaultLiteral(r.dialect, col.Default); d != "" { + b.WriteString(" DEFAULT " + d) + } + } + return b.String() +} + +// colType 列类型映射 +func (r *oracleRenderer) colType(col ColumnMeta) string { + switch col.Type { + case TypeBool, TypeTinyint, TypeSmallint: + return "NUMBER(3)" + case TypeInt: + return "NUMBER(10)" + case TypeBigint: + return "NUMBER(19)" + case TypeFloat: + return "BINARY_FLOAT" + case TypeDouble: + return "BINARY_DOUBLE" + case TypeDecimal: + if col.Length > 0 { + return fmt.Sprintf("NUMBER(%d,%d)", col.Length, col.Scale) + } + return "NUMBER" + case TypeVarchar: + if col.Length > 0 { + return fmt.Sprintf("NVARCHAR2(%d)", col.Length) + } + return "NVARCHAR2(255)" + case TypeChar: + if col.Length > 0 { + return fmt.Sprintf("NCHAR(%d)", col.Length) + } + return "NCHAR(36)" + case TypeText, TypeTinytext, TypeMediumText, TypeLongText, TypeJson, TypeEnum, TypeSet: + return "NCLOB" + case TypeDate: + return "DATE" + case TypeTime, TypeDateTime: + return "TIMESTAMP" + case TypeBlob: + return "BLOB" + } + return "NCLOB" +} + +// CreateTable 渲染建表语句 +func (r *oracleRenderer) CreateTable(t *TableMeta) string { + var lines []string + for _, c := range t.Columns { + lines = append(lines, " "+r.Column(c)) + } + if pk := pkColumns(t); len(pk) > 0 { + quoted := make([]string, len(pk)) + for i, c := range pk { + quoted[i] = quoteStd(c) + } + lines = append(lines, " PRIMARY KEY ("+strings.Join(quoted, ",")+")") + } + sql := "CREATE TABLE " + quoteStd(t.Name) + " (\n" + strings.Join(lines, ",\n") + "\n)" + if t.Comment != "" { + sql += "\nCOMMENT ON TABLE " + quoteStd(t.Name) + " IS '" + strings.ReplaceAll(t.Comment, "'", "''") + "'" + } + for _, c := range t.Columns { + if c.Comment != "" { + sql += "\nCOMMENT ON COLUMN " + quoteStd(t.Name) + "." + quoteStd(c.Name) + + " IS '" + strings.ReplaceAll(c.Comment, "'", "''") + "'" + } + } + return sql + ";" +} + +// CreateIndex 渲染索引语句 +// +// Oracle 无 IF NOT EXISTS,由编排层预检索引存在性后决定是否执行 +func (r *oracleRenderer) CreateIndex(t *TableMeta, idx IndexMeta) string { + unique := "" + if idx.Unique { + unique = "UNIQUE " + } + cols := make([]string, len(idx.Columns)) + for i, c := range idx.Columns { + cols[i] = quoteStd(c) + } + // oracle 无 IF NOT EXISTS,由编排层预检索引存在性后决定是否执行 + return fmt.Sprintf("CREATE %sINDEX %s ON %s (%s);", + unique, quoteStd(idx.Name), quoteStd(t.Name), strings.Join(cols, ",")) +} + +// DropAllTables 清空全部表 +func (r *oracleRenderer) DropAllTables(ctx context.Context, db gdb.DB) error { + rows, err := db.Ctx(ctx).Raw("SELECT table_name FROM user_tables").All() + if err != nil { + return err + } + for _, row := range rows { + if _, err = db.Exec(ctx, "DROP TABLE "+quoteStd(row["table_name"].String())+" CASCADE CONSTRAINTS PURGE"); err != nil { + return err + } + } + return nil +} + +// FixSequence 修正自增序列 +// +// Oracle 的 IDENTITY 由缓存失效自动续接,无需显式修序 +func (r *oracleRenderer) FixSequence(ctx context.Context, db gdb.DB, t *TableMeta) error { + for _, c := range t.Columns { + if !c.AutoInc { + continue + } + // oracle identity 由缓存失效自动续接,无需显式修序 + } + return nil +} diff --git a/internal/migrate/introspect.go b/internal/migrate/introspect.go new file mode 100644 index 0000000..7acc258 --- /dev/null +++ b/internal/migrate/introspect.go @@ -0,0 +1,285 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package migrate + +import ( + "context" + "sort" + "strings" + + "github.com/gogf/gf/v2/database/gdb" +) + +// ============================== 源库结构内省 ============================== + +// listTables 查询源库表清单 +// +// 受白名单过滤,按表名排序以保证结果可复现 +func listTables(ctx context.Context, db gdb.DB, whitelist []string) ([]string, error) { + tables, err := db.Tables(ctx) + if err != nil { + return nil, err + } + if len(whitelist) > 0 { + allow := make(map[string]bool, len(whitelist)) + for _, t := range whitelist { + allow[strings.TrimSpace(t)] = true + } + filtered := make([]string, 0, len(tables)) + for _, t := range tables { + if allow[t] { + filtered = append(filtered, t) + } + } + tables = filtered + } + sort.Strings(tables) + return tables, nil +} + +// introspectTable 单表全量内省 +// +// 采集列信息(含注释、默认值、自增、主键)、二级索引与表注释 +func introspectTable(ctx context.Context, db gdb.DB, dialect, table string) (*TableMeta, error) { + fields, err := db.TableFields(ctx, table) + if err != nil { + return nil, err + } + meta := &TableMeta{Name: table} + // TableFields 返回 map,按 Index 排序还原物理列序 + sorted := make([]*gdb.TableField, 0, len(fields)) + for _, f := range fields { + sorted = append(sorted, f) + } + sort.Slice(sorted, func(i, j int) bool { return sorted[i].Index < sorted[j].Index }) + for _, f := range sorted { + typ, length, scale, enumVals := normalizeType(dialect, f.Type) + meta.Columns = append(meta.Columns, ColumnMeta{ + Name: f.Name, + Type: typ, + Length: length, + Scale: scale, + EnumVals: enumVals, + Nullable: f.Null, + Default: cleanDefault(dialect, f.Default), + AutoInc: isAutoInc(f.Extra), + Comment: f.Comment, + IsPK: strings.EqualFold(f.Key, "PRI") || strings.EqualFold(f.Key, "pri"), + }) + } + // 索引与表注释(方言相关) + switch dialect { + case "mysql": + if err = introspectIndexesMySQL(ctx, db, table, meta); err != nil { + return nil, err + } + meta.Comment = tableCommentMySQL(ctx, db, table) + case "pgsql": + if err = introspectIndexesPgsql(ctx, db, table, meta); err != nil { + return nil, err + } + meta.Comment = tableCommentPgsql(ctx, db, table) + default: + // 其余方言:主键经列元数据已识别,二级索引降级跳过(首期实测链路 mysql→pgsql) + } + // 统一索引命名(迁移后全库一种规则,见 normalizeIndexName) + normalizeIndexes(meta) + return meta, nil +} + +// normalizeIndexes 归一化二级索引名 +// +// 主键随建表落地,不包含在索引集合中 +func normalizeIndexes(meta *TableMeta) { + for i := range meta.Indexes { + idx := &meta.Indexes[i] + idx.Name = normalizeIndexName(meta.Name, idx.Unique, idx.Columns) + } +} + +// normalizeIndexName 统一索引命名 +// +// 唯一索引使用 uk_ 前缀,普通索引使用 idx_ 前缀,后接表名与列名; +// 名称超过 63 字符时截断,兼容多数数据库的索引名长度上限 +func normalizeIndexName(table string, unique bool, cols []string) string { + prefix := "idx" + if unique { + prefix = "uk" + } + name := prefix + "_" + table + "_" + strings.Join(cols, "_") + if len(name) > 63 { + name = name[:63] + } + return name +} + +// isAutoInc 判断是否自增列 +// +// 兼容 MySQL 的 auto_increment 以及 PostgreSQL、SQL Server 的 identity +func isAutoInc(extra string) bool { + e := strings.ToLower(extra) + return strings.Contains(e, "auto_increment") || strings.Contains(e, "identity") +} + +// introspectIndexesMySQL 采集 MySQL 索引 +// +// 主键写入列元数据,其余二级索引写入索引集合 +func introspectIndexesMySQL(ctx context.Context, db gdb.DB, table string, meta *TableMeta) error { + rows, err := db.Ctx(ctx).Raw("SHOW INDEX FROM `" + table + "`").All() + if err != nil { + return err + } + type idxHolder struct { + unique bool + cols []string + } + holders := map[string]*idxHolder{} + order := make([]string, 0) + for _, r := range rows { + name := r["Key_name"].String() + h, ok := holders[name] + if !ok { + h = &idxHolder{unique: r["Non_unique"].Int() == 0} + holders[name] = h + order = append(order, name) + } + h.cols = append(h.cols, r["Column_name"].String()) + } + for _, name := range order { + h := holders[name] + if name == "PRIMARY" { + for i := range meta.Columns { + for _, c := range h.cols { + if meta.Columns[i].Name == c { + meta.Columns[i].IsPK = true + } + } + } + continue + } + meta.Indexes = append(meta.Indexes, IndexMeta{Unique: h.unique, Name: name, Columns: h.cols}) + } + return nil +} + +// tableCommentMySQL 查询 MySQL 表注释 +func tableCommentMySQL(ctx context.Context, db gdb.DB, table string) string { + row, err := db.Ctx(ctx).Raw( + "SELECT TABLE_COMMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?", table, + ).One() + if err != nil || len(row) == 0 { + return "" + } + return row["TABLE_COMMENT"].String() +} + +// introspectIndexesPgsql 采集 PostgreSQL 索引 +// +// 主键写入列元数据,其余二级索引写入索引集合 +func introspectIndexesPgsql(ctx context.Context, db gdb.DB, table string, meta *TableMeta) error { + rows, err := db.Ctx(ctx).Raw( + "SELECT indexname, indexdef FROM pg_indexes WHERE schemaname = current_schema() AND tablename = ?", table, + ).All() + if err != nil { + return err + } + for _, r := range rows { + name := r["indexname"].String() + def := r["indexdef"].String() + cols := parsePgIndexCols(def) + if len(cols) == 0 { + continue + } + if name == table+"_pkey" { + for i := range meta.Columns { + for _, c := range cols { + if meta.Columns[i].Name == c { + meta.Columns[i].IsPK = true + } + } + } + continue + } + meta.Indexes = append(meta.Indexes, IndexMeta{ + Unique: strings.Contains(strings.ToUpper(def), "UNIQUE"), + Name: name, + Columns: cols, + }) + } + return nil +} + +// parsePgIndexCols 从建索引语句中提取列清单 +// +// 取括号内内容按逗号切分,跳过表达式部分 +func parsePgIndexCols(def string) []string { + start := strings.Index(def, "(") + end := strings.LastIndex(def, ")") + if start < 0 || end <= start { + return nil + } + parts := strings.Split(def[start+1:end], ",") + cols := make([]string, 0, len(parts)) + for _, p := range parts { + p = strings.TrimSpace(p) + // 表达式索引(如 lower(name))取首标识符,跳过复杂表达式 + if i := strings.IndexAny(p, " ("); i > 0 { + p = p[:i] + } + p = strings.Trim(p, `"`) + if p != "" { + cols = append(cols, p) + } + } + return cols +} + +// tableCommentPgsql 查询 PostgreSQL 表注释 +func tableCommentPgsql(ctx context.Context, db gdb.DB, table string) string { + row, err := db.Ctx(ctx).Raw( + "SELECT obj_description(c.oid) AS comment FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = current_schema() AND c.relname = ?", table, + ).One() + if err != nil || len(row) == 0 { + return "" + } + return row["comment"].String() +} diff --git a/internal/migrate/migrate.go b/internal/migrate/migrate.go new file mode 100644 index 0000000..e900146 --- /dev/null +++ b/internal/migrate/migrate.go @@ -0,0 +1,239 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package migrate + +import ( + "context" + "fmt" + "time" + + "github.com/gogf/gf/v2/database/gdb" +) + +// ============================== 整库迁移主流程 ============================== + +// banner 迁移起止分隔线 +const banner = "==============================================================" + +// Run 整库迁移主流程 +// +// 共五个阶段:建目标库 → 建表结构 → 逐表迁移数据 → 重置自增序列 → 逐表行数校验。 +// progress 为进度输出回调,CLI 传入格式化输出函数,为 nil 时静默。 +func Run(ctx context.Context, opt Options, progress func(format string, args ...any)) (*Report, error) { + report := &Report{} + logf := func(format string, args ...any) { + if progress != nil { + progress(format, args...) + } + } + start := time.Now() + + src, err := openConn(ctx, "migrate_src", opt.Src) + if err != nil { + return nil, err + } + tables, err := listTables(ctx, src, opt.Tables) + if err != nil { + return nil, fmt.Errorf("读取源库表清单失败: %v", err) + } + if len(tables) == 0 { + return nil, fmt.Errorf("源库无表(或白名单未命中任何表)") + } + report.TableTotal = len(tables) + + renderer, err := NewRenderer(opt.Dst.Type) + if err != nil { + return nil, err + } + + // 抬头 + logf("%s\n", banner) + logf("[迁移开始] 源: %s:%s → 目标: %s:%s\n", opt.Src.Type, opt.Src.Database, opt.Dst.Type, opt.Dst.Database) + logf("[迁移开始] 待迁移 %d 张表\n", len(tables)) + logf("%s\n", banner) + + // ── 1/5 建目标库 ── + phase := time.Now() + logf("[·] [1/5] 建目标库...\n") + if err = ensureDatabase(ctx, opt.Dst); err != nil { + return nil, err + } + logf(" · 目标库 \"%s\" 已就绪\n", opt.Dst.Database) + logf("[✓] [1/5] 建目标库 完成(耗时 %s)\n", dur(time.Since(phase))) + + dst, err := openConn(ctx, "migrate_dst", opt.Dst) + if err != nil { + return nil, err + } + + // ── 2/5 建表结构 ── + phase = time.Now() + logf("[·] [2/5] 建表结构(%d 张表)...\n", len(tables)) + if opt.DropTargetFirst { + if err = renderer.DropAllTables(ctx, dst); err != nil { + return nil, fmt.Errorf("清空目标库失败: %v", err) + } + } + metas := make([]*TableMeta, 0, len(tables)) + totalIndexes := 0 + for _, table := range tables { + meta, e := introspectTable(ctx, src, opt.Src.Type, table) + if e != nil { + report.TableFailed++ + report.Failed = append(report.Failed, FailedTable{Table: table, Err: "内省失败: " + e.Error()}) + logf(" [✗ %s] 内省失败: %v\n", table, e) + continue + } + // 非清空模式下目标已有同名表则该表跳过(建表用 IF NOT EXISTS,数据会追加,须前置拦截) + if !opt.DropTargetFirst { + if exists, e2 := tableExists(ctx, dst, table); e2 == nil && exists { + report.TableFailed++ + report.Failed = append(report.Failed, FailedTable{Table: table, Err: "目标库已存在同名表"}) + logf(" [✗ %s] 目标库已存在同名表\n", table) + continue + } + } + if _, e = dst.Exec(ctx, renderer.CreateTable(meta)); e != nil { + report.TableFailed++ + report.Failed = append(report.Failed, FailedTable{Table: table, Err: "建表失败: " + e.Error()}) + logf(" [✗ %s] 建表失败: %v\n", table, e) + continue + } + for _, idx := range meta.Indexes { + if _, e = dst.Exec(ctx, renderer.CreateIndex(meta, idx)); e != nil { + report.TableFailed++ + report.Failed = append(report.Failed, FailedTable{Table: table, Err: "建索引 " + idx.Name + " 失败: " + e.Error()}) + logf(" [✗ %s] 建索引 %s 失败: %v\n", table, idx.Name, e) + break + } + } + totalIndexes += len(meta.Indexes) + metas = append(metas, meta) + } + logf("[✓] [2/5] 建表结构 完成(耗时 %s),%d 张表,%d 个索引,0 个外键\n", + dur(time.Since(phase)), len(metas), totalIndexes) + + // ── 3/5 逐表迁移数据 ── + phase = time.Now() + logf("[·] [3/5] 逐表迁移数据...\n") + totalRows := int64(0) + for i, meta := range metas { + logf(" · [%d/%d] 正在迁移 %s...\n", i+1, len(metas), meta.Name) + tableStart := time.Now() + rows, e := copyTable(ctx, src, dst, renderer, meta, opt.BatchSize, func(batch, total int64) { + logf(" [%s] +%d(累计 %d)\n", meta.Name, batch, total) + }) + if e != nil { + report.TableFailed++ + report.Failed = append(report.Failed, FailedTable{Table: meta.Name, Err: "数据搬运失败: " + e.Error()}) + logf(" [✗ %s] 失败: %v\n", meta.Name, e) + continue + } + report.TableOK++ + totalRows += rows + logf(" [✓ %s] 共 %d 行(耗时 %s)\n", meta.Name, rows, dur(time.Since(tableStart))) + } + report.RowTotal = totalRows + logf("[✓] [3/5] 逐表迁移数据 完成(耗时 %s),共 %d 行\n", dur(time.Since(phase)), totalRows) + + // ── 4/5 重置自增序列 ── + phase = time.Now() + logf("[·] [4/5] 重置自增序列...\n") + for _, meta := range metas { + if e := renderer.FixSequence(ctx, dst, meta); e != nil { + report.Failed = append(report.Failed, FailedTable{Table: meta.Name, Err: "序列修复失败: " + e.Error()}) + } + } + logf("[✓] [4/5] 重置自增序列 完成(耗时 %s)\n", dur(time.Since(phase))) + + // ── 5/5 逐表行数校验 ── + phase = time.Now() + logf("[·] [5/5] 逐表行数校验...\n") + allPass := true + for _, meta := range metas { + srcCount, e1 := countRows(ctx, src, meta.Name) + dstCount, e2 := countRows(ctx, dst, meta.Name) + if e1 != nil || e2 != nil || srcCount != dstCount { + allPass = false + logf(" [%s] source=%d target=%d FAIL\n", meta.Name, srcCount, dstCount) + continue + } + logf(" [%s] source=%d target=%d PASS\n", meta.Name, srcCount, dstCount) + } + if allPass { + logf("[✓] [5/5] 逐表行数校验 完成(耗时 %s),%d 张表全部 PASS\n", dur(time.Since(phase)), len(metas)) + } else { + logf("[✓] [5/5] 逐表行数校验 完成(耗时 %s),存在 FAIL\n", dur(time.Since(phase))) + } + + // 落款 + logf("%s\n", banner) + logf("[迁移完成] 成功:%d 行 / %d 张表 / 总耗时 %s\n", totalRows, report.TableOK, dur(time.Since(start))) + logf("%s\n", banner) + + if report.TableFailed > 0 { + return report, fmt.Errorf("%d 张表迁移失败", report.TableFailed) + } + if !allPass { + return report, fmt.Errorf("存在行数校验不一致的表") + } + return report, nil +} + +// countRows 统计表行数 +func countRows(ctx context.Context, db gdb.DB, table string) (int64, error) { + n, err := db.Ctx(ctx).Model(table).Count() + return int64(n), err +} + +// dur 时长格式化 +// +// 不足 1 秒输出毫秒,不足 1 分输出秒,否则输出分秒 +func dur(d time.Duration) string { + if d < time.Second { + return fmt.Sprintf("%dms", d.Milliseconds()) + } + if d < time.Minute { + return fmt.Sprintf("%ds", int(d.Seconds())) + } + return fmt.Sprintf("%dm%ds", int(d.Minutes()), int(d.Seconds())%60) +} diff --git a/internal/migrate/model.go b/internal/migrate/model.go new file mode 100644 index 0000000..0a808f5 --- /dev/null +++ b/internal/migrate/model.go @@ -0,0 +1,153 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// Package migrate 整库迁移 +// +// 处理流程:源库内省 → 中间表示 → 目标方言 DDL → 分批搬运数据 → 序列修复。 +// 独立于业务基座(不依赖 dao 与 logic),经 gdb 动态连接源库与目标库进行裸读裸写, +// 全程不触发全局钩子(租户隔离、软删除、审计盖章),保证数据原样落库。 +package migrate + +// ============================== 中间类型常量 ============================== + +// 中间类型常量 +// +// 源方言类型归一后的规范形态,各方言渲染器再翻译为目标方言 +const ( + TypeBool = "bool" + TypeTinyint = "tinyint" + TypeSmallint = "smallint" + TypeInt = "int" + TypeBigint = "bigint" + TypeFloat = "float" + TypeDouble = "double" + TypeDecimal = "decimal" + TypeVarchar = "varchar" + TypeChar = "char" + TypeText = "text" + TypeTinytext = "tinytext" + TypeMediumText = "mediumtext" + TypeLongText = "longtext" + TypeDate = "date" + TypeTime = "time" + TypeDateTime = "datetime" + TypeBlob = "blob" + TypeJson = "json" + TypeEnum = "enum" + TypeSet = "set" + TypeOther = "other" +) + +// ============================== 迁移任务参数 ============================== + +// Options 迁移任务参数 +type Options struct { + Src DSN // 源库连接 + Dst DSN // 目标库连接 + + DropTargetFirst bool // 迁移前清空目标库全部表 + Tables []string // 表白名单(空=全部) + BatchSize int // 数据搬运批大小(默认 500) +} + +// DSN 数据库连接要素 +// +// SQLite 仅使用 Database 作为文件路径,Host 与 Port 忽略 +type DSN struct { + Type string // 驱动名:mysql/pgsql/sqlite/mssql/oracle + Host string // 主机 + Port string // 端口 + Database string // 数据库名或文件路径 + Username string // 用户名 + Password string // 密码 +} + +// ============================== 元数据中间表示 ============================== + +// TableMeta 源库表元数据 +type TableMeta struct { + Name string // 表名 + Comment string // 表注释 + Columns []ColumnMeta // 列集合 + Indexes []IndexMeta // 二级索引集合 +} + +// ColumnMeta 列元数据 +// +// Type 为中间类型常量 +type ColumnMeta struct { + Name string // 列名 + Type string // 中间类型 + Length int // varchar/char 长度;decimal 精度 + Scale int // decimal 小数位 + EnumVals []string // 枚举值集合 + Nullable bool // 是否可空 + Default string // 默认值原文(空=无默认);表达式类原样保留 + AutoInc bool // 是否自增 + Comment string // 列注释 + IsPK bool // 是否主键 +} + +// IndexMeta 索引元数据 +// +// 不含主键,主键经 ColumnMeta.IsPK 表达 +type IndexMeta struct { + Unique bool // 是否唯一索引 + Name string // 索引名 + Columns []string // 索引列集合 +} + +// ============================== 迁移汇总 ============================== + +// Report 迁移汇总 +type Report struct { + TableTotal int // 待迁移表总数 + TableOK int // 成功表数 + TableFailed int // 失败表数 + RowTotal int64 // 总搬运行数 + Failed []FailedTable // 失败表记录 +} + +// FailedTable 失败表记录 +type FailedTable struct { + Table string // 表名 + Err string // 错误信息 +} diff --git a/internal/migrate/translate.go b/internal/migrate/translate.go new file mode 100644 index 0000000..df2b203 --- /dev/null +++ b/internal/migrate/translate.go @@ -0,0 +1,223 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package migrate + +import ( + "regexp" + "strconv" + "strings" +) + +// ============================== 类型归一 ============================== + +var ( + reParen = regexp.MustCompile(`\(([^)]*)\)`) // (11) / (10,2) / ('a','b') + reUnsigned = regexp.MustCompile(`(?i)\bunsigned\b`) // mysql unsigned 修饰 + rePgCast = regexp.MustCompile(`(?i)^'(.*)'::[\w\s\[\]]+$`) // pgsql 'x'::character varying + rePgFunc = regexp.MustCompile(`(?i)^(current_timestamp|now|localtime)\(\d*\)$`) +) + +// normalizeType 源方言原始列类型转换为中间类型 +// +// 同时解析长度、精度与枚举值 +func normalizeType(dialect, rawType string) (typ string, length, scale int, enumVals []string) { + t := strings.ToLower(strings.TrimSpace(rawType)) + unsigned := reUnsigned.MatchString(t) + t = reUnsigned.ReplaceAllString(t, "") + t = strings.TrimSpace(t) + + // 提取括号参数(长度/精度/枚举值) + pargs := "" + if m := reParen.FindStringSubmatch(t); m != nil { + pargs = strings.TrimSpace(m[1]) + t = strings.TrimSpace(reParen.ReplaceAllString(t, "")) + } + // 剥离 pgsql 时区/精度修饰 + t = strings.TrimSuffix(t, " without time zone") + t = strings.TrimSuffix(t, " with time zone") + + // 枚举/集合:括号内为引号列表 + if strings.HasPrefix(t, "enum") || strings.HasPrefix(t, "set") || strings.HasPrefix(t, "enum(") { + for _, v := range strings.Split(pargs, ",") { + if v = strings.Trim(strings.TrimSpace(v), "'\""); v != "" { + enumVals = append(enumVals, v) + } + } + if strings.HasPrefix(t, "set") { + return TypeSet, 0, 0, enumVals + } + return TypeEnum, 0, 0, enumVals + } + + // decimal/numeric:pargs = "10,2" + if parts := strings.Split(pargs, ","); len(parts) == 2 { + if p, e1 := strconv.Atoi(strings.TrimSpace(parts[0])); e1 == nil { + if s, e2 := strconv.Atoi(strings.TrimSpace(parts[1])); e2 == nil { + return TypeDecimal, p, s, nil + } + } + } + + // 单参数括号:varchar(150) / int(11) + if pargs != "" { + if n, err := strconv.Atoi(strings.TrimSpace(strings.Split(pargs, ",")[0])); err == nil { + length = n + } + } + + switch t { + case "bool", "boolean", "bit": + return TypeBool, 0, 0, nil + case "tinyint": + // mysql tinyint(1) 惯例为布尔 + if length == 1 { + return TypeBool, 0, 0, nil + } + if unsigned { + return TypeSmallint, 0, 0, nil + } + return TypeTinyint, 0, 0, nil + case "smallint", "year": + return TypeSmallint, 0, 0, nil + case "mediumint", "int", "integer": + return TypeInt, 0, 0, nil + case "bigint": + return TypeBigint, 0, 0, nil + case "float", "real", "binary_float": + return TypeFloat, 0, 0, nil + case "double", "double precision", "float8", "binary_double": + return TypeDouble, 0, 0, nil + case "decimal", "numeric", "number", "dec", "fixed": + if length == 0 { + return TypeDecimal, 0, 0, nil + } + return TypeDecimal, length, scale, nil + case "varchar", "character varying", "nvarchar", "varchar2": + return TypeVarchar, length, 0, nil + case "char", "character", "nchar", "char2": + return TypeChar, length, 0, nil + case "tinytext": + return TypeTinytext, 0, 0, nil + case "text", "mediumtext", "longtext", "clob", "ntext", "json", "jsonb", "xml": + if t == "json" || t == "jsonb" { + return TypeJson, 0, 0, nil + } + if t == "mediumtext" { + return TypeMediumText, 0, 0, nil + } + if t == "longtext" { + return TypeLongText, 0, 0, nil + } + return TypeText, 0, 0, nil + case "date": + return TypeDate, 0, 0, nil + case "time": + return TypeTime, 0, 0, nil + case "datetime", "timestamp", "smalldatetime", "datetime2": + return TypeDateTime, 0, 0, nil + case "tinyblob", "blob", "mediumblob", "longblob", "binary", "varbinary", + "bytea", "image", "raw", "long raw": + return TypeBlob, 0, 0, nil + case "uuid": + return TypeChar, 36, 0, nil + } + return TypeOther, 0, 0, nil +} + +// cleanDefault 归一默认值表达式 +// +// PostgreSQL 剥去类型转换与函数括号,其余方言原样保留 +func cleanDefault(dialect string, raw any) string { + if raw == nil { + return "" + } + s := strings.TrimSpace(toString(raw)) + if s == "" || strings.EqualFold(s, "null") { + return "" + } + if dialect == "pgsql" { + if m := rePgCast.FindStringSubmatch(s); m != nil { + s = m[1] + } else if rePgFunc.MatchString(s) { + s = "CURRENT_TIMESTAMP" + } + } + if dialect == "mysql" && strings.EqualFold(s, "current_timestamp()") { + s = "CURRENT_TIMESTAMP" + } + return s +} + +// toString 将任意默认值转为字符串 +// 兼容不同驱动的返回类型差异 +func toString(v any) string { + switch t := v.(type) { + case nil: + return "" + case string: + return t + case []byte: + return string(t) + default: + return strings.TrimSpace(strings.TrimSuffix(strval(t), ".0")) + } +} + +// strval 数值转字符串 +func strval(v any) string { + switch t := v.(type) { + case int64: + return strconv.FormatInt(t, 10) + case float64: + return strconv.FormatFloat(t, 'f', -1, 64) + default: + return strings.TrimSpace(toStr(v)) + } +} + +// toStr 尝试调用 String 方法转为字符串 +func toStr(v any) string { + if s, ok := v.(interface{ String() string }); ok { + return s.String() + } + return "" +} diff --git a/internal/model/do/article.go b/internal/model/do/article.go new file mode 100644 index 0000000..5adc735 --- /dev/null +++ b/internal/model/do/article.go @@ -0,0 +1,32 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// Article is the golang structure of table goframe_article for DAO operations like Where/Data. +type Article struct { + g.Meta `orm:"table:goframe_article, do:true"` + Id any // 主键ID + TenantId any // 租户ID:0=平台级 + CategoryId any // 分类ID + Title any // 文章标题 + Cover any // 文章封面 + Images any // 文章图集 + Intro any // 文章导读 + Content any // 文章内容 + Author any // 文章作者 + Status any // 文章状态:0-正常 1-下架 + Click any // 文章点击率 + Sort any // 文章排序 + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/do/category.go b/internal/model/do/category.go new file mode 100644 index 0000000..5097f5f --- /dev/null +++ b/internal/model/do/category.go @@ -0,0 +1,27 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// Category is the golang structure of table goframe_category for DAO operations like Where/Data. +type Category struct { + g.Meta `orm:"table:goframe_category, do:true"` + Id any // 主键ID + TenantId any // 租户ID:0=平台级 + CategoryName any // 分类名称 + CategoryCode any // 分类编码 + ParentId any // 上级ID + Sort any // 排序 + Note any // 分类备注 + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/do/city.go b/internal/model/do/city.go new file mode 100644 index 0000000..4af5db0 --- /dev/null +++ b/internal/model/do/city.go @@ -0,0 +1,33 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// City is the golang structure of table goframe_city for DAO operations like Where/Data. +type City struct { + g.Meta `orm:"table:goframe_city, do:true"` + Id any // 主键ID + CityCode any // 城市区号 + AreaCode any // 行政编码 + ParentCode any // 上级行政编码 + ZipCode any // 邮政编码 + Level any // 城市级别:0-省份 1-城市 2-县区 3-街道 4-社区 + Pid any // 上级城市ID + Name any // 城市名称 + ShortName any // 城市简称 + FullName any // 城市全称 + Pinyin any // 城市拼音 + Lng any // 城市经度 + Lat any // 城市纬度 + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/do/config.go b/internal/model/do/config.go new file mode 100644 index 0000000..8f12392 --- /dev/null +++ b/internal/model/do/config.go @@ -0,0 +1,25 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// Config is the golang structure of table goframe_config for DAO operations like Where/Data. +type Config struct { + g.Meta `orm:"table:goframe_config, do:true"` + Id any // 主键ID + Name any // 配置名称 + Code any // 配置编码 + Sort any // 配置排序 + Note any // 配置备注 + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/do/config_item.go b/internal/model/do/config_item.go new file mode 100644 index 0000000..53191dc --- /dev/null +++ b/internal/model/do/config_item.go @@ -0,0 +1,30 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// ConfigItem is the golang structure of table goframe_config_item for DAO operations like Where/Data. +type ConfigItem struct { + g.Meta `orm:"table:goframe_config_item, do:true"` + Id any // 主键ID + Name any // 配置项名称 + Code any // 配置项编码 + Value any // 配置项值 + Options any // 配置选项 + ConfigId any // 配置ID + Type any // 配置类型 + Status any // 配置状态:1-正常 2-停用 + Sort any // 配置项顺序 + Note any // 配置项备注 + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/do/dept.go b/internal/model/do/dept.go new file mode 100644 index 0000000..f171cd6 --- /dev/null +++ b/internal/model/do/dept.go @@ -0,0 +1,27 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// Dept is the golang structure of table goframe_dept for DAO operations like Where/Data. +type Dept struct { + g.Meta `orm:"table:goframe_dept, do:true"` + Id any // 主键ID + TenantId any // 租户ID:0=平台级 + Name any // 部门名称 + Type any // 部门类型:1-公司 2-子公司 3-部门 4-小组 + ParentId any // 上级部门ID + Sort any // 部门排序 + Note any // 备注 + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/do/dict.go b/internal/model/do/dict.go new file mode 100644 index 0000000..b85b497 --- /dev/null +++ b/internal/model/do/dict.go @@ -0,0 +1,25 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// Dict is the golang structure of table goframe_dict for DAO operations like Where/Data. +type Dict struct { + g.Meta `orm:"table:goframe_dict, do:true"` + Id any // 主键ID + Name any // 字典名称 + Code any // 字典编码 + Sort any // 字典排序 + Note any // 字典备注 + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/do/dict_item.go b/internal/model/do/dict_item.go new file mode 100644 index 0000000..87e89dd --- /dev/null +++ b/internal/model/do/dict_item.go @@ -0,0 +1,26 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// DictItem is the golang structure of table goframe_dict_item for DAO operations like Where/Data. +type DictItem struct { + g.Meta `orm:"table:goframe_dict_item, do:true"` + Id any // 主键ID + Name any // 字典项名称 + Value any // 字典项值 + DictId any // 字典ID + Sort any // 字典项顺序 + Note any // 字典项备注 + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/do/example.go b/internal/model/do/example.go new file mode 100644 index 0000000..6e33144 --- /dev/null +++ b/internal/model/do/example.go @@ -0,0 +1,26 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// Example is the golang structure of table goframe_example for DAO operations like Where/Data. +type Example struct { + g.Meta `orm:"table:goframe_example, do:true"` + Id any // 主键ID + Name any // 案例名称 + Avatar any // 案例图片 + Type any // 案例类型:1-类型1 2-类型2 3-类型3 4-类型4 + Status any // 案例状态:1-在用 2-停用 + Sort any // 案例排序 + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/do/file_template.go b/internal/model/do/file_template.go new file mode 100644 index 0000000..7c5d6d0 --- /dev/null +++ b/internal/model/do/file_template.go @@ -0,0 +1,27 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// FileTemplate is the golang structure of table goframe_file_template for DAO operations like Where/Data. +type FileTemplate struct { + g.Meta `orm:"table:goframe_file_template, do:true"` + Id any // 主键ID + Name any // 模板名称 + Code any // 模板编码 + FileName any // 文件名称 + FilePath any // 文件路径 + Note any // 文件模板备注 + Sort any // 文件模板顺序 + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/do/job.go b/internal/model/do/job.go new file mode 100644 index 0000000..bf9eae1 --- /dev/null +++ b/internal/model/do/job.go @@ -0,0 +1,32 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// Job is the golang structure of table goframe_job for DAO operations like Where/Data. +type Job struct { + g.Meta `orm:"table:goframe_job, do:true"` + Id any // 主键ID + JobName any // 任务名称 + JobAlias any // 任务别名 + JobGroup any // 任务分组 + JobTrigger any // 任务触发器 + Status any // 任务状态:0-未发布 1-运行中 2-暂停 3-删除 + CronExpression any // Cron表达式 + ExecutePolicy any // 执行策略:1-立即执行 2-执行一次 3-放弃执行 + IsSync any // 是否同步任务:0-否 1-是 + Url any // 任务URL + Params any // 任务参数 + Note any // 任务备注 + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/do/job_log.go b/internal/model/do/job_log.go new file mode 100644 index 0000000..f03d84a --- /dev/null +++ b/internal/model/do/job_log.go @@ -0,0 +1,32 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// JobLog is the golang structure of table goframe_job_log for DAO operations like Where/Data. +type JobLog struct { + g.Meta `orm:"table:goframe_job_log, do:true"` + Id any // 主键ID + JobId any // 任务ID + JobName any // 任务名称 + JobGroup any // 任务分组 + JobTrigger any // 任务触发器 + JobMessage any // 任务日志信息 + CronExpression any // Cron表达式 + Status any // 执行状态:0-正常 1-失败 + StartTime *gtime.Time // 执行开始时间 + EndTime *gtime.Time // 执行结束时间 + ConsumeTime any // 执行耗时,单位:毫秒 + ExceptionInfo any // 异常信息 + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/do/level.go b/internal/model/do/level.go new file mode 100644 index 0000000..15a99da --- /dev/null +++ b/internal/model/do/level.go @@ -0,0 +1,25 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// Level is the golang structure of table goframe_level for DAO operations like Where/Data. +type Level struct { + g.Meta `orm:"table:goframe_level, do:true"` + Id any // 主键ID + TenantId any // 租户ID:0=平台级 + Name any // 职级名称 + Status any // 职级状态:1-在用 2-停用 + Sort any // 职级排序 + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/do/link.go b/internal/model/do/link.go new file mode 100644 index 0000000..5247f7a --- /dev/null +++ b/internal/model/do/link.go @@ -0,0 +1,30 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// Link is the golang structure of table goframe_link for DAO operations like Where/Data. +type Link struct { + g.Meta `orm:"table:goframe_link, do:true"` + Id any // 主键ID + TenantId any // 租户ID:0=平台级 + Name any // 友链名称 + Type any // 友链类型:1-友情链接 2-合作伙伴 + Url any // 友链URL + Form any // 友链形式:1-文字链接 2-图片链接 + Image any // 友链图片 + Status any // 友链状态:1-正常 2-停用 + Sort any // 友链顺序 + Note any // 友链备注 + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/do/login_log.go b/internal/model/do/login_log.go new file mode 100644 index 0000000..d4c0f92 --- /dev/null +++ b/internal/model/do/login_log.go @@ -0,0 +1,36 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// LoginLog is the golang structure of table goframe_login_log for DAO operations like Where/Data. +type LoginLog struct { + g.Meta `orm:"table:goframe_login_log, do:true"` + Id any // 主键ID + Username any // 登录账号 + LogType any // 日志类型:1-登录,2-退出 + Source any // 登录来源:0-后台,1-前台 + Method any // 请求方法 + RequestMethod any // 请求方式 + Url any // 请求URL + Param any // 请求参数 + Result any // 返回结果 + Ip any // IP地址 + Location any // IP归属地 + Os any // 操作系统 + Browser any // 浏览器 + ConsumeTime any // 请求耗时 + Status any // 操作状态:0-正常,1-异常 + Error any // 错误信息 + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/do/menu.go b/internal/model/do/menu.go new file mode 100644 index 0000000..36bcfc9 --- /dev/null +++ b/internal/model/do/menu.go @@ -0,0 +1,34 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// Menu is the golang structure of table goframe_menu for DAO operations like Where/Data. +type Menu struct { + g.Meta `orm:"table:goframe_menu, do:true"` + Id any // 主键ID + Name any // 菜单名称 + Icon any // 菜单图标 + Icon2 any // 菜单图标 + ParentId any // 上级ID + Path any // 菜单路径 + Component any // 菜单组件 + Permission any // 权限节点 + Target any // 打开方式:0-组件 1-内链 2-外链 + Type any // 菜单类型:0-菜单 1-节点 + Status any // 菜单状态:0-正常 1-停用 + Hide any // 是否可见:0-可见 1-不可见 + Sort any // 排序 + Note any // 备注 + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/do/notice.go b/internal/model/do/notice.go new file mode 100644 index 0000000..85e1301 --- /dev/null +++ b/internal/model/do/notice.go @@ -0,0 +1,28 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// Notice is the golang structure of table goframe_notice for DAO operations like Where/Data. +type Notice struct { + g.Meta `orm:"table:goframe_notice, do:true"` + Id any // 主键ID + TenantId any // 租户ID:0=平台级 + Title any // 通知标题 + Cover any // 通知封面 + Type any // 通知类型:1-通知 2-公告 + ClickNum any // 点击率 + Status any // 通知状态:1-正常 2-停用 + Content any // 通知公告内容 + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/do/operation_log.go b/internal/model/do/operation_log.go new file mode 100644 index 0000000..3c49627 --- /dev/null +++ b/internal/model/do/operation_log.go @@ -0,0 +1,36 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// OperationLog is the golang structure of table goframe_operation_log for DAO operations like Where/Data. +type OperationLog struct { + g.Meta `orm:"table:goframe_operation_log, do:true"` + Id any // 主键ID + Title any // 请求标题 + Type any // 请求类型 + Source any // 请求来源:0-后台,1-前台 + Method any // 请求方法 + RequestMethod any // 请求方式 + Url any // 请求URL + Param any // 请求参数 + Result any // 请求结果 + Ip any // 请求IP + Location any // 请求所在区域 + Os any // 操作系统 + Browser any // 浏览器 + ConsumeTime any // 请求耗时 + Status any // 请求状态:0-正常,1-异常 + Error any // 错误描述 + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/do/param.go b/internal/model/do/param.go new file mode 100644 index 0000000..f2d482c --- /dev/null +++ b/internal/model/do/param.go @@ -0,0 +1,28 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// Param is the golang structure of table goframe_param for DAO operations like Where/Data. +type Param struct { + g.Meta `orm:"table:goframe_param, do:true"` + Id any // 主键ID + Name any // 参数名称 + Code any // 参数编码 + Value any // 参数值 + Type any // 参数类型:0-系统 1-业务 + Status any // 参数状态:1-正常 2-禁用 + Sort any // 参数排序 + Note any // 参数备注 + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/do/position.go b/internal/model/do/position.go new file mode 100644 index 0000000..9ed43a0 --- /dev/null +++ b/internal/model/do/position.go @@ -0,0 +1,25 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// Position is the golang structure of table goframe_position for DAO operations like Where/Data. +type Position struct { + g.Meta `orm:"table:goframe_position, do:true"` + Id any // 主键ID + TenantId any // 租户ID:0=平台级 + Name any // 岗位名称 + Status any // 岗位状态:1-在用 2-停用 + Sort any // 岗位排序 + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/do/role.go b/internal/model/do/role.go new file mode 100644 index 0000000..ea03130 --- /dev/null +++ b/internal/model/do/role.go @@ -0,0 +1,27 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// Role is the golang structure of table goframe_role for DAO operations like Where/Data. +type Role struct { + g.Meta `orm:"table:goframe_role, do:true"` + Id any // 主键ID + TenantId any // 租户ID:0=平台级 + Name any // 角色名称 + Code any // 角色编码 + DataScope any // 数据范围:1-全部 2-本部门及以下 3-本部门 4-仅本人 + Sort any // 角色排序 + Note any // 角色备注 + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/do/role_menu.go b/internal/model/do/role_menu.go new file mode 100644 index 0000000..306234d --- /dev/null +++ b/internal/model/do/role_menu.go @@ -0,0 +1,23 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// RoleMenu is the golang structure of table goframe_role_menu for DAO operations like Where/Data. +type RoleMenu struct { + g.Meta `orm:"table:goframe_role_menu, do:true"` + Id any // 主键ID + RoleId any // 角色ID + MenuId any // 菜单ID + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/do/tenant.go b/internal/model/do/tenant.go new file mode 100644 index 0000000..af4282a --- /dev/null +++ b/internal/model/do/tenant.go @@ -0,0 +1,32 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// Tenant is the golang structure of table goframe_tenant for DAO operations like Where/Data. +type Tenant struct { + g.Meta `orm:"table:goframe_tenant, do:true"` + Id any // + Name any // 租户名称 + Code any // 租户编码(系统自动生成:T+4位ID左补零) + Contact any // 联系人 + Mobile any // 联系电话 + Email any // 邮箱 + Address any // 地址 + Logo any // 租户LOGO + Status any // 状态:1=正常 2=禁用 + ExpireTime *gtime.Time // 过期时间 + MaxUserNum any // 最大用户数 + Note any // 备注 + CreateUser any // + CreateTime *gtime.Time // + UpdateUser any // + UpdateTime *gtime.Time // + DelFlag any // +} diff --git a/internal/model/do/user.go b/internal/model/do/user.go new file mode 100644 index 0000000..bb0e7d9 --- /dev/null +++ b/internal/model/do/user.go @@ -0,0 +1,44 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// User is the golang structure of table goframe_user for DAO operations like Where/Data. +type User struct { + g.Meta `orm:"table:goframe_user, do:true"` + Id any // 主键ID + TenantId any // 租户ID:0=平台用户 + Code any // 用户编码 + Realname any // 用户姓名 + Gender any // 性别:1-男 2-女 3-保密 + Avatar any // 用户头像 + Mobile any // 手机号 + Email any // 邮箱 + DeptId any // 部门ID + LevelId any // 职级ID + PositionId any // 岗位ID + ProvinceCode any // 省份编码 + CityCode any // 城市编码 + DistrictCode any // 县区编码 + StreetCode any // 街道编码 + CityInfo any // 省市区信息 + Address any // 详细地址 + Username any // 用户名 + Password any // 密码 + Salt any // 加密盐 + Intro any // 个人简介 + Status any // 状态:1-正常 2-禁用 + Sort any // 排序 + Note any // 个人备注 + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/do/user_role.go b/internal/model/do/user_role.go new file mode 100644 index 0000000..0bf43a7 --- /dev/null +++ b/internal/model/do/user_role.go @@ -0,0 +1,23 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// UserRole is the golang structure of table goframe_user_role for DAO operations like Where/Data. +type UserRole struct { + g.Meta `orm:"table:goframe_user_role, do:true"` + Id any // 主键ID + UserId any // 用户ID + RoleId any // 角色ID + CreateUser any // 创建人 + CreateTime *gtime.Time // 创建时间 + UpdateUser any // 更新人 + UpdateTime *gtime.Time // 更新时间 + DelFlag any // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/article.go b/internal/model/entity/article.go new file mode 100644 index 0000000..6b50d80 --- /dev/null +++ b/internal/model/entity/article.go @@ -0,0 +1,30 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// Article is the golang structure for table article. +type Article struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + TenantId int `json:"tenantId" orm:"tenant_id" description:"租户ID:0=平台级"` // 租户ID:0=平台级 + CategoryId int `json:"categoryId" orm:"category_id" description:"分类ID"` // 分类ID + Title string `json:"title" orm:"title" description:"文章标题"` // 文章标题 + Cover string `json:"cover" orm:"cover" description:"文章封面"` // 文章封面 + Images string `json:"images" orm:"images" description:"文章图集"` // 文章图集 + Intro string `json:"intro" orm:"intro" description:"文章导读"` // 文章导读 + Content string `json:"content" orm:"content" description:"文章内容"` // 文章内容 + Author string `json:"author" orm:"author" description:"文章作者"` // 文章作者 + Status int `json:"status" orm:"status" description:"文章状态:0-正常 1-下架"` // 文章状态:0-正常 1-下架 + Click int `json:"click" orm:"click" description:"文章点击率"` // 文章点击率 + Sort int `json:"sort" orm:"sort" description:"文章排序"` // 文章排序 + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/category.go b/internal/model/entity/category.go new file mode 100644 index 0000000..c9dceee --- /dev/null +++ b/internal/model/entity/category.go @@ -0,0 +1,25 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// Category is the golang structure for table category. +type Category struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + TenantId int `json:"tenantId" orm:"tenant_id" description:"租户ID:0=平台级"` // 租户ID:0=平台级 + CategoryName string `json:"categoryName" orm:"category_name" description:"分类名称"` // 分类名称 + CategoryCode string `json:"categoryCode" orm:"category_code" description:"分类编码"` // 分类编码 + ParentId int `json:"parentId" orm:"parent_id" description:"上级ID"` // 上级ID + Sort int `json:"sort" orm:"sort" description:"排序"` // 排序 + Note string `json:"note" orm:"note" description:"分类备注"` // 分类备注 + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/city.go b/internal/model/entity/city.go new file mode 100644 index 0000000..8cb9d35 --- /dev/null +++ b/internal/model/entity/city.go @@ -0,0 +1,31 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// City is the golang structure for table city. +type City struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + CityCode string `json:"cityCode" orm:"city_code" description:"城市区号"` // 城市区号 + AreaCode string `json:"areaCode" orm:"area_code" description:"行政编码"` // 行政编码 + ParentCode string `json:"parentCode" orm:"parent_code" description:"上级行政编码"` // 上级行政编码 + ZipCode string `json:"zipCode" orm:"zip_code" description:"邮政编码"` // 邮政编码 + Level int `json:"level" orm:"level" description:"城市级别:0-省份 1-城市 2-县区 3-街道 4-社区"` // 城市级别:0-省份 1-城市 2-县区 3-街道 4-社区 + Pid int `json:"pid" orm:"pid" description:"上级城市ID"` // 上级城市ID + Name string `json:"name" orm:"name" description:"城市名称"` // 城市名称 + ShortName string `json:"shortName" orm:"short_name" description:"城市简称"` // 城市简称 + FullName string `json:"fullName" orm:"full_name" description:"城市全称"` // 城市全称 + Pinyin string `json:"pinyin" orm:"pinyin" description:"城市拼音"` // 城市拼音 + Lng string `json:"lng" orm:"lng" description:"城市经度"` // 城市经度 + Lat string `json:"lat" orm:"lat" description:"城市纬度"` // 城市纬度 + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/config.go b/internal/model/entity/config.go new file mode 100644 index 0000000..da35b1a --- /dev/null +++ b/internal/model/entity/config.go @@ -0,0 +1,23 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// Config is the golang structure for table config. +type Config struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + Name string `json:"name" orm:"name" description:"配置名称"` // 配置名称 + Code string `json:"code" orm:"code" description:"配置编码"` // 配置编码 + Sort int `json:"sort" orm:"sort" description:"配置排序"` // 配置排序 + Note string `json:"note" orm:"note" description:"配置备注"` // 配置备注 + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/config_item.go b/internal/model/entity/config_item.go new file mode 100644 index 0000000..4911a95 --- /dev/null +++ b/internal/model/entity/config_item.go @@ -0,0 +1,28 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// ConfigItem is the golang structure for table config_item. +type ConfigItem struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + Name string `json:"name" orm:"name" description:"配置项名称"` // 配置项名称 + Code string `json:"code" orm:"code" description:"配置项编码"` // 配置项编码 + Value string `json:"value" orm:"value" description:"配置项值"` // 配置项值 + Options string `json:"options" orm:"options" description:"配置选项"` // 配置选项 + ConfigId int `json:"configId" orm:"config_id" description:"配置ID"` // 配置ID + Type string `json:"type" orm:"type" description:"配置类型"` // 配置类型 + Status int `json:"status" orm:"status" description:"配置状态:1-正常 2-停用"` // 配置状态:1-正常 2-停用 + Sort int `json:"sort" orm:"sort" description:"配置项顺序"` // 配置项顺序 + Note string `json:"note" orm:"note" description:"配置项备注"` // 配置项备注 + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/dept.go b/internal/model/entity/dept.go new file mode 100644 index 0000000..3069366 --- /dev/null +++ b/internal/model/entity/dept.go @@ -0,0 +1,25 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// Dept is the golang structure for table dept. +type Dept struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + TenantId int `json:"tenantId" orm:"tenant_id" description:"租户ID:0=平台级"` // 租户ID:0=平台级 + Name string `json:"name" orm:"name" description:"部门名称"` // 部门名称 + Type int `json:"type" orm:"type" description:"部门类型:1-公司 2-子公司 3-部门 4-小组"` // 部门类型:1-公司 2-子公司 3-部门 4-小组 + ParentId int `json:"parentId" orm:"parent_id" description:"上级部门ID"` // 上级部门ID + Sort int `json:"sort" orm:"sort" description:"部门排序"` // 部门排序 + Note string `json:"note" orm:"note" description:"备注"` // 备注 + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/dict.go b/internal/model/entity/dict.go new file mode 100644 index 0000000..16b6ed6 --- /dev/null +++ b/internal/model/entity/dict.go @@ -0,0 +1,23 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// Dict is the golang structure for table dict. +type Dict struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + Name string `json:"name" orm:"name" description:"字典名称"` // 字典名称 + Code string `json:"code" orm:"code" description:"字典编码"` // 字典编码 + Sort int `json:"sort" orm:"sort" description:"字典排序"` // 字典排序 + Note string `json:"note" orm:"note" description:"字典备注"` // 字典备注 + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/dict_item.go b/internal/model/entity/dict_item.go new file mode 100644 index 0000000..a59c8b2 --- /dev/null +++ b/internal/model/entity/dict_item.go @@ -0,0 +1,24 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// DictItem is the golang structure for table dict_item. +type DictItem struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + Name string `json:"name" orm:"name" description:"字典项名称"` // 字典项名称 + Value string `json:"value" orm:"value" description:"字典项值"` // 字典项值 + DictId int `json:"dictId" orm:"dict_id" description:"字典ID"` // 字典ID + Sort int `json:"sort" orm:"sort" description:"字典项顺序"` // 字典项顺序 + Note string `json:"note" orm:"note" description:"字典项备注"` // 字典项备注 + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/example.go b/internal/model/entity/example.go new file mode 100644 index 0000000..4f0e963 --- /dev/null +++ b/internal/model/entity/example.go @@ -0,0 +1,24 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// Example is the golang structure for table example. +type Example struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + Name string `json:"name" orm:"name" description:"案例名称"` // 案例名称 + Avatar string `json:"avatar" orm:"avatar" description:"案例图片"` // 案例图片 + Type int `json:"type" orm:"type" description:"案例类型:1-类型1 2-类型2 3-类型3 4-类型4"` // 案例类型:1-类型1 2-类型2 3-类型3 4-类型4 + Status int `json:"status" orm:"status" description:"案例状态:1-在用 2-停用"` // 案例状态:1-在用 2-停用 + Sort int `json:"sort" orm:"sort" description:"案例排序"` // 案例排序 + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/file_template.go b/internal/model/entity/file_template.go new file mode 100644 index 0000000..19dac4c --- /dev/null +++ b/internal/model/entity/file_template.go @@ -0,0 +1,25 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// FileTemplate is the golang structure for table file_template. +type FileTemplate struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + Name string `json:"name" orm:"name" description:"模板名称"` // 模板名称 + Code string `json:"code" orm:"code" description:"模板编码"` // 模板编码 + FileName string `json:"fileName" orm:"file_name" description:"文件名称"` // 文件名称 + FilePath string `json:"filePath" orm:"file_path" description:"文件路径"` // 文件路径 + Note string `json:"note" orm:"note" description:"文件模板备注"` // 文件模板备注 + Sort int `json:"sort" orm:"sort" description:"文件模板顺序"` // 文件模板顺序 + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/job.go b/internal/model/entity/job.go new file mode 100644 index 0000000..bc786a4 --- /dev/null +++ b/internal/model/entity/job.go @@ -0,0 +1,30 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// Job is the golang structure for table job. +type Job struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + JobName string `json:"jobName" orm:"job_name" description:"任务名称"` // 任务名称 + JobAlias string `json:"jobAlias" orm:"job_alias" description:"任务别名"` // 任务别名 + JobGroup string `json:"jobGroup" orm:"job_group" description:"任务分组"` // 任务分组 + JobTrigger string `json:"jobTrigger" orm:"job_trigger" description:"任务触发器"` // 任务触发器 + Status int `json:"status" orm:"status" description:"任务状态:0-未发布 1-运行中 2-暂停 3-删除"` // 任务状态:0-未发布 1-运行中 2-暂停 3-删除 + CronExpression string `json:"cronExpression" orm:"cron_expression" description:"Cron表达式"` // Cron表达式 + ExecutePolicy int `json:"executePolicy" orm:"execute_policy" description:"执行策略:1-立即执行 2-执行一次 3-放弃执行"` // 执行策略:1-立即执行 2-执行一次 3-放弃执行 + IsSync int `json:"isSync" orm:"is_sync" description:"是否同步任务:0-否 1-是"` // 是否同步任务:0-否 1-是 + Url string `json:"url" orm:"url" description:"任务URL"` // 任务URL + Params string `json:"params" orm:"params" description:"任务参数"` // 任务参数 + Note string `json:"note" orm:"note" description:"任务备注"` // 任务备注 + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/job_log.go b/internal/model/entity/job_log.go new file mode 100644 index 0000000..a26db54 --- /dev/null +++ b/internal/model/entity/job_log.go @@ -0,0 +1,30 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// JobLog is the golang structure for table job_log. +type JobLog struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + JobId int `json:"jobId" orm:"job_id" description:"任务ID"` // 任务ID + JobName string `json:"jobName" orm:"job_name" description:"任务名称"` // 任务名称 + JobGroup string `json:"jobGroup" orm:"job_group" description:"任务分组"` // 任务分组 + JobTrigger string `json:"jobTrigger" orm:"job_trigger" description:"任务触发器"` // 任务触发器 + JobMessage string `json:"jobMessage" orm:"job_message" description:"任务日志信息"` // 任务日志信息 + CronExpression string `json:"cronExpression" orm:"cron_expression" description:"Cron表达式"` // Cron表达式 + Status int `json:"status" orm:"status" description:"执行状态:0-正常 1-失败"` // 执行状态:0-正常 1-失败 + StartTime *gtime.Time `json:"startTime" orm:"start_time" description:"执行开始时间"` // 执行开始时间 + EndTime *gtime.Time `json:"endTime" orm:"end_time" description:"执行结束时间"` // 执行结束时间 + ConsumeTime int64 `json:"consumeTime" orm:"consume_time" description:"执行耗时,单位:毫秒"` // 执行耗时,单位:毫秒 + ExceptionInfo string `json:"exceptionInfo" orm:"exception_info" description:"异常信息"` // 异常信息 + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/level.go b/internal/model/entity/level.go new file mode 100644 index 0000000..9a61fc3 --- /dev/null +++ b/internal/model/entity/level.go @@ -0,0 +1,23 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// Level is the golang structure for table level. +type Level struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + TenantId int `json:"tenantId" orm:"tenant_id" description:"租户ID:0=平台级"` // 租户ID:0=平台级 + Name string `json:"name" orm:"name" description:"职级名称"` // 职级名称 + Status int `json:"status" orm:"status" description:"职级状态:1-在用 2-停用"` // 职级状态:1-在用 2-停用 + Sort int `json:"sort" orm:"sort" description:"职级排序"` // 职级排序 + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/link.go b/internal/model/entity/link.go new file mode 100644 index 0000000..2212c82 --- /dev/null +++ b/internal/model/entity/link.go @@ -0,0 +1,28 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// Link is the golang structure for table link. +type Link struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + TenantId int `json:"tenantId" orm:"tenant_id" description:"租户ID:0=平台级"` // 租户ID:0=平台级 + Name string `json:"name" orm:"name" description:"友链名称"` // 友链名称 + Type int `json:"type" orm:"type" description:"友链类型:1-友情链接 2-合作伙伴"` // 友链类型:1-友情链接 2-合作伙伴 + Url string `json:"url" orm:"url" description:"友链URL"` // 友链URL + Form int `json:"form" orm:"form" description:"友链形式:1-文字链接 2-图片链接"` // 友链形式:1-文字链接 2-图片链接 + Image string `json:"image" orm:"image" description:"友链图片"` // 友链图片 + Status int `json:"status" orm:"status" description:"友链状态:1-正常 2-停用"` // 友链状态:1-正常 2-停用 + Sort int `json:"sort" orm:"sort" description:"友链顺序"` // 友链顺序 + Note string `json:"note" orm:"note" description:"友链备注"` // 友链备注 + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/login_log.go b/internal/model/entity/login_log.go new file mode 100644 index 0000000..ee1ff83 --- /dev/null +++ b/internal/model/entity/login_log.go @@ -0,0 +1,34 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// LoginLog is the golang structure for table login_log. +type LoginLog struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + Username string `json:"username" orm:"username" description:"登录账号"` // 登录账号 + LogType int `json:"logType" orm:"log_type" description:"日志类型:1-登录,2-退出"` // 日志类型:1-登录,2-退出 + Source int `json:"source" orm:"source" description:"登录来源:0-后台,1-前台"` // 登录来源:0-后台,1-前台 + Method string `json:"method" orm:"method" description:"请求方法"` // 请求方法 + RequestMethod string `json:"requestMethod" orm:"request_method" description:"请求方式"` // 请求方式 + Url string `json:"url" orm:"url" description:"请求URL"` // 请求URL + Param string `json:"param" orm:"param" description:"请求参数"` // 请求参数 + Result string `json:"result" orm:"result" description:"返回结果"` // 返回结果 + Ip string `json:"ip" orm:"ip" description:"IP地址"` // IP地址 + Location string `json:"location" orm:"location" description:"IP归属地"` // IP归属地 + Os string `json:"os" orm:"os" description:"操作系统"` // 操作系统 + Browser string `json:"browser" orm:"browser" description:"浏览器"` // 浏览器 + ConsumeTime int `json:"consumeTime" orm:"consume_time" description:"请求耗时"` // 请求耗时 + Status int `json:"status" orm:"status" description:"操作状态:0-正常,1-异常"` // 操作状态:0-正常,1-异常 + Error string `json:"error" orm:"error" description:"错误信息"` // 错误信息 + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/menu.go b/internal/model/entity/menu.go new file mode 100644 index 0000000..295d698 --- /dev/null +++ b/internal/model/entity/menu.go @@ -0,0 +1,32 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// Menu is the golang structure for table menu. +type Menu struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + Name string `json:"name" orm:"name" description:"菜单名称"` // 菜单名称 + Icon string `json:"icon" orm:"icon" description:"菜单图标"` // 菜单图标 + Icon2 string `json:"icon2" orm:"icon2" description:"菜单图标"` // 菜单图标 + ParentId int `json:"parentId" orm:"parent_id" description:"上级ID"` // 上级ID + Path string `json:"path" orm:"path" description:"菜单路径"` // 菜单路径 + Component string `json:"component" orm:"component" description:"菜单组件"` // 菜单组件 + Permission string `json:"permission" orm:"permission" description:"权限节点"` // 权限节点 + Target int `json:"target" orm:"target" description:"打开方式:0-组件 1-内链 2-外链"` // 打开方式:0-组件 1-内链 2-外链 + Type int `json:"type" orm:"type" description:"菜单类型:0-菜单 1-节点"` // 菜单类型:0-菜单 1-节点 + Status int `json:"status" orm:"status" description:"菜单状态:0-正常 1-停用"` // 菜单状态:0-正常 1-停用 + Hide int `json:"hide" orm:"hide" description:"是否可见:0-可见 1-不可见"` // 是否可见:0-可见 1-不可见 + Sort int `json:"sort" orm:"sort" description:"排序"` // 排序 + Note string `json:"note" orm:"note" description:"备注"` // 备注 + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/notice.go b/internal/model/entity/notice.go new file mode 100644 index 0000000..7917b3f --- /dev/null +++ b/internal/model/entity/notice.go @@ -0,0 +1,26 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// Notice is the golang structure for table notice. +type Notice struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + TenantId int `json:"tenantId" orm:"tenant_id" description:"租户ID:0=平台级"` // 租户ID:0=平台级 + Title string `json:"title" orm:"title" description:"通知标题"` // 通知标题 + Cover string `json:"cover" orm:"cover" description:"通知封面"` // 通知封面 + Type int `json:"type" orm:"type" description:"通知类型:1-通知 2-公告"` // 通知类型:1-通知 2-公告 + ClickNum int `json:"clickNum" orm:"click_num" description:"点击率"` // 点击率 + Status int `json:"status" orm:"status" description:"通知状态:1-正常 2-停用"` // 通知状态:1-正常 2-停用 + Content string `json:"content" orm:"content" description:"通知公告内容"` // 通知公告内容 + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/operation_log.go b/internal/model/entity/operation_log.go new file mode 100644 index 0000000..ad4552b --- /dev/null +++ b/internal/model/entity/operation_log.go @@ -0,0 +1,34 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// OperationLog is the golang structure for table operation_log. +type OperationLog struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + Title string `json:"title" orm:"title" description:"请求标题"` // 请求标题 + Type int `json:"type" orm:"type" description:"请求类型"` // 请求类型 + Source int `json:"source" orm:"source" description:"请求来源:0-后台,1-前台"` // 请求来源:0-后台,1-前台 + Method string `json:"method" orm:"method" description:"请求方法"` // 请求方法 + RequestMethod string `json:"requestMethod" orm:"request_method" description:"请求方式"` // 请求方式 + Url string `json:"url" orm:"url" description:"请求URL"` // 请求URL + Param string `json:"param" orm:"param" description:"请求参数"` // 请求参数 + Result string `json:"result" orm:"result" description:"请求结果"` // 请求结果 + Ip string `json:"ip" orm:"ip" description:"请求IP"` // 请求IP + Location string `json:"location" orm:"location" description:"请求所在区域"` // 请求所在区域 + Os string `json:"os" orm:"os" description:"操作系统"` // 操作系统 + Browser string `json:"browser" orm:"browser" description:"浏览器"` // 浏览器 + ConsumeTime int64 `json:"consumeTime" orm:"consume_time" description:"请求耗时"` // 请求耗时 + Status int `json:"status" orm:"status" description:"请求状态:0-正常,1-异常"` // 请求状态:0-正常,1-异常 + Error string `json:"error" orm:"error" description:"错误描述"` // 错误描述 + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/param.go b/internal/model/entity/param.go new file mode 100644 index 0000000..ec50964 --- /dev/null +++ b/internal/model/entity/param.go @@ -0,0 +1,26 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// Param is the golang structure for table param. +type Param struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + Name string `json:"name" orm:"name" description:"参数名称"` // 参数名称 + Code string `json:"code" orm:"code" description:"参数编码"` // 参数编码 + Value string `json:"value" orm:"value" description:"参数值"` // 参数值 + Type int `json:"type" orm:"type" description:"参数类型:0-系统 1-业务"` // 参数类型:0-系统 1-业务 + Status int `json:"status" orm:"status" description:"参数状态:1-正常 2-禁用"` // 参数状态:1-正常 2-禁用 + Sort int `json:"sort" orm:"sort" description:"参数排序"` // 参数排序 + Note string `json:"note" orm:"note" description:"参数备注"` // 参数备注 + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/position.go b/internal/model/entity/position.go new file mode 100644 index 0000000..aee39a7 --- /dev/null +++ b/internal/model/entity/position.go @@ -0,0 +1,23 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// Position is the golang structure for table position. +type Position struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + TenantId int `json:"tenantId" orm:"tenant_id" description:"租户ID:0=平台级"` // 租户ID:0=平台级 + Name string `json:"name" orm:"name" description:"岗位名称"` // 岗位名称 + Status int `json:"status" orm:"status" description:"岗位状态:1-在用 2-停用"` // 岗位状态:1-在用 2-停用 + Sort int `json:"sort" orm:"sort" description:"岗位排序"` // 岗位排序 + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/role.go b/internal/model/entity/role.go new file mode 100644 index 0000000..de2ccb3 --- /dev/null +++ b/internal/model/entity/role.go @@ -0,0 +1,25 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// Role is the golang structure for table role. +type Role struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + TenantId int `json:"tenantId" orm:"tenant_id" description:"租户ID:0=平台级"` // 租户ID:0=平台级 + Name string `json:"name" orm:"name" description:"角色名称"` // 角色名称 + Code string `json:"code" orm:"code" description:"角色编码"` // 角色编码 + DataScope int `json:"dataScope" orm:"data_scope" description:"数据范围:1-全部 2-本部门及以下 3-本部门 4-仅本人"` // 数据范围:1-全部 2-本部门及以下 3-本部门 4-仅本人 + Sort int `json:"sort" orm:"sort" description:"角色排序"` // 角色排序 + Note string `json:"note" orm:"note" description:"角色备注"` // 角色备注 + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/role_menu.go b/internal/model/entity/role_menu.go new file mode 100644 index 0000000..031605b --- /dev/null +++ b/internal/model/entity/role_menu.go @@ -0,0 +1,21 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// RoleMenu is the golang structure for table role_menu. +type RoleMenu struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + RoleId int `json:"roleId" orm:"role_id" description:"角色ID"` // 角色ID + MenuId int `json:"menuId" orm:"menu_id" description:"菜单ID"` // 菜单ID + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/tenant.go b/internal/model/entity/tenant.go new file mode 100644 index 0000000..3a019d4 --- /dev/null +++ b/internal/model/entity/tenant.go @@ -0,0 +1,30 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// Tenant is the golang structure for table tenant. +type Tenant struct { + Id int `json:"id" orm:"id" description:""` // + Name string `json:"name" orm:"name" description:"租户名称"` // 租户名称 + Code string `json:"code" orm:"code" description:"租户编码(系统自动生成:T+4位ID左补零)"` // 租户编码(系统自动生成:T+4位ID左补零) + Contact string `json:"contact" orm:"contact" description:"联系人"` // 联系人 + Mobile string `json:"mobile" orm:"mobile" description:"联系电话"` // 联系电话 + Email string `json:"email" orm:"email" description:"邮箱"` // 邮箱 + Address string `json:"address" orm:"address" description:"地址"` // 地址 + Logo string `json:"logo" orm:"logo" description:"租户LOGO"` // 租户LOGO + Status int `json:"status" orm:"status" description:"状态:1=正常 2=禁用"` // 状态:1=正常 2=禁用 + ExpireTime *gtime.Time `json:"expireTime" orm:"expire_time" description:"过期时间"` // 过期时间 + MaxUserNum int `json:"maxUserNum" orm:"max_user_num" description:"最大用户数"` // 最大用户数 + Note string `json:"note" orm:"note" description:"备注"` // 备注 + CreateUser string `json:"createUser" orm:"create_user" description:""` // + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:""` // + UpdateUser string `json:"updateUser" orm:"update_user" description:""` // + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:""` // + DelFlag int `json:"delFlag" orm:"del_flag" description:""` // +} diff --git a/internal/model/entity/user.go b/internal/model/entity/user.go new file mode 100644 index 0000000..24f90f5 --- /dev/null +++ b/internal/model/entity/user.go @@ -0,0 +1,42 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// User is the golang structure for table user. +type User struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + TenantId int `json:"tenantId" orm:"tenant_id" description:"租户ID:0=平台用户"` // 租户ID:0=平台用户 + Code string `json:"code" orm:"code" description:"用户编码"` // 用户编码 + Realname string `json:"realname" orm:"realname" description:"用户姓名"` // 用户姓名 + Gender int `json:"gender" orm:"gender" description:"性别:1-男 2-女 3-保密"` // 性别:1-男 2-女 3-保密 + Avatar string `json:"avatar" orm:"avatar" description:"用户头像"` // 用户头像 + Mobile string `json:"mobile" orm:"mobile" description:"手机号"` // 手机号 + Email string `json:"email" orm:"email" description:"邮箱"` // 邮箱 + DeptId int `json:"deptId" orm:"dept_id" description:"部门ID"` // 部门ID + LevelId int `json:"levelId" orm:"level_id" description:"职级ID"` // 职级ID + PositionId int `json:"positionId" orm:"position_id" description:"岗位ID"` // 岗位ID + ProvinceCode string `json:"provinceCode" orm:"province_code" description:"省份编码"` // 省份编码 + CityCode string `json:"cityCode" orm:"city_code" description:"城市编码"` // 城市编码 + DistrictCode string `json:"districtCode" orm:"district_code" description:"县区编码"` // 县区编码 + StreetCode string `json:"streetCode" orm:"street_code" description:"街道编码"` // 街道编码 + CityInfo string `json:"cityInfo" orm:"city_info" description:"省市区信息"` // 省市区信息 + Address string `json:"address" orm:"address" description:"详细地址"` // 详细地址 + Username string `json:"username" orm:"username" description:"用户名"` // 用户名 + Password string `json:"password" orm:"password" description:"密码"` // 密码 + Salt string `json:"salt" orm:"salt" description:"加密盐"` // 加密盐 + Intro string `json:"intro" orm:"intro" description:"个人简介"` // 个人简介 + Status int `json:"status" orm:"status" description:"状态:1-正常 2-禁用"` // 状态:1-正常 2-禁用 + Sort int `json:"sort" orm:"sort" description:"排序"` // 排序 + Note string `json:"note" orm:"note" description:"个人备注"` // 个人备注 + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/entity/user_role.go b/internal/model/entity/user_role.go new file mode 100644 index 0000000..f5658be --- /dev/null +++ b/internal/model/entity/user_role.go @@ -0,0 +1,21 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// UserRole is the golang structure for table user_role. +type UserRole struct { + Id int `json:"id" orm:"id" description:"主键ID"` // 主键ID + UserId int `json:"userId" orm:"user_id" description:"用户ID"` // 用户ID + RoleId int `json:"roleId" orm:"role_id" description:"角色ID"` // 角色ID + CreateUser string `json:"createUser" orm:"create_user" description:"创建人"` // 创建人 + CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 + UpdateUser string `json:"updateUser" orm:"update_user" description:"更新人"` // 更新人 + UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 + DelFlag int `json:"delFlag" orm:"del_flag" description:"删除标识:0-正常 1-已删除"` // 删除标识:0-正常 1-已删除 +} diff --git a/internal/model/hooks/hooks.go b/internal/model/hooks/hooks.go new file mode 100644 index 0000000..808e3fb --- /dev/null +++ b/internal/model/hooks/hooks.go @@ -0,0 +1,198 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package hooks + +import ( + "context" + "database/sql" + "reflect" + "strings" + "time" + + "github.com/gogf/gf/v2/database/gdb" + + "easygoadmin/internal/consts" +) + +// ============================== 全局模型钩子 ============================== +// +// 全局钩子是架构红线的执行点: +// - Select、Update、Delete 的条件过滤在 dao.Ctx 覆写中注入(见 ApplyCtx,进入 SQL 构建前最可靠) +// - Insert:主键交给数据库自增,自动补齐审计字段(create_user/create_time/del_flag)与租户ID +// - Update:自动为审计字段(update_user/update_time)盖章 +// - Delete:物理删除一律改写为软删除 UPDATE del_flag=1 +// +// 所有表均含统一字段 id/create_user/create_time/update_user/update_time/del_flag。 + +// tenantTables 含 tenant_id 列的租户级表 +// +// 与初始化脚本保持一致,共 9 张 +var tenantTables = map[string]bool{ + "goframe_user": true, + "goframe_role": true, + "goframe_dept": true, + "goframe_level": true, + "goframe_position": true, + "goframe_article": true, + "goframe_category": true, + "goframe_notice": true, + "goframe_link": true, +} + +// IsTenantTable 判断是否为租户级表 +func IsTenantTable(table string) bool { return tenantTables[table] } + +// Handler 返回全局钩子 +// 由 dao 扩展模板挂载 +func Handler() gdb.HookHandler { + return gdb.HookHandler{ + Insert: insertHook, + Update: updateHook, + Delete: deleteHook, + } +} + +// ApplyCtx 注入软删除过滤与租户隔离条件 +// +// 由 dao 扩展模板的 Ctx 覆写方法调用,在模型层(SQL 构建之前)注入: +// +// 所有表:del_flag = 0 +// 租户级表:平台级(超管)不过滤,全量可见;租户用户追加 tenant_id IN (0, 自身租户) +func ApplyCtx(ctx context.Context, m *gdb.Model, table string) *gdb.Model { + m = m.Where("del_flag", 0) + if tenantTables[table] && !consts.IsPlatform(ctx) { + m = m.WhereIn("tenant_id", []int{0, consts.CtxTenantID(ctx)}) + } + return m +} + +// auditUser 获取审计操作人 +// +// 优先取用户名,保证租户内稳定可追溯 +func auditUser(ctx context.Context) string { + if u := consts.GetUsername(ctx); u != "" { + return u + } + return consts.GetRealname(ctx) +} + +// isBlank 判断插入值是否为空 +// +// 为空时由钩子补齐 +func isBlank(v any) bool { + if v == nil { + return true + } + if s, ok := v.(string); ok { + return s == "" + } + rv := reflect.ValueOf(v) + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + return rv.IsZero() + } + return false +} + +// hookTable 归一化钩子回调中的表名 +// +// gdb 传入的表名带反引号,如 `goframe_user` +func hookTable(table string) string { + return strings.Trim(table, "`") +} + +// insertHook 插入钩子 +// +// 自动补齐主键、审计字段与租户ID +func insertHook(ctx context.Context, in *gdb.HookInsertInput) (sql.Result, error) { + now := time.Now() + table := hookTable(in.Table) + for _, item := range in.Data { + // 主键交由数据库自增(id 统一 int,不使用雪花算法) + if isBlank(item["del_flag"]) { + item["del_flag"] = 0 + } + if isBlank(item["create_time"]) { + item["create_time"] = now + } + if isBlank(item["create_user"]) { + item["create_user"] = auditUser(ctx) + } + // 租户级表: + // - 租户用户未指定租户ID时自动携带自身(零值视为未指定,防跨租户逃逸) + // - 超管未指定租户ID时代入默认租户(tenant.defaultId); + // 显式传 0 可创建平台共享行,显式传具体租户从显式值 + if tenantTables[table] { + v, ok := item["tenant_id"] + if !consts.IsPlatform(ctx) { + if !ok || isBlank(v) { + item["tenant_id"] = consts.CtxTenantID(ctx) + } + } else if !ok || v == nil { + item["tenant_id"] = consts.DefaultTenantID(ctx) + } + } + } + return in.Next(ctx) +} + +// updateHook 更新钩子 +// +// 自动为审计字段盖章 +func updateHook(ctx context.Context, in *gdb.HookUpdateInput) (sql.Result, error) { + if data, ok := in.Data.(map[string]any); ok { + data["update_time"] = time.Now() + data["update_user"] = auditUser(ctx) + } + return in.Next(ctx) +} + +// deleteHook 删除钩子 +// +// 将物理删除改写为软删除,为架构红线; +// 注意:gdb 的 Model.Update 不接收 ctx,模型已携带上下文设置, +// 传入 ctx 会被误当作数据导致匹配字段失败 +func deleteHook(ctx context.Context, in *gdb.HookDeleteInput) (sql.Result, error) { + return in.Model.Update(map[string]any{"del_flag": 1}) +} diff --git a/internal/router/bind.go b/internal/router/bind.go new file mode 100644 index 0000000..5b0d88a --- /dev/null +++ b/internal/router/bind.go @@ -0,0 +1,169 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package router + +import ( + "fmt" + "os" + "reflect" + "strings" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/net/goai" + + "easygoadmin/internal/consts" + "easygoadmin/internal/router/meta" + "easygoadmin/utility/response" +) + +// ============================== 控制器反射绑定 ============================== + +// BodyUnmarshaler 自定义请求体解析逃生舱 +// +// 当请求体顶层形态无法用结构体标签表达时实现该接口; +// 实现后跳过默认的请求参数解析,gvalid 校验需在实现内自行完成 +type BodyUnmarshaler interface { + UnmarshalBody(r *ghttp.Request) error +} + +// Bind 反射注册控制器 +// +// 基于 gmeta 扩展标签完成路由注册与统一包装: +// +// 1. 扫描控制器导出方法,校验签名: +// func (c *Xxx) Page(ctx context.Context, req *v1.XxxReq) (*v1.XxxRes, error) +// 2. 解析请求结构体内嵌 gmeta.Meta 上的扩展标签: +// method、path、summary、permission、log、logType、public +// 3. 按 method:path 注册路由并写入路由元数据表 +// 4. 统一包装:权限校验 → 参数解析(含 gvalid 校验)→ 调用 → 信封写出 +func Bind(s *ghttp.Server, ctrl any) { + ctrlVal := reflect.ValueOf(ctrl) + ctrlType := reflect.TypeOf(ctrl) + for i := 0; i < ctrlType.NumMethod(); i++ { + m := ctrlType.Method(i) + // 目标签名:(ctx, *Req) (*Res, error);In(0) 为接收者 + if m.Type.NumIn() != 3 || m.Type.NumOut() != 2 { + continue + } + reqType := m.Type.In(2) + if reqType.Kind() != reflect.Ptr { + continue + } + metaField, ok := reqType.Elem().FieldByName("Meta") + if !ok { + continue + } + tag := metaField.Tag + method := strings.ToLower(tag.Get("method")) + path := tag.Get("path") + if method == "" || path == "" { + continue + } + rm := meta.RouteMeta{ + Method: strings.ToUpper(method), + Path: path, + Summary: tag.Get("summary"), + Permission: tag.Get("permission"), + Log: tag.Get("log"), + LogType: tag.Get("logType"), + Public: tag.Get("public") == "true", + } + meta.Register(rm) + // 补注册进 OpenAPI 文档:闭包路由非严格路由,gf 不会自动收集, + // 此处将带类型的控制器方法(签名 func(ctx,*Req)(*Res,error))注册进 goai, + // gmeta 的 path/method/summary 标签由 goai 解析为文档条目 + if err := s.GetOpenApi().Add(goai.AddInput{Object: ctrlVal.Method(i).Interface()}); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "openapi 注册失败 %s: %v\n", path, err) + } + handler := makeHandler(ctrlVal.Method(i), reqType, rm) + s.BindHandler(method+":"+path, handler) + } +} + +// makeHandler 构造统一请求处理包装 +// +// 依次执行权限校验、参数解析、业务调用与响应写出 +func makeHandler(method reflect.Value, reqType reflect.Type, rm meta.RouteMeta) func(r *ghttp.Request) { + return func(r *ghttp.Request) { + // 路由元数据入上下文(操作日志读取) + r.SetCtxVar(meta.CtxKey, rm) + + // 权限校验(与日志元数据同源;permission 为空 = 登录即可) + if rm.Permission != "" && !consts.HasPerm(r.Context(), rm.Permission) { + response.Write(r, response.Fail(gerror.NewCode(consts.CodeForbidden))) + return + } + + // 参数解析:路由参数 + query + body,并执行 gvalid 校验 + // 逃生舱:请求对象实现 BodyUnmarshaler 时走自定义解析(body 形态不规则的接口,如平铺 map) + req := reflect.New(reqType.Elem()) + if u, ok := req.Interface().(BodyUnmarshaler); ok { + if err := u.UnmarshalBody(r); err != nil { + response.Write(r, response.Fail(err)) + return + } + } else if err := r.Parse(req.Interface()); err != nil { + response.Write(r, response.Fail(err)) + return + } + + // 调用业务方法 + out := method.Call([]reflect.Value{ + reflect.ValueOf(r.Context()), + req, + }) + var err error + if !out[1].IsNil() { + err, _ = out[1].Interface().(error) + } + if err != nil { + response.Write(r, response.Fail(err)) + return + } + if out[0].IsNil() { + response.Write(r, response.Ok(nil)) + return + } + response.Write(r, response.Ok(out[0].Interface())) + } +} diff --git a/internal/router/meta/meta.go b/internal/router/meta/meta.go new file mode 100644 index 0000000..3e446bf --- /dev/null +++ b/internal/router/meta/meta.go @@ -0,0 +1,112 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package meta + +import ( + "sync" +) + +// ============================== 路由元数据 ============================== + +// RouteMeta 路由元数据 +// +// 由请求结构体内嵌 gmeta.Meta 的扩展标签在注册时反射解析得出, +// 权限校验、操作日志等横切能力按此元数据统一处理。 +type RouteMeta struct { + Method string `json:"method"` // 请求方法:GET/POST/PUT/DELETE + Path string `json:"path"` // 路由模板,如 /api/user/detail/:id + Summary string `json:"summary"` // 接口摘要(OpenAPI) + Permission string `json:"permission"` // 权限节点 sys:user:page;空表示登录即可 + Log string `json:"log"` // 操作日志标题;空表示不记录 + LogType string `json:"logType"` // 日志类型:query/add/update/delete/import/export/reset + Public bool `json:"public"` // 是否免登录(登录、验证码等) +} + +// CtxKey 路由元数据写入请求上下文的键 +// +// 由绑定层写入,操作日志中间件读取 +var CtxKey = struct{ name string }{"routeMeta"} + +// store 路由元数据存储 +// 使用读写锁保护并发访问 +type store struct { + mu sync.RWMutex + meta map[string]RouteMeta // key: "GET /api/user/page" +} + +// s 全局路由元数据存储实例 +var s = &store{meta: make(map[string]RouteMeta)} + +// key 生成存储键 +func key(method, path string) string { + return method + " " + path +} + +// Register 注册路由元数据 +// +// 由绑定层反射时调用 +func Register(m RouteMeta) { + s.mu.Lock() + defer s.mu.Unlock() + s.meta[key(m.Method, m.Path)] = m +} + +// Lookup 查询路由元数据 +func Lookup(method, path string) (RouteMeta, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + m, ok := s.meta[key(method, path)] + return m, ok +} + +// All 返回全部路由元数据 +// +// 供调试与文档生成使用 +func All() map[string]RouteMeta { + s.mu.RLock() + defer s.mu.RUnlock() + out := make(map[string]RouteMeta, len(s.meta)) + for k, v := range s.meta { + out[k] = v + } + return out +} diff --git a/internal/router/router.go b/internal/router/router.go new file mode 100644 index 0000000..2717f1f --- /dev/null +++ b/internal/router/router.go @@ -0,0 +1,151 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package router + +import ( + "fmt" + + "github.com/gogf/gf/v2/net/ghttp" + + "easygoadmin/internal/controller" + "easygoadmin/internal/middleware" + "easygoadmin/utility/response" +) + +// ============================== 路由注册 ============================== + +// errorf 构造状态页错误 +// +// 响应层将其映射为 code=1 并附上消息 +func errorf(format string, args ...any) error { + return fmt.Errorf(format, args...) +} + +// Register 注册全局中间件链与路由 +// +// 中间件顺序:CORS → Response(panic 兜底)→ RateLimit(限流)→ Demo(演示拦截)→ JWT → Tenant → OperationLog → 处理器。 +// 权限校验在 Bind 包装层完成,与路由元数据同源;操作日志按元数据异步落库。 +func Register(s *ghttp.Server) { + s.Use( + middleware.CORS, + middleware.Response, + middleware.RateLimit, + middleware.Demo, + middleware.JWT, + middleware.Tenant, + middleware.OperationLog, + ) + + // 非 200 状态统一输出 JSON(404/500 不返回 HTML) + s.BindStatusHandler(404, func(r *ghttp.Request) { + response.Write(r, response.Fail(errorf("接口不存在: %s %s", r.Method, r.URL.Path))) + }) + s.BindStatusHandler(500, func(r *ghttp.Request) { + response.Write(r, response.Fail(errorf("服务器内部错误"))) + }) + + // 静态文件:将 resource 目录作为静态资源根目录对外访问 + s.SetServerRoot("resource") + + // 健康检查(免登录;公开路由不带 /v1,见 middleware/public.go) + s.BindHandler("get:/api/health", func(r *ghttp.Request) { + response.Write(r, response.Ok(map[string]any{"status": "up"})) + }) + + // API 文档页(本地托管 Redoc,静态资源由 SetServerRoot("resource") 提供) + s.BindHandler("get:/swagger", func(r *ghttp.Request) { + r.Response.RedirectTo("/swagger/index.html") + }) + + registerControllers(s) +} + +// registerControllers 注册各模块控制器 +// +// 随实施进度追加 +func registerControllers(s *ghttp.Server) { + // —— auth 认证域 —— + Bind(s, new(controller.CAuth)) + + // —— index 个人中心 —— + Bind(s, new(controller.CIndex)) + + // —— system 域 —— + Bind(s, new(controller.CUser)) + Bind(s, new(controller.CRole)) + Bind(s, new(controller.CMenu)) + Bind(s, new(controller.CDept)) + Bind(s, new(controller.CLevel)) + Bind(s, new(controller.CPosition)) + Bind(s, new(controller.CTenant)) + + // —— data 域 —— + Bind(s, new(controller.CConfig)) + Bind(s, new(controller.CConfigItem)) + Bind(s, new(controller.CParam)) + Bind(s, new(controller.CDict)) + Bind(s, new(controller.CDictItem)) + Bind(s, new(controller.CNotice)) + Bind(s, new(controller.CCity)) + + // —— content 域 —— + Bind(s, new(controller.CArticle)) + Bind(s, new(controller.CCategory)) + Bind(s, new(controller.CLink)) + + // —— monitor 域 —— + Bind(s, new(controller.COperLog)) + Bind(s, new(controller.CLoginLog)) + Bind(s, new(controller.CJob)) + Bind(s, new(controller.CJobLog)) + Bind(s, new(controller.COnlineUser)) + Bind(s, new(controller.CServerMonitor)) + Bind(s, new(controller.CCacheMonitor)) + + // —— tool 域 —— + Bind(s, new(controller.CFileTemplate)) + Bind(s, new(controller.CGenerator)) + Bind(s, new(controller.CUpload)) + + // —— tool 域(代码生成器注册示例) —— + Bind(s, new(controller.CExample)) +} diff --git a/internal/service/article.go b/internal/service/article.go new file mode 100644 index 0000000..9e252fc --- /dev/null +++ b/internal/service/article.go @@ -0,0 +1,91 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// IArticle 文章服务接口 +type IArticle interface { + // Page 文章分页 + Page(ctx context.Context, req *v1.ArticlePageReq) (res *v1.ArticlePageRes, err error) + // List 文章列表 + List(ctx context.Context, req *v1.ArticleListReq) (res v1.ArticleListRes, err error) + // Detail 文章详情 + Detail(ctx context.Context, req *v1.ArticleDetailReq) (res *v1.ArticleDetailRes, err error) + // Add 新增文章 + Add(ctx context.Context, req *v1.ArticleAddReq) (res *v1.IdRes, err error) + // Update 修改文章 + Update(ctx context.Context, req *v1.ArticleUpdateReq) (res *v1.ArticleUpdateRes, err error) + // Status 文章状态 + Status(ctx context.Context, req *v1.ArticleStatusReq) (res *v1.ArticleStatusRes, err error) + // Delete 删除文章 + Delete(ctx context.Context, req *v1.ArticleDeleteReq) (res *v1.ArticleDeleteRes, err error) + // BatchDelete 批量删除文章 + BatchDelete(ctx context.Context, req *v1.ArticleBatchDeleteReq) (res *v1.ArticleBatchDeleteRes, err error) +} + +// localArticle 文章服务实现实例 +var localArticle IArticle + +// Article 获取文章服务实例 +func Article() IArticle { + if localArticle == nil { + panic("implement not found for interface IArticle, forgot register?") + } + return localArticle +} + +// RegisterArticle 注册文章服务实现 +func RegisterArticle(i IArticle) { + localArticle = i +} diff --git a/internal/service/auth.go b/internal/service/auth.go new file mode 100644 index 0000000..f3262f1 --- /dev/null +++ b/internal/service/auth.go @@ -0,0 +1,96 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// IAuth 认证服务接口 +type ( + IAuth interface { + // Login 登录 + // 流程:验证码校验 → 定位用户 → 状态与租户校验 → bcrypt 密码校验 → 签发双令牌 → 写入登录日志 + Login(ctx context.Context, req *v1.LoginReq) (res *v1.LoginRes, err error) + // Logout 登出 + // 将当前访问令牌的 jti 加入黑名单,并写入登录日志 + Logout(ctx context.Context, req *v1.LogoutReq) (res *v1.LogoutRes, err error) + // Captcha 生成图形验证码 + Captcha(ctx context.Context, req *v1.CaptchaReq) (res *v1.CaptchaRes, err error) + // Refresh 刷新令牌 + // 支持 refresh_token 轮换,并兼容密码模式兑换 + Refresh(ctx context.Context, req *v1.RefreshReq) (res *v1.RefreshRes, err error) + // CheckToken 校验令牌有效性 + // 身份认证中间件已通过即视为有效 + CheckToken(ctx context.Context, req *v1.CheckTokenReq) (res *v1.CheckTokenRes, err error) + // RemoveToken 撤销指定令牌 + // 将指定令牌加入黑名单,实现踢下线 + RemoveToken(ctx context.Context, req *v1.RemoveTokenReq) (res *v1.RemoveTokenRes, err error) + } +) + +// localAuth 认证服务实现实例 +var ( + localAuth IAuth +) + +// Auth 获取认证服务实例 +func Auth() IAuth { + if localAuth == nil { + panic("implement not found for interface IAuth, forgot register?") + } + return localAuth +} + +// RegisterAuth 注册认证服务实现 +func RegisterAuth(i IAuth) { + localAuth = i +} diff --git a/internal/service/category.go b/internal/service/category.go new file mode 100644 index 0000000..60d4b31 --- /dev/null +++ b/internal/service/category.go @@ -0,0 +1,92 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// ICategory 分类服务接口 +type ICategory interface { + // Page 分类分页 + // 返回平铺的分类列表 + Page(ctx context.Context, req *v1.CategoryPageReq) (res *v1.CategoryPageRes, err error) + // List 分类平铺列表 + // 树形结构由前端构建 + List(ctx context.Context, req *v1.CategoryListReq) (res v1.CategoryListRes, err error) + // Detail 分类详情 + Detail(ctx context.Context, req *v1.CategoryDetailReq) (res *v1.CategoryDetailRes, err error) + // Add 新增分类 + Add(ctx context.Context, req *v1.CategoryAddReq) (res *v1.IdRes, err error) + // Update 修改分类 + Update(ctx context.Context, req *v1.CategoryUpdateReq) (res *v1.CategoryUpdateRes, err error) + // Delete 删除分类 + // 存在子分类或文章引用时禁止删除 + Delete(ctx context.Context, req *v1.CategoryDeleteReq) (res *v1.CategoryDeleteRes, err error) + // BatchDelete 批量删除分类 + BatchDelete(ctx context.Context, req *v1.CategoryBatchDeleteReq) (res *v1.CategoryBatchDeleteRes, err error) +} + +// localCategory 分类服务实现实例 +var localCategory ICategory + +// Category 获取分类服务实例 +func Category() ICategory { + if localCategory == nil { + panic("implement not found for interface ICategory, forgot register?") + } + return localCategory +} + +// RegisterCategory 注册分类服务实现 +func RegisterCategory(i ICategory) { + localCategory = i +} diff --git a/internal/service/city.go b/internal/service/city.go new file mode 100644 index 0000000..f79d114 --- /dev/null +++ b/internal/service/city.go @@ -0,0 +1,99 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// ICity 城市服务接口 +type ICity interface { + // Page 城市分页 + Page(ctx context.Context, req *v1.CityPageReq) (res *v1.CityPageRes, err error) + // List 城市列表 + // 按父级编码查询,供级联选择器使用 + List(ctx context.Context, req *v1.CityListReq) (res v1.CityListRes, err error) + // Detail 城市详情 + Detail(ctx context.Context, req *v1.CityDetailReq) (res *v1.CityDetailRes, err error) + // Add 新增城市 + Add(ctx context.Context, req *v1.CityAddReq) (res *v1.IdRes, err error) + // Update 修改城市 + Update(ctx context.Context, req *v1.CityUpdateReq) (res *v1.CityUpdateRes, err error) + // Delete 删除城市 + Delete(ctx context.Context, req *v1.CityDeleteReq) (res *v1.CityDeleteRes, err error) + // BatchDelete 批量删除城市 + BatchDelete(ctx context.Context, req *v1.CityBatchDeleteReq) (res *v1.CityBatchDeleteRes, err error) + // ByParent 按父编码查询下级城市 + // 登录用户即可访问 + ByParent(ctx context.Context, req *v1.CityByParentReq) (res *v1.CityByParentRes, err error) + // AreaProvince 查询省份列表 + // 登录用户即可访问 + AreaProvince(ctx context.Context, req *v1.AreaParentReq) (res *v1.AreaParentRes, err error) + // GetCityList 按父编码获取下级城市 + // 登录用户即可访问,供级联选择器懒加载 + GetCityList(ctx context.Context, req *v1.CityGetListReq) (res v1.CityGetListRes, err error) +} + +// localCity 城市服务实现实例 +var localCity ICity + +// City 获取城市服务实例 +func City() ICity { + if localCity == nil { + panic("implement not found for interface ICity, forgot register?") + } + return localCity +} + +// RegisterCity 注册城市服务实现 +func RegisterCity(i ICity) { + localCity = i +} diff --git a/internal/service/config.go b/internal/service/config.go new file mode 100644 index 0000000..7f11c65 --- /dev/null +++ b/internal/service/config.go @@ -0,0 +1,98 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// IConfig 配置服务接口 +type IConfig interface { + // Page 配置分页 + Page(ctx context.Context, req *v1.ConfigPageReq) (res *v1.ConfigPageRes, err error) + // List 配置列表 + // 每项携带其下的配置项集合 + List(ctx context.Context, req *v1.ConfigListReq) (res v1.ConfigListRes, err error) + // Detail 配置详情 + // 包含配置项集合 + Detail(ctx context.Context, req *v1.ConfigDetailReq) (res *v1.ConfigDetailRes, err error) + // Add 新增配置 + Add(ctx context.Context, req *v1.ConfigAddReq) (res *v1.IdRes, err error) + // Update 修改配置 + Update(ctx context.Context, req *v1.ConfigUpdateReq) (res *v1.ConfigUpdateRes, err error) + // Delete 删除配置 + // 连带删除其下配置项 + Delete(ctx context.Context, req *v1.ConfigDeleteReq) (res *v1.ConfigDeleteRes, err error) + // BatchDelete 批量删除配置 + BatchDelete(ctx context.Context, req *v1.ConfigBatchDeleteReq) (res *v1.ConfigBatchDeleteRes, err error) + // WebIndex 网站配置聚合 + // 按配置分组返回,供网站配置页渲染 + WebIndex(ctx context.Context, req *v1.ConfigWebIndexReq) (res *v1.ConfigWebIndexRes, err error) + // WebSave 批量保存网站配置 + // 接收配置项编码到值的映射 + WebSave(ctx context.Context, req *v1.ConfigWebSaveReq) (res *v1.ConfigWebSaveRes, err error) +} + +// localConfig 配置服务实现实例 +var localConfig IConfig + +// Config 获取配置服务实例 +func Config() IConfig { + if localConfig == nil { + panic("implement not found for interface IConfig, forgot register?") + } + return localConfig +} + +// RegisterConfig 注册配置服务实现 +func RegisterConfig(i IConfig) { + localConfig = i +} diff --git a/internal/service/config_item.go b/internal/service/config_item.go new file mode 100644 index 0000000..22bbc5f --- /dev/null +++ b/internal/service/config_item.go @@ -0,0 +1,90 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// IConfigItem 配置项服务接口 +type IConfigItem interface { + // Page 配置项分页 + // 补充冗余的所属配置名 + Page(ctx context.Context, req *v1.ConfigItemPageReq) (res *v1.ConfigItemPageRes, err error) + // List 配置项列表 + List(ctx context.Context, req *v1.ConfigItemListReq) (res v1.ConfigItemListRes, err error) + // Detail 配置项详情 + Detail(ctx context.Context, req *v1.ConfigItemDetailReq) (res *v1.ConfigItemDetailRes, err error) + // Add 新增配置项 + Add(ctx context.Context, req *v1.ConfigItemAddReq) (res *v1.IdRes, err error) + // Update 修改配置项 + Update(ctx context.Context, req *v1.ConfigItemUpdateReq) (res *v1.ConfigItemUpdateRes, err error) + // Delete 删除配置项 + Delete(ctx context.Context, req *v1.ConfigItemDeleteReq) (res *v1.ConfigItemDeleteRes, err error) + // BatchDelete 批量删除配置项 + BatchDelete(ctx context.Context, req *v1.ConfigItemBatchDeleteReq) (res *v1.ConfigItemBatchDeleteRes, err error) +} + +// localConfigItem 配置项服务实现实例 +var localConfigItem IConfigItem + +// ConfigItem 获取配置项服务实例 +func ConfigItem() IConfigItem { + if localConfigItem == nil { + panic("implement not found for interface IConfigItem, forgot register?") + } + return localConfigItem +} + +// RegisterConfigItem 注册配置项服务实现 +func RegisterConfigItem(i IConfigItem) { + localConfigItem = i +} diff --git a/internal/service/dept.go b/internal/service/dept.go new file mode 100644 index 0000000..bbee3a7 --- /dev/null +++ b/internal/service/dept.go @@ -0,0 +1,93 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// IDept 部门服务接口 +type IDept interface { + // Page 部门分页 + // 返回平铺的部门列表 + Page(ctx context.Context, req *v1.DeptPageReq) (res *v1.DeptPageRes, err error) + // List 部门平铺列表 + // 树形结构由前端构建 + List(ctx context.Context, req *v1.DeptListReq) (res v1.DeptListRes, err error) + // Detail 部门详情 + Detail(ctx context.Context, req *v1.DeptDetailReq) (res *v1.DeptDetailRes, err error) + // Add 新增部门 + Add(ctx context.Context, req *v1.DeptAddReq) (res *v1.IdRes, err error) + // Update 修改部门 + // 禁止将上级设置为自身或后代节点 + Update(ctx context.Context, req *v1.DeptUpdateReq) (res *v1.DeptUpdateRes, err error) + // Delete 删除部门 + // 存在子部门或用户引用时禁止删除 + Delete(ctx context.Context, req *v1.DeptDeleteReq) (res *v1.DeptDeleteRes, err error) + // BatchDelete 批量删除部门 + BatchDelete(ctx context.Context, req *v1.DeptBatchDeleteReq) (res *v1.DeptBatchDeleteRes, err error) +} + +// localDept 部门服务实现实例 +var localDept IDept + +// Dept 获取部门服务实例 +func Dept() IDept { + if localDept == nil { + panic("implement not found for interface IDept, forgot register?") + } + return localDept +} + +// RegisterDept 注册部门服务实现 +func RegisterDept(i IDept) { + localDept = i +} diff --git a/internal/service/dict.go b/internal/service/dict.go new file mode 100644 index 0000000..1c1f61c --- /dev/null +++ b/internal/service/dict.go @@ -0,0 +1,98 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// IDict 字典服务接口 +type IDict interface { + // Page 字典分页 + Page(ctx context.Context, req *v1.DictPageReq) (res *v1.DictPageRes, err error) + // Detail 字典详情 + // 包含字典项集合 + Detail(ctx context.Context, req *v1.DictDetailReq) (res *v1.DictDetailRes, err error) + // Add 新增字典 + Add(ctx context.Context, req *v1.DictAddReq) (res *v1.IdRes, err error) + // Update 修改字典 + Update(ctx context.Context, req *v1.DictUpdateReq) (res *v1.DictUpdateRes, err error) + // Delete 删除字典 + // 连带删除其下字典项 + Delete(ctx context.Context, req *v1.DictDeleteReq) (res *v1.DictDeleteRes, err error) + // BatchDelete 批量删除字典 + BatchDelete(ctx context.Context, req *v1.DictBatchDeleteReq) (res *v1.DictBatchDeleteRes, err error) + // RefreshCache 刷新字典缓存 + // 重写 Redis 缓存 + RefreshCache(ctx context.Context, req *v1.DictRefreshCacheReq) (res *v1.DictRefreshCacheRes, err error) + // AliasList 字典别名列表 + // 登录用户即可访问 + AliasList(ctx context.Context, req *v1.DictAliasListReq) (res v1.DictAliasListRes, err error) + // AliasInfo 字典别名详情 + // 登录用户即可访问,缓存优先 + AliasInfo(ctx context.Context, req *v1.DictAliasInfoReq) (res *v1.DictAliasInfoRes, err error) +} + +// localDict 字典服务实现实例 +var localDict IDict + +// Dict 获取字典服务实例 +func Dict() IDict { + if localDict == nil { + panic("implement not found for interface IDict, forgot register?") + } + return localDict +} + +// RegisterDict 注册字典服务实现 +func RegisterDict(i IDict) { + localDict = i +} diff --git a/internal/service/dict_item.go b/internal/service/dict_item.go new file mode 100644 index 0000000..3d93341 --- /dev/null +++ b/internal/service/dict_item.go @@ -0,0 +1,91 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// IDictItem 字典项服务接口 +type IDictItem interface { + // Page 字典项分页 + // 补充冗余的字典名与字典编码 + Page(ctx context.Context, req *v1.DictItemPageReq) (res *v1.DictItemPageRes, err error) + // Detail 字典项详情 + Detail(ctx context.Context, req *v1.DictItemDetailReq) (res *v1.DictItemDetailRes, err error) + // Add 新增字典项 + Add(ctx context.Context, req *v1.DictItemAddReq) (res *v1.IdRes, err error) + // Update 修改字典项 + Update(ctx context.Context, req *v1.DictItemUpdateReq) (res *v1.DictItemUpdateRes, err error) + // Delete 删除字典项 + Delete(ctx context.Context, req *v1.DictItemDeleteReq) (res *v1.DictItemDeleteRes, err error) + // BatchDelete 批量删除字典项 + BatchDelete(ctx context.Context, req *v1.DictItemBatchDeleteReq) (res *v1.DictItemBatchDeleteRes, err error) + // QuickList 快捷字典项列表 + // 登录用户即可访问,按字典编码查询 + QuickList(ctx context.Context, req *v1.DictItemQuickListReq) (res v1.DictItemQuickListRes, err error) +} + +// localDictItem 字典项服务实现实例 +var localDictItem IDictItem + +// DictItem 获取字典项服务实例 +func DictItem() IDictItem { + if localDictItem == nil { + panic("implement not found for interface IDictItem, forgot register?") + } + return localDictItem +} + +// RegisterDictItem 注册字典项服务实现 +func RegisterDictItem(i IDictItem) { + localDictItem = i +} diff --git a/internal/service/example.go b/internal/service/example.go new file mode 100644 index 0000000..6abbee8 --- /dev/null +++ b/internal/service/example.go @@ -0,0 +1,104 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package service + +import ( + "context" + + "easygoadmin/api/v1" +) + +// 按模块独立拆分(代码生成器产出)。 +// logic 方法签名变更时需同步维护本文件(不再整体运行 gf gen service)。 + +// ============================== 案例演示服务接口 ============================== + +// IExample 案例演示服务接口 +// 实现位于 internal/logic/example.go +type IExample interface { + // Page 案例演示分页 + // 按查询条件分页返回案例演示列表 + Page(ctx context.Context, req *v1.ExamplePageReq) (res *v1.ExamplePageRes, err error) + // List 案例演示平铺列表 + // data 直接为数组,树形结构由前端构建 + List(ctx context.Context, req *v1.ExampleListReq) (res v1.ExampleListRes, err error) + // Detail 案例演示详情 + // 按主键ID查询详情 + Detail(ctx context.Context, req *v1.ExampleDetailReq) (res *v1.ExampleDetailRes, err error) + // Add 新增案例演示 + // 新增成功后返回新记录ID + Add(ctx context.Context, req *v1.ExampleAddReq) (res *v1.ExampleIdRes, err error) + // Update 修改案例演示 + // 按主键ID更新记录 + Update(ctx context.Context, req *v1.ExampleUpdateReq) (res *v1.ExampleUpdateRes, err error) + // Status 案例演示状态 + // 设置记录的状态字段 + Status(ctx context.Context, req *v1.ExampleStatusReq) (res *v1.ExampleStatusRes, err error) + // Delete 删除案例演示 + // 按主键ID删除单条记录 + Delete(ctx context.Context, req *v1.ExampleDeleteReq) (res *v1.ExampleDeleteRes, err error) + // BatchDelete 批量删除案例演示 + // 按主键ID列表批量删除记录 + BatchDelete(ctx context.Context, req *v1.ExampleBatchDeleteReq) (res *v1.ExampleBatchDeleteRes, err error) + // Import 导入案例演示 + // 接收上传的 xls/xlsx 文件并批量导入数据 + Import(ctx context.Context, req *v1.ExampleImportReq) (res *v1.ExampleImportRes, err error) + // Export 导出案例演示 + // 按查询条件导出数据并返回文件访问地址 + Export(ctx context.Context, req *v1.ExampleExportReq) (res *v1.ExampleExportRes, err error) +} + +// localExample 案例演示服务实现实例 +var localExample IExample + +// Example 获取案例演示服务实例 +func Example() IExample { + if localExample == nil { + panic("implement not found for interface IExample, forgot register?") + } + return localExample +} + +// RegisterExample 注册案例演示服务实现 +func RegisterExample(i IExample) { + localExample = i +} diff --git a/internal/service/file_template.go b/internal/service/file_template.go new file mode 100644 index 0000000..b8519f8 --- /dev/null +++ b/internal/service/file_template.go @@ -0,0 +1,92 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// IFileTemplate 文件模板服务接口 +type IFileTemplate interface { + // Page 文件模板分页 + Page(ctx context.Context, req *v1.FileTemplatePageReq) (res *v1.FileTemplatePageRes, err error) + // List 文件模板列表 + List(ctx context.Context, req *v1.FileTemplateListReq) (res v1.FileTemplateListRes, err error) + // Detail 文件模板详情 + Detail(ctx context.Context, req *v1.FileTemplateDetailReq) (res *v1.FileTemplateDetailRes, err error) + // Add 新增文件模板 + Add(ctx context.Context, req *v1.FileTemplateAddReq) (res *v1.IdRes, err error) + // Update 修改文件模板 + Update(ctx context.Context, req *v1.FileTemplateUpdateReq) (res *v1.FileTemplateUpdateRes, err error) + // Delete 删除文件模板 + Delete(ctx context.Context, req *v1.FileTemplateDeleteReq) (res *v1.FileTemplateDeleteRes, err error) + // BatchDelete 批量删除文件模板 + BatchDelete(ctx context.Context, req *v1.FileTemplateBatchDeleteReq) (res *v1.FileTemplateBatchDeleteRes, err error) + // ByCode 按编码查询模板 + // 登录用户即可访问 + ByCode(ctx context.Context, req *v1.FileTemplateByCodeReq) (res *v1.FileTemplateByCodeRes, err error) +} + +// localFileTemplate 文件模板服务实现实例 +var localFileTemplate IFileTemplate + +// FileTemplate 获取文件模板服务实例 +func FileTemplate() IFileTemplate { + if localFileTemplate == nil { + panic("implement not found for interface IFileTemplate, forgot register?") + } + return localFileTemplate +} + +// RegisterFileTemplate 注册文件模板服务实现 +func RegisterFileTemplate(i IFileTemplate) { + localFileTemplate = i +} diff --git a/internal/service/generator.go b/internal/service/generator.go new file mode 100644 index 0000000..53baeff --- /dev/null +++ b/internal/service/generator.go @@ -0,0 +1,87 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// IGenerator 代码生成服务接口 +type IGenerator interface { + // Page 数据表分页 + // MySQL 查询 information_schema,其他方言使用 gdb 元数据降级 + Page(ctx context.Context, req *v1.GeneratorPageReq) (res *v1.GeneratorPageRes, err error) + // Preview 预览代码 + // 渲染模板但不落盘 + Preview(ctx context.Context, req *v1.GeneratorPreviewReq) (res *v1.GeneratorPreviewRes, err error) + // Generate 一键生成 + // 已存在的文件默认跳过 + Generate(ctx context.Context, req *v1.GeneratorGenerateReq) (res *v1.GeneratorGenerateRes, err error) + // BatchGenerate 批量生成 + // 逐表渲染并统计结果 + BatchGenerate(ctx context.Context, req *v1.GeneratorBatchGenerateReq) (res *v1.GeneratorBatchGenerateRes, err error) +} + +// localGenerator 代码生成服务实现实例 +var localGenerator IGenerator + +// Generator 获取代码生成服务实例 +func Generator() IGenerator { + if localGenerator == nil { + panic("implement not found for interface IGenerator, forgot register?") + } + return localGenerator +} + +// RegisterGenerator 注册代码生成服务实现 +func RegisterGenerator(i IGenerator) { + localGenerator = i +} diff --git a/internal/service/index.go b/internal/service/index.go new file mode 100644 index 0000000..20e8b3a --- /dev/null +++ b/internal/service/index.go @@ -0,0 +1,94 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// IIndex 个人中心与菜单聚合服务接口 +type ( + IIndex interface { + // GetUser 获取当前登录用户信息 + // 敏感字段永不返回,角色与权限来自身份加载器 + GetUser(ctx context.Context, req *v1.GetUserReq) (res *v1.GetUserRes, err error) + // GetMenus 获取当前用户菜单树 + // 关联路径:user → user_role → role → role_menu → menu,超管返回全量 + GetMenus(ctx context.Context, req *v1.GetMenusReq) (res *v1.GetMenusRes, err error) + // GetMenusById 获取当前用户菜单树(带路径参数) + // 与无路径参数的菜单路由共用同一处理逻辑 + GetMenusById(ctx context.Context, req *v1.GetMenusByIdReq) (res *v1.GetMenusRes, err error) + // UpdatePassword 修改个人密码 + // 校验旧密码后使用 bcrypt 重写 + UpdatePassword(ctx context.Context, req *v1.UpdatePasswordReq) (res *v1.UpdatePasswordRes, err error) + // UpdateProfile 更新个人信息 + // 审计字段由全局钩子盖章 + UpdateProfile(ctx context.Context, req *v1.UpdateProfileReq) (res *v1.UpdateProfileRes, err error) + } +) + +// localIndex 个人中心与菜单聚合服务实现实例 +var ( + localIndex IIndex +) + +// Index 获取个人中心与菜单聚合服务实例 +func Index() IIndex { + if localIndex == nil { + panic("implement not found for interface IIndex, forgot register?") + } + return localIndex +} + +// RegisterIndex 注册个人中心与菜单聚合服务实现 +func RegisterIndex(i IIndex) { + localIndex = i +} diff --git a/internal/service/job.go b/internal/service/job.go new file mode 100644 index 0000000..536db4f --- /dev/null +++ b/internal/service/job.go @@ -0,0 +1,105 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// IJob 定时任务服务接口 +type IJob interface { + // Page 任务分页 + Page(ctx context.Context, req *v1.JobPageReq) (res *v1.JobPageRes, err error) + // List 任务列表 + List(ctx context.Context, req *v1.JobListReq) (res v1.JobListRes, err error) + // Detail 任务详情 + Detail(ctx context.Context, req *v1.JobDetailReq) (res *v1.JobDetailRes, err error) + // Add 新增任务 + // 默认未发布状态,发布操作走 Status 或 Resume 方法 + Add(ctx context.Context, req *v1.JobAddReq) (res *v1.IdRes, err error) + // Update 修改任务 + // 运行中任务先摘除再重挂 + Update(ctx context.Context, req *v1.JobUpdateReq) (res *v1.JobUpdateRes, err error) + // Delete 删除任务 + // 软删记录并摘除调度 + Delete(ctx context.Context, req *v1.JobDeleteReq) (res *v1.JobDeleteRes, err error) + // BatchDelete 批量删除任务 + BatchDelete(ctx context.Context, req *v1.JobBatchDeleteReq) (res *v1.JobBatchDeleteRes, err error) + // RunOnce 立即执行一次 + // 异步触发 + RunOnce(ctx context.Context, req *v1.JobRunOnceReq) (res *v1.JobRunOnceRes, err error) + // Pause 暂停任务 + Pause(ctx context.Context, req *v1.JobPauseReq) (res *v1.JobPauseRes, err error) + // Resume 恢复任务 + // 重新挂载调度 + Resume(ctx context.Context, req *v1.JobResumeReq) (res *v1.JobResumeRes, err error) + // Status 设置任务状态 + // 运行中挂载,暂停与未发布状态摘除调度 + Status(ctx context.Context, req *v1.JobStatusReq) (res *v1.JobStatusRes, err error) + // DaemonStatus 查询调度器状态 + DaemonStatus(ctx context.Context, req *v1.JobDaemonStatusReq) (res *v1.JobDaemonStatusRes, err error) +} + +// localJob 定时任务服务实现实例 +var localJob IJob + +// Job 获取定时任务服务实例 +func Job() IJob { + if localJob == nil { + panic("implement not found for interface IJob, forgot register?") + } + return localJob +} + +// RegisterJob 注册定时任务服务实现 +func RegisterJob(i IJob) { + localJob = i +} diff --git a/internal/service/job_log.go b/internal/service/job_log.go new file mode 100644 index 0000000..eca7751 --- /dev/null +++ b/internal/service/job_log.go @@ -0,0 +1,85 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// IJobLog 任务日志服务接口 +type IJobLog interface { + // Page 任务日志分页 + Page(ctx context.Context, req *v1.JobLogPageReq) (res *v1.JobLogPageRes, err error) + // List 任务日志列表 + List(ctx context.Context, req *v1.JobLogListReq) (res v1.JobLogListRes, err error) + // Detail 任务日志详情 + Detail(ctx context.Context, req *v1.JobLogDetailReq) (res *v1.JobLogDetailRes, err error) + // Delete 删除任务日志 + Delete(ctx context.Context, req *v1.JobLogDeleteReq) (res *v1.JobLogDeleteRes, err error) + // BatchDelete 批量删除任务日志 + BatchDelete(ctx context.Context, req *v1.JobLogBatchDeleteReq) (res *v1.JobLogBatchDeleteRes, err error) +} + +// localJobLog 任务日志服务实现实例 +var localJobLog IJobLog + +// JobLog 获取任务日志服务实例 +func JobLog() IJobLog { + if localJobLog == nil { + panic("implement not found for interface IJobLog, forgot register?") + } + return localJobLog +} + +// RegisterJobLog 注册任务日志服务实现 +func RegisterJobLog(i IJobLog) { + localJobLog = i +} diff --git a/internal/service/level.go b/internal/service/level.go new file mode 100644 index 0000000..f7c918e --- /dev/null +++ b/internal/service/level.go @@ -0,0 +1,98 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// ILevel 职级服务接口 +type ILevel interface { + // Page 职级分页 + // 分页结果包含关联用户数 + Page(ctx context.Context, req *v1.LevelPageReq) (res *v1.LevelPageRes, err error) + // List 职级全量列表 + List(ctx context.Context, req *v1.LevelListReq) (res v1.LevelListRes, err error) + // Detail 职级详情 + Detail(ctx context.Context, req *v1.LevelDetailReq) (res *v1.LevelDetailRes, err error) + // Add 新增职级 + Add(ctx context.Context, req *v1.LevelAddReq) (res *v1.IdRes, err error) + // Update 修改职级 + Update(ctx context.Context, req *v1.LevelUpdateReq) (res *v1.LevelUpdateRes, err error) + // Status 职级状态 + Status(ctx context.Context, req *v1.LevelStatusReq) (res *v1.LevelStatusRes, err error) + // Delete 删除职级 + // 存在用户引用时禁止删除 + Delete(ctx context.Context, req *v1.LevelDeleteReq) (res *v1.LevelDeleteRes, err error) + // BatchDelete 批量删除职级 + BatchDelete(ctx context.Context, req *v1.LevelBatchDeleteReq) (res *v1.LevelBatchDeleteRes, err error) + // Import 导入职级 + // 表格列依次为名称、状态、排序,名称重复时跳过 + Import(ctx context.Context, req *v1.LevelImportReq) (res *v1.LevelImportRes, err error) + // Export 导出职级 + Export(ctx context.Context, req *v1.LevelExportReq) (res *v1.LevelExportRes, err error) +} + +// localLevel 职级服务实现实例 +var localLevel ILevel + +// Level 获取职级服务实例 +func Level() ILevel { + if localLevel == nil { + panic("implement not found for interface ILevel, forgot register?") + } + return localLevel +} + +// RegisterLevel 注册职级服务实现 +func RegisterLevel(i ILevel) { + localLevel = i +} diff --git a/internal/service/link.go b/internal/service/link.go new file mode 100644 index 0000000..773c39d --- /dev/null +++ b/internal/service/link.go @@ -0,0 +1,87 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// ILink 友链服务接口 +type ILink interface { + // Page 友链分页 + Page(ctx context.Context, req *v1.LinkPageReq) (res *v1.LinkPageRes, err error) + // Detail 友链详情 + Detail(ctx context.Context, req *v1.LinkDetailReq) (res *v1.LinkDetailRes, err error) + // Add 新增友链 + Add(ctx context.Context, req *v1.LinkAddReq) (res *v1.IdRes, err error) + // Update 修改友链 + Update(ctx context.Context, req *v1.LinkUpdateReq) (res *v1.LinkUpdateRes, err error) + // Delete 删除友链 + Delete(ctx context.Context, req *v1.LinkDeleteReq) (res *v1.LinkDeleteRes, err error) + // BatchDelete 批量删除友链 + BatchDelete(ctx context.Context, req *v1.LinkBatchDeleteReq) (res *v1.LinkBatchDeleteRes, err error) +} + +// localLink 友链服务实现实例 +var localLink ILink + +// Link 获取友链服务实例 +func Link() ILink { + if localLink == nil { + panic("implement not found for interface ILink, forgot register?") + } + return localLink +} + +// RegisterLink 注册友链服务实现 +func RegisterLink(i ILink) { + localLink = i +} diff --git a/internal/service/login_log.go b/internal/service/login_log.go new file mode 100644 index 0000000..1931bce --- /dev/null +++ b/internal/service/login_log.go @@ -0,0 +1,85 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// ILoginLog 登录日志服务接口 +type ILoginLog interface { + // Page 登录日志分页 + Page(ctx context.Context, req *v1.LoginLogPageReq) (res *v1.LoginLogPageRes, err error) + // List 登录日志列表 + List(ctx context.Context, req *v1.LoginLogListReq) (res v1.LoginLogListRes, err error) + // Detail 登录日志详情 + Detail(ctx context.Context, req *v1.LoginLogDetailReq) (res *v1.LoginLogDetailRes, err error) + // Delete 删除登录日志 + Delete(ctx context.Context, req *v1.LoginLogDeleteReq) (res *v1.LoginLogDeleteRes, err error) + // BatchDelete 批量删除登录日志 + BatchDelete(ctx context.Context, req *v1.LoginLogBatchDeleteReq) (res *v1.LoginLogBatchDeleteRes, err error) +} + +// localLoginLog 登录日志服务实现实例 +var localLoginLog ILoginLog + +// LoginLog 获取登录日志服务实例 +func LoginLog() ILoginLog { + if localLoginLog == nil { + panic("implement not found for interface ILoginLog, forgot register?") + } + return localLoginLog +} + +// RegisterLoginLog 注册登录日志服务实现 +func RegisterLoginLog(i ILoginLog) { + localLoginLog = i +} diff --git a/internal/service/menu.go b/internal/service/menu.go new file mode 100644 index 0000000..b9fb73e --- /dev/null +++ b/internal/service/menu.go @@ -0,0 +1,93 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// IMenu 菜单服务接口 +type IMenu interface { + // Page 菜单分页 + // 返回平铺的菜单列表 + Page(ctx context.Context, req *v1.MenuPageReq) (res *v1.MenuPageRes, err error) + // List 菜单平铺列表 + // 树形结构由前端构建 + List(ctx context.Context, req *v1.MenuListReq) (res v1.MenuListRes, err error) + // Detail 菜单详情 + Detail(ctx context.Context, req *v1.MenuDetailReq) (res *v1.MenuDetailRes, err error) + // Add 新增菜单 + Add(ctx context.Context, req *v1.MenuAddReq) (res *v1.IdRes, err error) + // Update 修改菜单 + // 禁止将节点挂到自身或后代下 + Update(ctx context.Context, req *v1.MenuUpdateReq) (res *v1.MenuUpdateRes, err error) + // Delete 删除菜单 + // 存在子菜单时禁止删除,并连带清理角色菜单关联与失效缓存 + Delete(ctx context.Context, req *v1.MenuDeleteReq) (res *v1.MenuDeleteRes, err error) + // BatchDelete 批量删除菜单 + BatchDelete(ctx context.Context, req *v1.MenuBatchDeleteReq) (res *v1.MenuBatchDeleteRes, err error) +} + +// localMenu 菜单服务实现实例 +var localMenu IMenu + +// Menu 获取菜单服务实例 +func Menu() IMenu { + if localMenu == nil { + panic("implement not found for interface IMenu, forgot register?") + } + return localMenu +} + +// RegisterMenu 注册菜单服务实现 +func RegisterMenu(i IMenu) { + localMenu = i +} diff --git a/internal/service/monitor.go b/internal/service/monitor.go new file mode 100644 index 0000000..12eb993 --- /dev/null +++ b/internal/service/monitor.go @@ -0,0 +1,115 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// 手维护服务接口:监控域无表模块(在线用户/服务器监控/缓存监控) +// ================================================================================ + +package service + +import ( + "context" + + "easygoadmin/api/v1" +) + +// IOnlineUser 在线用户服务接口 +type IOnlineUser interface { + // Page 在线用户分页 + Page(ctx context.Context, req *v1.OnlineUserPageReq) (res *v1.OnlineUserPageRes, err error) + // Detail 在线会话详情(含令牌原文) + Detail(ctx context.Context, req *v1.OnlineUserDetailReq) (res *v1.OnlineUserDetailRes, err error) + // ForceLogout 强制下线 + ForceLogout(ctx context.Context, req *v1.OnlineUserForceLogoutReq) (res *v1.OnlineUserForceLogoutRes, err error) + // ForceLogoutUser 踢出该用户全部设备 + ForceLogoutUser(ctx context.Context, req *v1.OnlineUserForceLogoutUserReq) (res *v1.OnlineUserForceLogoutUserRes, err error) +} + +// IServerMonitor 服务器监控服务接口 +type IServerMonitor interface { + // Info 服务器监控信息 + Info(ctx context.Context, req *v1.ServerInfoReq) (res *v1.ServerInfoRes, err error) +} + +// ICacheMonitor 缓存监控服务接口 +type ICacheMonitor interface { + // Info 缓存监控信息 + Info(ctx context.Context, req *v1.CacheInfoReq) (res *v1.CacheInfoRes, err error) +} + +var ( + localOnlineUser IOnlineUser + localServerMonitor IServerMonitor + localCacheMonitor ICacheMonitor +) + +// OnlineUser 获取在线用户服务实例 +func OnlineUser() IOnlineUser { + if localOnlineUser == nil { + panic("implement not found for interface IOnlineUser, forgot register?") + } + return localOnlineUser +} + +// RegisterOnlineUser 注册在线用户服务实现 +func RegisterOnlineUser(i IOnlineUser) { localOnlineUser = i } + +// ServerMonitor 获取服务器监控服务实例 +func ServerMonitor() IServerMonitor { + if localServerMonitor == nil { + panic("implement not found for interface IServerMonitor, forgot register?") + } + return localServerMonitor +} + +// RegisterServerMonitor 注册服务器监控服务实现 +func RegisterServerMonitor(i IServerMonitor) { localServerMonitor = i } + +// CacheMonitor 获取缓存监控服务实例 +func CacheMonitor() ICacheMonitor { + if localCacheMonitor == nil { + panic("implement not found for interface ICacheMonitor, forgot register?") + } + return localCacheMonitor +} + +// RegisterCacheMonitor 注册缓存监控服务实现 +func RegisterCacheMonitor(i ICacheMonitor) { localCacheMonitor = i } diff --git a/internal/service/notice.go b/internal/service/notice.go new file mode 100644 index 0000000..e5451b0 --- /dev/null +++ b/internal/service/notice.go @@ -0,0 +1,92 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// INotice 公告服务接口 +type INotice interface { + // Page 公告分页 + Page(ctx context.Context, req *v1.NoticePageReq) (res *v1.NoticePageRes, err error) + // List 公告列表 + List(ctx context.Context, req *v1.NoticeListReq) (res v1.NoticeListRes, err error) + // Detail 公告详情 + // 查询时点击数自增 + Detail(ctx context.Context, req *v1.NoticeDetailReq) (res *v1.NoticeDetailRes, err error) + // Add 新增公告 + Add(ctx context.Context, req *v1.NoticeAddReq) (res *v1.IdRes, err error) + // Update 修改公告 + Update(ctx context.Context, req *v1.NoticeUpdateReq) (res *v1.NoticeUpdateRes, err error) + // Status 公告状态 + Status(ctx context.Context, req *v1.NoticeStatusReq) (res *v1.NoticeStatusRes, err error) + // Delete 删除公告 + Delete(ctx context.Context, req *v1.NoticeDeleteReq) (res *v1.NoticeDeleteRes, err error) + // BatchDelete 批量删除公告 + BatchDelete(ctx context.Context, req *v1.NoticeBatchDeleteReq) (res *v1.NoticeBatchDeleteRes, err error) +} + +// localNotice 公告服务实现实例 +var localNotice INotice + +// Notice 获取公告服务实例 +func Notice() INotice { + if localNotice == nil { + panic("implement not found for interface INotice, forgot register?") + } + return localNotice +} + +// RegisterNotice 注册公告服务实现 +func RegisterNotice(i INotice) { + localNotice = i +} diff --git a/internal/service/operation_log.go b/internal/service/operation_log.go new file mode 100644 index 0000000..bbf46a8 --- /dev/null +++ b/internal/service/operation_log.go @@ -0,0 +1,85 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// IOperLog 操作日志服务接口 +type IOperLog interface { + // Page 操作日志分页 + Page(ctx context.Context, req *v1.OperLogPageReq) (res *v1.OperLogPageRes, err error) + // List 操作日志列表 + List(ctx context.Context, req *v1.OperLogListReq) (res v1.OperLogListRes, err error) + // Detail 操作日志详情 + Detail(ctx context.Context, req *v1.OperLogDetailReq) (res *v1.OperLogDetailRes, err error) + // Delete 删除操作日志 + Delete(ctx context.Context, req *v1.OperLogDeleteReq) (res *v1.OperLogDeleteRes, err error) + // BatchDelete 批量删除操作日志 + BatchDelete(ctx context.Context, req *v1.OperLogBatchDeleteReq) (res *v1.OperLogBatchDeleteRes, err error) +} + +// localOperLog 操作日志服务实现实例 +var localOperLog IOperLog + +// OperLog 获取操作日志服务实例 +func OperLog() IOperLog { + if localOperLog == nil { + panic("implement not found for interface IOperLog, forgot register?") + } + return localOperLog +} + +// RegisterOperLog 注册操作日志服务实现 +func RegisterOperLog(i IOperLog) { + localOperLog = i +} diff --git a/internal/service/param.go b/internal/service/param.go new file mode 100644 index 0000000..eff6626 --- /dev/null +++ b/internal/service/param.go @@ -0,0 +1,89 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// IParam 系统参数服务接口 +type IParam interface { + // Page 参数分页 + Page(ctx context.Context, req *v1.ParamPageReq) (res *v1.ParamPageRes, err error) + // List 参数列表 + List(ctx context.Context, req *v1.ParamListReq) (res v1.ParamListRes, err error) + // Detail 参数详情 + Detail(ctx context.Context, req *v1.ParamDetailReq) (res *v1.ParamDetailRes, err error) + // Add 新增参数 + Add(ctx context.Context, req *v1.ParamAddReq) (res *v1.IdRes, err error) + // Update 修改参数 + Update(ctx context.Context, req *v1.ParamUpdateReq) (res *v1.ParamUpdateRes, err error) + // Delete 删除参数 + Delete(ctx context.Context, req *v1.ParamDeleteReq) (res *v1.ParamDeleteRes, err error) + // BatchDelete 批量删除参数 + BatchDelete(ctx context.Context, req *v1.ParamBatchDeleteReq) (res *v1.ParamBatchDeleteRes, err error) +} + +// localParam 系统参数服务实现实例 +var localParam IParam + +// Param 获取系统参数服务实例 +func Param() IParam { + if localParam == nil { + panic("implement not found for interface IParam, forgot register?") + } + return localParam +} + +// RegisterParam 注册系统参数服务实现 +func RegisterParam(i IParam) { + localParam = i +} diff --git a/internal/service/position.go b/internal/service/position.go new file mode 100644 index 0000000..2a003bc --- /dev/null +++ b/internal/service/position.go @@ -0,0 +1,93 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// IPosition 岗位服务接口 +type IPosition interface { + // Page 岗位分页 + // 分页结果包含关联用户数 + Page(ctx context.Context, req *v1.PositionPageReq) (res *v1.PositionPageRes, err error) + // List 岗位全量列表 + List(ctx context.Context, req *v1.PositionListReq) (res v1.PositionListRes, err error) + // Detail 岗位详情 + Detail(ctx context.Context, req *v1.PositionDetailReq) (res *v1.PositionDetailRes, err error) + // Add 新增岗位 + Add(ctx context.Context, req *v1.PositionAddReq) (res *v1.IdRes, err error) + // Update 修改岗位 + Update(ctx context.Context, req *v1.PositionUpdateReq) (res *v1.PositionUpdateRes, err error) + // Status 岗位状态 + Status(ctx context.Context, req *v1.PositionStatusReq) (res *v1.PositionStatusRes, err error) + // Delete 删除岗位 + // 存在用户引用时禁止删除 + Delete(ctx context.Context, req *v1.PositionDeleteReq) (res *v1.PositionDeleteRes, err error) + // BatchDelete 批量删除岗位 + BatchDelete(ctx context.Context, req *v1.PositionBatchDeleteReq) (res *v1.PositionBatchDeleteRes, err error) +} + +// localPosition 岗位服务实现实例 +var localPosition IPosition + +// Position 获取岗位服务实例 +func Position() IPosition { + if localPosition == nil { + panic("implement not found for interface IPosition, forgot register?") + } + return localPosition +} + +// RegisterPosition 注册岗位服务实现 +func RegisterPosition(i IPosition) { + localPosition = i +} diff --git a/internal/service/role.go b/internal/service/role.go new file mode 100644 index 0000000..59bcc54 --- /dev/null +++ b/internal/service/role.go @@ -0,0 +1,99 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// IRole 角色服务接口 +type IRole interface { + // Page 角色分页 + // 分页结果包含关联用户数 + Page(ctx context.Context, req *v1.RolePageReq) (res *v1.RolePageRes, err error) + // List 角色全量列表 + List(ctx context.Context, req *v1.RoleListReq) (res v1.RoleListRes, err error) + // Detail 角色详情 + Detail(ctx context.Context, req *v1.RoleDetailReq) (res *v1.RoleDetailRes, err error) + // Add 新增角色 + // 角色编码唯一 + Add(ctx context.Context, req *v1.RoleAddReq) (res *v1.IdRes, err error) + // Update 修改角色 + // 角色编码不允许变更 + Update(ctx context.Context, req *v1.RoleUpdateReq) (res *v1.RoleUpdateRes, err error) + // Delete 删除角色 + // 存在关联用户时禁止删除 + Delete(ctx context.Context, req *v1.RoleDeleteReq) (res *v1.RoleDeleteRes, err error) + // BatchDelete 批量删除角色 + BatchDelete(ctx context.Context, req *v1.RoleBatchDeleteReq) (res *v1.RoleBatchDeleteRes, err error) + // MenuList 角色菜单勾选列表 + // 返回平铺列表并附带勾选状态 + MenuList(ctx context.Context, req *v1.RoleMenuListReq) (res v1.RoleMenuListRes, err error) + // SavePermission 保存角色菜单权限 + // 全量替换角色菜单关联,并失效相关用户的身份缓存 + SavePermission(ctx context.Context, req *v1.RoleSavePermissionReq) (res *v1.RoleSavePermissionRes, err error) +} + +// localRole 角色服务实现实例 +var localRole IRole + +// Role 获取角色服务实例 +func Role() IRole { + if localRole == nil { + panic("implement not found for interface IRole, forgot register?") + } + return localRole +} + +// RegisterRole 注册角色服务实现 +func RegisterRole(i IRole) { + localRole = i +} diff --git a/internal/service/tenant.go b/internal/service/tenant.go new file mode 100644 index 0000000..d5769c5 --- /dev/null +++ b/internal/service/tenant.go @@ -0,0 +1,100 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// ITenant 租户服务接口 +type ITenant interface { + // Page 租户分页 + // 分页结果包含关联用户数 + Page(ctx context.Context, req *v1.TenantPageReq) (res *v1.TenantPageRes, err error) + // List 租户全量列表 + List(ctx context.Context, req *v1.TenantListReq) (res v1.TenantListRes, err error) + // Detail 租户详情 + Detail(ctx context.Context, req *v1.TenantDetailReq) (res *v1.TenantDetailRes, err error) + // Add 新增租户 + // 编码留空时自动生成,自增回填前先用临时编码占位 + Add(ctx context.Context, req *v1.TenantAddReq) (res *v1.IdRes, err error) + // Update 修改租户 + // 租户编码不允许变更 + Update(ctx context.Context, req *v1.TenantUpdateReq) (res *v1.TenantUpdateRes, err error) + // Status 租户状态 + Status(ctx context.Context, req *v1.TenantStatusReq) (res *v1.TenantStatusRes, err error) + // Delete 删除租户 + // 存在关联用户时禁止删除 + Delete(ctx context.Context, req *v1.TenantDeleteReq) (res *v1.TenantDeleteRes, err error) + // BatchDelete 批量删除租户 + BatchDelete(ctx context.Context, req *v1.TenantBatchDeleteReq) (res *v1.TenantBatchDeleteRes, err error) + // Account 创建租户账号 + // 同时创建租户及其初始管理员账号 + Account(ctx context.Context, req *v1.TenantAccountReq) (res *v1.TenantAccountRes, err error) + // Users 查询租户用户列表 + Users(ctx context.Context, req *v1.TenantUsersReq) (res *v1.TenantUsersRes, err error) +} + +// localTenant 租户服务实现实例 +var localTenant ITenant + +// Tenant 获取租户服务实例 +func Tenant() ITenant { + if localTenant == nil { + panic("implement not found for interface ITenant, forgot register?") + } + return localTenant +} + +// RegisterTenant 注册租户服务实现 +func RegisterTenant(i ITenant) { + localTenant = i +} diff --git a/internal/service/upload.go b/internal/service/upload.go new file mode 100644 index 0000000..df30941 --- /dev/null +++ b/internal/service/upload.go @@ -0,0 +1,81 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// IUpload 文件上传服务接口 +type IUpload interface { + // UploadFile 上传单文件 + // 文件先落入临时目录,表单保存时由 SaveFromTemp 转正至正式目录 + UploadFile(ctx context.Context, req *v1.UploadFileReq) (res *v1.UploadFileRes, err error) + // UploadFiles 批量上传文件 + // 逐个上传,任一文件失败时返回错误 + UploadFiles(ctx context.Context, req *v1.UploadFilesReq) (res *v1.UploadFilesRes, err error) +} + +// localUpload 文件上传服务实现实例 +var localUpload IUpload + +// Upload 获取文件上传服务实例 +func Upload() IUpload { + if localUpload == nil { + panic("implement not found for interface IUpload, forgot register?") + } + return localUpload +} + +// RegisterUpload 注册文件上传服务实现 +func RegisterUpload(i IUpload) { + localUpload = i +} diff --git a/internal/service/user.go b/internal/service/user.go new file mode 100644 index 0000000..d9e193e --- /dev/null +++ b/internal/service/user.go @@ -0,0 +1,111 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "easygoadmin/api/v1" +) + +// 按模块独立拆分(源:gf gen service 产物手工重组)。 +// logic 方法签名变更时需同步维护本文件(手维护,勿整体运行 gf gen service)。 + +// IUser 用户服务接口 +type IUser interface { + // Page 用户分页 + // 支持关键词、状态、部门、角色、职级、岗位筛选,并批量补全行内冗余名称 + Page(ctx context.Context, req *v1.UserPageReq) (res *v1.UserPageRes, err error) + // List 用户全量列表 + // 供下拉选择使用 + List(ctx context.Context, req *v1.UserListReq) (res v1.UserListRes, err error) + // Detail 用户详情 + Detail(ctx context.Context, req *v1.UserDetailReq) (res *v1.UserDetailRes, err error) + // Add 新增用户 + // 用户名与手机号租户内唯一,校验租户用户上限,并写入用户角色关联 + Add(ctx context.Context, req *v1.UserAddReq) (res *v1.IdRes, err error) + // Update 修改用户 + // 密码留空时不做修改,角色关联全量重写 + Update(ctx context.Context, req *v1.UserUpdateReq) (res *v1.UserUpdateRes, err error) + // Status 用户状态 + Status(ctx context.Context, req *v1.UserStatusReq) (res *v1.UserStatusRes, err error) + // ResetPwd 重置密码 + // 未传新密码时重置为系统默认密码 + ResetPwd(ctx context.Context, req *v1.UserResetPwdReq) (res *v1.UserResetPwdRes, err error) + // Delete 删除用户 + // 软删记录并清理用户角色关联 + Delete(ctx context.Context, req *v1.UserDeleteReq) (res *v1.UserDeleteRes, err error) + // BatchDelete 批量删除用户 + BatchDelete(ctx context.Context, req *v1.UserBatchDeleteReq) (res *v1.UserBatchDeleteRes, err error) + // Import 导入用户 + // 支持 xlsx 文件,列依次为用户名、姓名、密码、手机、邮箱、性别、部门、职级、岗位、状态、地址、备注; + // 密码留空时取系统默认密码 + Import(ctx context.Context, req *v1.UserImportReq) (res *v1.UserImportRes, err error) + // Export 导出用户 + // 每批 500 条防 OOM,写出 xlsx 并返回访问地址 + Export(ctx context.Context, req *v1.UserExportReq) (res *v1.UserExportRes, err error) + // Document 用户打印信息 + Document(ctx context.Context, req *v1.UserDocumentReq) (res *v1.UserDocumentRes, err error) + // AssignRoles 重写用户角色关联 + // 供新增与修改调用 + AssignRoles(ctx context.Context, userId int64, roleIds []int64) error +} + +// localUser 用户服务实现实例 +var localUser IUser + +// User 获取用户服务实例 +func User() IUser { + if localUser == nil { + panic("implement not found for interface IUser, forgot register?") + } + return localUser +} + +// RegisterUser 注册用户服务实现 +func RegisterUser(i IUser) { + localUser = i +} diff --git a/internal/task/hello_world.go b/internal/task/hello_world.go new file mode 100644 index 0000000..8c409ea --- /dev/null +++ b/internal/task/hello_world.go @@ -0,0 +1,64 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package task + +import ( + "context" + "fmt" + "time" + + "github.com/gogf/gf/v2/frame/g" + + "easygoadmin/internal/logic" +) + +func init() { + logic.RegisterJobHandler("HelloWorld", helloWorld) +} + +// helloWorld 示例任务 +// 仅打印日志,用于验证调度链路 +func helloWorld(ctx context.Context, params string) (string, error) { + now := time.Now().Format("2006-01-02 15:04:05") + g.Log().Infof(ctx, "[定时任务] HelloWorld 执行,时间: %s,参数: %s", now, paramsText(params)) + return fmt.Sprintf("Hello World! 时间: %s | 参数: %s", now, paramsText(params)), nil +} diff --git a/internal/task/log_clean.go b/internal/task/log_clean.go new file mode 100644 index 0000000..8dff0a0 --- /dev/null +++ b/internal/task/log_clean.go @@ -0,0 +1,144 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package task + +import ( + "context" + "database/sql" + "fmt" + + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + + "easygoadmin/internal/dao" + "easygoadmin/internal/logic" +) + +func init() { + logic.RegisterJobHandler("LogClean", logClean) +} + +const ( + logCleanBatch = 5000 // 单批删除行数 + logCleanMaxTotal = 200000 // 单表单次任务删除行数上限(防长事务与 runaway) +) + +// logClean 日志清理任务 +// +// 分批物理删除操作日志/登录日志/任务日志中超过保留天数的历史记录。 +// 任务参数 retentionDays 可覆盖配置 job.logRetentionDays(默认 90 天),为 0 时取配置值。 +func logClean(ctx context.Context, params string) (string, error) { + days := gjson.New(params).Get("retentionDays").Int() + if days <= 0 { + days = g.Cfg().MustGet(ctx, "job.logRetentionDays").Int() + } + if days <= 0 { + days = 90 + } + before := gtime.Now().AddDate(0, 0, -days) + + counts := map[string]int{} + for name, clean := range map[string]func(context.Context, *gtime.Time) (int, error){ + "操作日志": cleanOperationLog, + "登录日志": cleanLoginLog, + "任务日志": cleanJobLog, + } { + n, err := clean(ctx, before) + if err != nil { + return "", fmt.Errorf("%s清理失败: %w", name, err) + } + counts[name] = n + } + return fmt.Sprintf("保留 %d 天,清理完成: 操作日志 %d 条,登录日志 %d 条,任务日志 %d 条", + days, counts["操作日志"], counts["登录日志"], counts["任务日志"]), nil +} + +// cleanOperationLog 分批删除超过保留期限的操作日志 +func cleanOperationLog(ctx context.Context, before *gtime.Time) (int, error) { + return deleteInBatches(ctx, func() (int64, error) { + res, err := dao.OperationLog.Ctx(ctx).WhereLT("create_time", before).Limit(logCleanBatch).Delete() + return affectedOf(res), err + }) +} + +// cleanLoginLog 分批删除超过保留期限的登录日志 +func cleanLoginLog(ctx context.Context, before *gtime.Time) (int, error) { + return deleteInBatches(ctx, func() (int64, error) { + res, err := dao.LoginLog.Ctx(ctx).WhereLT("create_time", before).Limit(logCleanBatch).Delete() + return affectedOf(res), err + }) +} + +// cleanJobLog 分批删除超过保留期限的任务日志 +func cleanJobLog(ctx context.Context, before *gtime.Time) (int, error) { + return deleteInBatches(ctx, func() (int64, error) { + res, err := dao.JobLog.Ctx(ctx).WhereLT("create_time", before).Limit(logCleanBatch).Delete() + return affectedOf(res), err + }) +} + +// deleteInBatches 循环分批删除 +// 每批删除 logCleanBatch 行,直到不足一批或达到单次上限 +func deleteInBatches(ctx context.Context, del func() (int64, error)) (int, error) { + total := 0 + for { + n, err := del() + if err != nil { + return total, err + } + total += int(n) + if n < int64(logCleanBatch) || total >= logCleanMaxTotal { + return total, nil + } + } +} + +// affectedOf 读取受影响行数 +// RowsAffected 的错误在此场景可忽略(驱动不支持时按 0 处理,循环自然终止) +func affectedOf(res sql.Result) int64 { + if res == nil { + return 0 + } + n, _ := res.RowsAffected() + return n +} diff --git a/internal/task/send_sms.go b/internal/task/send_sms.go new file mode 100644 index 0000000..4f83526 --- /dev/null +++ b/internal/task/send_sms.go @@ -0,0 +1,61 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +package task + +import ( + "context" + + "github.com/gogf/gf/v2/frame/g" + + "easygoadmin/internal/logic" +) + +func init() { + logic.RegisterJobHandler("SendSms", sendSms) +} + +// sendSms 短信发送任务 +// 当前仅记录执行参数日志并返回成功,不对接短信通道 +func sendSms(ctx context.Context, params string) (string, error) { + g.Log().Infof(ctx, "[定时任务] SendSms 执行,参数: %s", paramsText(params)) + return "短信发送成功", nil +} diff --git a/internal/task/task.go b/internal/task/task.go new file mode 100644 index 0000000..486511d --- /dev/null +++ b/internal/task/task.go @@ -0,0 +1,64 @@ +// +====================================================================== +// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +// +---------------------------------------------------------------------- +// | Copyright (c) 2019-2026 半城风雨团队 版权所有 +// +---------------------------------------------------------------------- +// | 开源协议: Apache-2.0 +// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +// +---------------------------------------------------------------------- +// | 官方网站: https://www.easygoadmin.vip +// +---------------------------------------------------------------------- +// | 研发团队: 小蚂蚁云团队 荣誉出品 +// +====================================================================== +// | 版权和免责声明: +// | 一、知识产权声明 +// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +// | 二、使用限制与禁止条款 +// | 您理解并同意,禁止将本产品用于以下任何行为: +// | 1. 任何违反中华人民共和国法律法规的用途; +// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +// | 三、责任豁免 +// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +// | 四、违约与法律责任 +// | 如发现任何违反本声明的行为,本团队有权: +// | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +// | 3. 保留进一步采取刑事控告等法律措施的权利。 +// | 五、许可范围说明 +// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +// | 成的一切经济损失及商誉损害。 +// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +// | 许可协议》附件为准。 +// +====================================================================== + +// Package task 定时任务处理器 +// +// 本包存放全部定时任务的业务实现,与 HTTP 请求处理链路隔离: +// - 调度器基础设施(gcron 挂载/摘除/执行分派)在 internal/logic 的 job 模块 +// - 本包仅存放各任务处理方法,文件按任务名 snake_case 平铺 +// +// 约定: +// 1. 每个任务一个文件,init 中调用 logic.RegisterJobHandler 注册, +// 注册名与任务记录 url 字段值一致(数据驱动查找) +// 2. 依赖方向单向:task → logic(注册 + 共享能力),logic 不得反向依赖 task +// 3. 处理方法签名 func(ctx, params) (string, error): +// params 为任务参数(JSON 字符串,可为空),返回值写入任务日志摘要 +// 4. 本包不在 HTTP 启动链路内,由 internal/cmd 空白导入触发 init 注册 +package task + +// paramsText 任务参数展示 +// 空参数统一显示为"无参数" +func paramsText(params string) string { + if params == "" { + return "无参数" + } + return params +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..918751f --- /dev/null +++ b/main.go @@ -0,0 +1,30 @@ +package main + +import ( + _ "easygoadmin/internal/logic" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gctx" + + // 五种数据库驱动注册(按需切换 manifest/config/config.yaml 中 database.default.type/link) + _ "github.com/gogf/gf/contrib/drivers/mssql/v2" + _ "github.com/gogf/gf/contrib/drivers/mysql/v2" + _ "github.com/gogf/gf/contrib/drivers/oracle/v2" + _ "github.com/gogf/gf/contrib/drivers/pgsql/v2" + _ "github.com/gogf/gf/contrib/drivers/sqlite/v2" + + // Redis 适配器注册(gredis/gcache 黑名单、验证码、字典缓存) + _ "github.com/gogf/gf/contrib/nosql/redis/v2" + + "easygoadmin/internal/cmd" +) + +// ============================== 服务入口 ============================== + +// main 后端服务入口 +// +// 完成依赖注册后交由 cmd.Run 启动服务 +func main() { + g.Log().Info(gctx.New(), "EasyGoAdmin 服务启动中...") + cmd.Run(gctx.New()) +} diff --git a/manifest/config/config.yaml b/manifest/config/config.yaml new file mode 100644 index 0000000..c2e1478 --- /dev/null +++ b/manifest/config/config.yaml @@ -0,0 +1,156 @@ +# ====================================================================== +# EasyGoAdmin 运行时配置 +# 环境变量覆盖规则(12-Factor:环境变量 > 配置文件): +# 键名 = 配置路径大写、点换下划线并加 GF_ 前缀, +# 例如 database.default.link → GF_DATABASE_DEFAULT_LINK; +# 覆盖经 utility/config 在启动时接线(RegisterEnvOverride), +# 仅覆盖本文件中已存在的键,空值视为未设置 +# ====================================================================== + +# ---------------------------------------------------------------------------- +# 服务端:监听地址、OpenAPI 文档与优雅重启 +# ---------------------------------------------------------------------------- +server: + address: ":8000" # 监听地址与端口 + openapiPath: "/api.json" # OpenAPI 文档路径(gf 原生自动生成,bind.go 补注册闭包路由) + swaggerPath: "" # Swagger UI 路径(内置页依赖境外 CDN,已改为本地托管 resource/swagger/) + dumpRouterMap: false # 启动时是否打印路由表 + errorStack: true # 响应中是否携带错误堆栈 + graceful: true # 是否开启优雅重启 + +# ---------------------------------------------------------------------------- +# 日志:级别、输出目标与文件目录 +# ---------------------------------------------------------------------------- +logger: + level: "all" # 日志级别 + stdout: true # 是否输出到控制台 + path: "log" # 日志文件目录 + +# ---------------------------------------------------------------------------- +# 多数据库:切换数据库仅需修改 type 与 link(五方言示例) +# ---------------------------------------------------------------------------- +database: + default: + type: "mysql" # 数据库类型 + link: "mysql:root:root@tcp(127.0.0.1:3306)/easygoadmin.goframe.elevue?charset=utf8mb4&parseTime=true&loc=Local" # 连接串 + debug: false # 是否打印 SQL 调试日志(调试期开启,生产环境改回 false) + # 其他方言示例: + # type: "pgsql" link: "pgsql:user:password@tcp(127.0.0.1:5432)/easygoadmin?sslmode=disable" + # type: "mssql" link: "mssql:sa:Password1@tcp(127.0.0.1:1433)/easygoadmin?encrypt=disable" + # type: "oracle" link: "oracle:user:password@tcp(127.0.0.1:1521)/XE" + # type: "sqlite" link: "sqlite::@file(easygoadmin.db)" + +# ---------------------------------------------------------------------------- +# Redis:缓存连接(JWT 黑名单、验证码、字典、租户与身份缓存) +# ---------------------------------------------------------------------------- +redis: + default: # 分组名(g.Redis() 默认组) + address: "127.0.0.1:6379" # 地址 + db: 0 # 库编号 + pass: "123456" # 密码 + idle: 10 # 空闲连接数 + maxActive: 50 # 最大活跃连接数 + timeout: 300 # 超时时间(秒) + +# ---------------------------------------------------------------------------- +# JWT 双令牌:accessToken 短时效 + refreshToken 长时效轮换 +# ---------------------------------------------------------------------------- +jwt: + secret: "easygoadmin-goframe-elevue-jwt-secret-2026" # 签名密钥 + issuer: "easygoadmin" # 签发者 + accessTokenExpire: 7200 # 访问令牌有效期(秒),2 小时 + refreshTokenExpire: 604800 # 刷新令牌有效期(秒),7 天 + +# ---------------------------------------------------------------------------- +# 图形验证码:字符集、图片尺寸与干扰参数 +# ---------------------------------------------------------------------------- +captcha: + expire: 300 # 有效期(秒) + bypassCode: "618618" # 万能验证码:携带该值免校验,仅开发调试使用,生产环境留空 + length: 6 # 字符数,取值范围 4 到 8 + chars: "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" # 字符集,已排除 0O、1lI 等易混淆字符 + fontPath: "resource/static/fonts/Vera.ttf" # TTF 字体路径(相对项目根),缺失时降级为 SVG 渲染 + imageWidth: 160 # 图片宽度(像素),字符放不下时自动加宽防裁剪 + imageHeight: 50 # 图片高度(像素) + fontSize: 40 # 字体大小 + noiseLines: 8 # 干扰线数量,越多越难识别,影响可读性 + noiseDots: 500 # 噪点数量,越多越难识别,影响可读性 + +# ---------------------------------------------------------------------------- +# API 限流:固定窗口计数(键含窗口起点,缓存层 Redis 优先、内存降级) +# ---------------------------------------------------------------------------- +rateLimit: + enable: true # 是否启用限流 + limit: 120 # 普通接口每窗口每 IP 每路径最大请求数,0 关闭普通限流 + windowSeconds: 60 # 计数窗口(秒) + authLimit: 10 # 登录/验证码/令牌兑换每窗口每 IP 最大请求数(更严格) + +# ---------------------------------------------------------------------------- +# 操作日志:总开关与查询类记录开关 +# ---------------------------------------------------------------------------- +operationLog: + enable: true # 是否启用操作日志 + logQuery: true # 是否记录查询类操作 + +# ---------------------------------------------------------------------------- +# 定时任务:日志保留天数(LogClean 任务消费,任务参数可覆盖) +# ---------------------------------------------------------------------------- +job: + logRetentionDays: 90 # 操作/登录/任务日志保留天数,超期由 LogClean 任务物理删除 + +# ---------------------------------------------------------------------------- +# 租户:超管新增租户级数据时的默认归属 +# ---------------------------------------------------------------------------- +tenant: + defaultId: 1 # 超管新增租户级数据时的默认归属租户ID,显式指定租户时从显式值 + +# ---------------------------------------------------------------------------- +# 应用参数:时区、演示模式与错误信息开关 +# ---------------------------------------------------------------------------- +app: + timezone: "Asia/Shanghai" # 应用时区 + demo: false # 演示模式:为 true 时写操作一律拒绝,读操作与登录类公开接口放行 + errorMsg: true # 是否返回详细错误信息,生产环境建议置为 false + +# ---------------------------------------------------------------------------- +# API:驼峰转换与受信代理 +# ---------------------------------------------------------------------------- +api: + camelSnakeConvert: true # 是否启用驼峰与下划线自动转换,本项目全链路驼峰,恒开 + # 受信代理列表,逗号分隔并支持 CIDR; + # 直连来源在此列表内才信任 X-Forwarded-For 与 X-Real-IP + # 172.16.0.0/12 为 Docker 网桥默认网段:容器端口映射模式下 TCP 对端恒为 + # 网桥网关(如 172.19.0.1),CDN/反代回源经 NAT 后须凭此段进入 XFF 取真实 IP; + # 宿主机 Nginx 反代场景再补宿主机 IP 或其网段;勿配置 0.0.0.0/0(等于允许任意伪造) + proxyIps: "127.0.0.1,::1,172.16.0.0/12,101.226.0.0/15,112.65.0.0/16,140.207.0.0/16,180.101.0.0/16,182.254.0.0/16,223.166.0.0/16" + +# ---------------------------------------------------------------------------- +# 跨域:允许来源 +# ---------------------------------------------------------------------------- +cors: + allowOrigin: "*" # 允许的来源,为空或 * 时使用框架内置策略 + +# ---------------------------------------------------------------------------- +# 文件上传:临时目录中转,提交保存时转正至正式目录 +# ---------------------------------------------------------------------------- +file: + uploadDir: "E:\\EasyGoAdmin_V3\\EasyGoAdmin_GoFrame_EleVue\\upload" # 物理根目录,相对项目根 + domainUrl: "http://file.goframe.elevue" # 文件访问域名,为空时使用 /upload/ 相对路径 + tempDir: "temp" # 临时目录名,位于 uploadDir 下 + imageExt: "gif,jpg,jpeg,png,bmp,webp,svg" # 允许的图片扩展名 + fileExt: "gif,jpg,jpeg,png,bmp,webp,svg,doc,docx,xls,xlsx,ppt,pptx,pdf,zip,rar,txt,csv,mp3,mp4,sql,js,css" # 允许的文件扩展名 + maxSize: 10 # 单文件大小上限(MB) + tempExpireDays: 7 # 临时目录保留天数,超期自动清理,0 表示不清理 + +# ---------------------------------------------------------------------------- +# 代码生成器:菜单挂载位置与文件覆盖策略 +# ---------------------------------------------------------------------------- +generator: + parentMenuId: 189 # 新模块菜单挂载的父级ID + overwrite: true # 是否覆盖已存在文件:true 生成即覆盖,false 或未配置时缺失补生成、存在跳过 + +# ---------------------------------------------------------------------------- +# 归属地库:供登录日志与操作日志的归属地解析使用 +# ---------------------------------------------------------------------------- +ip2region: + path: "manifest/data/ip2region.xdb" # 归属地库文件路径 \ No newline at end of file diff --git a/manifest/data/ip2region.xdb b/manifest/data/ip2region.xdb new file mode 100644 index 0000000..c3d5915 Binary files /dev/null and b/manifest/data/ip2region.xdb differ diff --git a/resource/static/fonts/Vera.ttf b/resource/static/fonts/Vera.ttf new file mode 100644 index 0000000..58cd6b5 Binary files /dev/null and b/resource/static/fonts/Vera.ttf differ diff --git a/resource/swagger/index.html b/resource/swagger/index.html new file mode 100644 index 0000000..ede277e --- /dev/null +++ b/resource/swagger/index.html @@ -0,0 +1,18 @@ + + + + API Reference + + + + + + + + + diff --git a/resource/swagger/redoc.standalone.js b/resource/swagger/redoc.standalone.js new file mode 100644 index 0000000..a66ab51 --- /dev/null +++ b/resource/swagger/redoc.standalone.js @@ -0,0 +1,1826 @@ +/*! For license information please see redoc.standalone.js.LICENSE.txt */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("null")):"function"==typeof define&&define.amd?define(["null"],t):"object"==typeof exports?exports.Redoc=t(require("null")):e.Redoc=t(e.null)}(this,(function(e){return function(){var t={3675:function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))((function(i,o){function a(e){try{l(r.next(e))}catch(e){o(e)}}function s(e){try{l(r.throw(e))}catch(e){o(e)}}function l(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(a,s)}l((r=r.apply(e,t||[])).next())}))};Object.defineProperty(t,"__esModule",{value:!0}),t.mapTypeToComponent=t.bundleDocument=t.bundleFromString=t.bundle=t.OasVersion=void 0;const i=n(2307),o=n(4182),a=n(8065),s=n(5220),l=n(9443),c=n(1510),u=n(7468),p=n(5030),d=n(348),f=n(771),h=n(1094),m=n(4508),g=n(6350);var y;function b(e){return r(this,void 0,void 0,(function*(){const{document:t,config:n,customTypes:r,externalRefResolver:i,dereference:u=!1,skipRedoclyRegistryRefs:d=!1,removeUnusedComponents:f=!1,keepUrlRefs:h=!1}=e,y=(0,c.detectSpec)(t.parsed),b=(0,c.getMajorSpecVersion)(y),v=n.getRulesForOasVersion(b),w=(0,s.normalizeTypes)(n.extendTypes(null!=r?r:(0,c.getTypes)(y),y),n),k=(0,p.initRules)(v,n,"preprocessors",y),S=(0,p.initRules)(v,n,"decorators",y),O={problems:[],oasVersion:y,refTypes:new Map,visitorsData:{}};f&&S.push({severity:"error",ruleId:"remove-unused-components",visitor:b===c.SpecMajorVersion.OAS2?(0,m.RemoveUnusedComponents)({}):(0,g.RemoveUnusedComponents)({})});let E=yield(0,o.resolveDocument)({rootDocument:t,rootType:w.Root,externalRefResolver:i});k.length>0&&((0,l.walkDocument)({document:t,rootType:w.Root,normalizedVisitors:(0,a.normalizeVisitors)(k,w),resolvedRefMap:E,ctx:O}),E=yield(0,o.resolveDocument)({rootDocument:t,rootType:w.Root,externalRefResolver:i}));const _=(0,a.normalizeVisitors)([{severity:"error",ruleId:"bundler",visitor:x(b,u,d,t,E,h)},...S],w);return(0,l.walkDocument)({document:t,rootType:w.Root,normalizedVisitors:_,resolvedRefMap:E,ctx:O}),{bundle:t,problems:O.problems.map((e=>n.addProblemToIgnore(e))),fileDependencies:i.getFiles(),rootType:w.Root,refTypes:O.refTypes,visitorsData:O.visitorsData}}))}function v(e,t){switch(t){case c.SpecMajorVersion.OAS3:switch(e){case"Schema":return"schemas";case"Parameter":return"parameters";case"Response":return"responses";case"Example":return"examples";case"RequestBody":return"requestBodies";case"Header":return"headers";case"SecuritySchema":return"securitySchemes";case"Link":return"links";case"Callback":return"callbacks";default:return null}case c.SpecMajorVersion.OAS2:switch(e){case"Schema":return"definitions";case"Parameter":return"parameters";case"Response":return"responses";default:return null}case c.SpecMajorVersion.Async2:switch(e){case"Schema":return"schemas";case"Parameter":return"parameters";default:return null}}}function x(e,t,n,r,a,s){let l,p;const m={ref:{leave(i,l,c){if(!c.location||void 0===c.node)return void(0,d.reportUnresolvedRef)(c,l.report,l.location);if(c.location.source===r.source&&c.location.source===l.location.source&&"scalar"!==l.type.name&&!t)return;if(n&&(0,h.isRedoclyRegistryURL)(i.$ref))return;if(s&&(0,u.isAbsoluteUrl)(i.$ref))return;const p=v(l.type.name,e);p?t?(y(p,c,l),g(i,c,l)):(i.$ref=y(p,c,l),function(e,t,n){const i=(0,o.makeRefId)(n.location.source.absoluteRef,e.$ref);a.set(i,{document:r,isRemote:!1,node:t.node,nodePointer:e.$ref,resolved:!0})}(i,c,l)):g(i,c,l)}},Root:{enter(t,n){p=n.location,e===c.SpecMajorVersion.OAS3?l=t.components=t.components||{}:e===c.SpecMajorVersion.OAS2&&(l=t)}}};function g(e,t,n){if((0,f.isPlainObject)(t.node)){delete e.$ref;const n=Object.assign({},t.node,e);Object.assign(e,n)}else n.parent[n.key]=t.node}function y(t,n,r){l[t]=l[t]||{};const i=function(e,t,n){const[r,i]=[e.location.source.absoluteRef,e.location.pointer],o=l[t];let a="";const s=i.slice(2).split("/").filter(f.isTruthy);for(;s.length>0;)if(a=s.pop()+(a?`-${a}`:""),!o||!o[a]||b(o[a],e,n))return a;if(a=(0,u.refBaseName)(r)+(a?`_${a}`:""),!o[a]||b(o[a],e,n))return a;const c=a;let p=2;for(;o[a]&&!b(o[a],e,n);)a=`${c}-${p}`,p++;return o[a]||n.report({message:`Two schemas are referenced with the same name but different content. Renamed ${c} to ${a}.`,location:n.location,forceSeverity:"warn"}),a}(n,t,r);return l[t][i]=n.node,e===c.SpecMajorVersion.OAS3?`#/components/${t}/${i}`:`#/${t}/${i}`}function b(e,t,n){var r;return!(!(0,u.isRef)(e)||(null===(r=n.resolve(e,p.absolutePointer).location)||void 0===r?void 0:r.absolutePointer)!==t.location.absolutePointer)||i(e,t.node)}return e===c.SpecMajorVersion.OAS3&&(m.DiscriminatorMapping={leave(n,r){for(const i of Object.keys(n)){const o=n[i],a=r.resolve({$ref:o});if(!a.location||void 0===a.node)return void(0,d.reportUnresolvedRef)(a,r.report,r.location.child(i));const s=v("Schema",e);t?y(s,a,r):n[i]=y(s,a,r)}}}),m}!function(e){e.Version2="oas2",e.Version3_0="oas3_0",e.Version3_1="oas3_1"}(y||(t.OasVersion=y={})),t.bundle=function(e){return r(this,void 0,void 0,(function*(){const{ref:t,doc:n,externalRefResolver:r=new o.BaseResolver(e.config.resolve),base:i=null}=e;if(!t&&!n)throw new Error("Document or reference is required.\n");const a=void 0===n?yield r.resolveDocument(i,t,!0):n;if(a instanceof Error)throw a;return b(Object.assign(Object.assign({document:a},e),{config:e.config.styleguide,externalRefResolver:r}))}))},t.bundleFromString=function(e){return r(this,void 0,void 0,(function*(){const{source:t,absoluteRef:n,externalRefResolver:r=new o.BaseResolver(e.config.resolve)}=e,i=(0,o.makeDocumentFromString)(t,n||"/");return b(Object.assign(Object.assign({document:i},e),{externalRefResolver:r,config:e.config.styleguide}))}))},t.bundleDocument=b,t.mapTypeToComponent=v},3777:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.Config=t.StyleguideConfig=t.AVAILABLE_REGIONS=t.DOMAINS=t.DEFAULT_REGION=t.IGNORE_FILE=void 0;const r=n(5101),i=n(6470),o=n(5273),a=n(771),s=n(1510),l=n(8005),c=n(2565),u=n(7468);t.IGNORE_FILE=".redocly.lint-ignore.yaml",t.DEFAULT_REGION="us",t.DOMAINS=function(){const e={us:"redocly.com",eu:"eu.redocly.com"},t=l.env.REDOCLY_DOMAIN;return(null==t?void 0:t.endsWith(".redocly.host"))&&(e[t.split(".")[0]]=t),"redoc.online"===t&&(e[t]=t),e}(),t.AVAILABLE_REGIONS=Object.keys(t.DOMAINS);class p{constructor(e,n){this.rawConfig=e,this.configFile=n,this.ignore={},this._usedRules=new Set,this._usedVersions=new Set,this.plugins=e.plugins||[],this.doNotResolveExamples=!!e.doNotResolveExamples,this.recommendedFallback=e.recommendedFallback||!1,this.rules={[s.SpecVersion.OAS2]:Object.assign(Object.assign({},e.rules),e.oas2Rules),[s.SpecVersion.OAS3_0]:Object.assign(Object.assign({},e.rules),e.oas3_0Rules),[s.SpecVersion.OAS3_1]:Object.assign(Object.assign({},e.rules),e.oas3_1Rules),[s.SpecVersion.Async2]:Object.assign(Object.assign({},e.rules),e.async2Rules)},this.preprocessors={[s.SpecVersion.OAS2]:Object.assign(Object.assign({},e.preprocessors),e.oas2Preprocessors),[s.SpecVersion.OAS3_0]:Object.assign(Object.assign({},e.preprocessors),e.oas3_0Preprocessors),[s.SpecVersion.OAS3_1]:Object.assign(Object.assign({},e.preprocessors),e.oas3_1Preprocessors),[s.SpecVersion.Async2]:Object.assign(Object.assign({},e.preprocessors),e.async2Preprocessors)},this.decorators={[s.SpecVersion.OAS2]:Object.assign(Object.assign({},e.decorators),e.oas2Decorators),[s.SpecVersion.OAS3_0]:Object.assign(Object.assign({},e.decorators),e.oas3_0Decorators),[s.SpecVersion.OAS3_1]:Object.assign(Object.assign({},e.decorators),e.oas3_1Decorators),[s.SpecVersion.Async2]:Object.assign(Object.assign({},e.decorators),e.async2Decorators)},this.extendPaths=e.extendPaths||[],this.pluginPaths=e.pluginPaths||[],this.resolveIgnore(function(e){return e?(0,a.doesYamlFileExist)(e)?i.join(i.dirname(e),t.IGNORE_FILE):i.join(e,t.IGNORE_FILE):l.isBrowser?void 0:i.join(process.cwd(),t.IGNORE_FILE)}(n))}resolveIgnore(e){if(e&&(0,a.doesYamlFileExist)(e)){this.ignore=(0,o.parseYaml)(r.readFileSync(e,"utf-8"))||{};for(const t of Object.keys(this.ignore)){this.ignore[(0,u.isAbsoluteUrl)(t)?t:i.resolve(i.dirname(e),t)]=this.ignore[t];for(const e of Object.keys(this.ignore[t]))this.ignore[t][e]=new Set(this.ignore[t][e]);(0,u.isAbsoluteUrl)(t)||delete this.ignore[t]}}}saveIgnore(){const e=this.configFile?i.dirname(this.configFile):process.cwd(),n=i.join(e,t.IGNORE_FILE),s={};for(const t of Object.keys(this.ignore)){const n=s[(0,u.isAbsoluteUrl)(t)?t:(0,a.slash)(i.relative(e,t))]=this.ignore[t];for(const e of Object.keys(n))n[e]=Array.from(n[e])}r.writeFileSync(n,"# This file instructs Redocly's linter to ignore the rules contained for specific parts of your API.\n# See https://redoc.ly/docs/cli/ for more information.\n"+(0,o.stringifyYaml)(s))}addIgnore(e){const t=this.ignore,n=e.location[0];if(void 0===n.pointer)return;const r=t[n.source.absoluteRef]=t[n.source.absoluteRef]||{};(r[e.ruleId]=r[e.ruleId]||new Set).add(n.pointer)}addProblemToIgnore(e){const t=e.location[0];if(void 0===t.pointer)return e;const n=(this.ignore[t.source.absoluteRef]||{})[e.ruleId],r=n&&n.has(t.pointer);return r?Object.assign(Object.assign({},e),{ignored:r}):e}extendTypes(e,t){let n=e;for(const e of this.plugins)if(void 0!==e.typeExtension)switch(t){case s.SpecVersion.OAS3_0:case s.SpecVersion.OAS3_1:if(!e.typeExtension.oas3)continue;n=e.typeExtension.oas3(n,t);break;case s.SpecVersion.OAS2:if(!e.typeExtension.oas2)continue;n=e.typeExtension.oas2(n,t);break;case s.SpecVersion.Async2:if(!e.typeExtension.async2)continue;n=e.typeExtension.async2(n,t);break;default:throw new Error("Not implemented")}return n}getRuleSettings(e,t){this._usedRules.add(e),this._usedVersions.add(t);const n=this.rules[t][e]||"off";return"string"==typeof n?{severity:n}:Object.assign({severity:"error"},n)}getPreprocessorSettings(e,t){this._usedRules.add(e),this._usedVersions.add(t);const n=this.preprocessors[t][e]||"off";return"string"==typeof n?{severity:"on"===n?"error":n}:Object.assign({severity:"error"},n)}getDecoratorSettings(e,t){this._usedRules.add(e),this._usedVersions.add(t);const n=this.decorators[t][e]||"off";return"string"==typeof n?{severity:"on"===n?"error":n}:Object.assign({severity:"error"},n)}getUnusedRules(){const e=[],t=[],n=[];for(const r of Array.from(this._usedVersions))e.push(...Object.keys(this.rules[r]).filter((e=>!this._usedRules.has(e)))),t.push(...Object.keys(this.decorators[r]).filter((e=>!this._usedRules.has(e)))),n.push(...Object.keys(this.preprocessors[r]).filter((e=>!this._usedRules.has(e))));return{rules:e,preprocessors:n,decorators:t}}getRulesForOasVersion(e){switch(e){case s.SpecMajorVersion.OAS3:const e=[];return this.plugins.forEach((t=>{var n;return(null===(n=t.preprocessors)||void 0===n?void 0:n.oas3)&&e.push(t.preprocessors.oas3)})),this.plugins.forEach((t=>{var n;return(null===(n=t.rules)||void 0===n?void 0:n.oas3)&&e.push(t.rules.oas3)})),this.plugins.forEach((t=>{var n;return(null===(n=t.decorators)||void 0===n?void 0:n.oas3)&&e.push(t.decorators.oas3)})),e;case s.SpecMajorVersion.OAS2:const t=[];return this.plugins.forEach((e=>{var n;return(null===(n=e.preprocessors)||void 0===n?void 0:n.oas2)&&t.push(e.preprocessors.oas2)})),this.plugins.forEach((e=>{var n;return(null===(n=e.rules)||void 0===n?void 0:n.oas2)&&t.push(e.rules.oas2)})),this.plugins.forEach((e=>{var n;return(null===(n=e.decorators)||void 0===n?void 0:n.oas2)&&t.push(e.decorators.oas2)})),t;case s.SpecMajorVersion.Async2:const n=[];return this.plugins.forEach((e=>{var t;return(null===(t=e.preprocessors)||void 0===t?void 0:t.async2)&&n.push(e.preprocessors.async2)})),this.plugins.forEach((e=>{var t;return(null===(t=e.rules)||void 0===t?void 0:t.async2)&&n.push(e.rules.async2)})),this.plugins.forEach((e=>{var t;return(null===(t=e.decorators)||void 0===t?void 0:t.async2)&&n.push(e.decorators.async2)})),n}}skipRules(e){for(const t of e||[])for(const e of Object.values(s.SpecVersion))this.rules[e][t]&&(this.rules[e][t]="off")}skipPreprocessors(e){for(const t of e||[])for(const e of Object.values(s.SpecVersion))this.preprocessors[e][t]&&(this.preprocessors[e][t]="off")}skipDecorators(e){for(const t of e||[])for(const e of Object.values(s.SpecVersion))this.decorators[e][t]&&(this.decorators[e][t]="off")}}t.StyleguideConfig=p,t.Config=class{constructor(e,t){this.rawConfig=e,this.configFile=t,this.apis=e.apis||{},this.styleguide=new p(e.styleguide||{},t),this.theme=e.theme||{},this.resolve=(0,c.getResolveConfig)(null==e?void 0:e.resolve),this.region=e.region,this.organization=e.organization,this.files=e.files||[],this.telemetry=e.telemetry}}},5030:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.initRules=void 0;const r=n(771);t.initRules=function(e,t,n,i){return e.flatMap((e=>Object.keys(e).map((r=>{const o=e[r],a="rules"===n?t.getRuleSettings(r,i):"preprocessors"===n?t.getPreprocessorSettings(r,i):t.getDecoratorSettings(r,i);if("off"===a.severity)return;const s=a.severity,l=o(a);return Array.isArray(l)?l.map((e=>({severity:s,ruleId:r,visitor:e}))):{severity:s,ruleId:r,visitor:l}})))).flatMap((e=>e)).filter(r.isDefined)}},2565:function(e,t,n){"use strict";var r=this&&this.__rest||function(e,t){var n={};for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols){var i=0;for(r=Object.getOwnPropertySymbols(e);it[e]));n[e]&&null===t&&(0,i.showWarningForDeprecatedField)(e),n[e]&&t&&n[t]&&(0,i.showErrorForDeprecatedField)(e,t),n[e]&&r&&n[r]&&(0,i.showErrorForDeprecatedField)(e,t,r),(n[e]||o)&&(0,i.showWarningForDeprecatedField)(e,t,r)}t.parsePresetName=function(e){if(e.indexOf("/")>-1){const[t,n]=e.split("/");return{pluginId:t,configName:n}}return{pluginId:"",configName:e}},t.transformApiDefinitionsToApis=s,t.prefixRules=function(e,t){if(!t)return e;const n={};for(const r of Object.keys(e))n[`${t}/${r}`]=e[r];return n},t.mergeExtends=function(e){const t={rules:{},oas2Rules:{},oas3_0Rules:{},oas3_1Rules:{},async2Rules:{},preprocessors:{},oas2Preprocessors:{},oas3_0Preprocessors:{},oas3_1Preprocessors:{},async2Preprocessors:{},decorators:{},oas2Decorators:{},oas3_0Decorators:{},oas3_1Decorators:{},async2Decorators:{},plugins:[],pluginPaths:[],extendPaths:[]};for(const n of e){if(n.extends)throw new Error(`'extends' is not supported in shared configs yet: ${JSON.stringify(n,null,2)}.`);Object.assign(t.rules,n.rules),Object.assign(t.oas2Rules,n.oas2Rules),(0,i.assignExisting)(t.oas2Rules,n.rules||{}),Object.assign(t.oas3_0Rules,n.oas3_0Rules),(0,i.assignExisting)(t.oas3_0Rules,n.rules||{}),Object.assign(t.oas3_1Rules,n.oas3_1Rules),(0,i.assignExisting)(t.oas3_1Rules,n.rules||{}),Object.assign(t.async2Rules,n.async2Rules),(0,i.assignExisting)(t.async2Rules,n.rules||{}),Object.assign(t.preprocessors,n.preprocessors),Object.assign(t.oas2Preprocessors,n.oas2Preprocessors),(0,i.assignExisting)(t.oas2Preprocessors,n.preprocessors||{}),Object.assign(t.oas3_0Preprocessors,n.oas3_0Preprocessors),(0,i.assignExisting)(t.oas3_0Preprocessors,n.preprocessors||{}),Object.assign(t.oas3_1Preprocessors,n.oas3_1Preprocessors),(0,i.assignExisting)(t.oas3_1Preprocessors,n.preprocessors||{}),Object.assign(t.async2Preprocessors,n.async2Preprocessors),(0,i.assignExisting)(t.async2Preprocessors,n.preprocessors||{}),Object.assign(t.decorators,n.decorators),Object.assign(t.oas2Decorators,n.oas2Decorators),(0,i.assignExisting)(t.oas2Decorators,n.decorators||{}),Object.assign(t.oas3_0Decorators,n.oas3_0Decorators),(0,i.assignExisting)(t.oas3_0Decorators,n.decorators||{}),Object.assign(t.oas3_1Decorators,n.oas3_1Decorators),(0,i.assignExisting)(t.oas3_1Decorators,n.decorators||{}),Object.assign(t.async2Decorators,n.async2Decorators),(0,i.assignExisting)(t.async2Decorators,n.decorators||{}),t.plugins.push(...n.plugins||[]),t.pluginPaths.push(...n.pluginPaths||[]),t.extendPaths.push(...new Set(n.extendPaths))}return t},t.getMergedConfig=function(e,t){var n,r,a,s,l,c,u,p;const d=[...Object.values(e.apis).map((e=>{var t;return null===(t=null==e?void 0:e.styleguide)||void 0===t?void 0:t.extendPaths})),null===(r=null===(n=e.rawConfig)||void 0===n?void 0:n.styleguide)||void 0===r?void 0:r.extendPaths].flat().filter(i.isTruthy),f=[...Object.values(e.apis).map((e=>{var t;return null===(t=null==e?void 0:e.styleguide)||void 0===t?void 0:t.pluginPaths})),null===(s=null===(a=e.rawConfig)||void 0===a?void 0:a.styleguide)||void 0===s?void 0:s.pluginPaths].flat().filter(i.isTruthy);return t?new o.Config(Object.assign(Object.assign({},e.rawConfig),{styleguide:Object.assign(Object.assign({},e.apis[t]?e.apis[t].styleguide:e.rawConfig.styleguide),{extendPaths:d,pluginPaths:f}),theme:Object.assign(Object.assign({},e.rawConfig.theme),null===(l=e.apis[t])||void 0===l?void 0:l.theme),files:[...e.files,...null!==(p=null===(u=null===(c=e.apis)||void 0===c?void 0:c[t])||void 0===u?void 0:u.files)&&void 0!==p?p:[]]}),e.configFile):e},t.checkForDeprecatedFields=u,t.transformConfig=function(e){var t,n;const i=[["apiDefinitions","apis",void 0],["referenceDocs","openapi","theme"],["lint",void 0,void 0],["styleguide",void 0,void 0],["features.openapi","openapi","theme"]];for(const[t,n,r]of i)u(t,n,e,r);const{apis:o,apiDefinitions:p,referenceDocs:d,lint:f}=e,h=r(e,["apis","apiDefinitions","referenceDocs","lint"]),{styleguideConfig:m,rawConfigRest:g}=l(h),y=Object.assign({theme:{openapi:Object.assign(Object.assign(Object.assign({},d),e["features.openapi"]),null===(t=e.theme)||void 0===t?void 0:t.openapi),mockServer:Object.assign(Object.assign({},e["features.mockServer"]),null===(n=e.theme)||void 0===n?void 0:n.mockServer)},apis:c(o)||s(p),styleguide:m||f},g);return function(e){var t,n;let r=Object.assign({},null===(t=e.styleguide)||void 0===t?void 0:t.rules);for(const t of Object.values(e.apis||{}))r=Object.assign(Object.assign({},r),null===(n=null==t?void 0:t.styleguide)||void 0===n?void 0:n.rules);for(const e of Object.keys(r))e.startsWith("assert/")&&a.logger.warn(`\nThe 'assert/' syntax in ${e} is deprecated. Update your configuration to use 'rule/' instead. Examples and more information: https://redocly.com/docs/cli/rules/configurable-rules/\n`)}(y),y},t.getResolveConfig=function(e){var t,n;return{http:{headers:null!==(n=null===(t=null==e?void 0:e.http)||void 0===t?void 0:t.headers)&&void 0!==n?n:[],customFetch:void 0}}},t.getUniquePlugins=function(e){const t=new Set,n=[];for(const r of e)t.has(r.id)?r.id&&a.logger.warn(`Duplicate plugin id "${a.colorize.red(r.id)}".\n`):(n.push(r),t.add(r.id));return n};class p extends Error{}t.ConfigValidationError=p},8005:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.env=t.isBrowser=void 0,t.isBrowser="undefined"!=typeof window||"undefined"!=typeof self||"undefined"==typeof process,t.env=t.isBrowser?{}:{}||{}},5273:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.stringifyYaml=t.parseYaml=void 0;const r=n(3320),i=r.JSON_SCHEMA.extend({implicit:[r.types.merge],explicit:[r.types.binary,r.types.omap,r.types.pairs,r.types.set]});t.parseYaml=(e,t)=>(0,r.load)(e,Object.assign({schema:i},t)),t.stringifyYaml=(e,t)=>(0,r.dump)(e,t)},3065:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.logger=t.colorize=t.colorOptions=void 0;const r=n(4819);var i=n(4819);Object.defineProperty(t,"colorOptions",{enumerable:!0,get:function(){return i.options}});const o=n(8005),a=n(771);t.colorize=new Proxy(r,{get(e,t){return o.isBrowser?a.identity:e[t]}}),t.logger=new class{stderr(e){return process.stderr.write(e)}info(e){return o.isBrowser?console.log(e):this.stderr(e)}warn(e){return o.isBrowser?console.warn(e):this.stderr(t.colorize.yellow(e))}error(e){return o.isBrowser?console.error(e):this.stderr(t.colorize.red(e))}}},1510:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getTypes=t.getMajorSpecVersion=t.detectSpec=t.SpecMajorVersion=t.SpecVersion=void 0;const r=n(388),i=n(5241),o=n(2608),a=n(1576);var s,l;!function(e){e.OAS2="oas2",e.OAS3_0="oas3_0",e.OAS3_1="oas3_1",e.Async2="async2"}(s||(t.SpecVersion=s={})),function(e){e.OAS2="oas2",e.OAS3="oas3",e.Async2="async2"}(l||(t.SpecMajorVersion=l={}));const c={[s.OAS2]:r.Oas2Types,[s.OAS3_0]:i.Oas3Types,[s.OAS3_1]:o.Oas3_1Types,[s.Async2]:a.AsyncApi2Types};t.detectSpec=function(e){if("object"!=typeof e)throw new Error("Document must be JSON object, got "+typeof e);if(e.openapi&&"string"!=typeof e.openapi)throw new Error(`Invalid OpenAPI version: should be a string but got "${typeof e.openapi}"`);if(e.openapi&&e.openapi.startsWith("3.0"))return s.OAS3_0;if(e.openapi&&e.openapi.startsWith("3.1"))return s.OAS3_1;if(e.swagger&&"2.0"===e.swagger)return s.OAS2;if(e.openapi||e.swagger)throw new Error(`Unsupported OpenAPI version: ${e.openapi||e.swagger}`);if(e.asyncapi&&e.asyncapi.startsWith("2."))return s.Async2;if(e.asyncapi)throw new Error(`Unsupported AsyncAPI version: ${e.asyncapi}`);throw new Error("Unsupported specification")},t.getMajorSpecVersion=function(e){return e===s.OAS2?l.OAS2:e===s.Async2?l.Async2:l.OAS3},t.getTypes=function(e){return c[e]}},1094:function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))((function(i,o){function a(e){try{l(r.next(e))}catch(e){o(e)}}function s(e){try{l(r.throw(e))}catch(e){o(e)}}function l(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(a,s)}l((r=r.apply(e,t||[])).next())}))};Object.defineProperty(t,"__esModule",{value:!0}),t.isRedoclyRegistryURL=t.RedoclyClient=void 0;const i=n(2116),o=n(6470),a=n(6918),s=n(1390),l=n(3777),c=n(8005),u=n(771),p=n(3065),d=".redocly-config.json";t.RedoclyClient=class{constructor(e){this.accessTokens={},this.region=this.loadRegion(e),this.loadTokens(),this.domain=e?l.DOMAINS[e]:c.env.REDOCLY_DOMAIN||l.DOMAINS[l.DEFAULT_REGION],c.env.REDOCLY_DOMAIN=this.domain,this.registryApi=new s.RegistryApi(this.accessTokens,this.region)}loadRegion(e){if(e&&!l.DOMAINS[e])throw new Error(`Invalid argument: region in config file.\nGiven: ${p.colorize.green(e)}, choices: "us", "eu".`);return c.env.REDOCLY_DOMAIN?l.AVAILABLE_REGIONS.find((e=>l.DOMAINS[e]===c.env.REDOCLY_DOMAIN))||l.DEFAULT_REGION:e||l.DEFAULT_REGION}getRegion(){return this.region}hasTokens(){return(0,u.isNotEmptyObject)(this.accessTokens)}hasToken(){return!!this.accessTokens[this.region]}getAuthorizationHeader(){return r(this,void 0,void 0,(function*(){return this.accessTokens[this.region]}))}setAccessTokens(e){this.accessTokens=e}loadTokens(){const e=(0,o.resolve)((0,a.homedir)(),d),t=this.readCredentialsFile(e);(0,u.isNotEmptyObject)(t)&&this.setAccessTokens(Object.assign(Object.assign({},t),t.token&&!t[this.region]&&{[this.region]:t.token})),c.env.REDOCLY_AUTHORIZATION&&this.setAccessTokens(Object.assign(Object.assign({},this.accessTokens),{[this.region]:c.env.REDOCLY_AUTHORIZATION}))}getAllTokens(){return Object.entries(this.accessTokens).filter((([e])=>l.AVAILABLE_REGIONS.includes(e))).map((([e,t])=>({region:e,token:t})))}getValidTokens(){return r(this,void 0,void 0,(function*(){const e=this.getAllTokens(),t=yield Promise.allSettled(e.map((({token:e,region:t})=>this.verifyToken(e,t))));return e.filter(((e,n)=>"fulfilled"===t[n].status)).map((({token:e,region:t})=>({token:e,region:t,valid:!0})))}))}getTokens(){return r(this,void 0,void 0,(function*(){return this.hasTokens()?yield this.getValidTokens():[]}))}isAuthorizedWithRedoclyByRegion(){return r(this,void 0,void 0,(function*(){if(!this.hasTokens())return!1;const e=this.accessTokens[this.region];if(!e)return!1;try{return yield this.verifyToken(e,this.region),!0}catch(e){return!1}}))}isAuthorizedWithRedocly(){return r(this,void 0,void 0,(function*(){return this.hasTokens()&&(0,u.isNotEmptyObject)(yield this.getValidTokens())}))}readCredentialsFile(e){return(0,i.existsSync)(e)?JSON.parse((0,i.readFileSync)(e,"utf-8")):{}}verifyToken(e,t,n=!1){return r(this,void 0,void 0,(function*(){return this.registryApi.authStatus(e,t,n)}))}login(e,t=!1){return r(this,void 0,void 0,(function*(){const n=(0,o.resolve)((0,a.homedir)(),d);try{yield this.verifyToken(e,this.region,t)}catch(e){throw new Error("Authorization failed. Please check if you entered a valid API key.")}const r=Object.assign(Object.assign({},this.readCredentialsFile(n)),{[this.region]:e,token:e});this.accessTokens=r,this.registryApi.setAccessTokens(r),(0,i.writeFileSync)(n,JSON.stringify(r,null,2))}))}logout(){const e=(0,o.resolve)((0,a.homedir)(),d);(0,i.existsSync)(e)&&(0,i.unlinkSync)(e)}},t.isRedoclyRegistryURL=function(e){const t=c.env.REDOCLY_DOMAIN||l.DOMAINS[l.DEFAULT_REGION],n="redocly.com"===t?"redoc.ly":t;return!(!e.startsWith(`https://api.${t}/registry/`)&&!e.startsWith(`https://api.${n}/registry/`))}},1390:function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))((function(i,o){function a(e){try{l(r.next(e))}catch(e){o(e)}}function s(e){try{l(r.throw(e))}catch(e){o(e)}}function l(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(a,s)}l((r=r.apply(e,t||[])).next())}))};Object.defineProperty(t,"__esModule",{value:!0}),t.RegistryApi=void 0;const i=n(8150),o=n(3777),a=n(771),s=n(3244).i8;t.RegistryApi=class{constructor(e,t){this.accessTokens=e,this.region=t}get accessToken(){return(0,a.isNotEmptyObject)(this.accessTokens)&&this.accessTokens[this.region]}getBaseUrl(e=o.DEFAULT_REGION){return`https://api.${o.DOMAINS[e]}/registry`}setAccessTokens(e){return this.accessTokens=e,this}request(e="",t={},n){var o,a;return r(this,void 0,void 0,(function*(){const r="undefined"!=typeof process&&(null===(o={})||void 0===o?void 0:o.REDOCLY_CLI_COMMAND)||"",l="undefined"!=typeof process&&(null===(a={})||void 0===a?void 0:a.REDOCLY_ENVIRONMENT)||"",c=Object.assign({},t.headers||{},{"x-redocly-cli-version":s,"user-agent":`redocly-cli / ${s} ${r} ${l}`});if(!c.hasOwnProperty("authorization"))throw new Error("Unauthorized");const u=yield(0,i.default)(`${this.getBaseUrl(n)}${e}`,Object.assign({},t,{headers:c}));if(401===u.status)throw new Error("Unauthorized");if(404===u.status){const e=yield u.json();throw new Error(e.code)}return u}))}authStatus(e,t,n=!1){return r(this,void 0,void 0,(function*(){try{const n=yield this.request("",{headers:{authorization:e}},t);return yield n.json()}catch(e){throw n&&console.log(e),e}}))}prepareFileUpload({organizationId:e,name:t,version:n,filesHash:i,filename:o,isUpsert:a}){return r(this,void 0,void 0,(function*(){const r=yield this.request(`/${e}/${t}/${n}/prepare-file-upload`,{method:"POST",headers:{"content-type":"application/json",authorization:this.accessToken},body:JSON.stringify({filesHash:i,filename:o,isUpsert:a})},this.region);if(r.ok)return r.json();throw new Error("Could not prepare file upload")}))}pushApi({organizationId:e,name:t,version:n,rootFilePath:i,filePaths:o,branch:a,isUpsert:s,isPublic:l,batchId:c,batchSize:u}){return r(this,void 0,void 0,(function*(){if(!(yield this.request(`/${e}/${t}/${n}`,{method:"PUT",headers:{"content-type":"application/json",authorization:this.accessToken},body:JSON.stringify({rootFilePath:i,filePaths:o,branch:a,isUpsert:s,isPublic:l,batchId:c,batchSize:u})},this.region)).ok)throw new Error("Could not push api")}))}}},7468:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.isAnchor=t.isMappingRef=t.isAbsoluteUrl=t.refBaseName=t.pointerBaseName=t.parsePointer=t.parseRef=t.escapePointer=t.unescapePointer=t.Location=t.isRef=t.joinPointer=void 0;const r=n(771);function i(e,t){return""===e&&(e="#/"),"/"===e[e.length-1]?e+t:e+"/"+t}t.joinPointer=i,t.isRef=function(e){return e&&"string"==typeof e.$ref};class o{constructor(e,t){this.source=e,this.pointer=t}child(e){return new o(this.source,i(this.pointer,(Array.isArray(e)?e:[e]).map(s).join("/")))}key(){return Object.assign(Object.assign({},this),{reportOnKey:!0})}get absolutePointer(){return this.source.absoluteRef+("#/"===this.pointer?"":this.pointer)}}function a(e){return decodeURIComponent(e.replace(/~1/g,"/").replace(/~0/g,"~"))}function s(e){return"number"==typeof e?e:e.replace(/~/g,"~0").replace(/\//g,"~1")}t.Location=o,t.unescapePointer=a,t.escapePointer=s,t.parseRef=function(e){const[t,n]=e.split("#/");return{uri:t||null,pointer:n?n.split("/").map(a).filter(r.isTruthy):[]}},t.parsePointer=function(e){return e.substr(2).split("/").map(a)},t.pointerBaseName=function(e){const t=e.split("/");return t[t.length-1]},t.refBaseName=function(e){const t=e.split(/[\/\\]/);return t[t.length-1].replace(/\.[^.]+$/,"")},t.isAbsoluteUrl=function(e){return e.startsWith("http://")||e.startsWith("https://")},t.isMappingRef=function(e){return e.startsWith("#")||e.startsWith("https://")||e.startsWith("http://")||e.startsWith("./")||e.startsWith("../")||e.indexOf("/")>-1},t.isAnchor=function(e){return/^#[A-Za-z][A-Za-z0-9\-_:.]*$/.test(e)}},4182:function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))((function(i,o){function a(e){try{l(r.next(e))}catch(e){o(e)}}function s(e){try{l(r.throw(e))}catch(e){o(e)}}function l(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(a,s)}l((r=r.apply(e,t||[])).next())}))};Object.defineProperty(t,"__esModule",{value:!0}),t.resolveDocument=t.BaseResolver=t.makeDocumentFromString=t.makeRefId=t.YamlParseError=t.ResolveError=t.Source=void 0;const i=n(3197),o=n(6470),a=n(7468),s=n(5220),l=n(771);class c{constructor(e,t,n){this.absoluteRef=e,this.body=t,this.mimeType=n}getAst(e){var t;return void 0===this._ast&&(this._ast=null!==(t=e(this.body,{filename:this.absoluteRef}))&&void 0!==t?t:void 0,this._ast&&0===this._ast.kind&&""===this._ast.value&&1!==this._ast.startPosition&&(this._ast.startPosition=1,this._ast.endPosition=1)),this._ast}getLines(){return void 0===this._lines&&(this._lines=this.body.split(/\r\n|[\n\r]/g)),this._lines}}t.Source=c;class u extends Error{constructor(e){super(e.message),this.originalError=e,Object.setPrototypeOf(this,u.prototype)}}t.ResolveError=u;const p=/\((\d+):(\d+)\)$/;class d extends Error{constructor(e,t){super(e.message.split("\n")[0]),this.originalError=e,this.source=t,Object.setPrototypeOf(this,d.prototype);const[,n,r]=this.message.match(p)||[];this.line=parseInt(n,10),this.col=parseInt(r,10)}}function f(e,t){return e+"::"+t}function h(e,t){return{prev:e,node:t}}t.YamlParseError=d,t.makeRefId=f,t.makeDocumentFromString=function(e,t){const n=new c(t,e);try{return{source:n,parsed:(0,l.parseYaml)(e,{filename:t})}}catch(e){throw new d(e,n)}},t.BaseResolver=class{constructor(e={http:{headers:[]}}){this.config=e,this.cache=new Map}getFiles(){return new Set(Array.from(this.cache.keys()))}resolveExternalRef(e,t){return(0,a.isAbsoluteUrl)(t)?t:e&&(0,a.isAbsoluteUrl)(e)?new URL(t,e).href:o.resolve(e?o.dirname(e):process.cwd(),t)}loadExternalRef(e){return r(this,void 0,void 0,(function*(){try{if((0,a.isAbsoluteUrl)(e)){const{body:t,mimeType:n}=yield(0,l.readFileFromUrl)(e,this.config.http);return new c(e,t,n)}{if(i.lstatSync(e).isDirectory())throw new Error(`Expected a file but received a folder at ${e}`);const t=yield i.promises.readFile(e,"utf-8");return new c(e,t.replace(/\r\n/g,"\n"))}}catch(e){throw e.message=e.message.replace(", lstat",""),new u(e)}}))}parseDocument(e,t=!1){var n;const r=e.absoluteRef.substr(e.absoluteRef.lastIndexOf("."));if(![".json",".json",".yml",".yaml"].includes(r)&&!(null===(n=e.mimeType)||void 0===n?void 0:n.match(/(json|yaml|openapi)/))&&!t)return{source:e,parsed:e.body};try{return{source:e,parsed:(0,l.parseYaml)(e.body,{filename:e.absoluteRef})}}catch(t){throw new d(t,e)}}resolveDocument(e,t,n=!1){return r(this,void 0,void 0,(function*(){const r=this.resolveExternalRef(e,t),i=this.cache.get(r);if(i)return i;const o=this.loadExternalRef(r).then((e=>this.parseDocument(e,n)));return this.cache.set(r,o),o}))}};const m={name:"unknown",properties:{}},g={name:"scalar",properties:{}};t.resolveDocument=function(e){return r(this,void 0,void 0,(function*(){const{rootDocument:t,externalRefResolver:n,rootType:i}=e,o=new Map,c=new Set,u=[];let p;!function e(t,i,p,d){const y=i.source.absoluteRef,b=new Map;function v(e,t,i){return r(this,void 0,void 0,(function*(){if(function(e,t){for(;e;){if(e.node===t)return!0;e=e.prev}return!1}(i.prev,t))throw new Error("Self-referencing circular pointer");if((0,a.isAnchor)(t.$ref)){yield(0,l.nextTick)();const n={resolved:!0,isRemote:!1,node:b.get(t.$ref),document:e,nodePointer:t.$ref},r=f(e.source.absoluteRef,t.$ref);return o.set(r,n),n}const{uri:r,pointer:s}=(0,a.parseRef)(t.$ref),c=null!==r;let u;try{u=c?yield n.resolveDocument(e.source.absoluteRef,r):e}catch(n){const r={resolved:!1,isRemote:c,document:void 0,error:n},i=f(e.source.absoluteRef,t.$ref);return o.set(i,r),r}let p={resolved:!0,document:u,isRemote:c,node:e.parsed,nodePointer:"#/"},d=u.parsed;const m=s;for(const e of m){if("object"!=typeof d){d=void 0;break}if(void 0!==d[e])d=d[e],p.nodePointer=(0,a.joinPointer)(p.nodePointer,(0,a.escapePointer)(e));else{if(!(0,a.isRef)(d)){d=void 0;break}if(p=yield v(u,d,h(i,d)),u=p.document||u,"object"!=typeof p.node){d=void 0;break}d=p.node[e],p.nodePointer=(0,a.joinPointer)(p.nodePointer,(0,a.escapePointer)(e))}}p.node=d,p.document=u;const g=f(e.source.absoluteRef,t.$ref);return p.document&&(0,a.isRef)(d)&&(p=yield v(p.document,d,h(i,d))),o.set(g,p),Object.assign({},p)}))}!function t(n,r,o){if("object"!=typeof n||null===n)return;const l=`${r.name}::${o}`;if(c.has(l))return;c.add(l);const[p,d]=Object.entries(n).find((([e])=>"$anchor"===e))||[];if(d&&b.set(`#${d}`,n),Array.isArray(n)){const e=r.items;if(void 0===e&&r!==m&&r!==s.SpecExtension)return;for(let r=0;r{t.resolved&&e(t.node,t.document,t.nodePointer,r)}));u.push(t)}}}(t,d,y+p)}(t.parsed,t,"#/",i);do{p=yield Promise.all(u)}while(u.length!==p.length);return o}))}},348:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.reportUnresolvedRef=t.NoUnresolvedRefs=void 0;const r=n(4182);function i(e,t,n){var i;const o=e.error;o instanceof r.YamlParseError&&t({message:"Failed to parse: "+o.message,location:{source:o.source,pointer:void 0,start:{col:o.col,line:o.line}}});const a=null===(i=e.error)||void 0===i?void 0:i.message;t({location:n,message:"Can't resolve $ref"+(a?": "+a:"")})}t.NoUnresolvedRefs=()=>({ref:{leave(e,{report:t,location:n},r){void 0===r.node&&i(r,t,n)}},DiscriminatorMapping(e,{report:t,resolve:n,location:r}){for(const o of Object.keys(e)){const a=n({$ref:e[o]});if(void 0!==a.node)return;i(a,t,r.child(o))}}}),t.reportUnresolvedRef=i},4508:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.RemoveUnusedComponents=void 0;const r=n(771);t.RemoveUnusedComponents=()=>{const e=new Map;function t(t,n,r){var i;e.set(t.absolutePointer,{used:(null===(i=e.get(t.absolutePointer))||void 0===i?void 0:i.used)||!1,componentType:n,name:r})}return{ref:{leave(t,{type:n,resolve:r,key:i}){if(["Schema","Parameter","Response","SecurityScheme"].includes(n.name)){const n=r(t);if(!n.location)return;const[o,a]=n.location.absolutePointer.split("#",2),s=`${o}#${a.split("/").slice(0,3).join("/")}`;e.set(s,{used:!0,name:i.toString()})}}},Root:{leave(t,n){const i=n.getVisitorData();i.removedCount=0;const o=new Set;e.forEach((e=>{const{used:n,name:r,componentType:a}=e;!n&&a&&(o.add(a),delete t[a][r],i.removedCount++)}));for(const e of o)(0,r.isEmptyObject)(t[e])&&delete t[e]}},NamedSchemas:{Schema(e,{location:n,key:r}){e.allOf||t(n,"definitions",r.toString())}},NamedParameters:{Parameter(e,{location:n,key:r}){t(n,"parameters",r.toString())}},NamedResponses:{Response(e,{location:n,key:r}){t(n,"responses",r.toString())}},NamedSecuritySchemes:{SecurityScheme(e,{location:n,key:r}){t(n,"securityDefinitions",r.toString())}}}}},6350:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.RemoveUnusedComponents=void 0;const r=n(771);t.RemoveUnusedComponents=()=>{const e=new Map;function t(t,n,r){var i;e.set(t.absolutePointer,{used:(null===(i=e.get(t.absolutePointer))||void 0===i?void 0:i.used)||!1,componentType:n,name:r})}return{ref:{leave(t,{type:n,resolve:r,key:i}){if(["Schema","Header","Parameter","Response","Example","RequestBody"].includes(n.name)){const n=r(t);if(!n.location)return;const[o,a]=n.location.absolutePointer.split("#",2),s=`${o}#${a.split("/").slice(0,4).join("/")}`;e.set(s,{used:!0,name:i.toString()})}}},Root:{leave(t,n){const i=n.getVisitorData();i.removedCount=0,e.forEach((e=>{const{used:n,componentType:o,name:a}=e;if(!n&&o&&t.components){const e=t.components[o];delete e[a],i.removedCount++,(0,r.isEmptyObject)(e)&&delete t.components[o]}})),(0,r.isEmptyObject)(t.components)&&delete t.components}},NamedSchemas:{Schema(e,{location:n,key:r}){e.allOf||t(n,"schemas",r.toString())}},NamedParameters:{Parameter(e,{location:n,key:r}){t(n,"parameters",r.toString())}},NamedResponses:{Response(e,{location:n,key:r}){t(n,"responses",r.toString())}},NamedExamples:{Example(e,{location:n,key:r}){t(n,"examples",r.toString())}},NamedRequestBodies:{RequestBody(e,{location:n,key:r}){t(n,"requestBodies",r.toString())}},NamedHeaders:{Header(e,{location:n,key:r}){t(n,"headers",r.toString())}}}}},1576:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.AsyncApi2Types=void 0;const r=n(5220),i=n(7468),o={properties:{},allowed(){return["http","ws","kafka","anypointmq","amqp","amqp1","mqtt","mqtt5","nats","jms","sns","solace","sqs","stomp","redis","mercure","ibmmq","googlepubsub","pulsar"]},additionalProperties:{type:"object"}},a={properties:{},allowed(){return["http","ws","kafka","anypointmq","amqp","amqp1","mqtt","mqtt5","nats","jms","sns","solace","sqs","stomp","redis","mercure","ibmmq","googlepubsub","pulsar"]},additionalProperties:{type:"object"}},s={properties:{},allowed(){return["http","ws","kafka","anypointmq","amqp","amqp1","mqtt","mqtt5","nats","jms","sns","solace","sqs","stomp","redis","mercure","ibmmq","googlepubsub","pulsar"]},additionalProperties:{type:"object"}},l={properties:{},allowed(){return["http","ws","kafka","anypointmq","amqp","amqp1","mqtt","mqtt5","nats","jms","sns","solace","sqs","stomp","redis","mercure","ibmmq","googlepubsub","pulsar"]},additionalProperties:{type:"object"}},c={properties:{$id:{type:"string"},id:{type:"string"},$schema:{type:"string"},definitions:"NamedSchemas",$defs:"NamedSchemas",$vocabulary:{type:"string"},externalDocs:"ExternalDocs",discriminator:"Discriminator",myArbitraryKeyword:{type:"boolean"},title:{type:"string"},multipleOf:{type:"number",minimum:0},maximum:{type:"number"},minimum:{type:"number"},exclusiveMaximum:{type:"number"},exclusiveMinimum:{type:"number"},maxLength:{type:"integer",minimum:0},minLength:{type:"integer",minimum:0},pattern:{type:"string"},maxItems:{type:"integer",minimum:0},minItems:{type:"integer",minimum:0},uniqueItems:{type:"boolean"},maxProperties:{type:"integer",minimum:0},minProperties:{type:"integer",minimum:0},required:{type:"array",items:{type:"string"}},enum:{type:"array"},type:e=>Array.isArray(e)?{type:"array",items:{enum:["object","array","string","number","integer","boolean","null"]}}:{enum:["object","array","string","number","integer","boolean","null"]},allOf:(0,r.listOf)("Schema"),anyOf:(0,r.listOf)("Schema"),oneOf:(0,r.listOf)("Schema"),not:"Schema",if:"Schema",then:"Schema",else:"Schema",dependentSchemas:(0,r.listOf)("Schema"),prefixItems:(0,r.listOf)("Schema"),contains:"Schema",minContains:{type:"integer",minimum:0},maxContains:{type:"integer",minimum:0},patternProperties:{type:"object"},propertyNames:"Schema",unevaluatedItems:e=>"boolean"==typeof e?{type:"boolean"}:"Schema",unevaluatedProperties:e=>"boolean"==typeof e?{type:"boolean"}:"Schema",summary:{type:"string"},properties:"SchemaProperties",items:e=>"boolean"==typeof e?{type:"boolean"}:"Schema",additionalProperties:e=>"boolean"==typeof e?{type:"boolean"}:"Schema",description:{type:"string"},format:{type:"string"},contentEncoding:{type:"string"},contentMediaType:{type:"string"},default:null,readOnly:{type:"boolean"},writeOnly:{type:"boolean"},examples:{type:"array"},example:{isExample:!0},deprecated:{type:"boolean"},const:null,$comment:{type:"string"},dependencies:{type:"object"}}},u={properties:{},additionalProperties:e=>(0,i.isMappingRef)(e)?{type:"string",directResolveAs:"Schema"}:{type:"string"}},p={properties:{type:{enum:["userPassword","apiKey","X509","symmetricEncryption","asymmetricEncryption","httpApiKey","http","oauth2","openIdConnect","plain","scramSha256","scramSha512","gssapi"]},description:{type:"string"},name:{type:"string"},in:{type:"string",enum:["query","header","cookie","user","password"]},scheme:{type:"string"},bearerFormat:{type:"string"},flows:"SecuritySchemeFlows",openIdConnectUrl:{type:"string"}},required(e){switch(null==e?void 0:e.type){case"apiKey":return["type","in"];case"httpApiKey":return["type","name","in"];case"http":return["type","scheme"];case"oauth2":return["type","flows"];case"openIdConnect":return["type","openIdConnectUrl"];default:return["type"]}},allowed(e){switch(null==e?void 0:e.type){case"apiKey":return["type","in","description"];case"httpApiKey":return["type","name","in","description"];case"http":return["type","scheme","bearerFormat","description"];case"oauth2":return["type","flows","description"];case"openIdConnect":return["type","openIdConnectUrl","description"];default:return["type","description"]}},extensionsPrefix:"x-"},d={properties:{}};o.properties.http=d;const f={properties:{}};a.properties.http=f;const h={properties:{headers:"Schema",bindingVersion:{type:"string"}}};s.properties.http=h;const m={properties:{type:{type:"string"},method:{type:"string",enum:["GET","POST","PUT","PATCH","DELETE","HEAD","OPTIONS","CONNECT","TRACE"]},headers:"Schema",bindingVersion:{type:"string"}}};l.properties.http=m;const g={properties:{method:{type:"string"},query:"Schema",headers:"Schema",bindingVersion:{type:"string"}}};o.properties.ws=g;const y={properties:{}};a.properties.ws=y;const b={properties:{}};s.properties.ws=b;const v={properties:{}};l.properties.ws=v;const x={properties:{topic:{type:"string"},partitions:{type:"integer"},replicas:{type:"integer"},topicConfiguration:"KafkaTopicConfiguration",bindingVersion:{type:"string"}}};o.properties.kafka=x;const w={properties:{}};a.properties.kafka=w;const k={properties:{key:"Schema",schemaIdLocation:{type:"string"},schemaIdPayloadEncoding:{type:"string"},schemaLookupStrategy:{type:"string"},bindingVersion:{type:"string"}}};s.properties.kafka=k;const S={properties:{groupId:"Schema",clientId:"Schema",bindingVersion:{type:"string"}}};l.properties.kafka=S;const O={properties:{destination:{type:"string"},destinationType:{type:"string"},bindingVersion:{type:"string"}}};o.properties.anypointmq=O;const E={properties:{}};a.properties.anypointmq=E;const _={properties:{headers:"Schema",bindingVersion:{type:"string"}}};s.properties.anypointmq=_;const A={properties:{}};l.properties.anypointmq=A;const j={properties:{}};o.properties.amqp=j;const C={properties:{}};a.properties.amqp=C;const P={properties:{contentEncoding:{type:"string"},messageType:{type:"string"},bindingVersion:{type:"string"}}};s.properties.amqp=P;const T={properties:{expiration:{type:"integer"},userId:{type:"string"},cc:{type:"array",items:{type:"string"}},priority:{type:"integer"},deliveryMode:{type:"integer"},mandatory:{type:"boolean"},bcc:{type:"array",items:{type:"string"}},replyTo:{type:"string"},timestamp:{type:"boolean"},ack:{type:"boolean"},bindingVersion:{type:"string"}}};l.properties.amqp=T;const I={properties:{}};o.properties.amqp1=I;const R={properties:{}};a.properties.amqp1=R;const N={properties:{}};s.properties.amqp1=N;const $={properties:{}};l.properties.amqp1=$;const L={properties:{qos:{type:"integer"},retain:{type:"boolean"},bindingVersion:{type:"string"}}};o.properties.mqtt=L;const D={properties:{clientId:{type:"string"},cleanSession:{type:"boolean"},lastWill:"MqttServerBindingLastWill",keepAlive:{type:"integer"},bindingVersion:{type:"string"}}};a.properties.mqtt=D;const M={properties:{bindingVersion:{type:"string"}}};s.properties.mqtt=M;const F={properties:{qos:{type:"integer"},retain:{type:"boolean"},bindingVersion:{type:"string"}}};l.properties.mqtt=F;const z={properties:{}};o.properties.mqtt5=z;const U={properties:{}};a.properties.mqtt5=U;const B={properties:{}};s.properties.mqtt5=B;const q={properties:{}};l.properties.mqtt5=q;const V={properties:{}};o.properties.nats=V;const W={properties:{}};a.properties.nats=W;const H={properties:{}};s.properties.nats=H;const Y={properties:{queue:{type:"string"},bindingVersion:{type:"string"}}};l.properties.nats=Y;const G={properties:{destination:{type:"string"},destinationType:{type:"string"},bindingVersion:{type:"string"}}};o.properties.jms=G;const Q={properties:{}};a.properties.jms=Q;const X={properties:{headers:"Schema",bindingVersion:{type:"string"}}};s.properties.jms=X;const K={properties:{headers:"Schema",bindingVersion:{type:"string"}}};l.properties.jms=K;const Z={properties:{}};o.properties.solace=Z;const J={properties:{bindingVersion:{type:"string"},msgVpn:{type:"string"}}};a.properties.solace=J;const ee={properties:{}};s.properties.solace=ee;const te={properties:{bindingVersion:{type:"string"},destinations:(0,r.listOf)("SolaceDestination")}};l.properties.solace=te;const ne={properties:{}};o.properties.stomp=ne;const re={properties:{}};a.properties.stomp=re;const ie={properties:{}};s.properties.stomp=ie;const oe={properties:{}};l.properties.stomp=oe;const ae={properties:{}};o.properties.redis=ae;const se={properties:{}};a.properties.redis=se;const le={properties:{}};s.properties.redis=le;const ce={properties:{}};l.properties.redis=ce;const ue={properties:{}};o.properties.mercure=ue;const pe={properties:{}};a.properties.mercure=pe;const de={properties:{}};s.properties.mercure=de;const fe={properties:{}};l.properties.mercure=fe,t.AsyncApi2Types={Root:{properties:{asyncapi:null,info:"Info",id:{type:"string"},servers:"ServerMap",channels:"ChannelMap",components:"Components",tags:"TagList",externalDocs:"ExternalDocs",defaultContentType:{type:"string"}},required:["asyncapi","channels","info"]},Tag:{properties:{name:{type:"string"},description:{type:"string"},externalDocs:"ExternalDocs"},required:["name"]},TagList:(0,r.listOf)("Tag"),ServerMap:{properties:{},additionalProperties:(e,t)=>t.match(/^[A-Za-z0-9_\-]+$/)?"Server":void 0},ExternalDocs:{properties:{description:{type:"string"},url:{type:"string"}},required:["url"]},Server:{properties:{url:{type:"string"},protocol:{type:"string"},protocolVersion:{type:"string"},description:{type:"string"},variables:"ServerVariablesMap",security:"SecurityRequirementList",bindings:"ServerBindings",tags:"TagList"},required:["url","protocol"]},ServerVariable:{properties:{enum:{type:"array",items:{type:"string"}},default:{type:"string"},description:{type:"string"},examples:{type:"array",items:{type:"string"}}},required:[]},ServerVariablesMap:(0,r.mapOf)("ServerVariable"),SecurityRequirement:{properties:{},additionalProperties:{type:"array",items:{type:"string"}}},SecurityRequirementList:(0,r.listOf)("SecurityRequirement"),Info:{properties:{title:{type:"string"},version:{type:"string"},description:{type:"string"},termsOfService:{type:"string"},contact:"Contact",license:"License"},required:["title","version"]},Contact:{properties:{name:{type:"string"},url:{type:"string"},email:{type:"string"}}},License:{properties:{name:{type:"string"},url:{type:"string"}},required:["name"]},HttpServerBinding:f,HttpChannelBinding:d,HttpMessageBinding:h,HttpOperationBinding:m,WsServerBinding:y,WsChannelBinding:g,WsMessageBinding:b,WsOperationBinding:v,KafkaServerBinding:w,KafkaTopicConfiguration:{properties:{"cleanup.policy":{type:"array",items:{enum:["delete","compact"]}},"retention.ms":{type:"integer"},"retention.bytes":{type:"integer"},"delete.retention.ms":{type:"integer"},"max.message.bytes":{type:"integer"}}},KafkaChannelBinding:x,KafkaMessageBinding:k,KafkaOperationBinding:S,AnypointmqServerBinding:E,AnypointmqChannelBinding:O,AnypointmqMessageBinding:_,AnypointmqOperationBinding:A,AmqpServerBinding:C,AmqpChannelBinding:j,AmqpMessageBinding:P,AmqpOperationBinding:T,Amqp1ServerBinding:R,Amqp1ChannelBinding:I,Amqp1MessageBinding:N,Amqp1OperationBinding:$,MqttServerBindingLastWill:{properties:{topic:{type:"string"},qos:{type:"integer"},message:{type:"string"},retain:{type:"boolean"}}},MqttServerBinding:D,MqttChannelBinding:L,MqttMessageBinding:M,MqttOperationBinding:F,Mqtt5ServerBinding:U,Mqtt5ChannelBinding:z,Mqtt5MessageBinding:B,Mqtt5OperationBinding:q,NatsServerBinding:W,NatsChannelBinding:V,NatsMessageBinding:H,NatsOperationBinding:Y,JmsServerBinding:Q,JmsChannelBinding:G,JmsMessageBinding:X,JmsOperationBinding:K,SolaceServerBinding:J,SolaceChannelBinding:Z,SolaceMessageBinding:ee,SolaceDestination:{properties:{destinationType:{type:"string",enum:["queue","topic"]},deliveryMode:{type:"string",enum:["direct","persistent"]},"queue.name":{type:"string"},"queue.topicSubscriptions":{type:"array",items:{type:"string"}},"queue.accessType":{type:"string",enum:["exclusive","nonexclusive"]},"queue.maxMsgSpoolSize":{type:"string"},"queue.maxTtl":{type:"string"},"topic.topicSubscriptions":{type:"array",items:{type:"string"}}}},SolaceOperationBinding:te,StompServerBinding:re,StompChannelBinding:ne,StompMessageBinding:ie,StompOperationBinding:oe,RedisServerBinding:se,RedisChannelBinding:ae,RedisMessageBinding:le,RedisOperationBinding:ce,MercureServerBinding:pe,MercureChannelBinding:ue,MercureMessageBinding:de,MercureOperationBinding:fe,ServerBindings:a,ChannelBindings:o,ChannelMap:{properties:{},additionalProperties:"Channel"},Channel:{properties:{description:{type:"string"},subscribe:"Operation",publish:"Operation",parameters:"ParametersMap",bindings:"ChannelBindings",servers:{type:"array",items:{type:"string"}}}},Parameter:{properties:{description:{type:"string"},schema:"Schema",location:{type:"string"}}},ParametersMap:(0,r.mapOf)("Parameter"),Operation:{properties:{tags:{type:"array",items:{type:"string"}},summary:{type:"string"},description:{type:"string"},externalDocs:"ExternalDocs",operationId:{type:"string"},security:"SecurityRequirementList",bindings:"OperationBindings",traits:"OperationTraitList",message:"Message"},required:[]},Schema:c,MessageExample:{properties:{payload:{isExample:!0},summary:{type:"string"},name:{type:"string"},headers:{type:"object"}}},SchemaProperties:{properties:{},additionalProperties:e=>"boolean"==typeof e?{type:"boolean"}:"Schema"},DiscriminatorMapping:u,Discriminator:{properties:{propertyName:{type:"string"},mapping:"DiscriminatorMapping"},required:["propertyName"]},Components:{properties:{messages:"NamedMessages",parameters:"NamedParameters",schemas:"NamedSchemas",correlationIds:"NamedCorrelationIds",messageTraits:"NamedMessageTraits",operationTraits:"NamedOperationTraits",streamHeaders:"NamedStreamHeaders",securitySchemes:"NamedSecuritySchemes",servers:"ServerMap",serverVariables:"ServerVariablesMap",channels:"ChannelMap",serverBindings:"ServerBindings",channelBindings:"ChannelBindings",operationBindings:"OperationBindings",messageBindings:"MessageBindings"}},NamedSchemas:(0,r.mapOf)("Schema"),NamedMessages:(0,r.mapOf)("Message"),NamedMessageTraits:(0,r.mapOf)("MessageTrait"),NamedOperationTraits:(0,r.mapOf)("OperationTrait"),NamedParameters:(0,r.mapOf)("Parameter"),NamedSecuritySchemes:(0,r.mapOf)("SecurityScheme"),NamedCorrelationIds:(0,r.mapOf)("CorrelationId"),NamedStreamHeaders:(0,r.mapOf)("StreamHeader"),ImplicitFlow:{properties:{refreshUrl:{type:"string"},scopes:{type:"object",additionalProperties:{type:"string"}},authorizationUrl:{type:"string"}},required:["authorizationUrl","scopes"]},PasswordFlow:{properties:{refreshUrl:{type:"string"},scopes:{type:"object",additionalProperties:{type:"string"}},tokenUrl:{type:"string"}},required:["tokenUrl","scopes"]},ClientCredentials:{properties:{refreshUrl:{type:"string"},scopes:{type:"object",additionalProperties:{type:"string"}},tokenUrl:{type:"string"}},required:["tokenUrl","scopes"]},AuthorizationCode:{properties:{refreshUrl:{type:"string"},authorizationUrl:{type:"string"},scopes:{type:"object",additionalProperties:{type:"string"}},tokenUrl:{type:"string"}},required:["authorizationUrl","tokenUrl","scopes"]},SecuritySchemeFlows:{properties:{implicit:"ImplicitFlow",password:"PasswordFlow",clientCredentials:"ClientCredentials",authorizationCode:"AuthorizationCode"}},SecurityScheme:p,Message:{properties:{messageId:{type:"string"},headers:"Schema",payload:"Schema",correlationId:"CorrelationId",schemaFormat:{type:"string"},contentType:{type:"string"},name:{type:"string"},title:{type:"string"},summary:{type:"string"},description:{type:"string"},tags:"TagList",externalDocs:"ExternalDocs",bindings:"MessageBindings",traits:"MessageTraitList"},additionalProperties:{}},MessageBindings:s,OperationBindings:l,OperationTrait:{properties:{tags:{type:"array",items:{type:"string"}},summary:{type:"string"},description:{type:"string"},externalDocs:"ExternalDocs",operationId:{type:"string"},security:"SecurityRequirementList",bindings:"OperationBindings"},required:[]},OperationTraitList:(0,r.listOf)("OperationTrait"),MessageTrait:{properties:{messageId:{type:"string"},headers:"Schema",correlationId:"CorrelationId",schemaFormat:{type:"string"},contentType:{type:"string"},name:{type:"string"},title:{type:"string"},summary:{type:"string"},description:{type:"string"},tags:"TagList",externalDocs:"ExternalDocs",bindings:"MessageBindings"},additionalProperties:{}},MessageTraitList:(0,r.listOf)("MessageTrait"),CorrelationId:{properties:{description:{type:"string"},location:{type:"string"}},required:["location"]}}},5220:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.isNamedType=t.normalizeTypes=t.SpecExtension=t.mapOf=t.listOf=void 0,t.listOf=function(e){return{name:`${e}List`,properties:{},items:e}},t.mapOf=function(e){return{name:`${e}Map`,properties:{},additionalProperties:()=>e}},t.SpecExtension={name:"SpecExtension",properties:{},additionalProperties:{resolvable:!0}},t.normalizeTypes=function(e,n={}){const r={};for(const t of Object.keys(e))r[t]=Object.assign(Object.assign({},e[t]),{name:t});for(const e of Object.values(r))i(e);return r.SpecExtension=t.SpecExtension,r;function i(e){if(e.additionalProperties&&(e.additionalProperties=o(e.additionalProperties)),e.items&&(e.items=o(e.items)),e.properties){const t={};for(const[r,i]of Object.entries(e.properties))t[r]=o(i),n.doNotResolveExamples&&i&&i.isExample&&(t[r]=Object.assign(Object.assign({},i),{resolvable:!1}));e.properties=t}}function o(e){if("string"==typeof e){if(!r[e])throw new Error(`Unknown type name found: ${e}`);return r[e]}return"function"==typeof e?(t,n)=>o(e(t,n)):e&&e.name?(i(e=Object.assign({},e)),e):e&&e.directResolveAs?Object.assign(Object.assign({},e),{directResolveAs:o(e.directResolveAs)}):e}},t.isNamedType=function(e){return"string"==typeof(null==e?void 0:e.name)}},388:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.Oas2Types=void 0;const r=n(5220),i=/^[0-9][0-9Xx]{2}$/,o={properties:{name:{type:"string"},in:{type:"string",enum:["query","header","path","formData","body"]},description:{type:"string"},required:{type:"boolean"},schema:"Schema",type:{type:"string",enum:["string","number","integer","boolean","array","file"]},format:{type:"string"},allowEmptyValue:{type:"boolean"},items:"ParameterItems",collectionFormat:{type:"string",enum:["csv","ssv","tsv","pipes","multi"]},default:null,maximum:{type:"integer"},exclusiveMaximum:{type:"boolean"},minimum:{type:"integer"},exclusiveMinimum:{type:"boolean"},maxLength:{type:"integer"},minLength:{type:"integer"},pattern:{type:"string"},maxItems:{type:"integer"},minItems:{type:"integer"},uniqueItems:{type:"boolean"},enum:{type:"array"},multipleOf:{type:"number"},"x-example":"Example","x-examples":"ExamplesMap"},required(e){return e&&e.in?"body"===e.in?["name","in","schema"]:"array"===e.type?["name","in","type","items"]:["name","in","type"]:["name","in"]},extensionsPrefix:"x-"},a={properties:{type:{type:"string",enum:["string","number","integer","boolean","array"]},format:{type:"string"},items:"ParameterItems",collectionFormat:{type:"string",enum:["csv","ssv","tsv","pipes","multi"]},default:null,maximum:{type:"integer"},exclusiveMaximum:{type:"boolean"},minimum:{type:"integer"},exclusiveMinimum:{type:"boolean"},maxLength:{type:"integer"},minLength:{type:"integer"},pattern:{type:"string"},maxItems:{type:"integer"},minItems:{type:"integer"},uniqueItems:{type:"boolean"},enum:{type:"array"},multipleOf:{type:"number"}},required(e){return e&&"array"===e.type?["type","items"]:["type"]},extensionsPrefix:"x-"},s={properties:{default:"Response"},additionalProperties:(e,t)=>i.test(t)?"Response":void 0},l={properties:{description:{type:"string"},schema:"Schema",headers:(0,r.mapOf)("Header"),examples:"Examples","x-summary":{type:"string"}},required:["description"],extensionsPrefix:"x-"},c={properties:{description:{type:"string"},type:{type:"string",enum:["string","number","integer","boolean","array"]},format:{type:"string"},items:"ParameterItems",collectionFormat:{type:"string",enum:["csv","ssv","tsv","pipes","multi"]},default:null,maximum:{type:"integer"},exclusiveMaximum:{type:"boolean"},minimum:{type:"integer"},exclusiveMinimum:{type:"boolean"},maxLength:{type:"integer"},minLength:{type:"integer"},pattern:{type:"string"},maxItems:{type:"integer"},minItems:{type:"integer"},uniqueItems:{type:"boolean"},enum:{type:"array"},multipleOf:{type:"number"}},required(e){return e&&"array"===e.type?["type","items"]:["type"]},extensionsPrefix:"x-"},u={properties:{format:{type:"string"},title:{type:"string"},description:{type:"string"},default:null,multipleOf:{type:"number"},maximum:{type:"number"},minimum:{type:"number"},exclusiveMaximum:{type:"boolean"},exclusiveMinimum:{type:"boolean"},maxLength:{type:"number"},minLength:{type:"number"},pattern:{type:"string"},maxItems:{type:"number"},minItems:{type:"number"},uniqueItems:{type:"boolean"},maxProperties:{type:"number"},minProperties:{type:"number"},required:{type:"array",items:{type:"string"}},enum:{type:"array"},type:{type:"string",enum:["object","array","string","number","integer","boolean","null"]},items:e=>Array.isArray(e)?(0,r.listOf)("Schema"):"Schema",allOf:(0,r.listOf)("Schema"),properties:"SchemaProperties",additionalProperties:e=>"boolean"==typeof e?{type:"boolean"}:"Schema",discriminator:{type:"string"},readOnly:{type:"boolean"},xml:"Xml",externalDocs:"ExternalDocs",example:{isExample:!0},"x-tags":{type:"array",items:{type:"string"}},"x-nullable":{type:"boolean"},"x-extendedDiscriminator":{type:"string"},"x-additionalPropertiesName":{type:"string"},"x-explicitMappingOnly":{type:"boolean"},"x-enumDescriptions":"EnumDescriptions"},extensionsPrefix:"x-"},p={properties:{type:{enum:["basic","apiKey","oauth2"]},description:{type:"string"},name:{type:"string"},in:{type:"string",enum:["query","header"]},flow:{enum:["implicit","password","application","accessCode"]},authorizationUrl:{type:"string"},tokenUrl:{type:"string"},scopes:{type:"object",additionalProperties:{type:"string"}},"x-defaultClientId":{type:"string"}},required(e){switch(null==e?void 0:e.type){case"apiKey":return["type","name","in"];case"oauth2":switch(null==e?void 0:e.flow){case"implicit":return["type","flow","authorizationUrl","scopes"];case"accessCode":return["type","flow","authorizationUrl","tokenUrl","scopes"];case"application":case"password":return["type","flow","tokenUrl","scopes"];default:return["type","flow","scopes"]}default:return["type"]}},allowed(e){switch(null==e?void 0:e.type){case"basic":return["type","description"];case"apiKey":return["type","name","in","description"];case"oauth2":switch(null==e?void 0:e.flow){case"implicit":return["type","flow","authorizationUrl","description","scopes"];case"accessCode":return["type","flow","authorizationUrl","tokenUrl","description","scopes"];case"application":case"password":return["type","flow","tokenUrl","description","scopes"];default:return["type","flow","tokenUrl","authorizationUrl","description","scopes"]}default:return["type","description"]}},extensionsPrefix:"x-"};t.Oas2Types={Root:{properties:{swagger:{type:"string"},info:"Info",host:{type:"string"},basePath:{type:"string"},schemes:{type:"array",items:{type:"string"}},consumes:{type:"array",items:{type:"string"}},produces:{type:"array",items:{type:"string"}},paths:"Paths",definitions:"NamedSchemas",parameters:"NamedParameters",responses:"NamedResponses",securityDefinitions:"NamedSecuritySchemes",security:"SecurityRequirementList",tags:"TagList",externalDocs:"ExternalDocs","x-servers":"XServerList","x-tagGroups":"TagGroups","x-ignoredHeaderParameters":{type:"array",items:{type:"string"}}},required:["swagger","paths","info"],extensionsPrefix:"x-"},Tag:{properties:{name:{type:"string"},description:{type:"string"},externalDocs:"ExternalDocs","x-traitTag":{type:"boolean"},"x-displayName":{type:"string"}},required:["name"],extensionsPrefix:"x-"},TagList:(0,r.listOf)("Tag"),TagGroups:(0,r.listOf)("TagGroup"),TagGroup:{properties:{name:{type:"string"},tags:{type:"array",items:{type:"string"}}}},ExternalDocs:{properties:{description:{type:"string"},url:{type:"string"}},required:["url"],extensionsPrefix:"x-"},Example:{properties:{value:{isExample:!0},summary:{type:"string"},description:{type:"string"},externalValue:{type:"string"}},extensionsPrefix:"x-"},ExamplesMap:(0,r.mapOf)("Example"),EnumDescriptions:{properties:{},additionalProperties:{type:"string"}},SecurityRequirement:{properties:{},additionalProperties:{type:"array",items:{type:"string"}}},SecurityRequirementList:(0,r.listOf)("SecurityRequirement"),Info:{properties:{title:{type:"string"},description:{type:"string"},termsOfService:{type:"string"},contact:"Contact",license:"License",version:{type:"string"},"x-logo":"Logo"},required:["title","version"],extensionsPrefix:"x-"},Contact:{properties:{name:{type:"string"},url:{type:"string"},email:{type:"string"}},extensionsPrefix:"x-"},License:{properties:{name:{type:"string"},url:{type:"string"}},required:["name"],extensionsPrefix:"x-"},Logo:{properties:{url:{type:"string"},altText:{type:"string"},backgroundColor:{type:"string"},href:{type:"string"}},extensionsPrefix:"x-"},Paths:{properties:{},additionalProperties:(e,t)=>t.startsWith("/")?"PathItem":void 0},PathItem:{properties:{$ref:{type:"string"},parameters:"ParameterList",get:"Operation",put:"Operation",post:"Operation",delete:"Operation",options:"Operation",head:"Operation",patch:"Operation"},extensionsPrefix:"x-"},Parameter:o,ParameterItems:a,ParameterList:(0,r.listOf)("Parameter"),Operation:{properties:{tags:{type:"array",items:{type:"string"}},summary:{type:"string"},description:{type:"string"},externalDocs:"ExternalDocs",operationId:{type:"string"},consumes:{type:"array",items:{type:"string"}},produces:{type:"array",items:{type:"string"}},parameters:"ParameterList",responses:"Responses",schemes:{type:"array",items:{type:"string"}},deprecated:{type:"boolean"},security:"SecurityRequirementList","x-codeSamples":"XCodeSampleList","x-code-samples":"XCodeSampleList","x-hideTryItPanel":{type:"boolean"}},required:["responses"],extensionsPrefix:"x-"},Examples:{properties:{},additionalProperties:{isExample:!0}},Header:c,Responses:s,Response:l,Schema:u,Xml:{properties:{name:{type:"string"},namespace:{type:"string"},prefix:{type:"string"},attribute:{type:"boolean"},wrapped:{type:"boolean"}},extensionsPrefix:"x-"},SchemaProperties:{properties:{},additionalProperties:"Schema"},NamedSchemas:(0,r.mapOf)("Schema"),NamedResponses:(0,r.mapOf)("Response"),NamedParameters:(0,r.mapOf)("Parameter"),NamedSecuritySchemes:(0,r.mapOf)("SecurityScheme"),SecurityScheme:p,XCodeSample:{properties:{lang:{type:"string"},label:{type:"string"},source:{type:"string"}}},XCodeSampleList:(0,r.listOf)("XCodeSample"),XServerList:(0,r.listOf)("XServer"),XServer:{properties:{url:{type:"string"},description:{type:"string"}},required:["url"]}}},5241:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.Oas3Types=void 0;const r=n(5220),i=n(7468),o=/^[0-9][0-9Xx]{2}$/,a={properties:{default:"Response"},additionalProperties:(e,t)=>o.test(t)?"Response":void 0},s={properties:{externalDocs:"ExternalDocs",discriminator:"Discriminator",title:{type:"string"},multipleOf:{type:"number",minimum:0},maximum:{type:"number"},minimum:{type:"number"},exclusiveMaximum:{type:"boolean"},exclusiveMinimum:{type:"boolean"},maxLength:{type:"integer",minimum:0},minLength:{type:"integer",minimum:0},pattern:{type:"string"},maxItems:{type:"integer",minimum:0},minItems:{type:"integer",minimum:0},uniqueItems:{type:"boolean"},maxProperties:{type:"integer",minimum:0},minProperties:{type:"integer",minimum:0},required:{type:"array",items:{type:"string"}},enum:{type:"array"},type:{enum:["object","array","string","number","integer","boolean","null"]},allOf:(0,r.listOf)("Schema"),anyOf:(0,r.listOf)("Schema"),oneOf:(0,r.listOf)("Schema"),not:"Schema",properties:"SchemaProperties",items:e=>Array.isArray(e)?(0,r.listOf)("Schema"):"Schema",additionalItems:e=>"boolean"==typeof e?{type:"boolean"}:"Schema",additionalProperties:e=>"boolean"==typeof e?{type:"boolean"}:"Schema",description:{type:"string"},format:{type:"string"},default:null,nullable:{type:"boolean"},readOnly:{type:"boolean"},writeOnly:{type:"boolean"},xml:"Xml",example:{isExample:!0},deprecated:{type:"boolean"},"x-tags":{type:"array",items:{type:"string"}},"x-additionalPropertiesName":{type:"string"},"x-explicitMappingOnly":{type:"boolean"}},extensionsPrefix:"x-"},l={properties:{},additionalProperties:e=>(0,i.isMappingRef)(e)?{type:"string",directResolveAs:"Schema"}:{type:"string"}},c={properties:{type:{enum:["apiKey","http","oauth2","openIdConnect"]},description:{type:"string"},name:{type:"string"},in:{type:"string",enum:["query","header","cookie"]},scheme:{type:"string"},bearerFormat:{type:"string"},flows:"OAuth2Flows",openIdConnectUrl:{type:"string"},"x-defaultClientId":{type:"string"}},required(e){switch(null==e?void 0:e.type){case"apiKey":return["type","name","in"];case"http":return["type","scheme"];case"oauth2":return["type","flows"];case"openIdConnect":return["type","openIdConnectUrl"];default:return["type"]}},allowed(e){switch(null==e?void 0:e.type){case"apiKey":return["type","name","in","description"];case"http":return["type","scheme","bearerFormat","description"];case"oauth2":return["type","flows","description"];case"openIdConnect":return["type","openIdConnectUrl","description"];default:return["type","description"]}},extensionsPrefix:"x-"};t.Oas3Types={Root:{properties:{openapi:null,info:"Info",servers:"ServerList",security:"SecurityRequirementList",tags:"TagList",externalDocs:"ExternalDocs",paths:"Paths",components:"Components","x-webhooks":"WebhooksMap","x-tagGroups":"TagGroups","x-ignoredHeaderParameters":{type:"array",items:{type:"string"}}},required:["openapi","paths","info"],extensionsPrefix:"x-"},Tag:{properties:{name:{type:"string"},description:{type:"string"},externalDocs:"ExternalDocs","x-traitTag":{type:"boolean"},"x-displayName":{type:"string"}},required:["name"],extensionsPrefix:"x-"},TagList:(0,r.listOf)("Tag"),TagGroups:(0,r.listOf)("TagGroup"),TagGroup:{properties:{name:{type:"string"},tags:{type:"array",items:{type:"string"}}},extensionsPrefix:"x-"},ExternalDocs:{properties:{description:{type:"string"},url:{type:"string"}},required:["url"],extensionsPrefix:"x-"},Server:{properties:{url:{type:"string"},description:{type:"string"},variables:"ServerVariablesMap"},required:["url"],extensionsPrefix:"x-"},ServerList:(0,r.listOf)("Server"),ServerVariable:{properties:{enum:{type:"array",items:{type:"string"}},default:{type:"string"},description:{type:"string"}},required:["default"],extensionsPrefix:"x-"},ServerVariablesMap:(0,r.mapOf)("ServerVariable"),SecurityRequirement:{properties:{},additionalProperties:{type:"array",items:{type:"string"}}},SecurityRequirementList:(0,r.listOf)("SecurityRequirement"),Info:{properties:{title:{type:"string"},version:{type:"string"},description:{type:"string"},termsOfService:{type:"string"},contact:"Contact",license:"License","x-logo":"Logo"},required:["title","version"],extensionsPrefix:"x-"},Contact:{properties:{name:{type:"string"},url:{type:"string"},email:{type:"string"}},extensionsPrefix:"x-"},License:{properties:{name:{type:"string"},url:{type:"string"}},required:["name"],extensionsPrefix:"x-"},Paths:{properties:{},additionalProperties:(e,t)=>t.startsWith("/")?"PathItem":void 0},PathItem:{properties:{$ref:{type:"string"},servers:"ServerList",parameters:"ParameterList",summary:{type:"string"},description:{type:"string"},get:"Operation",put:"Operation",post:"Operation",delete:"Operation",options:"Operation",head:"Operation",patch:"Operation",trace:"Operation"},extensionsPrefix:"x-"},Parameter:{properties:{name:{type:"string"},in:{enum:["query","header","path","cookie"]},description:{type:"string"},required:{type:"boolean"},deprecated:{type:"boolean"},allowEmptyValue:{type:"boolean"},style:{enum:["form","simple","label","matrix","spaceDelimited","pipeDelimited","deepObject"]},explode:{type:"boolean"},allowReserved:{type:"boolean"},schema:"Schema",example:{isExample:!0},examples:"ExamplesMap",content:"MediaTypesMap"},required:["name","in"],requiredOneOf:["schema","content"],extensionsPrefix:"x-"},ParameterList:(0,r.listOf)("Parameter"),Operation:{properties:{tags:{type:"array",items:{type:"string"}},summary:{type:"string"},description:{type:"string"},externalDocs:"ExternalDocs",operationId:{type:"string"},parameters:"ParameterList",security:"SecurityRequirementList",servers:"ServerList",requestBody:"RequestBody",responses:"Responses",deprecated:{type:"boolean"},callbacks:"CallbacksMap","x-codeSamples":"XCodeSampleList","x-code-samples":"XCodeSampleList","x-hideTryItPanel":{type:"boolean"}},required:["responses"],extensionsPrefix:"x-"},Callback:(0,r.mapOf)("PathItem"),CallbacksMap:(0,r.mapOf)("Callback"),RequestBody:{properties:{description:{type:"string"},required:{type:"boolean"},content:"MediaTypesMap"},required:["content"],extensionsPrefix:"x-"},MediaTypesMap:{properties:{},additionalProperties:"MediaType"},MediaType:{properties:{schema:"Schema",example:{isExample:!0},examples:"ExamplesMap",encoding:"EncodingMap"},extensionsPrefix:"x-"},Example:{properties:{value:{isExample:!0},summary:{type:"string"},description:{type:"string"},externalValue:{type:"string"}},extensionsPrefix:"x-"},ExamplesMap:(0,r.mapOf)("Example"),Encoding:{properties:{contentType:{type:"string"},headers:"HeadersMap",style:{enum:["form","simple","label","matrix","spaceDelimited","pipeDelimited","deepObject"]},explode:{type:"boolean"},allowReserved:{type:"boolean"}},extensionsPrefix:"x-"},EncodingMap:(0,r.mapOf)("Encoding"),EnumDescriptions:{properties:{},additionalProperties:{type:"string"}},Header:{properties:{description:{type:"string"},required:{type:"boolean"},deprecated:{type:"boolean"},allowEmptyValue:{type:"boolean"},style:{enum:["form","simple","label","matrix","spaceDelimited","pipeDelimited","deepObject"]},explode:{type:"boolean"},allowReserved:{type:"boolean"},schema:"Schema",example:{isExample:!0},examples:"ExamplesMap",content:"MediaTypesMap"},requiredOneOf:["schema","content"],extensionsPrefix:"x-"},HeadersMap:(0,r.mapOf)("Header"),Responses:a,Response:{properties:{description:{type:"string"},headers:"HeadersMap",content:"MediaTypesMap",links:"LinksMap","x-summary":{type:"string"}},required:["description"],extensionsPrefix:"x-"},Link:{properties:{operationRef:{type:"string"},operationId:{type:"string"},parameters:null,requestBody:null,description:{type:"string"},server:"Server"},extensionsPrefix:"x-"},Logo:{properties:{url:{type:"string"},altText:{type:"string"},backgroundColor:{type:"string"},href:{type:"string"}}},Schema:s,Xml:{properties:{name:{type:"string"},namespace:{type:"string"},prefix:{type:"string"},attribute:{type:"boolean"},wrapped:{type:"boolean"}},extensionsPrefix:"x-"},SchemaProperties:{properties:{},additionalProperties:"Schema"},DiscriminatorMapping:l,Discriminator:{properties:{propertyName:{type:"string"},mapping:"DiscriminatorMapping"},required:["propertyName"],extensionsPrefix:"x-"},Components:{properties:{parameters:"NamedParameters",schemas:"NamedSchemas",responses:"NamedResponses",examples:"NamedExamples",requestBodies:"NamedRequestBodies",headers:"NamedHeaders",securitySchemes:"NamedSecuritySchemes",links:"NamedLinks",callbacks:"NamedCallbacks"},extensionsPrefix:"x-"},LinksMap:(0,r.mapOf)("Link"),NamedSchemas:(0,r.mapOf)("Schema"),NamedResponses:(0,r.mapOf)("Response"),NamedParameters:(0,r.mapOf)("Parameter"),NamedExamples:(0,r.mapOf)("Example"),NamedRequestBodies:(0,r.mapOf)("RequestBody"),NamedHeaders:(0,r.mapOf)("Header"),NamedSecuritySchemes:(0,r.mapOf)("SecurityScheme"),NamedLinks:(0,r.mapOf)("Link"),NamedCallbacks:(0,r.mapOf)("Callback"),ImplicitFlow:{properties:{refreshUrl:{type:"string"},scopes:{type:"object",additionalProperties:{type:"string"}},authorizationUrl:{type:"string"}},required:["authorizationUrl","scopes"],extensionsPrefix:"x-"},PasswordFlow:{properties:{refreshUrl:{type:"string"},scopes:{type:"object",additionalProperties:{type:"string"}},tokenUrl:{type:"string"}},required:["tokenUrl","scopes"],extensionsPrefix:"x-"},ClientCredentials:{properties:{refreshUrl:{type:"string"},scopes:{type:"object",additionalProperties:{type:"string"}},tokenUrl:{type:"string"}},required:["tokenUrl","scopes"],extensionsPrefix:"x-"},AuthorizationCode:{properties:{refreshUrl:{type:"string"},authorizationUrl:{type:"string"},scopes:{type:"object",additionalProperties:{type:"string"}},tokenUrl:{type:"string"},"x-usePkce":e=>"boolean"==typeof e?{type:"boolean"}:"XUsePkce"},required:["authorizationUrl","tokenUrl","scopes"],extensionsPrefix:"x-"},OAuth2Flows:{properties:{implicit:"ImplicitFlow",password:"PasswordFlow",clientCredentials:"ClientCredentials",authorizationCode:"AuthorizationCode"},extensionsPrefix:"x-"},SecurityScheme:c,XCodeSample:{properties:{lang:{type:"string"},label:{type:"string"},source:{type:"string"}}},XCodeSampleList:(0,r.listOf)("XCodeSample"),XUsePkce:{properties:{disableManualConfiguration:{type:"boolean"},hideClientSecretInput:{type:"boolean"}}},WebhooksMap:{properties:{},additionalProperties:()=>"PathItem"}}},2608:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.Oas3_1Types=void 0;const r=n(5220),i=n(5241),o={properties:{$id:{type:"string"},$anchor:{type:"string"},id:{type:"string"},$schema:{type:"string"},definitions:"NamedSchemas",$defs:"NamedSchemas",$vocabulary:{type:"string"},externalDocs:"ExternalDocs",discriminator:"Discriminator",title:{type:"string"},multipleOf:{type:"number",minimum:0},maximum:{type:"number"},minimum:{type:"number"},exclusiveMaximum:{type:"number"},exclusiveMinimum:{type:"number"},maxLength:{type:"integer",minimum:0},minLength:{type:"integer",minimum:0},pattern:{type:"string"},maxItems:{type:"integer",minimum:0},minItems:{type:"integer",minimum:0},uniqueItems:{type:"boolean"},maxProperties:{type:"integer",minimum:0},minProperties:{type:"integer",minimum:0},required:{type:"array",items:{type:"string"}},enum:{type:"array"},type:e=>Array.isArray(e)?{type:"array",items:{enum:["object","array","string","number","integer","boolean","null"]}}:{enum:["object","array","string","number","integer","boolean","null"]},allOf:(0,r.listOf)("Schema"),anyOf:(0,r.listOf)("Schema"),oneOf:(0,r.listOf)("Schema"),not:"Schema",if:"Schema",then:"Schema",else:"Schema",dependentSchemas:(0,r.listOf)("Schema"),prefixItems:(0,r.listOf)("Schema"),contains:"Schema",minContains:{type:"integer",minimum:0},maxContains:{type:"integer",minimum:0},patternProperties:{type:"object"},propertyNames:"Schema",unevaluatedItems:e=>"boolean"==typeof e?{type:"boolean"}:"Schema",unevaluatedProperties:e=>"boolean"==typeof e?{type:"boolean"}:"Schema",summary:{type:"string"},properties:"SchemaProperties",items:e=>"boolean"==typeof e?{type:"boolean"}:"Schema",additionalProperties:e=>"boolean"==typeof e?{type:"boolean"}:"Schema",description:{type:"string"},format:{type:"string"},contentEncoding:{type:"string"},contentMediaType:{type:"string"},default:null,readOnly:{type:"boolean"},writeOnly:{type:"boolean"},xml:"Xml",examples:{type:"array"},example:{isExample:!0},deprecated:{type:"boolean"},const:null,$comment:{type:"string"},"x-tags":{type:"array",items:{type:"string"}}},extensionsPrefix:"x-"},a={properties:{type:{enum:["apiKey","http","oauth2","openIdConnect","mutualTLS"]},description:{type:"string"},name:{type:"string"},in:{type:"string",enum:["query","header","cookie"]},scheme:{type:"string"},bearerFormat:{type:"string"},flows:"OAuth2Flows",openIdConnectUrl:{type:"string"}},required(e){switch(null==e?void 0:e.type){case"apiKey":return["type","name","in"];case"http":return["type","scheme"];case"oauth2":return["type","flows"];case"openIdConnect":return["type","openIdConnectUrl"];default:return["type"]}},allowed(e){switch(null==e?void 0:e.type){case"apiKey":return["type","name","in","description"];case"http":return["type","scheme","bearerFormat","description"];case"oauth2":switch(null==e?void 0:e.flows){case"implicit":return["type","flows","authorizationUrl","refreshUrl","description","scopes"];case"password":case"clientCredentials":return["type","flows","tokenUrl","refreshUrl","description","scopes"];default:return["type","flows","authorizationUrl","refreshUrl","tokenUrl","description","scopes"]}case"openIdConnect":return["type","openIdConnectUrl","description"];default:return["type","description"]}},extensionsPrefix:"x-"};t.Oas3_1Types=Object.assign(Object.assign({},i.Oas3Types),{Info:{properties:{title:{type:"string"},version:{type:"string"},description:{type:"string"},termsOfService:{type:"string"},summary:{type:"string"},contact:"Contact",license:"License","x-logo":"Logo"},required:["title","version"],extensionsPrefix:"x-"},Root:{properties:{openapi:null,info:"Info",servers:"ServerList",security:"SecurityRequirementList",tags:"TagList",externalDocs:"ExternalDocs",paths:"Paths",webhooks:"WebhooksMap",components:"Components",jsonSchemaDialect:{type:"string"}},required:["openapi","info"],requiredOneOf:["paths","components","webhooks"],extensionsPrefix:"x-"},Schema:o,License:{properties:{name:{type:"string"},url:{type:"string"},identifier:{type:"string"}},required:["name"],extensionsPrefix:"x-"},Components:{properties:{parameters:"NamedParameters",schemas:"NamedSchemas",responses:"NamedResponses",examples:"NamedExamples",requestBodies:"NamedRequestBodies",headers:"NamedHeaders",securitySchemes:"NamedSecuritySchemes",links:"NamedLinks",callbacks:"NamedCallbacks",pathItems:"NamedPathItems"},extensionsPrefix:"x-"},NamedPathItems:(0,r.mapOf)("PathItem"),SecurityScheme:a,Operation:{properties:{tags:{type:"array",items:{type:"string"}},summary:{type:"string"},description:{type:"string"},externalDocs:"ExternalDocs",operationId:{type:"string"},parameters:"ParameterList",security:"SecurityRequirementList",servers:"ServerList",requestBody:"RequestBody",responses:"Responses",deprecated:{type:"boolean"},callbacks:"CallbacksMap","x-codeSamples":"XCodeSampleList","x-code-samples":"XCodeSampleList","x-hideTryItPanel":{type:"boolean"}},extensionsPrefix:"x-"}})},771:function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))((function(i,o){function a(e){try{l(r.next(e))}catch(e){o(e)}}function s(e){try{l(r.throw(e))}catch(e){o(e)}}function l(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(a,s)}l((r=r.apply(e,t||[])).next())}))};Object.defineProperty(t,"__esModule",{value:!0}),t.nextTick=t.pickDefined=t.keysOf=t.identity=t.isTruthy=t.showErrorForDeprecatedField=t.showWarningForDeprecatedField=t.doesYamlFileExist=t.isCustomRuleId=t.getMatchingStatusCodeRange=t.assignExisting=t.isNotString=t.isString=t.isNotEmptyObject=t.slash=t.isPathParameter=t.yamlAndJsonSyncReader=t.readFileAsStringSync=t.isSingular=t.validateMimeTypeOAS3=t.validateMimeType=t.splitCamelCaseIntoWords=t.omitObjectProps=t.pickObjectProps=t.readFileFromUrl=t.isEmptyArray=t.isEmptyObject=t.isPlainObject=t.isDefined=t.loadYaml=t.popStack=t.pushStack=t.stringifyYaml=t.parseYaml=void 0;const i=n(3197),o=n(6470),a=n(4099),s=n(8150),l=n(3450),c=n(5273),u=n(8005),p=n(3065);var d=n(5273);function f(e){return null!==e&&"object"==typeof e&&!Array.isArray(e)}function h(e,t){return t.match(/^https?:\/\//)||(e=e.replace(/^https?:\/\//,"")),a(e,t)}function m(e){return"string"==typeof e}function g(e){return!!e}function y(e,t){return`${void 0!==t?`${t}.`:""}${e}`}Object.defineProperty(t,"parseYaml",{enumerable:!0,get:function(){return d.parseYaml}}),Object.defineProperty(t,"stringifyYaml",{enumerable:!0,get:function(){return d.stringifyYaml}}),t.pushStack=function(e,t){return{prev:e,value:t}},t.popStack=function(e){var t;return null!==(t=null==e?void 0:e.prev)&&void 0!==t?t:null},t.loadYaml=function(e){return r(this,void 0,void 0,(function*(){const t=yield i.promises.readFile(e,"utf-8");return(0,c.parseYaml)(t)}))},t.isDefined=function(e){return void 0!==e},t.isPlainObject=f,t.isEmptyObject=function(e){return f(e)&&0===Object.keys(e).length},t.isEmptyArray=function(e){return Array.isArray(e)&&0===e.length},t.readFileFromUrl=function(e,t){return r(this,void 0,void 0,(function*(){const n={};for(const r of t.headers)h(e,r.matches)&&(n[r.name]=void 0!==r.envVariable?u.env[r.envVariable]||"":r.value);const r=yield(t.customFetch||s.default)(e,{headers:n});if(!r.ok)throw new Error(`Failed to load ${e}: ${r.status} ${r.statusText}`);return{body:yield r.text(),mimeType:r.headers.get("content-type")}}))},t.pickObjectProps=function(e,t){return Object.fromEntries(t.filter((t=>t in e)).map((t=>[t,e[t]])))},t.omitObjectProps=function(e,t){return Object.fromEntries(Object.entries(e).filter((([e])=>!t.includes(e))))},t.splitCamelCaseIntoWords=function(e){const t=e.split(/(?:[-._])|([A-Z][a-z]+)/).filter(g).map((e=>e.toLocaleLowerCase())),n=e.split(/([A-Z]{2,})/).filter((e=>e&&e===e.toUpperCase())).map((e=>e.toLocaleLowerCase()));return new Set([...t,...n])},t.validateMimeType=function({type:e,value:t},{report:n,location:r},i){if(!i)throw new Error(`Parameter "allowedValues" is not provided for "${"consumes"===e?"request":"response"}-mime-type" rule`);if(t[e])for(const o of t[e])i.includes(o)||n({message:`Mime type "${o}" is not allowed`,location:r.child(t[e].indexOf(o)).key()})},t.validateMimeTypeOAS3=function({type:e,value:t},{report:n,location:r},i){if(!i)throw new Error(`Parameter "allowedValues" is not provided for "${"consumes"===e?"request":"response"}-mime-type" rule`);if(t.content)for(const e of Object.keys(t.content))i.includes(e)||n({message:`Mime type "${e}" is not allowed`,location:r.child("content").child(e).key()})},t.isSingular=function(e){return l.isSingular(e)},t.readFileAsStringSync=function(e){return i.readFileSync(e,"utf-8")},t.yamlAndJsonSyncReader=function(e){const t=i.readFileSync(e,"utf-8");return(0,c.parseYaml)(t)},t.isPathParameter=function(e){return e.startsWith("{")&&e.endsWith("}")},t.slash=function(e){return/^\\\\\?\\/.test(e)?e:e.replace(/\\/g,"/")},t.isNotEmptyObject=function(e){return!!e&&Object.keys(e).length>0},t.isString=m,t.isNotString=function(e){return!m(e)},t.assignExisting=function(e,t){for(const n of Object.keys(t))e.hasOwnProperty(n)&&(e[n]=t[n])},t.getMatchingStatusCodeRange=function(e){return`${e}`.replace(/^(\d)\d\d$/,((e,t)=>`${t}XX`))},t.isCustomRuleId=function(e){return e.includes("/")},t.doesYamlFileExist=function(e){return(".yaml"===(0,o.extname)(e)||".yml"===(0,o.extname)(e))&&i.hasOwnProperty("existsSync")&&i.existsSync(e)},t.showWarningForDeprecatedField=function(e,t,n){p.logger.warn(`The '${p.colorize.red(e)}' field is deprecated. ${t?`Use ${p.colorize.green(y(t,n))} instead. `:""}Read more about this change: https://redocly.com/docs/api-registry/guides/migration-guide-config-file/#changed-properties\n`)},t.showErrorForDeprecatedField=function(e,t,n){throw new Error(`Do not use '${e}' field. ${t?`Use '${y(t,n)}' instead. `:""}\n`)},t.isTruthy=g,t.identity=function(e){return e},t.keysOf=function(e){return e?Object.keys(e):[]},t.pickDefined=function(e){if(!e)return;const t={};for(const n in e)void 0!==e[n]&&(t[n]=e[n]);return t},t.nextTick=function(){new Promise((e=>{setTimeout(e)}))}},8065:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.normalizeVisitors=void 0;const r=n(5220),i={Root:"DefinitionRoot",ServerVariablesMap:"ServerVariableMap",Paths:["PathMap","PathsMap"],CallbacksMap:"CallbackMap",MediaTypesMap:"MediaTypeMap",ExamplesMap:"ExampleMap",EncodingMap:"EncodingsMap",HeadersMap:"HeaderMap",LinksMap:"LinkMap",OAuth2Flows:"SecuritySchemeFlows",Responses:"ResponsesMap"};t.normalizeVisitors=function(e,t){const n={any:{enter:[],leave:[]}};for(const e of Object.keys(t))n[e]={enter:[],leave:[]};n.ref={enter:[],leave:[]};for(const{ruleId:t,severity:n,visitor:r}of e)s({ruleId:t,severity:n},r,null);for(const e of Object.keys(n))n[e].enter.sort(((e,t)=>t.depth-e.depth)),n[e].leave.sort(((e,t)=>e.depth-t.depth));return n;function o(e,t,i,a,s=[]){if(s.includes(t))return;s=[...s,t];const l=new Set;for(const n of Object.values(t.properties))n!==i?"object"==typeof n&&null!==n&&n.name&&l.add(n):c(e,s);t.additionalProperties&&"function"!=typeof t.additionalProperties&&(t.additionalProperties===i?c(e,s):void 0!==t.additionalProperties.name&&l.add(t.additionalProperties)),t.items&&(t.items===i?c(e,s):void 0!==t.items.name&&l.add(t.items)),t.extensionsPrefix&&l.add(r.SpecExtension);for(const t of Array.from(l.values()))o(e,t,i,a,s);function c(e,t){for(const r of t.slice(1))n[r.name]=n[r.name]||{enter:[],leave:[]},n[r.name].enter.push(Object.assign(Object.assign({},e),{visit:()=>{},depth:0,context:{isSkippedLevel:!0,seen:new Set,parent:a}}))}}function a(e,t){if(Array.isArray(t)){const n=t.find((t=>e[t]))||void 0;return n&&e[n]}return e[t]}function s(e,r,l,c=0){const u=Object.keys(t);if(0===c)u.push("any"),u.push("ref");else{if(r.any)throw new Error("any() is allowed only on top level");if(r.ref)throw new Error("ref() is allowed only on top level")}for(const p of u){const u=r[p]||a(r,i[p]),d=n[p];if(!u)continue;let f,h,m;const g="object"==typeof u;if("ref"===p&&g&&u.skip)throw new Error("ref() visitor does not support skip");"function"==typeof u?f=u:g&&(f=u.enter,h=u.leave,m=u.skip);const y={activatedOn:null,type:t[p],parent:l,isSkippedLevel:!1};if("object"==typeof u&&s(e,u,y,c+1),l&&o(e,l.type,t[p],l),f||g){if(f&&"function"!=typeof f)throw new Error("DEV: should be function");d.enter.push(Object.assign(Object.assign({},e),{visit:f||(()=>{}),skip:m,depth:c,context:y}))}if(h){if("function"!=typeof h)throw new Error("DEV: should be function");d.leave.push(Object.assign(Object.assign({},e),{visit:h,depth:c,context:y}))}}}}},9443:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.walkDocument=void 0;const r=n(7468),i=n(771),o=n(4182),a=n(5220);function s(e){var t,n;const r={};for(;e.parent;)(null===(t=e.parent.activatedOn)||void 0===t?void 0:t.value.location)&&(r[e.parent.type.name]=null===(n=e.parent.activatedOn)||void 0===n?void 0:n.value.location),e=e.parent;return r}t.walkDocument=function(e){const{document:t,rootType:n,normalizedVisitors:l,resolvedRefMap:c,ctx:u}=e,p={},d=new Set;!function e(t,n,f,h,m){var g,y,b,v,x,w,k,S,O,E,_;const A=(e,t=C.source.absoluteRef)=>{if(!(0,r.isRef)(e))return{location:f,node:e};const n=(0,o.makeRefId)(t,e.$ref),i=c.get(n);if(!i)return{location:void 0,node:void 0};const{resolved:a,node:s,document:l,nodePointer:u,error:p}=i;return{location:a?new r.Location(l.source,u):p instanceof o.YamlParseError?new r.Location(p.source,""):void 0,node:s,error:p}},j=f;let C=f;const{node:P,location:T,error:I}=A(t),R=new Set;if((0,r.isRef)(t)){const e=l.ref.enter;for(const{visit:r,ruleId:i,severity:o,context:a}of e)R.add(a),r(t,{report:$.bind(void 0,i,o),resolve:A,rawNode:t,rawLocation:j,location:f,type:n,parent:h,key:m,parentLocations:{},oasVersion:u.oasVersion,getVisitorData:L.bind(void 0,i)},{node:P,location:T,error:I}),(null==T?void 0:T.source.absoluteRef)&&u.refTypes&&u.refTypes.set(null==T?void 0:T.source.absoluteRef,n)}if(void 0!==P&&T&&"scalar"!==n.name){C=T;const o=null===(y=null===(g=p[n.name])||void 0===g?void 0:g.has)||void 0===y?void 0:y.call(g,P);let s=!1;const c=l.any.enter.concat((null===(b=l[n.name])||void 0===b?void 0:b.enter)||[]),u=[];for(const{context:e,visit:r,skip:a,ruleId:l,severity:p}of c){if(d.has(C.pointer))break;if(e.isSkippedLevel)!e.parent.activatedOn||e.parent.activatedOn.value.nextLevelTypeActivated||e.seen.has(t)||(e.seen.add(t),s=!0,u.push(e));else if(e.parent&&e.parent.activatedOn&&(null===(v=e.activatedOn)||void 0===v?void 0:v.value.withParentNode)!==e.parent.activatedOn.value.node&&(null===(x=e.parent.activatedOn.value.nextLevelTypeActivated)||void 0===x?void 0:x.value)!==n||!e.parent&&!o){u.push(e);const o={node:P,location:T,nextLevelTypeActivated:null,withParentNode:null===(k=null===(w=e.parent)||void 0===w?void 0:w.activatedOn)||void 0===k?void 0:k.value.node,skipped:null!==(E=(null===(O=null===(S=e.parent)||void 0===S?void 0:S.activatedOn)||void 0===O?void 0:O.value.skipped)||(null==a?void 0:a(P,m,{location:f,rawLocation:j,resolve:A,rawNode:t})))&&void 0!==E&&E};e.activatedOn=(0,i.pushStack)(e.activatedOn,o);let c=e.parent;for(;c;)c.activatedOn.value.nextLevelTypeActivated=(0,i.pushStack)(c.activatedOn.value.nextLevelTypeActivated,n),c=c.parent;o.skipped||(s=!0,R.add(e),N(r,P,t,e,l,p))}}if(s||!o)if(p[n.name]=p[n.name]||new Set,p[n.name].add(P),Array.isArray(P)){const t=n.items;if(void 0!==t)for(let n=0;n!i.includes(e)))):n.extensionsPrefix&&i.push(...Object.keys(P).filter((e=>e.startsWith(n.extensionsPrefix)))),(0,r.isRef)(t)&&i.push(...Object.keys(t).filter((e=>"$ref"!==e&&!i.includes(e))));for(const o of i){let i=P[o],s=T;void 0===i&&(i=t[o],s=f);let l=n.properties[o];void 0===l&&(l=n.additionalProperties),"function"==typeof l&&(l=l(i,o)),void 0===l&&n.extensionsPrefix&&o.startsWith(n.extensionsPrefix)&&(l=a.SpecExtension),!(0,a.isNamedType)(l)&&(null==l?void 0:l.directResolveAs)&&(l=l.directResolveAs,i={$ref:i}),l&&void 0===l.name&&!1!==l.resolvable&&(l={name:"scalar",properties:{}}),(0,a.isNamedType)(l)&&("scalar"!==l.name||(0,r.isRef)(i))&&e(i,l,s.child([o]),P,o)}}const h=l.any.leave,I=((null===(_=l[n.name])||void 0===_?void 0:_.leave)||[]).concat(h);for(const e of u.reverse())if(e.isSkippedLevel)e.seen.delete(P);else if(e.activatedOn=(0,i.popStack)(e.activatedOn),e.parent){let t=e.parent;for(;t;)t.activatedOn.value.nextLevelTypeActivated=(0,i.popStack)(t.activatedOn.value.nextLevelTypeActivated),t=t.parent}for(const{context:e,visit:n,ruleId:r,severity:i}of I)!e.isSkippedLevel&&R.has(e)&&N(n,P,t,e,r,i)}if(C=f,(0,r.isRef)(t)){const e=l.ref.leave;for(const{visit:r,ruleId:i,severity:o,context:a}of e)R.has(a)&&r(t,{report:$.bind(void 0,i,o),resolve:A,rawNode:t,rawLocation:j,location:f,type:n,parent:h,key:m,parentLocations:{},oasVersion:u.oasVersion,getVisitorData:L.bind(void 0,i)},{node:P,location:T,error:I})}function N(e,t,r,i,o,a){e(t,{report:$.bind(void 0,o,a),resolve:A,rawNode:r,location:C,rawLocation:j,type:n,parent:h,key:m,parentLocations:s(i),oasVersion:u.oasVersion,ignoreNextVisitorsOnNode:()=>{d.add(C.pointer)},getVisitorData:L.bind(void 0,o)},function(e){var t;const n={};for(;e.parent;)n[e.parent.type.name]=null===(t=e.parent.activatedOn)||void 0===t?void 0:t.value.node,e=e.parent;return n}(i),i)}function $(e,t,n){const r=(n.location?Array.isArray(n.location)?n.location:[n.location]:[Object.assign(Object.assign({},C),{reportOnKey:!1})]).map((e=>Object.assign(Object.assign(Object.assign({},C),{reportOnKey:!1}),e))),i=n.forceSeverity||t;"off"!==i&&u.problems.push(Object.assign(Object.assign({ruleId:n.ruleId||e,severity:i},n),{suggest:n.suggest||[],location:r}))}function L(e){return u.visitorsData[e]=u.visitorsData[e]||{},u.visitorsData[e]}}(t.parsed,n,new r.Location(t.source,"#/"),void 0,"")}},5019:function(e,t,n){var r=n(5623);e.exports=function(e){return e?("{}"===e.substr(0,2)&&(e="\\{\\}"+e.substr(2)),g(function(e){return e.split("\\\\").join(i).split("\\{").join(o).split("\\}").join(a).split("\\,").join(s).split("\\.").join(l)}(e),!0).map(u)):[]};var i="\0SLASH"+Math.random()+"\0",o="\0OPEN"+Math.random()+"\0",a="\0CLOSE"+Math.random()+"\0",s="\0COMMA"+Math.random()+"\0",l="\0PERIOD"+Math.random()+"\0";function c(e){return parseInt(e,10)==e?parseInt(e,10):e.charCodeAt(0)}function u(e){return e.split(i).join("\\").split(o).join("{").split(a).join("}").split(s).join(",").split(l).join(".")}function p(e){if(!e)return[""];var t=[],n=r("{","}",e);if(!n)return e.split(",");var i=n.pre,o=n.body,a=n.post,s=i.split(",");s[s.length-1]+="{"+o+"}";var l=p(a);return a.length&&(s[s.length-1]+=l.shift(),s.push.apply(s,l)),t.push.apply(t,s),t}function d(e){return"{"+e+"}"}function f(e){return/^-?0\d/.test(e)}function h(e,t){return e<=t}function m(e,t){return e>=t}function g(e,t){var n=[],i=r("{","}",e);if(!i)return[e];var o=i.pre,s=i.post.length?g(i.post,!1):[""];if(/\$$/.test(i.pre))for(var l=0;l=0;if(!w&&!k)return i.post.match(/,.*\}/)?g(e=i.pre+"{"+i.body+a+i.post):[e];if(w)y=i.body.split(/\.\./);else if(1===(y=p(i.body)).length&&1===(y=g(y[0],!1).map(d)).length)return s.map((function(e){return i.pre+y[0]+e}));if(w){var S=c(y[0]),O=c(y[1]),E=Math.max(y[0].length,y[1].length),_=3==y.length?Math.abs(c(y[2])):1,A=h;O0){var I=new Array(T+1).join("0");P=C<0?"-"+I+P.slice(1):I+P}}b.push(P)}}else{b=[];for(var R=0;R(g(t),!(!n.nocomment&&"#"===t.charAt(0))&&new v(t,n).match(e));e.exports=r;const i=n(5751);r.sep=i.sep;const o=Symbol("globstar **");r.GLOBSTAR=o;const a=n(5019),s={"!":{open:"(?:(?!(?:",close:"))[^/]*?)"},"?":{open:"(?:",close:")?"},"+":{open:"(?:",close:")+"},"*":{open:"(?:",close:")*"},"@":{open:"(?:",close:")"}},l="[^/]",c=l+"*?",u=e=>e.split("").reduce(((e,t)=>(e[t]=!0,e)),{}),p=u("().*{}+?[]^$\\!"),d=u("[.("),f=/\/+/;r.filter=(e,t={})=>(n,i,o)=>r(n,e,t);const h=(e,t={})=>{const n={};return Object.keys(e).forEach((t=>n[t]=e[t])),Object.keys(t).forEach((e=>n[e]=t[e])),n};r.defaults=e=>{if(!e||"object"!=typeof e||!Object.keys(e).length)return r;const t=r,n=(n,r,i)=>t(n,r,h(e,i));return(n.Minimatch=class extends t.Minimatch{constructor(t,n){super(t,h(e,n))}}).defaults=n=>t.defaults(h(e,n)).Minimatch,n.filter=(n,r)=>t.filter(n,h(e,r)),n.defaults=n=>t.defaults(h(e,n)),n.makeRe=(n,r)=>t.makeRe(n,h(e,r)),n.braceExpand=(n,r)=>t.braceExpand(n,h(e,r)),n.match=(n,r,i)=>t.match(n,r,h(e,i)),n},r.braceExpand=(e,t)=>m(e,t);const m=(e,t={})=>(g(e),t.nobrace||!/\{(?:(?!\{).)*\}/.test(e)?[e]:a(e)),g=e=>{if("string"!=typeof e)throw new TypeError("invalid pattern");if(e.length>65536)throw new TypeError("pattern is too long")},y=Symbol("subparse");r.makeRe=(e,t)=>new v(e,t||{}).makeRe(),r.match=(e,t,n={})=>{const r=new v(t,n);return e=e.filter((e=>r.match(e))),r.options.nonull&&!e.length&&e.push(t),e};const b=e=>e.replace(/[[\]\\]/g,"\\$&");class v{constructor(e,t){g(e),t||(t={}),this.options=t,this.set=[],this.pattern=e,this.windowsPathsNoEscape=!!t.windowsPathsNoEscape||!1===t.allowWindowsEscape,this.windowsPathsNoEscape&&(this.pattern=this.pattern.replace(/\\/g,"/")),this.regexp=null,this.negate=!1,this.comment=!1,this.empty=!1,this.partial=!!t.partial,this.make()}debug(){}make(){const e=this.pattern,t=this.options;if(!t.nocomment&&"#"===e.charAt(0))return void(this.comment=!0);if(!e)return void(this.empty=!0);this.parseNegate();let n=this.globSet=this.braceExpand();t.debug&&(this.debug=(...e)=>console.error(...e)),this.debug(this.pattern,n),n=this.globParts=n.map((e=>e.split(f))),this.debug(this.pattern,n),n=n.map(((e,t,n)=>e.map(this.parse,this))),this.debug(this.pattern,n),n=n.filter((e=>-1===e.indexOf(!1))),this.debug(this.pattern,n),this.set=n}parseNegate(){if(this.options.nonegate)return;const e=this.pattern;let t=!1,n=0;for(let r=0;r>> no match, partial?",e,d,t,f),d!==s))}if("string"==typeof u?(c=p===u,this.debug("string match",u,p,c)):(c=p.match(u),this.debug("pattern match",u,p,c)),!c)return!1}if(i===s&&a===l)return!0;if(i===s)return n;if(a===l)return i===s-1&&""===e[i];throw new Error("wtf?")}braceExpand(){return m(this.pattern,this.options)}parse(e,t){g(e);const n=this.options;if("**"===e){if(!n.noglobstar)return o;e="*"}if(""===e)return"";let r="",i=!1,a=!1;const u=[],f=[];let h,m,v,x,w=!1,k=-1,S=-1,O="."===e.charAt(0),E=n.dot||O;const _=e=>"."===e.charAt(0)?"":n.dot?"(?!(?:^|\\/)\\.{1,2}(?:$|\\/))":"(?!\\.)",A=()=>{if(h){switch(h){case"*":r+=c,i=!0;break;case"?":r+=l,i=!0;break;default:r+="\\"+h}this.debug("clearStateChar %j %j",h,r),h=!1}};for(let t,o=0;o(n||(n="\\"),t+t+n+"|"))),this.debug("tail=%j\n %s",e,e,v,r);const t="*"===v.type?c:"?"===v.type?l:"\\"+v.type;i=!0,r=r.slice(0,v.reStart)+t+"\\("+e}A(),a&&(r+="\\\\");const j=d[r.charAt(0)];for(let e=f.length-1;e>-1;e--){const n=f[e],i=r.slice(0,n.reStart),o=r.slice(n.reStart,n.reEnd-8);let a=r.slice(n.reEnd);const s=r.slice(n.reEnd-8,n.reEnd)+a,l=i.split(")").length,c=i.split("(").length-l;let u=a;for(let e=0;ee.replace(/\\(.)/g,"$1"))(e);const C=n.nocase?"i":"";try{return Object.assign(new RegExp("^"+r+"$",C),{_glob:e,_src:r})}catch(e){return new RegExp("$.")}}makeRe(){if(this.regexp||!1===this.regexp)return this.regexp;const e=this.set;if(!e.length)return this.regexp=!1,this.regexp;const t=this.options,n=t.noglobstar?c:t.dot?"(?:(?!(?:\\/|^)(?:\\.{1,2})($|\\/)).)*?":"(?:(?!(?:\\/|^)\\.).)*?",r=t.nocase?"i":"";let i=e.map((e=>(e=e.map((e=>"string"==typeof e?e.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&"):e===o?o:e._src)).reduce(((e,t)=>(e[e.length-1]===o&&t===o||e.push(t),e)),[]),e.forEach(((t,r)=>{t===o&&e[r-1]!==o&&(0===r?e.length>1?e[r+1]="(?:\\/|"+n+"\\/)?"+e[r+1]:e[r]=n:r===e.length-1?e[r-1]+="(?:\\/|"+n+")?":(e[r-1]+="(?:\\/|\\/"+n+"\\/)"+e[r+1],e[r+1]=o))})),e.filter((e=>e!==o)).join("/")))).join("|");i="^(?:"+i+")$",this.negate&&(i="^(?!"+i+").*$");try{this.regexp=new RegExp(i,r)}catch(e){this.regexp=!1}return this.regexp}match(e,t=this.partial){if(this.debug("match",e,this.pattern),this.comment)return!1;if(this.empty)return""===e;if("/"===e&&t)return!0;const n=this.options;"/"!==i.sep&&(e=e.split(i.sep).join("/")),e=e.split(f),this.debug(this.pattern,"split",e);const r=this.set;let o;this.debug(this.pattern,"set",r);for(let t=e.length-1;t>=0&&(o=e[t],!o);t--);for(let i=0;i=0&&c>0){if(e===t)return[l,c];for(r=[],o=n.length;u>=0&&!s;)u==l?(r.push(u),l=n.indexOf(e,u+1)):1==r.length?s=[r.pop(),c]:((i=r.pop())=0?l:c;r.length&&(s=[o,a])}return s}e.exports=t,t.range=r},472:function(e,t,n){"use strict";var r=n(4663);e.exports=function(e,t){return e?void t.then((function(t){r((function(){e(null,t)}))}),(function(t){r((function(){e(t)}))})):t}},4663:function(e){"use strict";e.exports="object"==typeof process&&"function"==typeof process.nextTick?process.nextTick:"function"==typeof setImmediate?setImmediate:function(e){setTimeout(e,0)}},4184:function(e,t){var n;!function(){"use strict";var r={}.hasOwnProperty;function i(){for(var e=[],t=0;tu;)if((s=l[u++])!=s)return!0}else for(;c>u;u++)if((e||u in l)&&l[u]===n)return e||u||0;return!e&&-1}};e.exports={includes:a(!0),indexOf:a(!1)}},2092:function(e,t,n){var r=n(9974),i=n(8361),o=n(7908),a=n(7466),s=n(5417),l=[].push,c=function(e){var t=1==e,n=2==e,c=3==e,u=4==e,p=6==e,d=7==e,f=5==e||p;return function(h,m,g,y){for(var b,v,x=o(h),w=i(x),k=r(m,g,3),S=a(w.length),O=0,E=y||s,_=t?E(h,S):n||d?E(h,0):void 0;S>O;O++)if((f||O in w)&&(v=k(b=w[O],O,x),e))if(t)_[O]=v;else if(v)switch(e){case 3:return!0;case 5:return b;case 6:return O;case 2:l.call(_,b)}else switch(e){case 4:return!1;case 7:l.call(_,b)}return p?-1:c||u?u:_}};e.exports={forEach:c(0),map:c(1),filter:c(2),some:c(3),every:c(4),find:c(5),findIndex:c(6),filterOut:c(7)}},1194:function(e,t,n){var r=n(7293),i=n(5112),o=n(7392),a=i("species");e.exports=function(e){return o>=51||!r((function(){var t=[];return(t.constructor={})[a]=function(){return{foo:1}},1!==t[e](Boolean).foo}))}},5417:function(e,t,n){var r=n(111),i=n(3157),o=n(5112)("species");e.exports=function(e,t){var n;return i(e)&&("function"!=typeof(n=e.constructor)||n!==Array&&!i(n.prototype)?r(n)&&null===(n=n[o])&&(n=void 0):n=void 0),new(void 0===n?Array:n)(0===t?0:t)}},4326:function(e){var t={}.toString;e.exports=function(e){return t.call(e).slice(8,-1)}},648:function(e,t,n){var r=n(1694),i=n(4326),o=n(5112)("toStringTag"),a="Arguments"==i(function(){return arguments}());e.exports=r?i:function(e){var t,n,r;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(n=function(e,t){try{return e[t]}catch(e){}}(t=Object(e),o))?n:a?i(t):"Object"==(r=i(t))&&"function"==typeof t.callee?"Arguments":r}},9920:function(e,t,n){var r=n(6656),i=n(3887),o=n(1236),a=n(3070);e.exports=function(e,t){for(var n=i(t),s=a.f,l=o.f,c=0;c=74)&&(r=a.match(/Chrome\/(\d+)/))&&(i=r[1]),e.exports=i&&+i},748:function(e){e.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]},2109:function(e,t,n){var r=n(7854),i=n(1236).f,o=n(8880),a=n(1320),s=n(3505),l=n(9920),c=n(4705);e.exports=function(e,t){var n,u,p,d,f,h=e.target,m=e.global,g=e.stat;if(n=m?r:g?r[h]||s(h,{}):(r[h]||{}).prototype)for(u in t){if(d=t[u],p=e.noTargetGet?(f=i(n,u))&&f.value:n[u],!c(m?u:h+(g?".":"#")+u,e.forced)&&void 0!==p){if(typeof d==typeof p)continue;l(d,p)}(e.sham||p&&p.sham)&&o(d,"sham",!0),a(n,u,d,e)}}},7293:function(e){e.exports=function(e){try{return!!e()}catch(e){return!0}}},9974:function(e,t,n){var r=n(3099);e.exports=function(e,t,n){if(r(e),void 0===t)return e;switch(n){case 0:return function(){return e.call(t)};case 1:return function(n){return e.call(t,n)};case 2:return function(n,r){return e.call(t,n,r)};case 3:return function(n,r,i){return e.call(t,n,r,i)}}return function(){return e.apply(t,arguments)}}},5005:function(e,t,n){var r=n(857),i=n(7854),o=function(e){return"function"==typeof e?e:void 0};e.exports=function(e,t){return arguments.length<2?o(r[e])||o(i[e]):r[e]&&r[e][t]||i[e]&&i[e][t]}},7854:function(e,t,n){var r=function(e){return e&&e.Math==Math&&e};e.exports=r("object"==typeof globalThis&&globalThis)||r("object"==typeof window&&window)||r("object"==typeof self&&self)||r("object"==typeof n.g&&n.g)||function(){return this}()||Function("return this")()},6656:function(e,t,n){var r=n(7908),i={}.hasOwnProperty;e.exports=Object.hasOwn||function(e,t){return i.call(r(e),t)}},3501:function(e){e.exports={}},490:function(e,t,n){var r=n(5005);e.exports=r("document","documentElement")},4664:function(e,t,n){var r=n(9781),i=n(7293),o=n(317);e.exports=!r&&!i((function(){return 7!=Object.defineProperty(o("div"),"a",{get:function(){return 7}}).a}))},8361:function(e,t,n){var r=n(7293),i=n(4326),o="".split;e.exports=r((function(){return!Object("z").propertyIsEnumerable(0)}))?function(e){return"String"==i(e)?o.call(e,""):Object(e)}:Object},2788:function(e,t,n){var r=n(5465),i=Function.toString;"function"!=typeof r.inspectSource&&(r.inspectSource=function(e){return i.call(e)}),e.exports=r.inspectSource},9909:function(e,t,n){var r,i,o,a=n(8536),s=n(7854),l=n(111),c=n(8880),u=n(6656),p=n(5465),d=n(6200),f=n(3501),h="Object already initialized",m=s.WeakMap;if(a||p.state){var g=p.state||(p.state=new m),y=g.get,b=g.has,v=g.set;r=function(e,t){if(b.call(g,e))throw new TypeError(h);return t.facade=e,v.call(g,e,t),t},i=function(e){return y.call(g,e)||{}},o=function(e){return b.call(g,e)}}else{var x=d("state");f[x]=!0,r=function(e,t){if(u(e,x))throw new TypeError(h);return t.facade=e,c(e,x,t),t},i=function(e){return u(e,x)?e[x]:{}},o=function(e){return u(e,x)}}e.exports={set:r,get:i,has:o,enforce:function(e){return o(e)?i(e):r(e,{})},getterFor:function(e){return function(t){var n;if(!l(t)||(n=i(t)).type!==e)throw TypeError("Incompatible receiver, "+e+" required");return n}}}},3157:function(e,t,n){var r=n(4326);e.exports=Array.isArray||function(e){return"Array"==r(e)}},4705:function(e,t,n){var r=n(7293),i=/#|\.prototype\./,o=function(e,t){var n=s[a(e)];return n==c||n!=l&&("function"==typeof t?r(t):!!t)},a=o.normalize=function(e){return String(e).replace(i,".").toLowerCase()},s=o.data={},l=o.NATIVE="N",c=o.POLYFILL="P";e.exports=o},111:function(e){e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},1913:function(e){e.exports=!1},133:function(e,t,n){var r=n(7392),i=n(7293);e.exports=!!Object.getOwnPropertySymbols&&!i((function(){var e=Symbol();return!String(e)||!(Object(e)instanceof Symbol)||!Symbol.sham&&r&&r<41}))},8536:function(e,t,n){var r=n(7854),i=n(2788),o=r.WeakMap;e.exports="function"==typeof o&&/native code/.test(i(o))},30:function(e,t,n){var r,i=n(9670),o=n(6048),a=n(748),s=n(3501),l=n(490),c=n(317),u=n(6200),p="prototype",d="script",f=u("IE_PROTO"),h=function(){},m=function(e){return"<"+d+">"+e+""},g=function(){try{r=document.domain&&new ActiveXObject("htmlfile")}catch(e){}var e,t,n;g=r?function(e){e.write(m("")),e.close();var t=e.parentWindow.Object;return e=null,t}(r):(t=c("iframe"),n="java"+d+":",t.style.display="none",l.appendChild(t),t.src=String(n),(e=t.contentWindow.document).open(),e.write(m("document.F=Object")),e.close(),e.F);for(var i=a.length;i--;)delete g[p][a[i]];return g()};s[f]=!0,e.exports=Object.create||function(e,t){var n;return null!==e?(h[p]=i(e),n=new h,h[p]=null,n[f]=e):n=g(),void 0===t?n:o(n,t)}},6048:function(e,t,n){var r=n(9781),i=n(3070),o=n(9670),a=n(1956);e.exports=r?Object.defineProperties:function(e,t){o(e);for(var n,r=a(t),s=r.length,l=0;s>l;)i.f(e,n=r[l++],t[n]);return e}},3070:function(e,t,n){var r=n(9781),i=n(4664),o=n(9670),a=n(7593),s=Object.defineProperty;t.f=r?s:function(e,t,n){if(o(e),t=a(t,!0),o(n),i)try{return s(e,t,n)}catch(e){}if("get"in n||"set"in n)throw TypeError("Accessors not supported");return"value"in n&&(e[t]=n.value),e}},1236:function(e,t,n){var r=n(9781),i=n(5296),o=n(9114),a=n(5656),s=n(7593),l=n(6656),c=n(4664),u=Object.getOwnPropertyDescriptor;t.f=r?u:function(e,t){if(e=a(e),t=s(t,!0),c)try{return u(e,t)}catch(e){}if(l(e,t))return o(!i.f.call(e,t),e[t])}},1156:function(e,t,n){var r=n(5656),i=n(8006).f,o={}.toString,a="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[];e.exports.f=function(e){return a&&"[object Window]"==o.call(e)?function(e){try{return i(e)}catch(e){return a.slice()}}(e):i(r(e))}},8006:function(e,t,n){var r=n(6324),i=n(748).concat("length","prototype");t.f=Object.getOwnPropertyNames||function(e){return r(e,i)}},5181:function(e,t){t.f=Object.getOwnPropertySymbols},6324:function(e,t,n){var r=n(6656),i=n(5656),o=n(1318).indexOf,a=n(3501);e.exports=function(e,t){var n,s=i(e),l=0,c=[];for(n in s)!r(a,n)&&r(s,n)&&c.push(n);for(;t.length>l;)r(s,n=t[l++])&&(~o(c,n)||c.push(n));return c}},1956:function(e,t,n){var r=n(6324),i=n(748);e.exports=Object.keys||function(e){return r(e,i)}},5296:function(e,t){"use strict";var n={}.propertyIsEnumerable,r=Object.getOwnPropertyDescriptor,i=r&&!n.call({1:2},1);t.f=i?function(e){var t=r(this,e);return!!t&&t.enumerable}:n},288:function(e,t,n){"use strict";var r=n(1694),i=n(648);e.exports=r?{}.toString:function(){return"[object "+i(this)+"]"}},3887:function(e,t,n){var r=n(5005),i=n(8006),o=n(5181),a=n(9670);e.exports=r("Reflect","ownKeys")||function(e){var t=i.f(a(e)),n=o.f;return n?t.concat(n(e)):t}},857:function(e,t,n){var r=n(7854);e.exports=r},1320:function(e,t,n){var r=n(7854),i=n(8880),o=n(6656),a=n(3505),s=n(2788),l=n(9909),c=l.get,u=l.enforce,p=String(String).split("String");(e.exports=function(e,t,n,s){var l,c=!!s&&!!s.unsafe,d=!!s&&!!s.enumerable,f=!!s&&!!s.noTargetGet;"function"==typeof n&&("string"!=typeof t||o(n,"name")||i(n,"name",t),(l=u(n)).source||(l.source=p.join("string"==typeof t?t:""))),e!==r?(c?!f&&e[t]&&(d=!0):delete e[t],d?e[t]=n:i(e,t,n)):d?e[t]=n:a(t,n)})(Function.prototype,"toString",(function(){return"function"==typeof this&&c(this).source||s(this)}))},4488:function(e){e.exports=function(e){if(null==e)throw TypeError("Can't call method on "+e);return e}},3505:function(e,t,n){var r=n(7854),i=n(8880);e.exports=function(e,t){try{i(r,e,t)}catch(n){r[e]=t}return t}},8003:function(e,t,n){var r=n(3070).f,i=n(6656),o=n(5112)("toStringTag");e.exports=function(e,t,n){e&&!i(e=n?e:e.prototype,o)&&r(e,o,{configurable:!0,value:t})}},6200:function(e,t,n){var r=n(2309),i=n(9711),o=r("keys");e.exports=function(e){return o[e]||(o[e]=i(e))}},5465:function(e,t,n){var r=n(7854),i=n(3505),o="__core-js_shared__",a=r[o]||i(o,{});e.exports=a},2309:function(e,t,n){var r=n(1913),i=n(5465);(e.exports=function(e,t){return i[e]||(i[e]=void 0!==t?t:{})})("versions",[]).push({version:"3.14.0",mode:r?"pure":"global",copyright:"© 2021 Denis Pushkarev (zloirock.ru)"})},1400:function(e,t,n){var r=n(9958),i=Math.max,o=Math.min;e.exports=function(e,t){var n=r(e);return n<0?i(n+t,0):o(n,t)}},5656:function(e,t,n){var r=n(8361),i=n(4488);e.exports=function(e){return r(i(e))}},9958:function(e){var t=Math.ceil,n=Math.floor;e.exports=function(e){return isNaN(e=+e)?0:(e>0?n:t)(e)}},7466:function(e,t,n){var r=n(9958),i=Math.min;e.exports=function(e){return e>0?i(r(e),9007199254740991):0}},7908:function(e,t,n){var r=n(4488);e.exports=function(e){return Object(r(e))}},7593:function(e,t,n){var r=n(111);e.exports=function(e,t){if(!r(e))return e;var n,i;if(t&&"function"==typeof(n=e.toString)&&!r(i=n.call(e)))return i;if("function"==typeof(n=e.valueOf)&&!r(i=n.call(e)))return i;if(!t&&"function"==typeof(n=e.toString)&&!r(i=n.call(e)))return i;throw TypeError("Can't convert object to primitive value")}},1694:function(e,t,n){var r={};r[n(5112)("toStringTag")]="z",e.exports="[object z]"===String(r)},9711:function(e){var t=0,n=Math.random();e.exports=function(e){return"Symbol("+String(void 0===e?"":e)+")_"+(++t+n).toString(36)}},3307:function(e,t,n){var r=n(133);e.exports=r&&!Symbol.sham&&"symbol"==typeof Symbol.iterator},6061:function(e,t,n){var r=n(5112);t.f=r},5112:function(e,t,n){var r=n(7854),i=n(2309),o=n(6656),a=n(9711),s=n(133),l=n(3307),c=i("wks"),u=r.Symbol,p=l?u:u&&u.withoutSetter||a;e.exports=function(e){return o(c,e)&&(s||"string"==typeof c[e])||(s&&o(u,e)?c[e]=u[e]:c[e]=p("Symbol."+e)),c[e]}},2222:function(e,t,n){"use strict";var r=n(2109),i=n(7293),o=n(3157),a=n(111),s=n(7908),l=n(7466),c=n(6135),u=n(5417),p=n(1194),d=n(5112),f=n(7392),h=d("isConcatSpreadable"),m=9007199254740991,g="Maximum allowed index exceeded",y=f>=51||!i((function(){var e=[];return e[h]=!1,e.concat()[0]!==e})),b=p("concat"),v=function(e){if(!a(e))return!1;var t=e[h];return void 0!==t?!!t:o(e)};r({target:"Array",proto:!0,forced:!y||!b},{concat:function(e){var t,n,r,i,o,a=s(this),p=u(a,0),d=0;for(t=-1,r=arguments.length;tm)throw TypeError(g);for(n=0;n=m)throw TypeError(g);c(p,d++,o)}return p.length=d,p}})},3706:function(e,t,n){var r=n(7854);n(8003)(r.JSON,"JSON",!0)},408:function(e,t,n){n(8003)(Math,"Math",!0)},1539:function(e,t,n){var r=n(1694),i=n(1320),o=n(288);r||i(Object.prototype,"toString",o,{unsafe:!0})},1299:function(e,t,n){var r=n(2109),i=n(7854),o=n(8003);r({global:!0},{Reflect:{}}),o(i.Reflect,"Reflect",!0)},2443:function(e,t,n){n(7235)("asyncIterator")},1817:function(e,t,n){"use strict";var r=n(2109),i=n(9781),o=n(7854),a=n(6656),s=n(111),l=n(3070).f,c=n(9920),u=o.Symbol;if(i&&"function"==typeof u&&(!("description"in u.prototype)||void 0!==u().description)){var p={},d=function(){var e=arguments.length<1||void 0===arguments[0]?void 0:String(arguments[0]),t=this instanceof d?new u(e):void 0===e?u():u(e);return""===e&&(p[t]=!0),t};c(d,u);var f=d.prototype=u.prototype;f.constructor=d;var h=f.toString,m="Symbol(test)"==String(u("test")),g=/^Symbol\((.*)\)[^)]+$/;l(f,"description",{configurable:!0,get:function(){var e=s(this)?this.valueOf():this,t=h.call(e);if(a(p,e))return"";var n=m?t.slice(7,-1):t.replace(g,"$1");return""===n?void 0:n}}),r({global:!0,forced:!0},{Symbol:d})}},2401:function(e,t,n){n(7235)("hasInstance")},8722:function(e,t,n){n(7235)("isConcatSpreadable")},2165:function(e,t,n){n(7235)("iterator")},2526:function(e,t,n){"use strict";var r=n(2109),i=n(7854),o=n(5005),a=n(1913),s=n(9781),l=n(133),c=n(3307),u=n(7293),p=n(6656),d=n(3157),f=n(111),h=n(9670),m=n(7908),g=n(5656),y=n(7593),b=n(9114),v=n(30),x=n(1956),w=n(8006),k=n(1156),S=n(5181),O=n(1236),E=n(3070),_=n(5296),A=n(8880),j=n(1320),C=n(2309),P=n(6200),T=n(3501),I=n(9711),R=n(5112),N=n(6061),$=n(7235),L=n(8003),D=n(9909),M=n(2092).forEach,F=P("hidden"),z="Symbol",U="prototype",B=R("toPrimitive"),q=D.set,V=D.getterFor(z),W=Object[U],H=i.Symbol,Y=o("JSON","stringify"),G=O.f,Q=E.f,X=k.f,K=_.f,Z=C("symbols"),J=C("op-symbols"),ee=C("string-to-symbol-registry"),te=C("symbol-to-string-registry"),ne=C("wks"),re=i.QObject,ie=!re||!re[U]||!re[U].findChild,oe=s&&u((function(){return 7!=v(Q({},"a",{get:function(){return Q(this,"a",{value:7}).a}})).a}))?function(e,t,n){var r=G(W,t);r&&delete W[t],Q(e,t,n),r&&e!==W&&Q(W,t,r)}:Q,ae=function(e,t){var n=Z[e]=v(H[U]);return q(n,{type:z,tag:e,description:t}),s||(n.description=t),n},se=c?function(e){return"symbol"==typeof e}:function(e){return Object(e)instanceof H},le=function(e,t,n){e===W&&le(J,t,n),h(e);var r=y(t,!0);return h(n),p(Z,r)?(n.enumerable?(p(e,F)&&e[F][r]&&(e[F][r]=!1),n=v(n,{enumerable:b(0,!1)})):(p(e,F)||Q(e,F,b(1,{})),e[F][r]=!0),oe(e,r,n)):Q(e,r,n)},ce=function(e,t){h(e);var n=g(t),r=x(n).concat(fe(n));return M(r,(function(t){s&&!ue.call(n,t)||le(e,t,n[t])})),e},ue=function(e){var t=y(e,!0),n=K.call(this,t);return!(this===W&&p(Z,t)&&!p(J,t))&&(!(n||!p(this,t)||!p(Z,t)||p(this,F)&&this[F][t])||n)},pe=function(e,t){var n=g(e),r=y(t,!0);if(n!==W||!p(Z,r)||p(J,r)){var i=G(n,r);return!i||!p(Z,r)||p(n,F)&&n[F][r]||(i.enumerable=!0),i}},de=function(e){var t=X(g(e)),n=[];return M(t,(function(e){p(Z,e)||p(T,e)||n.push(e)})),n},fe=function(e){var t=e===W,n=X(t?J:g(e)),r=[];return M(n,(function(e){!p(Z,e)||t&&!p(W,e)||r.push(Z[e])})),r};l||(H=function(){if(this instanceof H)throw TypeError("Symbol is not a constructor");var e=arguments.length&&void 0!==arguments[0]?String(arguments[0]):void 0,t=I(e),n=function(e){this===W&&n.call(J,e),p(this,F)&&p(this[F],t)&&(this[F][t]=!1),oe(this,t,b(1,e))};return s&&ie&&oe(W,t,{configurable:!0,set:n}),ae(t,e)},j(H[U],"toString",(function(){return V(this).tag})),j(H,"withoutSetter",(function(e){return ae(I(e),e)})),_.f=ue,E.f=le,O.f=pe,w.f=k.f=de,S.f=fe,N.f=function(e){return ae(R(e),e)},s&&(Q(H[U],"description",{configurable:!0,get:function(){return V(this).description}}),a||j(W,"propertyIsEnumerable",ue,{unsafe:!0}))),r({global:!0,wrap:!0,forced:!l,sham:!l},{Symbol:H}),M(x(ne),(function(e){$(e)})),r({target:z,stat:!0,forced:!l},{for:function(e){var t=String(e);if(p(ee,t))return ee[t];var n=H(t);return ee[t]=n,te[n]=t,n},keyFor:function(e){if(!se(e))throw TypeError(e+" is not a symbol");if(p(te,e))return te[e]},useSetter:function(){ie=!0},useSimple:function(){ie=!1}}),r({target:"Object",stat:!0,forced:!l,sham:!s},{create:function(e,t){return void 0===t?v(e):ce(v(e),t)},defineProperty:le,defineProperties:ce,getOwnPropertyDescriptor:pe}),r({target:"Object",stat:!0,forced:!l},{getOwnPropertyNames:de,getOwnPropertySymbols:fe}),r({target:"Object",stat:!0,forced:u((function(){S.f(1)}))},{getOwnPropertySymbols:function(e){return S.f(m(e))}}),Y&&r({target:"JSON",stat:!0,forced:!l||u((function(){var e=H();return"[null]"!=Y([e])||"{}"!=Y({a:e})||"{}"!=Y(Object(e))}))},{stringify:function(e,t,n){for(var r,i=[e],o=1;arguments.length>o;)i.push(arguments[o++]);if(r=t,(f(t)||void 0!==e)&&!se(e))return d(t)||(t=function(e,t){if("function"==typeof r&&(t=r.call(this,e,t)),!se(t))return t}),i[1]=t,Y.apply(null,i)}}),H[U][B]||A(H[U],B,H[U].valueOf),L(H,z),T[F]=!0},6066:function(e,t,n){n(7235)("matchAll")},9007:function(e,t,n){n(7235)("match")},3510:function(e,t,n){n(7235)("replace")},1840:function(e,t,n){n(7235)("search")},6982:function(e,t,n){n(7235)("species")},2159:function(e,t,n){n(7235)("split")},6649:function(e,t,n){n(7235)("toPrimitive")},9341:function(e,t,n){n(7235)("toStringTag")},543:function(e,t,n){n(7235)("unscopables")},8260:function(e,t,n){"use strict";var r=n(4015),i=n.n(r),o=n(3645),a=n.n(o)()(i());a.push([e.id,".ps{overflow:hidden!important;overflow-anchor:none;-ms-overflow-style:none;touch-action:auto;-ms-touch-action:auto}.ps__rail-x{display:none;opacity:0;transition:background-color .2s linear,opacity .2s linear;-webkit-transition:background-color .2s linear,opacity .2s linear;height:15px;bottom:0;position:absolute}.ps__rail-y{display:none;opacity:0;transition:background-color .2s linear,opacity .2s linear;-webkit-transition:background-color .2s linear,opacity .2s linear;width:15px;right:0;position:absolute}.ps--active-x>.ps__rail-x,.ps--active-y>.ps__rail-y{display:block;background-color:transparent}.ps:hover>.ps__rail-x,.ps:hover>.ps__rail-y,.ps--focus>.ps__rail-x,.ps--focus>.ps__rail-y,.ps--scrolling-x>.ps__rail-x,.ps--scrolling-y>.ps__rail-y{opacity:.6}.ps .ps__rail-x:hover,.ps .ps__rail-y:hover,.ps .ps__rail-x:focus,.ps .ps__rail-y:focus,.ps .ps__rail-x.ps--clicking,.ps .ps__rail-y.ps--clicking{background-color:#eee;opacity:.9}.ps__thumb-x{background-color:#aaa;border-radius:6px;transition:background-color .2s linear,height .2s ease-in-out;-webkit-transition:background-color .2s linear,height .2s ease-in-out;height:6px;bottom:2px;position:absolute}.ps__thumb-y{background-color:#aaa;border-radius:6px;transition:background-color .2s linear,width .2s ease-in-out;-webkit-transition:background-color .2s linear,width .2s ease-in-out;width:6px;right:2px;position:absolute}.ps__rail-x:hover>.ps__thumb-x,.ps__rail-x:focus>.ps__thumb-x,.ps__rail-x.ps--clicking .ps__thumb-x{background-color:#999;height:11px}.ps__rail-y:hover>.ps__thumb-y,.ps__rail-y:focus>.ps__thumb-y,.ps__rail-y.ps--clicking .ps__thumb-y{background-color:#999;width:11px}@supports (-ms-overflow-style: none){.ps{overflow:auto!important}}@media screen and (-ms-high-contrast: active),(-ms-high-contrast: none){.ps{overflow:auto!important}}\n","",{version:3,sources:["webpack://./node_modules/perfect-scrollbar/css/perfect-scrollbar.css"],names:[],mappings:"AAGA,IACE,yBAAA,CACA,oBAAA,CACA,uBAAA,CACA,iBAAA,CACA,qBAAA,CAMF,YACE,YAAA,CACA,SAAA,CACA,yDAAA,CACA,iEAAA,CACA,WAAA,CAEA,QAAA,CAEA,iBAAA,CAGF,YACE,YAAA,CACA,SAAA,CACA,yDAAA,CACA,iEAAA,CACA,UAAA,CAEA,OAAA,CAEA,iBAAA,CAGF,oDAEE,aAAA,CACA,4BAAA,CAGF,oJAME,UAAA,CAGF,kJAME,qBAAA,CACA,UAAA,CAMF,aACE,qBAAA,CAnEF,iBAAA,CAqEE,6DAAA,CACA,qEAAA,CACA,UAAA,CAEA,UAAA,CAEA,iBAAA,CAGF,aACE,qBAAA,CA/EF,iBAAA,CAiFE,4DAAA,CACA,oEAAA,CACA,SAAA,CAEA,SAAA,CAEA,iBAAA,CAGF,oGAGE,qBAAA,CACA,WAAA,CAGF,oGAGE,qBAAA,CACA,UAAA,CAIF,qCACE,IACE,uBAAA,CAAA,CAIJ,wEACE,IACE,uBAAA,CAAA",sourcesContent:["/*\n * Container style\n */\n.ps {\n overflow: hidden !important;\n overflow-anchor: none;\n -ms-overflow-style: none;\n touch-action: auto;\n -ms-touch-action: auto;\n}\n\n/*\n * Scrollbar rail styles\n */\n.ps__rail-x {\n display: none;\n opacity: 0;\n transition: background-color .2s linear, opacity .2s linear;\n -webkit-transition: background-color .2s linear, opacity .2s linear;\n height: 15px;\n /* there must be 'bottom' or 'top' for ps__rail-x */\n bottom: 0px;\n /* please don't change 'position' */\n position: absolute;\n}\n\n.ps__rail-y {\n display: none;\n opacity: 0;\n transition: background-color .2s linear, opacity .2s linear;\n -webkit-transition: background-color .2s linear, opacity .2s linear;\n width: 15px;\n /* there must be 'right' or 'left' for ps__rail-y */\n right: 0;\n /* please don't change 'position' */\n position: absolute;\n}\n\n.ps--active-x > .ps__rail-x,\n.ps--active-y > .ps__rail-y {\n display: block;\n background-color: transparent;\n}\n\n.ps:hover > .ps__rail-x,\n.ps:hover > .ps__rail-y,\n.ps--focus > .ps__rail-x,\n.ps--focus > .ps__rail-y,\n.ps--scrolling-x > .ps__rail-x,\n.ps--scrolling-y > .ps__rail-y {\n opacity: 0.6;\n}\n\n.ps .ps__rail-x:hover,\n.ps .ps__rail-y:hover,\n.ps .ps__rail-x:focus,\n.ps .ps__rail-y:focus,\n.ps .ps__rail-x.ps--clicking,\n.ps .ps__rail-y.ps--clicking {\n background-color: #eee;\n opacity: 0.9;\n}\n\n/*\n * Scrollbar thumb styles\n */\n.ps__thumb-x {\n background-color: #aaa;\n border-radius: 6px;\n transition: background-color .2s linear, height .2s ease-in-out;\n -webkit-transition: background-color .2s linear, height .2s ease-in-out;\n height: 6px;\n /* there must be 'bottom' for ps__thumb-x */\n bottom: 2px;\n /* please don't change 'position' */\n position: absolute;\n}\n\n.ps__thumb-y {\n background-color: #aaa;\n border-radius: 6px;\n transition: background-color .2s linear, width .2s ease-in-out;\n -webkit-transition: background-color .2s linear, width .2s ease-in-out;\n width: 6px;\n /* there must be 'right' for ps__thumb-y */\n right: 2px;\n /* please don't change 'position' */\n position: absolute;\n}\n\n.ps__rail-x:hover > .ps__thumb-x,\n.ps__rail-x:focus > .ps__thumb-x,\n.ps__rail-x.ps--clicking .ps__thumb-x {\n background-color: #999;\n height: 11px;\n}\n\n.ps__rail-y:hover > .ps__thumb-y,\n.ps__rail-y:focus > .ps__thumb-y,\n.ps__rail-y.ps--clicking .ps__thumb-y {\n background-color: #999;\n width: 11px;\n}\n\n/* MS supports */\n@supports (-ms-overflow-style: none) {\n .ps {\n overflow: auto !important;\n }\n}\n\n@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {\n .ps {\n overflow: auto !important;\n }\n}\n"],sourceRoot:""}]),t.Z=a},3645:function(e){"use strict";e.exports=function(e){var t=[];return t.toString=function(){return this.map((function(t){var n=e(t);return t[2]?"@media ".concat(t[2]," {").concat(n,"}"):n})).join("")},t.i=function(e,n,r){"string"==typeof e&&(e=[[null,e,""]]);var i={};if(r)for(var o=0;oe.length)&&(t=e.length);for(var n=0,r=new Array(t);n2?r:e).apply(void 0,i)}}e.memoize=a,e.debounce=s,e.bind=l,e.default={memoize:a,debounce:s,bind:l}},void 0===(r=n.apply(t,[t]))||(e.exports=r)},7856:function(e){e.exports=function(){"use strict";const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:r,getOwnPropertyDescriptor:i}=Object;let{freeze:o,seal:a,create:s}=Object,{apply:l,construct:c}="undefined"!=typeof Reflect&&Reflect;o||(o=function(e){return e}),a||(a=function(e){return e}),l||(l=function(e,t,n){return e.apply(t,n)}),c||(c=function(e,t){return new e(...t)});const u=k(Array.prototype.forEach),p=k(Array.prototype.pop),d=k(Array.prototype.push),f=k(String.prototype.toLowerCase),h=k(String.prototype.toString),m=k(String.prototype.match),g=k(String.prototype.replace),y=k(String.prototype.indexOf),b=k(String.prototype.trim),v=k(RegExp.prototype.test),x=(w=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n1?n-1:0),i=1;i2&&void 0!==arguments[2]?arguments[2]:f;t&&t(e,null);let o=r.length;for(;o--;){let t=r[o];if("string"==typeof t){const e=i(t);e!==t&&(n(r)||(r[o]=e),t=e)}e[t]=!0}return e}function O(t){const n=s(null);for(const[r,o]of e(t))void 0!==i(t,r)&&(n[r]=o);return n}function E(e,t){for(;null!==e;){const n=i(e,t);if(n){if(n.get)return k(n.get);if("function"==typeof n.value)return k(n.value)}e=r(e)}return function(e){return console.warn("fallback value for",e),null}}const _=o(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","section","select","shadow","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),A=o(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","filter","font","g","glyph","glyphref","hkern","image","line","lineargradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),j=o(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),C=o(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),P=o(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),T=o(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),I=o(["#text"]),R=o(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","face","for","headers","height","hidden","high","href","hreflang","id","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","pattern","placeholder","playsinline","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","xmlns","slot"]),N=o(["accent-height","accumulate","additive","alignment-baseline","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dur","edgemode","elevation","end","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),$=o(["accent","accentunder","align","bevelled","close","columnsalign","columnlines","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lspace","lquote","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),L=o(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),D=a(/\{\{[\w\W]*|[\w\W]*\}\}/gm),M=a(/<%[\w\W]*|[\w\W]*%>/gm),F=a(/\${[\w\W]*}/gm),z=a(/^data-[\-\w.\u00B7-\uFFFF]/),U=a(/^aria-[\-\w]+$/),B=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),q=a(/^(?:\w+script|data):/i),V=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),W=a(/^html$/i);var H=Object.freeze({__proto__:null,MUSTACHE_EXPR:D,ERB_EXPR:M,TMPLIT_EXPR:F,DATA_ATTR:z,ARIA_ATTR:U,IS_ALLOWED_URI:B,IS_SCRIPT_OR_DATA:q,ATTR_WHITESPACE:V,DOCTYPE_NAME:W});const Y=function(){return"undefined"==typeof window?null:window};return function t(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:Y();const r=e=>t(e);if(r.version="3.0.6",r.removed=[],!n||!n.document||9!==n.document.nodeType)return r.isSupported=!1,r;let{document:i}=n;const a=i,l=a.currentScript,{DocumentFragment:c,HTMLTemplateElement:w,Node:k,Element:D,NodeFilter:M,NamedNodeMap:F=n.NamedNodeMap||n.MozNamedAttrMap,HTMLFormElement:z,DOMParser:U,trustedTypes:q}=n,V=D.prototype,G=E(V,"cloneNode"),Q=E(V,"nextSibling"),X=E(V,"childNodes"),K=E(V,"parentNode");if("function"==typeof w){const e=i.createElement("template");e.content&&e.content.ownerDocument&&(i=e.content.ownerDocument)}let Z,J="";const{implementation:ee,createNodeIterator:te,createDocumentFragment:ne,getElementsByTagName:re}=i,{importNode:ie}=a;let oe={};r.isSupported="function"==typeof e&&"function"==typeof K&&ee&&void 0!==ee.createHTMLDocument;const{MUSTACHE_EXPR:ae,ERB_EXPR:se,TMPLIT_EXPR:le,DATA_ATTR:ce,ARIA_ATTR:ue,IS_SCRIPT_OR_DATA:pe,ATTR_WHITESPACE:de}=H;let{IS_ALLOWED_URI:fe}=H,he=null;const me=S({},[..._,...A,...j,...P,...I]);let ge=null;const ye=S({},[...R,...N,...$,...L]);let be=Object.seal(s(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),ve=null,xe=null,we=!0,ke=!0,Se=!1,Oe=!0,Ee=!1,_e=!1,Ae=!1,je=!1,Ce=!1,Pe=!1,Te=!1,Ie=!0,Re=!1,Ne=!0,$e=!1,Le={},De=null;const Me=S({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Fe=null;const ze=S({},["audio","video","img","source","image","track"]);let Ue=null;const Be=S({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),qe="http://www.w3.org/1998/Math/MathML",Ve="http://www.w3.org/2000/svg",We="http://www.w3.org/1999/xhtml";let He=We,Ye=!1,Ge=null;const Qe=S({},[qe,Ve,We],h);let Xe=null;const Ke=["application/xhtml+xml","text/html"];let Ze=null,Je=null;const et=i.createElement("form"),tt=function(e){return e instanceof RegExp||e instanceof Function},nt=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!Je||Je!==e){if(e&&"object"==typeof e||(e={}),e=O(e),Xe=Xe=-1===Ke.indexOf(e.PARSER_MEDIA_TYPE)?"text/html":e.PARSER_MEDIA_TYPE,Ze="application/xhtml+xml"===Xe?h:f,he="ALLOWED_TAGS"in e?S({},e.ALLOWED_TAGS,Ze):me,ge="ALLOWED_ATTR"in e?S({},e.ALLOWED_ATTR,Ze):ye,Ge="ALLOWED_NAMESPACES"in e?S({},e.ALLOWED_NAMESPACES,h):Qe,Ue="ADD_URI_SAFE_ATTR"in e?S(O(Be),e.ADD_URI_SAFE_ATTR,Ze):Be,Fe="ADD_DATA_URI_TAGS"in e?S(O(ze),e.ADD_DATA_URI_TAGS,Ze):ze,De="FORBID_CONTENTS"in e?S({},e.FORBID_CONTENTS,Ze):Me,ve="FORBID_TAGS"in e?S({},e.FORBID_TAGS,Ze):{},xe="FORBID_ATTR"in e?S({},e.FORBID_ATTR,Ze):{},Le="USE_PROFILES"in e&&e.USE_PROFILES,we=!1!==e.ALLOW_ARIA_ATTR,ke=!1!==e.ALLOW_DATA_ATTR,Se=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Oe=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,Ee=e.SAFE_FOR_TEMPLATES||!1,_e=e.WHOLE_DOCUMENT||!1,Ce=e.RETURN_DOM||!1,Pe=e.RETURN_DOM_FRAGMENT||!1,Te=e.RETURN_TRUSTED_TYPE||!1,je=e.FORCE_BODY||!1,Ie=!1!==e.SANITIZE_DOM,Re=e.SANITIZE_NAMED_PROPS||!1,Ne=!1!==e.KEEP_CONTENT,$e=e.IN_PLACE||!1,fe=e.ALLOWED_URI_REGEXP||B,He=e.NAMESPACE||We,be=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&tt(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(be.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&tt(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(be.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(be.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Ee&&(ke=!1),Pe&&(Ce=!0),Le&&(he=S({},[...I]),ge=[],!0===Le.html&&(S(he,_),S(ge,R)),!0===Le.svg&&(S(he,A),S(ge,N),S(ge,L)),!0===Le.svgFilters&&(S(he,j),S(ge,N),S(ge,L)),!0===Le.mathMl&&(S(he,P),S(ge,$),S(ge,L))),e.ADD_TAGS&&(he===me&&(he=O(he)),S(he,e.ADD_TAGS,Ze)),e.ADD_ATTR&&(ge===ye&&(ge=O(ge)),S(ge,e.ADD_ATTR,Ze)),e.ADD_URI_SAFE_ATTR&&S(Ue,e.ADD_URI_SAFE_ATTR,Ze),e.FORBID_CONTENTS&&(De===Me&&(De=O(De)),S(De,e.FORBID_CONTENTS,Ze)),Ne&&(he["#text"]=!0),_e&&S(he,["html","head","body"]),he.table&&(S(he,["tbody"]),delete ve.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw x('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw x('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');Z=e.TRUSTED_TYPES_POLICY,J=Z.createHTML("")}else void 0===Z&&(Z=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const r="data-tt-policy-suffix";t&&t.hasAttribute(r)&&(n=t.getAttribute(r));const i="dompurify"+(n?"#"+n:"");try{return e.createPolicy(i,{createHTML(e){return e},createScriptURL(e){return e}})}catch(e){return console.warn("TrustedTypes policy "+i+" could not be created."),null}}(q,l)),null!==Z&&"string"==typeof J&&(J=Z.createHTML(""));o&&o(e),Je=e}},rt=S({},["mi","mo","mn","ms","mtext"]),it=S({},["foreignobject","desc","title","annotation-xml"]),ot=S({},["title","style","font","a","script"]),at=S({},A);S(at,j),S(at,C);const st=S({},P);S(st,T);const lt=function(e){d(r.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){e.remove()}},ct=function(e,t){try{d(r.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){d(r.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!ge[e])if(Ce||Pe)try{lt(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},ut=function(e){let t=null,n=null;if(je)e=""+e;else{const t=m(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===Xe&&He===We&&(e=''+e+"");const r=Z?Z.createHTML(e):e;if(He===We)try{t=(new U).parseFromString(r,Xe)}catch(e){}if(!t||!t.documentElement){t=ee.createDocument(He,"template",null);try{t.documentElement.innerHTML=Ye?J:r}catch(e){}}const o=t.body||t.documentElement;return e&&n&&o.insertBefore(i.createTextNode(n),o.childNodes[0]||null),He===We?re.call(t,_e?"html":"body")[0]:_e?t.documentElement:o},pt=function(e){return te.call(e.ownerDocument||e,e,M.SHOW_ELEMENT|M.SHOW_COMMENT|M.SHOW_TEXT,null)},dt=function(e){return"function"==typeof k&&e instanceof k},ft=function(e,t,n){oe[e]&&u(oe[e],(e=>{e.call(r,t,n,Je)}))},ht=function(e){let t=null;if(ft("beforeSanitizeElements",e,null),(n=e)instanceof z&&("string"!=typeof n.nodeName||"string"!=typeof n.textContent||"function"!=typeof n.removeChild||!(n.attributes instanceof F)||"function"!=typeof n.removeAttribute||"function"!=typeof n.setAttribute||"string"!=typeof n.namespaceURI||"function"!=typeof n.insertBefore||"function"!=typeof n.hasChildNodes))return lt(e),!0;var n;const i=Ze(e.nodeName);if(ft("uponSanitizeElement",e,{tagName:i,allowedTags:he}),e.hasChildNodes()&&!dt(e.firstElementChild)&&v(/<[/\w]/g,e.innerHTML)&&v(/<[/\w]/g,e.textContent))return lt(e),!0;if(!he[i]||ve[i]){if(!ve[i]&>(i)){if(be.tagNameCheck instanceof RegExp&&v(be.tagNameCheck,i))return!1;if(be.tagNameCheck instanceof Function&&be.tagNameCheck(i))return!1}if(Ne&&!De[i]){const t=K(e)||e.parentNode,n=X(e)||e.childNodes;if(n&&t)for(let r=n.length-1;r>=0;--r)t.insertBefore(G(n[r],!0),Q(e))}return lt(e),!0}return e instanceof D&&!function(e){let t=K(e);t&&t.tagName||(t={namespaceURI:He,tagName:"template"});const n=f(e.tagName),r=f(t.tagName);return!!Ge[e.namespaceURI]&&(e.namespaceURI===Ve?t.namespaceURI===We?"svg"===n:t.namespaceURI===qe?"svg"===n&&("annotation-xml"===r||rt[r]):Boolean(at[n]):e.namespaceURI===qe?t.namespaceURI===We?"math"===n:t.namespaceURI===Ve?"math"===n&&it[r]:Boolean(st[n]):e.namespaceURI===We?!(t.namespaceURI===Ve&&!it[r])&&!(t.namespaceURI===qe&&!rt[r])&&!st[n]&&(ot[n]||!at[n]):!("application/xhtml+xml"!==Xe||!Ge[e.namespaceURI]))}(e)?(lt(e),!0):"noscript"!==i&&"noembed"!==i&&"noframes"!==i||!v(/<\/no(script|embed|frames)/i,e.innerHTML)?(Ee&&3===e.nodeType&&(t=e.textContent,u([ae,se,le],(e=>{t=g(t,e," ")})),e.textContent!==t&&(d(r.removed,{element:e.cloneNode()}),e.textContent=t)),ft("afterSanitizeElements",e,null),!1):(lt(e),!0)},mt=function(e,t,n){if(Ie&&("id"===t||"name"===t)&&(n in i||n in et))return!1;if(ke&&!xe[t]&&v(ce,t));else if(we&&v(ue,t));else if(!ge[t]||xe[t]){if(!(gt(e)&&(be.tagNameCheck instanceof RegExp&&v(be.tagNameCheck,e)||be.tagNameCheck instanceof Function&&be.tagNameCheck(e))&&(be.attributeNameCheck instanceof RegExp&&v(be.attributeNameCheck,t)||be.attributeNameCheck instanceof Function&&be.attributeNameCheck(t))||"is"===t&&be.allowCustomizedBuiltInElements&&(be.tagNameCheck instanceof RegExp&&v(be.tagNameCheck,n)||be.tagNameCheck instanceof Function&&be.tagNameCheck(n))))return!1}else if(Ue[t]);else if(v(fe,g(n,de,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==y(n,"data:")||!Fe[e])if(Se&&!v(pe,g(n,de,"")));else if(n)return!1;return!0},gt=function(e){return e.indexOf("-")>0},yt=function(e){ft("beforeSanitizeAttributes",e,null);const{attributes:t}=e;if(!t)return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:ge};let i=t.length;for(;i--;){const o=t[i],{name:a,namespaceURI:s,value:l}=o,c=Ze(a);let d="value"===a?l:b(l);if(n.attrName=c,n.attrValue=d,n.keepAttr=!0,n.forceKeepAttr=void 0,ft("uponSanitizeAttribute",e,n),d=n.attrValue,n.forceKeepAttr)continue;if(ct(a,e),!n.keepAttr)continue;if(!Oe&&v(/\/>/i,d)){ct(a,e);continue}Ee&&u([ae,se,le],(e=>{d=g(d,e," ")}));const f=Ze(e.nodeName);if(mt(f,c,d)){if(!Re||"id"!==c&&"name"!==c||(ct(a,e),d="user-content-"+d),Z&&"object"==typeof q&&"function"==typeof q.getAttributeType)if(s);else switch(q.getAttributeType(f,c)){case"TrustedHTML":d=Z.createHTML(d);break;case"TrustedScriptURL":d=Z.createScriptURL(d)}try{s?e.setAttributeNS(s,a,d):e.setAttribute(a,d),p(r.removed)}catch(e){}}}ft("afterSanitizeAttributes",e,null)},bt=function e(t){let n=null;const r=pt(t);for(ft("beforeSanitizeShadowDOM",t,null);n=r.nextNode();)ft("uponSanitizeShadowNode",n,null),ht(n)||(n.content instanceof c&&e(n.content),yt(n));ft("afterSanitizeShadowDOM",t,null)};return r.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,i=null,o=null,s=null;if(Ye=!e,Ye&&(e="\x3c!--\x3e"),"string"!=typeof e&&!dt(e)){if("function"!=typeof e.toString)throw x("toString is not a function");if("string"!=typeof(e=e.toString()))throw x("dirty is not a string, aborting")}if(!r.isSupported)return e;if(Ae||nt(t),r.removed=[],"string"==typeof e&&($e=!1),$e){if(e.nodeName){const t=Ze(e.nodeName);if(!he[t]||ve[t])throw x("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof k)n=ut("\x3c!----\x3e"),i=n.ownerDocument.importNode(e,!0),1===i.nodeType&&"BODY"===i.nodeName||"HTML"===i.nodeName?n=i:n.appendChild(i);else{if(!Ce&&!Ee&&!_e&&-1===e.indexOf("<"))return Z&&Te?Z.createHTML(e):e;if(n=ut(e),!n)return Ce?null:Te?J:""}n&&je&<(n.firstChild);const l=pt($e?e:n);for(;o=l.nextNode();)ht(o)||(o.content instanceof c&&bt(o.content),yt(o));if($e)return e;if(Ce){if(Pe)for(s=ne.call(n.ownerDocument);n.firstChild;)s.appendChild(n.firstChild);else s=n;return(ge.shadowroot||ge.shadowrootmode)&&(s=ie.call(a,s,!0)),s}let p=_e?n.outerHTML:n.innerHTML;return _e&&he["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&v(W,n.ownerDocument.doctype.name)&&(p="\n"+p),Ee&&u([ae,se,le],(e=>{p=g(p,e," ")})),Z&&Te?Z.createHTML(p):p},r.setConfig=function(){nt(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),Ae=!0},r.clearConfig=function(){Je=null,Ae=!1},r.isValidAttribute=function(e,t,n){Je||nt({});const r=Ze(e),i=Ze(t);return mt(r,i,n)},r.addHook=function(e,t){"function"==typeof t&&(oe[e]=oe[e]||[],d(oe[e],t))},r.removeHook=function(e){if(oe[e])return p(oe[e])},r.removeHooks=function(e){oe[e]&&(oe[e]=[])},r.removeAllHooks=function(){oe={}},r}()}()},3825:function(e){e.exports={}},6729:function(e){"use strict";var t=Object.prototype.hasOwnProperty,n="~";function r(){}function i(e,t,n){this.fn=e,this.context=t,this.once=n||!1}function o(e,t,r,o,a){if("function"!=typeof r)throw new TypeError("The listener must be a function");var s=new i(r,o||e,a),l=n?n+t:t;return e._events[l]?e._events[l].fn?e._events[l]=[e._events[l],s]:e._events[l].push(s):(e._events[l]=s,e._eventsCount++),e}function a(e,t){0==--e._eventsCount?e._events=new r:delete e._events[t]}function s(){this._events=new r,this._eventsCount=0}Object.create&&(r.prototype=Object.create(null),(new r).__proto__||(n=!1)),s.prototype.eventNames=function(){var e,r,i=[];if(0===this._eventsCount)return i;for(r in e=this._events)t.call(e,r)&&i.push(n?r.slice(1):r);return Object.getOwnPropertySymbols?i.concat(Object.getOwnPropertySymbols(e)):i},s.prototype.listeners=function(e){var t=n?n+e:e,r=this._events[t];if(!r)return[];if(r.fn)return[r.fn];for(var i=0,o=r.length,a=new Array(o);iu.depthLimit)return void s(t,e,r,a);if(void 0!==u.edgesLimit&&i+1>u.edgesLimit)return void s(t,e,r,a);if(o.push(e),Array.isArray(e))for(p=0;pt?1:0}function u(e,t,n,a){void 0===a&&(a=o());var s,l=p(e,"",0,[],void 0,0,a)||e;try{s=0===i.length?JSON.stringify(l,t,n):JSON.stringify(l,d(t),n)}catch(e){return JSON.stringify("[unable to serialize, circular reference is too complex to analyze]")}finally{for(;0!==r.length;){var c=r.pop();4===c.length?Object.defineProperty(c[0],c[1],c[3]):c[0][c[1]]=c[2]}}return s}function p(e,i,o,a,l,u,d){var f;if(u+=1,"object"==typeof e&&null!==e){for(f=0;fd.depthLimit)return void s(t,e,i,l);if(void 0!==d.edgesLimit&&o+1>d.edgesLimit)return void s(t,e,i,l);if(a.push(e),Array.isArray(e))for(f=0;f0)for(var r=0;r=55296&&r<=56319&&t+1=56320&&n<=57343?1024*(r-55296)+n-56320+65536:r}function H(e){return/^\n* /.test(e)}var Y=1,G=2,Q=3,X=4,K=5;function Z(e,t,n,r,o){e.dump=function(){if(0===t.length)return e.quotingType===D?'""':"''";if(!e.noCompatMode&&(-1!==N.indexOf(t)||$.test(t)))return e.quotingType===D?'"'+t+'"':"'"+t+"'";var a=e.indent*Math.max(1,n),s=-1===e.lineWidth?-1:Math.max(Math.min(e.lineWidth,40),e.lineWidth-a),c=r||e.flowLevel>-1&&n>=e.flowLevel;switch(function(e,t,n,r,i,o,a,s){var c,p,d=0,R=null,N=!1,$=!1,L=-1!==r,M=-1,F=B(p=W(e,0))&&p!==l&&!U(p)&&p!==w&&p!==E&&p!==k&&p!==x&&p!==A&&p!==j&&p!==P&&p!==I&&p!==m&&p!==y&&p!==v&&p!==f&&p!==T&&p!==S&&p!==O&&p!==b&&p!==h&&p!==g&&p!==_&&p!==C&&function(e){return!U(e)&&e!==k}(W(e,e.length-1));if(t||a)for(c=0;c=65536?c+=2:c++){if(!B(d=W(e,c)))return K;F=F&&V(d,R,s),R=d}else{for(c=0;c=65536?c+=2:c++){if((d=W(e,c))===u)N=!0,L&&($=$||c-M-1>r&&" "!==e[M+1],M=c);else if(!B(d))return K;F=F&&V(d,R,s),R=d}$=$||L&&c-M-1>r&&" "!==e[M+1]}return N||$?n>9&&H(e)?K:a?o===D?K:G:$?X:Q:!F||a||i(e)?o===D?K:G:Y}(t,c,e.indent,s,(function(t){return function(e,t){var n,r;for(n=0,r=e.implicitTypes.length;n"+J(t,e.indent)+ee(F(function(e,t){for(var n,r,i,o=/(\n+)([^\n]*)/g,a=(i=-1!==(i=e.indexOf("\n"))?i:e.length,o.lastIndex=i,te(e.slice(0,i),t)),s="\n"===e[0]||" "===e[0];r=o.exec(e);){var l=r[1],c=r[2];n=" "===c[0],a+=l+(s||n||""===c?"":"\n")+te(c,t),s=n}return a}(t,s),a));case K:return'"'+function(e){for(var t,n="",r=0,i=0;i=65536?i+=2:i++)r=W(e,i),!(t=R[r])&&B(r)?(n+=e[i],r>=65536&&(n+=e[i+1])):n+=t||L(r);return n}(t)+'"';default:throw new i("impossible error: invalid scalar style")}}()}function J(e,t){var n=H(e)?String(t):"",r="\n"===e[e.length-1];return n+(!r||"\n"!==e[e.length-2]&&"\n"!==e?r?"":"-":"+")+"\n"}function ee(e){return"\n"===e[e.length-1]?e.slice(0,-1):e}function te(e,t){if(""===e||" "===e[0])return e;for(var n,r,i=/ [^ ]/g,o=0,a=0,s=0,l="";n=i.exec(e);)(s=n.index)-o>t&&(r=a>o?a:s,l+="\n"+e.slice(o,r),o=r+1),a=s;return l+="\n",e.length-o>t&&a>o?l+=e.slice(o,a)+"\n"+e.slice(a+1):l+=e.slice(o),l.slice(1)}function ne(e,t,n,r){var i,o,a,s="",l=e.tag;for(i=0,o=n.length;i tag resolver accepts not "'+p+'" style');r=u.represent[p](t,p)}e.dump=r}return!0}return!1}function ie(e,t,n,r,o,s,l){e.tag=null,e.dump=n,re(e,n,!1)||re(e,n,!0);var c,p=a.call(e.dump),d=r;r&&(r=e.flowLevel<0||e.flowLevel>t);var f,h,m="[object Object]"===p||"[object Array]"===p;if(m&&(h=-1!==(f=e.duplicates.indexOf(n))),(null!==e.tag&&"?"!==e.tag||h||2!==e.indent&&t>0)&&(o=!1),h&&e.usedDuplicates[f])e.dump="*ref_"+f;else{if(m&&h&&!e.usedDuplicates[f]&&(e.usedDuplicates[f]=!0),"[object Object]"===p)r&&0!==Object.keys(e.dump).length?(function(e,t,n,r){var o,a,s,l,c,p,d="",f=e.tag,h=Object.keys(n);if(!0===e.sortKeys)h.sort();else if("function"==typeof e.sortKeys)h.sort(e.sortKeys);else if(e.sortKeys)throw new i("sortKeys must be a boolean or a function");for(o=0,a=h.length;o1024)&&(e.dump&&u===e.dump.charCodeAt(0)?p+="?":p+="? "),p+=e.dump,c&&(p+=z(e,t)),ie(e,t+1,l,!0,c)&&(e.dump&&u===e.dump.charCodeAt(0)?p+=":":p+=": ",d+=p+=e.dump));e.tag=f,e.dump=d||"{}"}(e,t,e.dump,o),h&&(e.dump="&ref_"+f+e.dump)):(function(e,t,n){var r,i,o,a,s,l="",c=e.tag,u=Object.keys(n);for(r=0,i=u.length;r1024&&(s+="? "),s+=e.dump+(e.condenseFlow?'"':"")+":"+(e.condenseFlow?"":" "),ie(e,t,a,!1,!1)&&(l+=s+=e.dump));e.tag=c,e.dump="{"+l+"}"}(e,t,e.dump),h&&(e.dump="&ref_"+f+" "+e.dump));else if("[object Array]"===p)r&&0!==e.dump.length?(e.noArrayIndent&&!l&&t>0?ne(e,t-1,e.dump,o):ne(e,t,e.dump,o),h&&(e.dump="&ref_"+f+e.dump)):(function(e,t,n){var r,i,o,a="",s=e.tag;for(r=0,i=n.length;r",e.dump=c+" "+e.dump)}return!0}function oe(e,t){var n,r,i=[],o=[];for(ae(e,i,o),n=0,r=o.length;n>10),56320+(e-65536&1023))}for(var j=new Array(256),C=new Array(256),P=0;P<256;P++)j[P]=_(P)?1:0,C[P]=_(P);function T(e,t){this.input=e,this.filename=t.filename||null,this.schema=t.schema||a,this.onWarning=t.onWarning||null,this.legacy=t.legacy||!1,this.json=t.json||!1,this.listener=t.listener||null,this.implicitTypes=this.schema.compiledImplicit,this.typeMap=this.schema.compiledTypeMap,this.length=e.length,this.position=0,this.line=0,this.lineStart=0,this.lineIndent=0,this.firstTabInLine=-1,this.documents=[]}function I(e,t){var n={name:e.filename,buffer:e.input.slice(0,-1),position:e.position,line:e.line,column:e.position-e.lineStart};return n.snippet=o(n),new i(t,n)}function R(e,t){throw I(e,t)}function N(e,t){e.onWarning&&e.onWarning.call(null,I(e,t))}var $={YAML:function(e,t,n){var r,i,o;null!==e.version&&R(e,"duplication of %YAML directive"),1!==n.length&&R(e,"YAML directive accepts exactly one argument"),null===(r=/^([0-9]+)\.([0-9]+)$/.exec(n[0]))&&R(e,"ill-formed argument of the YAML directive"),i=parseInt(r[1],10),o=parseInt(r[2],10),1!==i&&R(e,"unacceptable YAML version of the document"),e.version=n[0],e.checkLineBreaks=o<2,1!==o&&2!==o&&N(e,"unsupported YAML version of the document")},TAG:function(e,t,n){var r,i;2!==n.length&&R(e,"TAG directive accepts exactly two arguments"),r=n[0],i=n[1],b.test(r)||R(e,"ill-formed tag handle (first argument) of the TAG directive"),s.call(e.tagMap,r)&&R(e,'there is a previously declared suffix for "'+r+'" tag handle'),v.test(i)||R(e,"ill-formed tag prefix (second argument) of the TAG directive");try{i=decodeURIComponent(i)}catch(t){R(e,"tag prefix is malformed: "+i)}e.tagMap[r]=i}};function L(e,t,n,r){var i,o,a,s;if(t1&&(e.result+=r.repeat("\n",t-1))}function q(e,t){var n,r,i=e.tag,o=e.anchor,a=[],s=!1;if(-1!==e.firstTabInLine)return!1;for(null!==e.anchor&&(e.anchorMap[e.anchor]=a),r=e.input.charCodeAt(e.position);0!==r&&(-1!==e.firstTabInLine&&(e.position=e.firstTabInLine,R(e,"tab characters must not be used in indentation")),45===r)&&S(e.input.charCodeAt(e.position+1));)if(s=!0,e.position++,z(e,!0,-1)&&e.lineIndent<=t)a.push(null),r=e.input.charCodeAt(e.position);else if(n=e.line,H(e,t,u,!1,!0),a.push(e.result),z(e,!0,-1),r=e.input.charCodeAt(e.position),(e.line===n||e.lineIndent>t)&&0!==r)R(e,"bad indentation of a sequence entry");else if(e.lineIndentt?T=1:e.lineIndent===t?T=0:e.lineIndentt?T=1:e.lineIndent===t?T=0:e.lineIndentt)&&(v&&(a=e.line,s=e.lineStart,l=e.position),H(e,t,p,!0,i)&&(v?y=e.result:b=e.result),v||(M(e,h,m,g,y,b,a,s,l),g=y=b=null),z(e,!0,-1),u=e.input.charCodeAt(e.position)),(e.line===o||e.lineIndent>t)&&0!==u)R(e,"bad indentation of a mapping entry");else if(e.lineIndent=0))break;0===o?R(e,"bad explicit indentation width of a block scalar; it cannot be less than one"):u?R(e,"repeat of an indentation width identifier"):(p=t+o-1,u=!0)}if(k(a)){do{a=e.input.charCodeAt(++e.position)}while(k(a));if(35===a)do{a=e.input.charCodeAt(++e.position)}while(!w(a)&&0!==a)}for(;0!==a;){for(F(e),e.lineIndent=0,a=e.input.charCodeAt(e.position);(!u||e.lineIndentp&&(p=e.lineIndent),w(a))m++;else{if(e.lineIndent0){for(i=a,o=0;i>0;i--)(a=E(s=e.input.charCodeAt(++e.position)))>=0?o=(o<<4)+a:R(e,"expected hexadecimal character");e.result+=A(o),e.position++}else R(e,"unknown escape sequence");n=r=e.position}else w(s)?(L(e,n,r,!0),B(e,z(e,!1,t)),n=r=e.position):e.position===e.lineStart&&U(e)?R(e,"unexpected end of the document within a double quoted scalar"):(e.position++,r=e.position)}R(e,"unexpected end of the stream within a double quoted scalar")}(e,_)?N=!0:function(e){var t,n,r;if(42!==(r=e.input.charCodeAt(e.position)))return!1;for(r=e.input.charCodeAt(++e.position),t=e.position;0!==r&&!S(r)&&!O(r);)r=e.input.charCodeAt(++e.position);return e.position===t&&R(e,"name of an alias node must contain at least one character"),n=e.input.slice(t,e.position),s.call(e.anchorMap,n)||R(e,'unidentified alias "'+n+'"'),e.result=e.anchorMap[n],z(e,!0,-1),!0}(e)?(N=!0,null===e.tag&&null===e.anchor||R(e,"alias node should not have any properties")):function(e,t,n){var r,i,o,a,s,l,c,u,p=e.kind,d=e.result;if(S(u=e.input.charCodeAt(e.position))||O(u)||35===u||38===u||42===u||33===u||124===u||62===u||39===u||34===u||37===u||64===u||96===u)return!1;if((63===u||45===u)&&(S(r=e.input.charCodeAt(e.position+1))||n&&O(r)))return!1;for(e.kind="scalar",e.result="",i=o=e.position,a=!1;0!==u;){if(58===u){if(S(r=e.input.charCodeAt(e.position+1))||n&&O(r))break}else if(35===u){if(S(e.input.charCodeAt(e.position-1)))break}else{if(e.position===e.lineStart&&U(e)||n&&O(u))break;if(w(u)){if(s=e.line,l=e.lineStart,c=e.lineIndent,z(e,!1,-1),e.lineIndent>=t){a=!0,u=e.input.charCodeAt(e.position);continue}e.position=o,e.line=s,e.lineStart=l,e.lineIndent=c;break}}a&&(L(e,i,o,!1),B(e,e.line-s),i=o=e.position,a=!1),k(u)||(o=e.position+1),u=e.input.charCodeAt(++e.position)}return L(e,i,o,!1),!!e.result||(e.kind=p,e.result=d,!1)}(e,_,l===n)&&(N=!0,null===e.tag&&(e.tag="?")),null!==e.anchor&&(e.anchorMap[e.anchor]=e.result)):0===T&&(N=g&&q(e,P))),null===e.tag)null!==e.anchor&&(e.anchorMap[e.anchor]=e.result);else if("?"===e.tag){for(null!==e.result&&"scalar"!==e.kind&&R(e,'unacceptable node kind for ! tag; it should be "scalar", not "'+e.kind+'"'),y=0,b=e.implicitTypes.length;y"),null!==e.result&&x.kind!==e.kind&&R(e,"unacceptable node kind for !<"+e.tag+'> tag; it should be "'+x.kind+'", not "'+e.kind+'"'),x.resolve(e.result,e.tag)?(e.result=x.construct(e.result,e.tag),null!==e.anchor&&(e.anchorMap[e.anchor]=e.result)):R(e,"cannot resolve a node with !<"+e.tag+"> explicit tag")}return null!==e.listener&&e.listener("close",e),null!==e.tag||null!==e.anchor||N}function Y(e){var t,n,r,i,o=e.position,a=!1;for(e.version=null,e.checkLineBreaks=e.legacy,e.tagMap=Object.create(null),e.anchorMap=Object.create(null);0!==(i=e.input.charCodeAt(e.position))&&(z(e,!0,-1),i=e.input.charCodeAt(e.position),!(e.lineIndent>0||37!==i));){for(a=!0,i=e.input.charCodeAt(++e.position),t=e.position;0!==i&&!S(i);)i=e.input.charCodeAt(++e.position);for(r=[],(n=e.input.slice(t,e.position)).length<1&&R(e,"directive name must not be less than one character in length");0!==i;){for(;k(i);)i=e.input.charCodeAt(++e.position);if(35===i){do{i=e.input.charCodeAt(++e.position)}while(0!==i&&!w(i));break}if(w(i))break;for(t=e.position;0!==i&&!S(i);)i=e.input.charCodeAt(++e.position);r.push(e.input.slice(t,e.position))}0!==i&&F(e),s.call($,n)?$[n](e,n,r):N(e,'unknown document directive "'+n+'"')}z(e,!0,-1),0===e.lineIndent&&45===e.input.charCodeAt(e.position)&&45===e.input.charCodeAt(e.position+1)&&45===e.input.charCodeAt(e.position+2)?(e.position+=3,z(e,!0,-1)):a&&R(e,"directives end mark is expected"),H(e,e.lineIndent-1,p,!1,!0),z(e,!0,-1),e.checkLineBreaks&&g.test(e.input.slice(o,e.position))&&N(e,"non-ASCII line breaks are interpreted as content"),e.documents.push(e.result),e.position===e.lineStart&&U(e)?46===e.input.charCodeAt(e.position)&&(e.position+=3,z(e,!0,-1)):e.positions&&(t=r-s+(o=" ... ").length),n-r>s&&(n=r+s-(a=" ...").length),{str:o+e.slice(t,n).replace(/\t/g,"→")+a,pos:r-t+o.length}}function o(e,t){return r.repeat(" ",t-e.length)+e}e.exports=function(e,t){if(t=Object.create(t||null),!e.buffer)return null;t.maxLength||(t.maxLength=79),"number"!=typeof t.indent&&(t.indent=1),"number"!=typeof t.linesBefore&&(t.linesBefore=3),"number"!=typeof t.linesAfter&&(t.linesAfter=2);for(var n,a=/\r?\n|\r|\0/g,s=[0],l=[],c=-1;n=a.exec(e.buffer);)l.push(n.index),s.push(n.index+n[0].length),e.position<=n.index&&c<0&&(c=s.length-2);c<0&&(c=s.length-1);var u,p,d="",f=Math.min(e.line+t.linesAfter,l.length).toString().length,h=t.maxLength-(t.indent+f+3);for(u=1;u<=t.linesBefore&&!(c-u<0);u++)p=i(e.buffer,s[c-u],l[c-u],e.position-(s[c]-s[c-u]),h),d=r.repeat(" ",t.indent)+o((e.line-u+1).toString(),f)+" | "+p.str+"\n"+d;for(p=i(e.buffer,s[c],l[c],e.position,h),d+=r.repeat(" ",t.indent)+o((e.line+1).toString(),f)+" | "+p.str+"\n",d+=r.repeat("-",t.indent+f+3+p.pos)+"^\n",u=1;u<=t.linesAfter&&!(c+u>=l.length);u++)p=i(e.buffer,s[c+u],l[c+u],e.position-(s[c]-s[c+u]),h),d+=r.repeat(" ",t.indent)+o((e.line+u+1).toString(),f)+" | "+p.str+"\n";return d.replace(/\n$/,"")}},1364:function(e,t,n){"use strict";var r=n(8425),i=["kind","multi","resolve","construct","instanceOf","predicate","represent","representName","defaultStyle","styleAliases"],o=["scalar","sequence","mapping"];e.exports=function(e,t){var n,a;if(t=t||{},Object.keys(t).forEach((function(t){if(-1===i.indexOf(t))throw new r('Unknown option "'+t+'" is met in definition of "'+e+'" YAML type.')})),this.options=t,this.tag=e,this.kind=t.kind||null,this.resolve=t.resolve||function(){return!0},this.construct=t.construct||function(e){return e},this.instanceOf=t.instanceOf||null,this.predicate=t.predicate||null,this.represent=t.represent||null,this.representName=t.representName||null,this.defaultStyle=t.defaultStyle||null,this.multi=t.multi||!1,this.styleAliases=(n=t.styleAliases||null,a={},null!==n&&Object.keys(n).forEach((function(e){n[e].forEach((function(t){a[String(t)]=e}))})),a),-1===o.indexOf(this.kind))throw new r('Unknown kind "'+this.kind+'" is specified for "'+e+'" YAML type.')}},3531:function(e,t,n){"use strict";var r=n(1364),i="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\r";e.exports=new r("tag:yaml.org,2002:binary",{kind:"scalar",resolve:function(e){if(null===e)return!1;var t,n,r=0,o=e.length,a=i;for(n=0;n64)){if(t<0)return!1;r+=6}return r%8==0},construct:function(e){var t,n,r=e.replace(/[\r\n=]/g,""),o=r.length,a=i,s=0,l=[];for(t=0;t>16&255),l.push(s>>8&255),l.push(255&s)),s=s<<6|a.indexOf(r.charAt(t));return 0==(n=o%4*6)?(l.push(s>>16&255),l.push(s>>8&255),l.push(255&s)):18===n?(l.push(s>>10&255),l.push(s>>2&255)):12===n&&l.push(s>>4&255),new Uint8Array(l)},predicate:function(e){return"[object Uint8Array]"===Object.prototype.toString.call(e)},represent:function(e){var t,n,r="",o=0,a=e.length,s=i;for(t=0;t>18&63],r+=s[o>>12&63],r+=s[o>>6&63],r+=s[63&o]),o=(o<<8)+e[t];return 0==(n=a%3)?(r+=s[o>>18&63],r+=s[o>>12&63],r+=s[o>>6&63],r+=s[63&o]):2===n?(r+=s[o>>10&63],r+=s[o>>4&63],r+=s[o<<2&63],r+=s[64]):1===n&&(r+=s[o>>2&63],r+=s[o<<4&63],r+=s[64],r+=s[64]),r}})},8771:function(e,t,n){"use strict";var r=n(1364);e.exports=new r("tag:yaml.org,2002:bool",{kind:"scalar",resolve:function(e){if(null===e)return!1;var t=e.length;return 4===t&&("true"===e||"True"===e||"TRUE"===e)||5===t&&("false"===e||"False"===e||"FALSE"===e)},construct:function(e){return"true"===e||"True"===e||"TRUE"===e},predicate:function(e){return"[object Boolean]"===Object.prototype.toString.call(e)},represent:{lowercase:function(e){return e?"true":"false"},uppercase:function(e){return e?"TRUE":"FALSE"},camelcase:function(e){return e?"True":"False"}},defaultStyle:"lowercase"})},5215:function(e,t,n){"use strict";var r=n(8347),i=n(1364),o=new RegExp("^(?:[-+]?(?:[0-9][0-9_]*)(?:\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?|\\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$"),a=/^[-+]?[0-9]+e/;e.exports=new i("tag:yaml.org,2002:float",{kind:"scalar",resolve:function(e){return null!==e&&!(!o.test(e)||"_"===e[e.length-1])},construct:function(e){var t,n;return n="-"===(t=e.replace(/_/g,"").toLowerCase())[0]?-1:1,"+-".indexOf(t[0])>=0&&(t=t.slice(1)),".inf"===t?1===n?Number.POSITIVE_INFINITY:Number.NEGATIVE_INFINITY:".nan"===t?NaN:n*parseFloat(t,10)},predicate:function(e){return"[object Number]"===Object.prototype.toString.call(e)&&(e%1!=0||r.isNegativeZero(e))},represent:function(e,t){var n;if(isNaN(e))switch(t){case"lowercase":return".nan";case"uppercase":return".NAN";case"camelcase":return".NaN"}else if(Number.POSITIVE_INFINITY===e)switch(t){case"lowercase":return".inf";case"uppercase":return".INF";case"camelcase":return".Inf"}else if(Number.NEGATIVE_INFINITY===e)switch(t){case"lowercase":return"-.inf";case"uppercase":return"-.INF";case"camelcase":return"-.Inf"}else if(r.isNegativeZero(e))return"-0.0";return n=e.toString(10),a.test(n)?n.replace("e",".e"):n},defaultStyle:"lowercase"})},1518:function(e,t,n){"use strict";var r=n(8347),i=n(1364);function o(e){return 48<=e&&e<=55}function a(e){return 48<=e&&e<=57}e.exports=new i("tag:yaml.org,2002:int",{kind:"scalar",resolve:function(e){if(null===e)return!1;var t,n,r=e.length,i=0,s=!1;if(!r)return!1;if("-"!==(t=e[i])&&"+"!==t||(t=e[++i]),"0"===t){if(i+1===r)return!0;if("b"===(t=e[++i])){for(i++;i=0?"0b"+e.toString(2):"-0b"+e.toString(2).slice(1)},octal:function(e){return e>=0?"0o"+e.toString(8):"-0o"+e.toString(8).slice(1)},decimal:function(e){return e.toString(10)},hexadecimal:function(e){return e>=0?"0x"+e.toString(16).toUpperCase():"-0x"+e.toString(16).toUpperCase().slice(1)}},defaultStyle:"decimal",styleAliases:{binary:[2,"bin"],octal:[8,"oct"],decimal:[10,"dec"],hexadecimal:[16,"hex"]}})},945:function(e,t,n){"use strict";var r=n(1364);e.exports=new r("tag:yaml.org,2002:map",{kind:"mapping",construct:function(e){return null!==e?e:{}}})},7452:function(e,t,n){"use strict";var r=n(1364);e.exports=new r("tag:yaml.org,2002:merge",{kind:"scalar",resolve:function(e){return"<<"===e||null===e}})},151:function(e,t,n){"use strict";var r=n(1364);e.exports=new r("tag:yaml.org,2002:null",{kind:"scalar",resolve:function(e){if(null===e)return!0;var t=e.length;return 1===t&&"~"===e||4===t&&("null"===e||"Null"===e||"NULL"===e)},construct:function(){return null},predicate:function(e){return null===e},represent:{canonical:function(){return"~"},lowercase:function(){return"null"},uppercase:function(){return"NULL"},camelcase:function(){return"Null"},empty:function(){return""}},defaultStyle:"lowercase"})},1605:function(e,t,n){"use strict";var r=n(1364),i=Object.prototype.hasOwnProperty,o=Object.prototype.toString;e.exports=new r("tag:yaml.org,2002:omap",{kind:"sequence",resolve:function(e){if(null===e)return!0;var t,n,r,a,s,l=[],c=e;for(t=0,n=c.length;tc))return!1;var p=s.get(e);if(p&&s.get(t))return p==t;var d=-1,f=!0,h=n&o?new Ae:void 0;for(s.set(e,t),s.set(t,e);++d-1},Ee.prototype.set=function(e,t){var n=this.__data__,r=Ce(n,e);return r<0?(++this.size,n.push([e,t])):n[r][1]=t,this},_e.prototype.clear=function(){this.size=0,this.__data__={hash:new Oe,map:new(de||Ee),string:new Oe}},_e.prototype.delete=function(e){var t=$e(this,e).delete(e);return this.size-=t?1:0,t},_e.prototype.get=function(e){return $e(this,e).get(e)},_e.prototype.has=function(e){return $e(this,e).has(e)},_e.prototype.set=function(e,t){var n=$e(this,e),r=n.size;return n.set(e,t),this.size+=n.size==r?0:1,this},Ae.prototype.add=Ae.prototype.push=function(e){return this.__data__.set(e,r),this},Ae.prototype.has=function(e){return this.__data__.has(e)},je.prototype.clear=function(){this.__data__=new Ee,this.size=0},je.prototype.delete=function(e){var t=this.__data__,n=t.delete(e);return this.size=t.size,n},je.prototype.get=function(e){return this.__data__.get(e)},je.prototype.has=function(e){return this.__data__.has(e)},je.prototype.set=function(e,t){var n=this.__data__;if(n instanceof Ee){var r=n.__data__;if(!de||r.length<199)return r.push([e,t]),this.size=++n.size,this;n=this.__data__=new _e(r)}return n.set(e,t),this.size=n.size,this};var De=le?function(e){return null==e?[]:(e=Object(e),function(t,n){for(var r=-1,i=null==t?0:t.length,o=0,a=[];++r-1&&e%1==0&&e-1&&e%1==0&&e<=a}function Ye(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}function Ge(e){return null!=e&&"object"==typeof e}var Qe=F?function(e){return function(t){return e(t)}}(F):function(e){return Ge(e)&&He(e.length)&&!!P[Pe(e)]};function Xe(e){return null!=(t=e)&&He(t.length)&&!We(t)?function(e,t){var n=qe(e),r=!n&&Be(e),i=!n&&!r&&Ve(e),o=!n&&!r&&!i&&Qe(e),a=n||r||i||o,s=a?function(e,t){for(var n=-1,r=Array(e);++n1&&void 0!==arguments[1])||arguments[1],i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[],o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:5e3;t(this,e),this.ctx=n,this.iframes=r,this.exclude=i,this.iframesTimeout=o}return n(e,[{key:"getContexts",value:function(){var e=[];return(void 0!==this.ctx&&this.ctx?NodeList.prototype.isPrototypeOf(this.ctx)?Array.prototype.slice.call(this.ctx):Array.isArray(this.ctx)?this.ctx:"string"==typeof this.ctx?Array.prototype.slice.call(document.querySelectorAll(this.ctx)):[this.ctx]:[]).forEach((function(t){var n=e.filter((function(e){return e.contains(t)})).length>0;-1!==e.indexOf(t)||n||e.push(t)})),e}},{key:"getIframeContents",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){},r=void 0;try{var i=e.contentWindow;if(r=i.document,!i||!r)throw new Error("iframe inaccessible")}catch(e){n()}r&&t(r)}},{key:"isIframeBlank",value:function(e){var t="about:blank",n=e.getAttribute("src").trim();return e.contentWindow.location.href===t&&n!==t&&n}},{key:"observeIframeLoad",value:function(e,t,n){var r=this,i=!1,o=null,a=function a(){if(!i){i=!0,clearTimeout(o);try{r.isIframeBlank(e)||(e.removeEventListener("load",a),r.getIframeContents(e,t,n))}catch(e){n()}}};e.addEventListener("load",a),o=setTimeout(a,this.iframesTimeout)}},{key:"onIframeReady",value:function(e,t,n){try{"complete"===e.contentWindow.document.readyState?this.isIframeBlank(e)?this.observeIframeLoad(e,t,n):this.getIframeContents(e,t,n):this.observeIframeLoad(e,t,n)}catch(e){n()}}},{key:"waitForIframes",value:function(e,t){var n=this,r=0;this.forEachIframe(e,(function(){return!0}),(function(e){r++,n.waitForIframes(e.querySelector("html"),(function(){--r||t()}))}),(function(e){e||t()}))}},{key:"forEachIframe",value:function(t,n,r){var i=this,o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:function(){},a=t.querySelectorAll("iframe"),s=a.length,l=0;a=Array.prototype.slice.call(a);var c=function(){--s<=0&&o(l)};s||c(),a.forEach((function(t){e.matches(t,i.exclude)?c():i.onIframeReady(t,(function(e){n(t)&&(l++,r(e)),c()}),c)}))}},{key:"createIterator",value:function(e,t,n){return document.createNodeIterator(e,t,n,!1)}},{key:"createInstanceOnIframe",value:function(t){return new e(t.querySelector("html"),this.iframes)}},{key:"compareNodeIframe",value:function(e,t,n){if(e.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_PRECEDING){if(null===t)return!0;if(t.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_FOLLOWING)return!0}return!1}},{key:"getIteratorNode",value:function(e){var t=e.previousNode();return{prevNode:t,node:(null===t||e.nextNode())&&e.nextNode()}}},{key:"checkIframeFilter",value:function(e,t,n,r){var i=!1,o=!1;return r.forEach((function(e,t){e.val===n&&(i=t,o=e.handled)})),this.compareNodeIframe(e,t,n)?(!1!==i||o?!1===i||o||(r[i].handled=!0):r.push({val:n,handled:!0}),!0):(!1===i&&r.push({val:n,handled:!1}),!1)}},{key:"handleOpenIframes",value:function(e,t,n,r){var i=this;e.forEach((function(e){e.handled||i.getIframeContents(e.val,(function(e){i.createInstanceOnIframe(e).forEachNode(t,n,r)}))}))}},{key:"iterateThroughNodes",value:function(e,t,n,r,i){for(var o=this,a=this.createIterator(t,e,r),s=[],l=[],c=void 0,u=void 0;p=void 0,p=o.getIteratorNode(a),u=p.prevNode,c=p.node;)this.iframes&&this.forEachIframe(t,(function(e){return o.checkIframeFilter(c,u,e,s)}),(function(t){o.createInstanceOnIframe(t).forEachNode(e,(function(e){return l.push(e)}),r)})),l.push(c);var p;l.forEach((function(e){n(e)})),this.iframes&&this.handleOpenIframes(s,e,n,r),i()}},{key:"forEachNode",value:function(e,t,n){var r=this,i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:function(){},o=this.getContexts(),a=o.length;a||i(),o.forEach((function(o){var s=function(){r.iterateThroughNodes(e,o,t,n,(function(){--a<=0&&i()}))};r.iframes?r.waitForIframes(o,s):s()}))}}],[{key:"matches",value:function(e,t){var n="string"==typeof t?[t]:t,r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.oMatchesSelector||e.webkitMatchesSelector;if(r){var i=!1;return n.every((function(t){return!r.call(e,t)||(i=!0,!1)})),i}return!1}}]),e}(),o=function(){function o(e){t(this,o),this.ctx=e,this.ie=!1;var n=window.navigator.userAgent;(n.indexOf("MSIE")>-1||n.indexOf("Trident")>-1)&&(this.ie=!0)}return n(o,[{key:"log",value:function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"debug",r=this.opt.log;this.opt.debug&&"object"===(void 0===r?"undefined":e(r))&&"function"==typeof r[n]&&r[n]("mark.js: "+t)}},{key:"escapeStr",value:function(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}},{key:"createRegExp",value:function(e){return"disabled"!==this.opt.wildcards&&(e=this.setupWildcardsRegExp(e)),e=this.escapeStr(e),Object.keys(this.opt.synonyms).length&&(e=this.createSynonymsRegExp(e)),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),this.opt.diacritics&&(e=this.createDiacriticsRegExp(e)),e=this.createMergedBlanksRegExp(e),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.createJoinersRegExp(e)),"disabled"!==this.opt.wildcards&&(e=this.createWildcardsRegExp(e)),this.createAccuracyRegExp(e)}},{key:"createSynonymsRegExp",value:function(e){var t=this.opt.synonyms,n=this.opt.caseSensitive?"":"i",r=this.opt.ignoreJoiners||this.opt.ignorePunctuation.length?"\0":"";for(var i in t)if(t.hasOwnProperty(i)){var o=t[i],a="disabled"!==this.opt.wildcards?this.setupWildcardsRegExp(i):this.escapeStr(i),s="disabled"!==this.opt.wildcards?this.setupWildcardsRegExp(o):this.escapeStr(o);""!==a&&""!==s&&(e=e.replace(new RegExp("("+this.escapeStr(a)+"|"+this.escapeStr(s)+")","gm"+n),r+"("+this.processSynomyms(a)+"|"+this.processSynomyms(s)+")"+r))}return e}},{key:"processSynomyms",value:function(e){return(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),e}},{key:"setupWildcardsRegExp",value:function(e){return(e=e.replace(/(?:\\)*\?/g,(function(e){return"\\"===e.charAt(0)?"?":""}))).replace(/(?:\\)*\*/g,(function(e){return"\\"===e.charAt(0)?"*":""}))}},{key:"createWildcardsRegExp",value:function(e){var t="withSpaces"===this.opt.wildcards;return e.replace(/\u0001/g,t?"[\\S\\s]?":"\\S?").replace(/\u0002/g,t?"[\\S\\s]*?":"\\S*")}},{key:"setupIgnoreJoinersRegExp",value:function(e){return e.replace(/[^(|)\\]/g,(function(e,t,n){var r=n.charAt(t+1);return/[(|)\\]/.test(r)||""===r?e:e+"\0"}))}},{key:"createJoinersRegExp",value:function(e){var t=[],n=this.opt.ignorePunctuation;return Array.isArray(n)&&n.length&&t.push(this.escapeStr(n.join(""))),this.opt.ignoreJoiners&&t.push("\\u00ad\\u200b\\u200c\\u200d"),t.length?e.split(/\u0000+/).join("["+t.join("")+"]*"):e}},{key:"createDiacriticsRegExp",value:function(e){var t=this.opt.caseSensitive?"":"i",n=this.opt.caseSensitive?["aàáảãạăằắẳẵặâầấẩẫậäåāą","AÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ","cçćč","CÇĆČ","dđď","DĐĎ","eèéẻẽẹêềếểễệëěēę","EÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ","iìíỉĩịîïī","IÌÍỈĨỊÎÏĪ","lł","LŁ","nñňń","NÑŇŃ","oòóỏõọôồốổỗộơởỡớờợöøō","OÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ","rř","RŘ","sšśșş","SŠŚȘŞ","tťțţ","TŤȚŢ","uùúủũụưừứửữựûüůū","UÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ","yýỳỷỹỵÿ","YÝỲỶỸỴŸ","zžżź","ZŽŻŹ"]:["aàáảãạăằắẳẵặâầấẩẫậäåāąAÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ","cçćčCÇĆČ","dđďDĐĎ","eèéẻẽẹêềếểễệëěēęEÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ","iìíỉĩịîïīIÌÍỈĨỊÎÏĪ","lłLŁ","nñňńNÑŇŃ","oòóỏõọôồốổỗộơởỡớờợöøōOÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ","rřRŘ","sšśșşSŠŚȘŞ","tťțţTŤȚŢ","uùúủũụưừứửữựûüůūUÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ","yýỳỷỹỵÿYÝỲỶỸỴŸ","zžżźZŽŻŹ"],r=[];return e.split("").forEach((function(i){n.every((function(n){if(-1!==n.indexOf(i)){if(r.indexOf(n)>-1)return!1;e=e.replace(new RegExp("["+n+"]","gm"+t),"["+n+"]"),r.push(n)}return!0}))})),e}},{key:"createMergedBlanksRegExp",value:function(e){return e.replace(/[\s]+/gim,"[\\s]+")}},{key:"createAccuracyRegExp",value:function(e){var t=this,n=this.opt.accuracy,r="string"==typeof n?n:n.value,i="string"==typeof n?[]:n.limiters,o="";switch(i.forEach((function(e){o+="|"+t.escapeStr(e)})),r){case"partially":default:return"()("+e+")";case"complementary":return"()([^"+(o="\\s"+(o||this.escapeStr("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~¡¿")))+"]*"+e+"[^"+o+"]*)";case"exactly":return"(^|\\s"+o+")("+e+")(?=$|\\s"+o+")"}}},{key:"getSeparatedKeywords",value:function(e){var t=this,n=[];return e.forEach((function(e){t.opt.separateWordSearch?e.split(" ").forEach((function(e){e.trim()&&-1===n.indexOf(e)&&n.push(e)})):e.trim()&&-1===n.indexOf(e)&&n.push(e)})),{keywords:n.sort((function(e,t){return t.length-e.length})),length:n.length}}},{key:"isNumeric",value:function(e){return Number(parseFloat(e))==e}},{key:"checkRanges",value:function(e){var t=this;if(!Array.isArray(e)||"[object Object]"!==Object.prototype.toString.call(e[0]))return this.log("markRanges() will only accept an array of objects"),this.opt.noMatch(e),[];var n=[],r=0;return e.sort((function(e,t){return e.start-t.start})).forEach((function(e){var i=t.callNoMatchOnInvalidRanges(e,r),o=i.start,a=i.end;i.valid&&(e.start=o,e.length=a-o,n.push(e),r=a)})),n}},{key:"callNoMatchOnInvalidRanges",value:function(e,t){var n=void 0,r=void 0,i=!1;return e&&void 0!==e.start?(r=(n=parseInt(e.start,10))+parseInt(e.length,10),this.isNumeric(e.start)&&this.isNumeric(e.length)&&r-t>0&&r-n>0?i=!0:(this.log("Ignoring invalid or overlapping range: "+JSON.stringify(e)),this.opt.noMatch(e))):(this.log("Ignoring invalid range: "+JSON.stringify(e)),this.opt.noMatch(e)),{start:n,end:r,valid:i}}},{key:"checkWhitespaceRanges",value:function(e,t,n){var r=void 0,i=!0,o=n.length,a=t-o,s=parseInt(e.start,10)-a;return(r=(s=s>o?o:s)+parseInt(e.length,10))>o&&(r=o,this.log("End range automatically set to the max value of "+o)),s<0||r-s<0||s>o||r>o?(i=!1,this.log("Invalid range: "+JSON.stringify(e)),this.opt.noMatch(e)):""===n.substring(s,r).replace(/\s+/g,"")&&(i=!1,this.log("Skipping whitespace only range: "+JSON.stringify(e)),this.opt.noMatch(e)),{start:s,end:r,valid:i}}},{key:"getTextNodes",value:function(e){var t=this,n="",r=[];this.iterator.forEachNode(NodeFilter.SHOW_TEXT,(function(e){r.push({start:n.length,end:(n+=e.textContent).length,node:e})}),(function(e){return t.matchesExclude(e.parentNode)?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT}),(function(){e({value:n,nodes:r})}))}},{key:"matchesExclude",value:function(e){return i.matches(e,this.opt.exclude.concat(["script","style","title","head","html"]))}},{key:"wrapRangeInTextNode",value:function(e,t,n){var r=this.opt.element?this.opt.element:"mark",i=e.splitText(t),o=i.splitText(n-t),a=document.createElement(r);return a.setAttribute("data-markjs","true"),this.opt.className&&a.setAttribute("class",this.opt.className),a.textContent=i.textContent,i.parentNode.replaceChild(a,i),o}},{key:"wrapRangeInMappedTextNode",value:function(e,t,n,r,i){var o=this;e.nodes.every((function(a,s){var l=e.nodes[s+1];if(void 0===l||l.start>t){if(!r(a.node))return!1;var c=t-a.start,u=(n>a.end?a.end:n)-a.start,p=e.value.substr(0,a.start),d=e.value.substr(u+a.start);if(a.node=o.wrapRangeInTextNode(a.node,c,u),e.value=p+d,e.nodes.forEach((function(t,n){n>=s&&(e.nodes[n].start>0&&n!==s&&(e.nodes[n].start-=u),e.nodes[n].end-=u)})),n-=u,i(a.node.previousSibling,a.start),!(n>a.end))return!1;t=a.end}return!0}))}},{key:"wrapMatches",value:function(e,t,n,r,i){var o=this,a=0===t?0:t+1;this.getTextNodes((function(t){t.nodes.forEach((function(t){t=t.node;for(var i=void 0;null!==(i=e.exec(t.textContent))&&""!==i[a];)if(n(i[a],t)){var s=i.index;if(0!==a)for(var l=1;l1&&console.warn("Replacing with",t),m++}}else{let i=u(l(t,e[n]));if(a.verbose>1&&console.warn((!1===i?f.colour.red:f.colour.green)+"Fragment resolution",e[n],f.colour.normal),!1===i){if(r.parent[r.pkey]={},a.fatal){let t=new Error("Fragment $ref resolution failed "+e[n]);if(!a.promise)throw t;a.promise.reject(t)}}else m++,r.parent[r.pkey]=i,h[e[n]]=r.path.replace("/%24ref","")}else if(p.protocol){let t=o.resolve(i,e[n]).toString();a.verbose>1&&console.warn(f.colour.yellow+"Rewriting external url ref",e[n],"as",t,f.colour.normal),e["x-miro"]=e[n],a.externalRefs[e[n]]&&(a.externalRefs[t]||(a.externalRefs[t]=a.externalRefs[e[n]]),a.externalRefs[t].failed=a.externalRefs[e[n]].failed),e[n]=t}else if(!e["x-miro"]){let t=o.resolve(i,e[n]).toString(),r=!1;a.externalRefs[e[n]]&&(r=a.externalRefs[e[n]].failed),r||(a.verbose>1&&console.warn(f.colour.yellow+"Rewriting external ref",e[n],"as",t,f.colour.normal),e["x-miro"]=e[n],e[n]=t)}}));return c(e,{},(function(e,t,n){d(e,t)&&void 0!==e.$fixed&&delete e.$fixed})),a.verbose>1&&console.warn("Finished fragment resolution"),e}function m(e,t){if(!t.filters||!t.filters.length)return e;for(let n of t.filters)e=n(e,t);return e}function g(e,t,n,a){var c=o.parse(n.source),p=n.source.split("\\").join("/").split("/");p.pop()||p.pop();let d="",f=t.split("#");f.length>1&&(d="#"+f[1],t=f[0]),p=p.join("/");let g=(y=o.parse(t).protocol,b=c.protocol,y&&y.length>2?y:b&&b.length>2?b:"file:");var y,b;let v;if(v="file:"===g?i.resolve(p?p+"/":"",t):o.resolve(p?p+"/":"",t),n.cache[v]){n.verbose&&console.warn("CACHED",v,d);let e=u(n.cache[v]),r=n.externalRef=e;if(d&&(r=l(r,d),!1===r&&(r={},n.fatal))){let e=new Error("Cached $ref resolution failed "+v+d);if(!n.promise)throw e;n.promise.reject(e)}return r=h(r,e,t,d,v,n),r=m(r,n),a(u(r),v,n),Promise.resolve(r)}if(n.verbose&&console.warn("GET",v,d),n.handlers&&n.handlers[g])return n.handlers[g](p,t,d,n).then((function(e){return n.externalRef=e,e=m(e,n),n.cache[v]=e,a(e,v,n),e})).catch((function(e){throw n.verbose&&console.warn(e),e}));if(g&&g.startsWith("http")){const e=Object.assign({},n.fetchOptions,{agent:n.agent});return n.fetch(v,e).then((function(e){if(200!==e.status){if(n.ignoreIOErrors)return n.verbose&&console.warn("FAILED",t),n.externalRefs[t].failed=!0,'{"$ref":"'+t+'"}';throw new Error(`Received status code ${e.status}: ${v}`)}return e.text()})).then((function(e){try{let r=s.parse(e,{schema:"core",prettyErrors:!0});if(e=n.externalRef=r,n.cache[v]=u(e),d&&!1===(e=l(e,d))&&(e={},n.fatal)){let e=new Error("Remote $ref resolution failed "+v+d);if(!n.promise)throw e;n.promise.reject(e)}e=m(e=h(e,r,t,d,v,n),n)}catch(e){if(n.verbose&&console.warn(e),!n.promise||!n.fatal)throw e;n.promise.reject(e)}return a(e,v,n),e})).catch((function(e){if(n.verbose&&console.warn(e),n.cache[v]={},!n.promise||!n.fatal)throw e;n.promise.reject(e)}))}{const e='{"$ref":"'+t+'"}';return function(e,t,n,i,o){return new Promise((function(a,s){r.readFile(e,t,(function(e,t){e?n.ignoreIOErrors&&o?(n.verbose&&console.warn("FAILED",i),n.externalRefs[i].failed=!0,a(o)):s(e):a(t)}))}))}(v,n.encoding||"utf8",n,t,e).then((function(e){try{let r=s.parse(e,{schema:"core",prettyErrors:!0});if(e=n.externalRef=r,n.cache[v]=u(e),d&&!1===(e=l(e,d))&&(e={},n.fatal)){let e=new Error("File $ref resolution failed "+v+d);if(!n.promise)throw e;n.promise.reject(e)}e=m(e=h(e,r,t,d,v,n),n)}catch(e){if(n.verbose&&console.warn(e),!n.promise||!n.fatal)throw e;n.promise.reject(e)}return a(e,v,n),e})).catch((function(e){if(n.verbose&&console.warn(e),!n.promise||!n.fatal)throw e;n.promise.reject(e)}))}}function y(e){return new Promise((function(t,n){(function(e){return new Promise((function(t,n){function r(t,n,r){if(t[n]&&d(t[n],"$ref")){let o=t[n].$ref;if(!o.startsWith("#")){let a="";if(!i[o]){let t=Object.keys(i).find((function(e,t,n){return o.startsWith(e+"/")}));t&&(e.verbose&&console.warn("Found potential subschema at",t),a="/"+(o.split("#")[1]||"").replace(t.split("#")[1]||""),a=a.split("/undefined").join(""),o=t)}if(i[o]||(i[o]={resolved:!1,paths:[],extras:{},description:t[n].description}),i[o].resolved)if(i[o].failed);else if(e.rewriteRefs){let r=i[o].resolvedAt;e.verbose>1&&console.warn("Rewriting ref",o,r),t[n]["x-miro"]=o,t[n].$ref=r+a}else t[n]=u(i[o].data);else i[o].paths.push(r.path),i[o].extras[r.path]=a}}}let i=e.externalRefs;if(e.resolver.depth>0&&e.source===e.resolver.base)return t(i);c(e.openapi.definitions,{identityDetection:!0,path:"#/definitions"},r),c(e.openapi.components,{identityDetection:!0,path:"#/components"},r),c(e.openapi,{identityDetection:!0},r),t(i)}))})(e).then((function(t){for(let n in t)if(!t[n].resolved){let r=e.resolver.depth;r>0&&r++,e.resolver.actions[r].push((function(){return g(e.openapi,n,e,(function(e,r,i){if(!t[n].resolved){let o={};o.context=t[n],o.$ref=n,o.original=u(e),o.updated=e,o.source=r,i.externals.push(o),t[n].resolved=!0}let o=Object.assign({},i,{source:"",resolver:{actions:i.resolver.actions,depth:i.resolver.actions.length-1,base:i.resolver.base}});i.patch&&t[n].description&&!e.description&&"object"==typeof e&&(e.description=t[n].description),t[n].data=e;let a=(s=t[n].paths,[...new Set(s)]);var s;a=a.sort((function(e,t){const n=e.startsWith("#/components/")||e.startsWith("#/definitions/"),r=t.startsWith("#/components/")||t.startsWith("#/definitions/");return n&&!r?-1:r&&!n?1:0}));for(let r of a)if(t[n].resolvedAt&&r!==t[n].resolvedAt&&r.indexOf("x-ms-examples/")<0)i.verbose>1&&console.warn("Creating pointer to data at",r),l(i.openapi,r,{$ref:t[n].resolvedAt+t[n].extras[r],"x-miro":n+t[n].extras[r]});else{t[n].resolvedAt?i.verbose>1&&console.warn("Avoiding circular reference"):(t[n].resolvedAt=r,i.verbose>1&&console.warn("Creating initial clone of data at",r));let o=u(e);l(i.openapi,r,o)}0===i.resolver.actions[o.resolver.depth].length&&i.resolver.actions[o.resolver.depth].push((function(){return y(o)}))}))}))}})).catch((function(t){e.verbose&&console.warn(t),n(t)}));let r={options:e};r.actions=e.resolver.actions[e.resolver.depth],t(r)}))}function b(e,t,n){e.resolver.actions.push([]),y(e).then((function(r){var i;(i=r.actions,i.reduce(((e,t)=>e.then((e=>t().then(Array.prototype.concat.bind(e))))),Promise.resolve([]))).then((function(){if(e.resolver.depth>=e.resolver.actions.length)return console.warn("Ran off the end of resolver actions"),t(!0);e.resolver.depth++,e.resolver.actions[e.resolver.depth].length?setTimeout((function(){b(r.options,t,n)}),0):(e.verbose>1&&console.warn(f.colour.yellow+"Finished external resolution!",f.colour.normal),e.resolveInternal&&(e.verbose>1&&console.warn(f.colour.yellow+"Starting internal resolution!",f.colour.normal),e.openapi=p(e.openapi,e.original,{verbose:e.verbose-1}),e.verbose>1&&console.warn(f.colour.yellow+"Finished internal resolution!",f.colour.normal)),c(e.openapi,{},(function(t,n,r){d(t,n)&&(e.preserveMiro||delete t["x-miro"])})),t(e))})).catch((function(t){e.verbose&&console.warn(t),n(t)}))})).catch((function(t){e.verbose&&console.warn(t),n(t)}))}function v(e){if(e.cache||(e.cache={}),e.fetch||(e.fetch=a),e.source){let t=o.parse(e.source);(!t.protocol||t.protocol.length<=2)&&(e.source=i.resolve(e.source))}e.externals=[],e.externalRefs={},e.rewriteRefs=!0,e.resolver={},e.resolver.depth=0,e.resolver.base=e.source,e.resolver.actions=[[]]}e.exports={optionalResolve:function(e){return v(e),new Promise((function(t,n){e.resolve?b(e,t,n):t(e)}))},resolve:function(e,t,n){return n||(n={}),n.openapi=e,n.source=t,n.resolve=!0,v(n),new Promise((function(e,t){b(n,e,t)}))}}},1804:function(e){"use strict";function t(){return{depth:0,seen:new WeakMap,top:!0,combine:!1,allowRefSiblings:!1}}e.exports={getDefaultState:t,walkSchema:function e(n,r,i,o){if(void 0===i.depth&&(i=t()),null==n)return n;if(void 0!==n.$ref){let e={$ref:n.$ref};return i.allowRefSiblings&&n.description&&(e.description=n.description),o(e,r,i),e}if(i.combine&&(n.allOf&&Array.isArray(n.allOf)&&1===n.allOf.length&&delete(n=Object.assign({},n.allOf[0],n)).allOf,n.anyOf&&Array.isArray(n.anyOf)&&1===n.anyOf.length&&delete(n=Object.assign({},n.anyOf[0],n)).anyOf,n.oneOf&&Array.isArray(n.oneOf)&&1===n.oneOf.length&&delete(n=Object.assign({},n.oneOf[0],n)).oneOf),o(n,r,i),i.seen.has(n))return n;if("object"==typeof n&&null!==n&&i.seen.set(n,!0),i.top=!1,i.depth++,void 0!==n.items&&(i.property="items",e(n.items,n,i,o)),n.additionalItems&&"object"==typeof n.additionalItems&&(i.property="additionalItems",e(n.additionalItems,n,i,o)),n.additionalProperties&&"object"==typeof n.additionalProperties&&(i.property="additionalProperties",e(n.additionalProperties,n,i,o)),n.properties)for(let t in n.properties){let r=n.properties[t];i.property="properties/"+t,e(r,n,i,o)}if(n.patternProperties)for(let t in n.patternProperties){let r=n.patternProperties[t];i.property="patternProperties/"+t,e(r,n,i,o)}if(n.allOf)for(let t in n.allOf){let r=n.allOf[t];i.property="allOf/"+t,e(r,n,i,o)}if(n.anyOf)for(let t in n.anyOf){let r=n.anyOf[t];i.property="anyOf/"+t,e(r,n,i,o)}if(n.oneOf)for(let t in n.oneOf){let r=n.oneOf[t];i.property="oneOf/"+t,e(r,n,i,o)}return n.not&&(i.property="not",e(n.not,n,i,o)),i.depth--,n}}},6470:function(e){"use strict";function t(e){if("string"!=typeof e)throw new TypeError("Path must be a string. Received "+JSON.stringify(e))}function n(e,t){for(var n,r="",i=0,o=-1,a=0,s=0;s<=e.length;++s){if(s2){var l=r.lastIndexOf("/");if(l!==r.length-1){-1===l?(r="",i=0):i=(r=r.slice(0,l)).length-1-r.lastIndexOf("/"),o=s,a=0;continue}}else if(2===r.length||1===r.length){r="",i=0,o=s,a=0;continue}t&&(r.length>0?r+="/..":r="..",i=2)}else r.length>0?r+="/"+e.slice(o+1,s):r=e.slice(o+1,s),i=s-o-1;o=s,a=0}else 46===n&&-1!==a?++a:a=-1}return r}var r={resolve:function(){for(var e,r="",i=!1,o=arguments.length-1;o>=-1&&!i;o--){var a;o>=0?a=arguments[o]:(void 0===e&&(e=process.cwd()),a=e),t(a),0!==a.length&&(r=a+"/"+r,i=47===a.charCodeAt(0))}return r=n(r,!i),i?r.length>0?"/"+r:"/":r.length>0?r:"."},normalize:function(e){if(t(e),0===e.length)return".";var r=47===e.charCodeAt(0),i=47===e.charCodeAt(e.length-1);return 0!==(e=n(e,!r)).length||r||(e="."),e.length>0&&i&&(e+="/"),r?"/"+e:e},isAbsolute:function(e){return t(e),e.length>0&&47===e.charCodeAt(0)},join:function(){if(0===arguments.length)return".";for(var e,n=0;n0&&(void 0===e?e=i:e+="/"+i)}return void 0===e?".":r.normalize(e)},relative:function(e,n){if(t(e),t(n),e===n)return"";if((e=r.resolve(e))===(n=r.resolve(n)))return"";for(var i=1;ic){if(47===n.charCodeAt(s+p))return n.slice(s+p+1);if(0===p)return n.slice(s+p)}else a>c&&(47===e.charCodeAt(i+p)?u=p:0===p&&(u=0));break}var d=e.charCodeAt(i+p);if(d!==n.charCodeAt(s+p))break;47===d&&(u=p)}var f="";for(p=i+u+1;p<=o;++p)p!==o&&47!==e.charCodeAt(p)||(0===f.length?f+="..":f+="/..");return f.length>0?f+n.slice(s+u):(s+=u,47===n.charCodeAt(s)&&++s,n.slice(s))},_makeLong:function(e){return e},dirname:function(e){if(t(e),0===e.length)return".";for(var n=e.charCodeAt(0),r=47===n,i=-1,o=!0,a=e.length-1;a>=1;--a)if(47===(n=e.charCodeAt(a))){if(!o){i=a;break}}else o=!1;return-1===i?r?"/":".":r&&1===i?"//":e.slice(0,i)},basename:function(e,n){if(void 0!==n&&"string"!=typeof n)throw new TypeError('"ext" argument must be a string');t(e);var r,i=0,o=-1,a=!0;if(void 0!==n&&n.length>0&&n.length<=e.length){if(n.length===e.length&&n===e)return"";var s=n.length-1,l=-1;for(r=e.length-1;r>=0;--r){var c=e.charCodeAt(r);if(47===c){if(!a){i=r+1;break}}else-1===l&&(a=!1,l=r+1),s>=0&&(c===n.charCodeAt(s)?-1==--s&&(o=r):(s=-1,o=l))}return i===o?o=l:-1===o&&(o=e.length),e.slice(i,o)}for(r=e.length-1;r>=0;--r)if(47===e.charCodeAt(r)){if(!a){i=r+1;break}}else-1===o&&(a=!1,o=r+1);return-1===o?"":e.slice(i,o)},extname:function(e){t(e);for(var n=-1,r=0,i=-1,o=!0,a=0,s=e.length-1;s>=0;--s){var l=e.charCodeAt(s);if(47!==l)-1===i&&(o=!1,i=s+1),46===l?-1===n?n=s:1!==a&&(a=1):-1!==n&&(a=-1);else if(!o){r=s+1;break}}return-1===n||-1===i||0===a||1===a&&n===i-1&&n===r+1?"":e.slice(n,i)},format:function(e){if(null===e||"object"!=typeof e)throw new TypeError('The "pathObject" argument must be of type Object. Received type '+typeof e);return function(e,t){var n=t.dir||t.root,r=t.base||(t.name||"")+(t.ext||"");return n?n===t.root?n+r:n+"/"+r:r}(0,e)},parse:function(e){t(e);var n={root:"",dir:"",base:"",ext:"",name:""};if(0===e.length)return n;var r,i=e.charCodeAt(0),o=47===i;o?(n.root="/",r=1):r=0;for(var a=-1,s=0,l=-1,c=!0,u=e.length-1,p=0;u>=r;--u)if(47!==(i=e.charCodeAt(u)))-1===l&&(c=!1,l=u+1),46===i?-1===a?a=u:1!==p&&(p=1):-1!==a&&(p=-1);else if(!c){s=u+1;break}return-1===a||-1===l||0===p||1===p&&a===l-1&&a===s+1?-1!==l&&(n.base=n.name=0===s&&o?e.slice(1,l):e.slice(s,l)):(0===s&&o?(n.name=e.slice(1,a),n.base=e.slice(1,l)):(n.name=e.slice(s,a),n.base=e.slice(s,l)),n.ext=e.slice(a,l)),s>0?n.dir=e.slice(0,s-1):o&&(n.dir="/"),n},sep:"/",delimiter:":",win32:null,posix:null};r.posix=r,e.exports=r},3450:function(e){e.exports=function(){var e=[],t=[],n={},r={},i={};function o(e){return"string"==typeof e?new RegExp("^"+e+"$","i"):e}function a(e,t){return e===t?t:e===e.toLowerCase()?t.toLowerCase():e===e.toUpperCase()?t.toUpperCase():e[0]===e[0].toUpperCase()?t.charAt(0).toUpperCase()+t.substr(1).toLowerCase():t.toLowerCase()}function s(e,t){return e.replace(t[0],(function(n,r){var i,o,s=(i=t[1],o=arguments,i.replace(/\$(\d{1,2})/g,(function(e,t){return o[t]||""})));return a(""===n?e[r-1]:n,s)}))}function l(e,t,r){if(!e.length||n.hasOwnProperty(e))return t;for(var i=r.length;i--;){var o=r[i];if(o[0].test(t))return s(t,o)}return t}function c(e,t,n){return function(r){var i=r.toLowerCase();return t.hasOwnProperty(i)?a(r,i):e.hasOwnProperty(i)?a(r,e[i]):l(i,r,n)}}function u(e,t,n,r){return function(r){var i=r.toLowerCase();return!!t.hasOwnProperty(i)||!e.hasOwnProperty(i)&&l(i,i,n)===i}}function p(e,t,n){return(n?t+" ":"")+(1===t?p.singular(e):p.plural(e))}return p.plural=c(i,r,e),p.isPlural=u(i,r,e),p.singular=c(r,i,t),p.isSingular=u(r,i,t),p.addPluralRule=function(t,n){e.push([o(t),n])},p.addSingularRule=function(e,n){t.push([o(e),n])},p.addUncountableRule=function(e){"string"!=typeof e?(p.addPluralRule(e,"$0"),p.addSingularRule(e,"$0")):n[e.toLowerCase()]=!0},p.addIrregularRule=function(e,t){t=t.toLowerCase(),e=e.toLowerCase(),i[e]=t,r[t]=e},[["I","we"],["me","us"],["he","they"],["she","they"],["them","them"],["myself","ourselves"],["yourself","yourselves"],["itself","themselves"],["herself","themselves"],["himself","themselves"],["themself","themselves"],["is","are"],["was","were"],["has","have"],["this","these"],["that","those"],["echo","echoes"],["dingo","dingoes"],["volcano","volcanoes"],["tornado","tornadoes"],["torpedo","torpedoes"],["genus","genera"],["viscus","viscera"],["stigma","stigmata"],["stoma","stomata"],["dogma","dogmata"],["lemma","lemmata"],["schema","schemata"],["anathema","anathemata"],["ox","oxen"],["axe","axes"],["die","dice"],["yes","yeses"],["foot","feet"],["eave","eaves"],["goose","geese"],["tooth","teeth"],["quiz","quizzes"],["human","humans"],["proof","proofs"],["carve","carves"],["valve","valves"],["looey","looies"],["thief","thieves"],["groove","grooves"],["pickaxe","pickaxes"],["passerby","passersby"]].forEach((function(e){return p.addIrregularRule(e[0],e[1])})),[[/s?$/i,"s"],[/[^\u0000-\u007F]$/i,"$0"],[/([^aeiou]ese)$/i,"$1"],[/(ax|test)is$/i,"$1es"],[/(alias|[^aou]us|t[lm]as|gas|ris)$/i,"$1es"],[/(e[mn]u)s?$/i,"$1s"],[/([^l]ias|[aeiou]las|[ejzr]as|[iu]am)$/i,"$1"],[/(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$/i,"$1i"],[/(alumn|alg|vertebr)(?:a|ae)$/i,"$1ae"],[/(seraph|cherub)(?:im)?$/i,"$1im"],[/(her|at|gr)o$/i,"$1oes"],[/(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|automat|quor)(?:a|um)$/i,"$1a"],[/(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)(?:a|on)$/i,"$1a"],[/sis$/i,"ses"],[/(?:(kni|wi|li)fe|(ar|l|ea|eo|oa|hoo)f)$/i,"$1$2ves"],[/([^aeiouy]|qu)y$/i,"$1ies"],[/([^ch][ieo][ln])ey$/i,"$1ies"],[/(x|ch|ss|sh|zz)$/i,"$1es"],[/(matr|cod|mur|sil|vert|ind|append)(?:ix|ex)$/i,"$1ices"],[/\b((?:tit)?m|l)(?:ice|ouse)$/i,"$1ice"],[/(pe)(?:rson|ople)$/i,"$1ople"],[/(child)(?:ren)?$/i,"$1ren"],[/eaux$/i,"$0"],[/m[ae]n$/i,"men"],["thou","you"]].forEach((function(e){return p.addPluralRule(e[0],e[1])})),[[/s$/i,""],[/(ss)$/i,"$1"],[/(wi|kni|(?:after|half|high|low|mid|non|night|[^\w]|^)li)ves$/i,"$1fe"],[/(ar|(?:wo|[ae])l|[eo][ao])ves$/i,"$1f"],[/ies$/i,"y"],[/\b([pl]|zomb|(?:neck|cross)?t|coll|faer|food|gen|goon|group|lass|talk|goal|cut)ies$/i,"$1ie"],[/\b(mon|smil)ies$/i,"$1ey"],[/\b((?:tit)?m|l)ice$/i,"$1ouse"],[/(seraph|cherub)im$/i,"$1"],[/(x|ch|ss|sh|zz|tto|go|cho|alias|[^aou]us|t[lm]as|gas|(?:her|at|gr)o|[aeiou]ris)(?:es)?$/i,"$1"],[/(analy|diagno|parenthe|progno|synop|the|empha|cri|ne)(?:sis|ses)$/i,"$1sis"],[/(movie|twelve|abuse|e[mn]u)s$/i,"$1"],[/(test)(?:is|es)$/i,"$1is"],[/(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$/i,"$1us"],[/(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|quor)a$/i,"$1um"],[/(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)a$/i,"$1on"],[/(alumn|alg|vertebr)ae$/i,"$1a"],[/(cod|mur|sil|vert|ind)ices$/i,"$1ex"],[/(matr|append)ices$/i,"$1ix"],[/(pe)(rson|ople)$/i,"$1rson"],[/(child)ren$/i,"$1"],[/(eau)x?$/i,"$1"],[/men$/i,"man"]].forEach((function(e){return p.addSingularRule(e[0],e[1])})),["adulthood","advice","agenda","aid","aircraft","alcohol","ammo","analytics","anime","athletics","audio","bison","blood","bream","buffalo","butter","carp","cash","chassis","chess","clothing","cod","commerce","cooperation","corps","debris","diabetes","digestion","elk","energy","equipment","excretion","expertise","firmware","flounder","fun","gallows","garbage","graffiti","hardware","headquarters","health","herpes","highjinks","homework","housework","information","jeans","justice","kudos","labour","literature","machinery","mackerel","mail","media","mews","moose","music","mud","manga","news","only","personnel","pike","plankton","pliers","police","pollution","premises","rain","research","rice","salmon","scissors","series","sewage","shambles","shrimp","software","species","staff","swine","tennis","traffic","transportation","trout","tuna","wealth","welfare","whiting","wildebeest","wildlife","you",/pok[eé]mon$/i,/[^aeiou]ese$/i,/deer$/i,/fish$/i,/measles$/i,/o[iu]s$/i,/pox$/i,/sheep$/i].forEach(p.addUncountableRule),p}()},7874:function(){!function(e){var t="\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\b",n={pattern:/(^(["']?)\w+\2)[ \t]+\S.*/,lookbehind:!0,alias:"punctuation",inside:null},r={bash:n,environment:{pattern:RegExp("\\$"+t),alias:"constant"},variable:[{pattern:/\$?\(\([\s\S]+?\)\)/,greedy:!0,inside:{variable:[{pattern:/(^\$\(\([\s\S]+)\)\)/,lookbehind:!0},/^\$\(\(/],number:/\b0x[\dA-Fa-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:[Ee]-?\d+)?/,operator:/--|\+\+|\*\*=?|<<=?|>>=?|&&|\|\||[=!+\-*/%<>^&|]=?|[?~:]/,punctuation:/\(\(?|\)\)?|,|;/}},{pattern:/\$\((?:\([^)]+\)|[^()])+\)|`[^`]+`/,greedy:!0,inside:{variable:/^\$\(|^`|\)$|`$/}},{pattern:/\$\{[^}]+\}/,greedy:!0,inside:{operator:/:[-=?+]?|[!\/]|##?|%%?|\^\^?|,,?/,punctuation:/[\[\]]/,environment:{pattern:RegExp("(\\{)"+t),lookbehind:!0,alias:"constant"}}},/\$(?:\w+|[#?*!@$])/],entity:/\\(?:[abceEfnrtv\\"]|O?[0-7]{1,3}|U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{1,2})/};e.languages.bash={shebang:{pattern:/^#!\s*\/.*/,alias:"important"},comment:{pattern:/(^|[^"{\\$])#.*/,lookbehind:!0},"function-name":[{pattern:/(\bfunction\s+)[\w-]+(?=(?:\s*\(?:\s*\))?\s*\{)/,lookbehind:!0,alias:"function"},{pattern:/\b[\w-]+(?=\s*\(\s*\)\s*\{)/,alias:"function"}],"for-or-select":{pattern:/(\b(?:for|select)\s+)\w+(?=\s+in\s)/,alias:"variable",lookbehind:!0},"assign-left":{pattern:/(^|[\s;|&]|[<>]\()\w+(?:\.\w+)*(?=\+?=)/,inside:{environment:{pattern:RegExp("(^|[\\s;|&]|[<>]\\()"+t),lookbehind:!0,alias:"constant"}},alias:"variable",lookbehind:!0},parameter:{pattern:/(^|\s)-{1,2}(?:\w+:[+-]?)?\w+(?:\.\w+)*(?=[=\s]|$)/,alias:"variable",lookbehind:!0},string:[{pattern:/((?:^|[^<])<<-?\s*)(\w+)\s[\s\S]*?(?:\r?\n|\r)\2/,lookbehind:!0,greedy:!0,inside:r},{pattern:/((?:^|[^<])<<-?\s*)(["'])(\w+)\2\s[\s\S]*?(?:\r?\n|\r)\3/,lookbehind:!0,greedy:!0,inside:{bash:n}},{pattern:/(^|[^\\](?:\\\\)*)"(?:\\[\s\S]|\$\([^)]+\)|\$(?!\()|`[^`]+`|[^"\\`$])*"/,lookbehind:!0,greedy:!0,inside:r},{pattern:/(^|[^$\\])'[^']*'/,lookbehind:!0,greedy:!0},{pattern:/\$'(?:[^'\\]|\\[\s\S])*'/,greedy:!0,inside:{entity:r.entity}}],environment:{pattern:RegExp("\\$?"+t),alias:"constant"},variable:r.variable,function:{pattern:/(^|[\s;|&]|[<>]\()(?:add|apropos|apt|apt-cache|apt-get|aptitude|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cargo|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|composer|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|docker|docker-compose|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|java|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|node|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|podman|podman-compose|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|sysctl|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vcpkg|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=$|[)\s;|&])/,lookbehind:!0},keyword:{pattern:/(^|[\s;|&]|[<>]\()(?:case|do|done|elif|else|esac|fi|for|function|if|in|select|then|until|while)(?=$|[)\s;|&])/,lookbehind:!0},builtin:{pattern:/(^|[\s;|&]|[<>]\()(?:\.|:|alias|bind|break|builtin|caller|cd|command|continue|declare|echo|enable|eval|exec|exit|export|getopts|hash|help|let|local|logout|mapfile|printf|pwd|read|readarray|readonly|return|set|shift|shopt|source|test|times|trap|type|typeset|ulimit|umask|unalias|unset)(?=$|[)\s;|&])/,lookbehind:!0,alias:"class-name"},boolean:{pattern:/(^|[\s;|&]|[<>]\()(?:false|true)(?=$|[)\s;|&])/,lookbehind:!0},"file-descriptor":{pattern:/\B&\d\b/,alias:"important"},operator:{pattern:/\d?<>|>\||\+=|=[=~]?|!=?|<<[<-]?|[&\d]?>>|\d[<>]&?|[<>][&=]?|&[>&]?|\|[&|]?/,inside:{"file-descriptor":{pattern:/^\d/,alias:"important"}}},punctuation:/\$?\(\(?|\)\)?|\.\.|[{}[\];\\]/,number:{pattern:/(^|\s)(?:[1-9]\d*|0)(?:[.,]\d+)?\b/,lookbehind:!0}},n.inside=e.languages.bash;for(var i=["comment","function-name","for-or-select","assign-left","parameter","string","environment","function","keyword","builtin","boolean","file-descriptor","operator","punctuation","number"],o=r.variable[1].inside,a=0;a>=?|<<=?|->|([-+&|:])\1|[?:~]|[-+*/%&|^!=<>]=?/}),Prism.languages.insertBefore("c","string",{char:{pattern:/'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n]){0,32}'/,greedy:!0}}),Prism.languages.insertBefore("c","string",{macro:{pattern:/(^[\t ]*)#\s*[a-z](?:[^\r\n\\/]|\/(?!\*)|\/\*(?:[^*]|\*(?!\/))*\*\/|\\(?:\r\n|[\s\S]))*/im,lookbehind:!0,greedy:!0,alias:"property",inside:{string:[{pattern:/^(#\s*include\s*)<[^>]+>/,lookbehind:!0},Prism.languages.c.string],char:Prism.languages.c.char,comment:Prism.languages.c.comment,"macro-name":[{pattern:/(^#\s*define\s+)\w+\b(?!\()/i,lookbehind:!0},{pattern:/(^#\s*define\s+)\w+\b(?=\()/i,lookbehind:!0,alias:"function"}],directive:{pattern:/^(#\s*)[a-z]+/,lookbehind:!0,alias:"keyword"},"directive-hash":/^#/,punctuation:/##|\\(?=[\r\n])/,expression:{pattern:/\S[\s\S]*/,inside:Prism.languages.c}}}}),Prism.languages.insertBefore("c","function",{constant:/\b(?:EOF|NULL|SEEK_CUR|SEEK_END|SEEK_SET|__DATE__|__FILE__|__LINE__|__TIMESTAMP__|__TIME__|__func__|stderr|stdin|stdout)\b/}),delete Prism.languages.c.boolean},5433:function(){Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}},6213:function(){!function(e){var t=/#(?!\{).+/,n={pattern:/#\{[^}]+\}/,alias:"variable"};e.languages.coffeescript=e.languages.extend("javascript",{comment:t,string:[{pattern:/'(?:\\[\s\S]|[^\\'])*'/,greedy:!0},{pattern:/"(?:\\[\s\S]|[^\\"])*"/,greedy:!0,inside:{interpolation:n}}],keyword:/\b(?:and|break|by|catch|class|continue|debugger|delete|do|each|else|extend|extends|false|finally|for|if|in|instanceof|is|isnt|let|loop|namespace|new|no|not|null|of|off|on|or|own|return|super|switch|then|this|throw|true|try|typeof|undefined|unless|until|when|while|window|with|yes|yield)\b/,"class-member":{pattern:/@(?!\d)\w+/,alias:"variable"}}),e.languages.insertBefore("coffeescript","comment",{"multiline-comment":{pattern:/###[\s\S]+?###/,alias:"comment"},"block-regex":{pattern:/\/{3}[\s\S]*?\/{3}/,alias:"regex",inside:{comment:t,interpolation:n}}}),e.languages.insertBefore("coffeescript","string",{"inline-javascript":{pattern:/`(?:\\[\s\S]|[^\\`])*`/,inside:{delimiter:{pattern:/^`|`$/,alias:"punctuation"},script:{pattern:/[\s\S]+/,alias:"language-javascript",inside:e.languages.javascript}}},"multiline-string":[{pattern:/'''[\s\S]*?'''/,greedy:!0,alias:"string"},{pattern:/"""[\s\S]*?"""/,greedy:!0,alias:"string",inside:{interpolation:n}}]}),e.languages.insertBefore("coffeescript","keyword",{property:/(?!\d)\w+(?=\s*:(?!:))/}),delete e.languages.coffeescript["template-string"],e.languages.coffee=e.languages.coffeescript}(Prism)},2731:function(){!function(e){var t=/\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\b/,n=/\b(?!)\w+(?:\s*\.\s*\w+)*\b/.source.replace(//g,(function(){return t.source}));e.languages.cpp=e.languages.extend("c",{"class-name":[{pattern:RegExp(/(\b(?:class|concept|enum|struct|typename)\s+)(?!)\w+/.source.replace(//g,(function(){return t.source}))),lookbehind:!0},/\b[A-Z]\w*(?=\s*::\s*\w+\s*\()/,/\b[A-Z_]\w*(?=\s*::\s*~\w+\s*\()/i,/\b\w+(?=\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>\s*::\s*\w+\s*\()/],keyword:t,number:{pattern:/(?:\b0b[01']+|\b0x(?:[\da-f']+(?:\.[\da-f']*)?|\.[\da-f']+)(?:p[+-]?[\d']+)?|(?:\b[\d']+(?:\.[\d']*)?|\B\.[\d']+)(?:e[+-]?[\d']+)?)[ful]{0,4}/i,greedy:!0},operator:/>>=?|<<=?|->|--|\+\+|&&|\|\||[?:~]|<=>|[-+*/%&|^!=<>]=?|\b(?:and|and_eq|bitand|bitor|not|not_eq|or|or_eq|xor|xor_eq)\b/,boolean:/\b(?:false|true)\b/}),e.languages.insertBefore("cpp","string",{module:{pattern:RegExp(/(\b(?:import|module)\s+)/.source+"(?:"+/"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|<[^<>\r\n]*>/.source+"|"+/(?:\s*:\s*)?|:\s*/.source.replace(//g,(function(){return n}))+")"),lookbehind:!0,greedy:!0,inside:{string:/^[<"][\s\S]+/,operator:/:/,punctuation:/\./}},"raw-string":{pattern:/R"([^()\\ ]{0,16})\([\s\S]*?\)\1"/,alias:"string",greedy:!0}}),e.languages.insertBefore("cpp","keyword",{"generic-function":{pattern:/\b(?!operator\b)[a-z_]\w*\s*<(?:[^<>]|<[^<>]*>)*>(?=\s*\()/i,inside:{function:/^\w+/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:e.languages.cpp}}}}),e.languages.insertBefore("cpp","operator",{"double-colon":{pattern:/::/,alias:"punctuation"}}),e.languages.insertBefore("cpp","class-name",{"base-clause":{pattern:/(\b(?:class|struct)\s+\w+\s*:\s*)[^;{}"'\s]+(?:\s+[^;{}"'\s]+)*(?=\s*[;{])/,lookbehind:!0,greedy:!0,inside:e.languages.extend("cpp",{})}}),e.languages.insertBefore("inside","double-colon",{"class-name":/\b[a-z_]\w*\b(?!\s*::)/i},e.languages.cpp["base-clause"])}(Prism)},9016:function(){!function(e){function t(e,t){return e.replace(/<<(\d+)>>/g,(function(e,n){return"(?:"+t[+n]+")"}))}function n(e,n,r){return RegExp(t(e,n),r||"")}function r(e,t){for(var n=0;n>/g,(function(){return"(?:"+e+")"}));return e.replace(/<>/g,"[^\\s\\S]")}var i="bool byte char decimal double dynamic float int long object sbyte short string uint ulong ushort var void",o="class enum interface record struct",a="add alias and ascending async await by descending from(?=\\s*(?:\\w|$)) get global group into init(?=\\s*;) join let nameof not notnull on or orderby partial remove select set unmanaged value when where with(?=\\s*{)",s="abstract as base break case catch checked const continue default delegate do else event explicit extern finally fixed for foreach goto if implicit in internal is lock namespace new null operator out override params private protected public readonly ref return sealed sizeof stackalloc static switch this throw try typeof unchecked unsafe using virtual volatile while yield";function l(e){return"\\b(?:"+e.trim().replace(/ /g,"|")+")\\b"}var c=l(o),u=RegExp(l(i+" "+o+" "+a+" "+s)),p=l(o+" "+a+" "+s),d=l(i+" "+o+" "+s),f=r(/<(?:[^<>;=+\-*/%&|^]|<>)*>/.source,2),h=r(/\((?:[^()]|<>)*\)/.source,2),m=/@?\b[A-Za-z_]\w*\b/.source,g=t(/<<0>>(?:\s*<<1>>)?/.source,[m,f]),y=t(/(?!<<0>>)<<1>>(?:\s*\.\s*<<1>>)*/.source,[p,g]),b=/\[\s*(?:,\s*)*\]/.source,v=t(/<<0>>(?:\s*(?:\?\s*)?<<1>>)*(?:\s*\?)?/.source,[y,b]),x=t(/[^,()<>[\];=+\-*/%&|^]|<<0>>|<<1>>|<<2>>/.source,[f,h,b]),w=t(/\(<<0>>+(?:,<<0>>+)+\)/.source,[x]),k=t(/(?:<<0>>|<<1>>)(?:\s*(?:\?\s*)?<<2>>)*(?:\s*\?)?/.source,[w,y,b]),S={keyword:u,punctuation:/[<>()?,.:[\]]/},O=/'(?:[^\r\n'\\]|\\.|\\[Uux][\da-fA-F]{1,8})'/.source,E=/"(?:\\.|[^\\"\r\n])*"/.source,_=/@"(?:""|\\[\s\S]|[^\\"])*"(?!")/.source;e.languages.csharp=e.languages.extend("clike",{string:[{pattern:n(/(^|[^$\\])<<0>>/.source,[_]),lookbehind:!0,greedy:!0},{pattern:n(/(^|[^@$\\])<<0>>/.source,[E]),lookbehind:!0,greedy:!0}],"class-name":[{pattern:n(/(\busing\s+static\s+)<<0>>(?=\s*;)/.source,[y]),lookbehind:!0,inside:S},{pattern:n(/(\busing\s+<<0>>\s*=\s*)<<1>>(?=\s*;)/.source,[m,k]),lookbehind:!0,inside:S},{pattern:n(/(\busing\s+)<<0>>(?=\s*=)/.source,[m]),lookbehind:!0},{pattern:n(/(\b<<0>>\s+)<<1>>/.source,[c,g]),lookbehind:!0,inside:S},{pattern:n(/(\bcatch\s*\(\s*)<<0>>/.source,[y]),lookbehind:!0,inside:S},{pattern:n(/(\bwhere\s+)<<0>>/.source,[m]),lookbehind:!0},{pattern:n(/(\b(?:is(?:\s+not)?|as)\s+)<<0>>/.source,[v]),lookbehind:!0,inside:S},{pattern:n(/\b<<0>>(?=\s+(?!<<1>>|with\s*\{)<<2>>(?:\s*[=,;:{)\]]|\s+(?:in|when)\b))/.source,[k,d,m]),inside:S}],keyword:u,number:/(?:\b0(?:x[\da-f_]*[\da-f]|b[01_]*[01])|(?:\B\.\d+(?:_+\d+)*|\b\d+(?:_+\d+)*(?:\.\d+(?:_+\d+)*)?)(?:e[-+]?\d+(?:_+\d+)*)?)(?:[dflmu]|lu|ul)?\b/i,operator:/>>=?|<<=?|[-=]>|([-+&|])\1|~|\?\?=?|[-+*/%&|^!=<>]=?/,punctuation:/\?\.?|::|[{}[\];(),.:]/}),e.languages.insertBefore("csharp","number",{range:{pattern:/\.\./,alias:"operator"}}),e.languages.insertBefore("csharp","punctuation",{"named-parameter":{pattern:n(/([(,]\s*)<<0>>(?=\s*:)/.source,[m]),lookbehind:!0,alias:"punctuation"}}),e.languages.insertBefore("csharp","class-name",{namespace:{pattern:n(/(\b(?:namespace|using)\s+)<<0>>(?:\s*\.\s*<<0>>)*(?=\s*[;{])/.source,[m]),lookbehind:!0,inside:{punctuation:/\./}},"type-expression":{pattern:n(/(\b(?:default|sizeof|typeof)\s*\(\s*(?!\s))(?:[^()\s]|\s(?!\s)|<<0>>)*(?=\s*\))/.source,[h]),lookbehind:!0,alias:"class-name",inside:S},"return-type":{pattern:n(/<<0>>(?=\s+(?:<<1>>\s*(?:=>|[({]|\.\s*this\s*\[)|this\s*\[))/.source,[k,y]),inside:S,alias:"class-name"},"constructor-invocation":{pattern:n(/(\bnew\s+)<<0>>(?=\s*[[({])/.source,[k]),lookbehind:!0,inside:S,alias:"class-name"},"generic-method":{pattern:n(/<<0>>\s*<<1>>(?=\s*\()/.source,[m,f]),inside:{function:n(/^<<0>>/.source,[m]),generic:{pattern:RegExp(f),alias:"class-name",inside:S}}},"type-list":{pattern:n(/\b((?:<<0>>\s+<<1>>|record\s+<<1>>\s*<<5>>|where\s+<<2>>)\s*:\s*)(?:<<3>>|<<4>>|<<1>>\s*<<5>>|<<6>>)(?:\s*,\s*(?:<<3>>|<<4>>|<<6>>))*(?=\s*(?:where|[{;]|=>|$))/.source,[c,g,m,k,u.source,h,/\bnew\s*\(\s*\)/.source]),lookbehind:!0,inside:{"record-arguments":{pattern:n(/(^(?!new\s*\()<<0>>\s*)<<1>>/.source,[g,h]),lookbehind:!0,greedy:!0,inside:e.languages.csharp},keyword:u,"class-name":{pattern:RegExp(k),greedy:!0,inside:S},punctuation:/[,()]/}},preprocessor:{pattern:/(^[\t ]*)#.*/m,lookbehind:!0,alias:"property",inside:{directive:{pattern:/(#)\b(?:define|elif|else|endif|endregion|error|if|line|nullable|pragma|region|undef|warning)\b/,lookbehind:!0,alias:"keyword"}}}});var A=E+"|"+O,j=t(/\/(?![*/])|\/\/[^\r\n]*[\r\n]|\/\*(?:[^*]|\*(?!\/))*\*\/|<<0>>/.source,[A]),C=r(t(/[^"'/()]|<<0>>|\(<>*\)/.source,[j]),2),P=/\b(?:assembly|event|field|method|module|param|property|return|type)\b/.source,T=t(/<<0>>(?:\s*\(<<1>>*\))?/.source,[y,C]);e.languages.insertBefore("csharp","class-name",{attribute:{pattern:n(/((?:^|[^\s\w>)?])\s*\[\s*)(?:<<0>>\s*:\s*)?<<1>>(?:\s*,\s*<<1>>)*(?=\s*\])/.source,[P,T]),lookbehind:!0,greedy:!0,inside:{target:{pattern:n(/^<<0>>(?=\s*:)/.source,[P]),alias:"keyword"},"attribute-arguments":{pattern:n(/\(<<0>>*\)/.source,[C]),inside:e.languages.csharp},"class-name":{pattern:RegExp(y),inside:{punctuation:/\./}},punctuation:/[:,]/}}});var I=/:[^}\r\n]+/.source,R=r(t(/[^"'/()]|<<0>>|\(<>*\)/.source,[j]),2),N=t(/\{(?!\{)(?:(?![}:])<<0>>)*<<1>>?\}/.source,[R,I]),$=r(t(/[^"'/()]|\/(?!\*)|\/\*(?:[^*]|\*(?!\/))*\*\/|<<0>>|\(<>*\)/.source,[A]),2),L=t(/\{(?!\{)(?:(?![}:])<<0>>)*<<1>>?\}/.source,[$,I]);function D(t,r){return{interpolation:{pattern:n(/((?:^|[^{])(?:\{\{)*)<<0>>/.source,[t]),lookbehind:!0,inside:{"format-string":{pattern:n(/(^\{(?:(?![}:])<<0>>)*)<<1>>(?=\}$)/.source,[r,I]),lookbehind:!0,inside:{punctuation:/^:/}},punctuation:/^\{|\}$/,expression:{pattern:/[\s\S]+/,alias:"language-csharp",inside:e.languages.csharp}}},string:/[\s\S]+/}}e.languages.insertBefore("csharp","string",{"interpolation-string":[{pattern:n(/(^|[^\\])(?:\$@|@\$)"(?:""|\\[\s\S]|\{\{|<<0>>|[^\\{"])*"/.source,[N]),lookbehind:!0,greedy:!0,inside:D(N,R)},{pattern:n(/(^|[^@\\])\$"(?:\\.|\{\{|<<0>>|[^\\"{])*"/.source,[L]),lookbehind:!0,greedy:!0,inside:D(L,$)}],char:{pattern:RegExp(O),greedy:!0}}),e.languages.dotnet=e.languages.cs=e.languages.csharp}(Prism)},7899:function(){Prism.languages.csv={value:/[^\r\n,"]+|"(?:[^"]|"")*"(?!")/,punctuation:/,/}},7046:function(){Prism.languages.go=Prism.languages.extend("clike",{string:{pattern:/(^|[^\\])"(?:\\.|[^"\\\r\n])*"|`[^`]*`/,lookbehind:!0,greedy:!0},keyword:/\b(?:break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go(?:to)?|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b/,boolean:/\b(?:_|false|iota|nil|true)\b/,number:[/\b0(?:b[01_]+|o[0-7_]+)i?\b/i,/\b0x(?:[a-f\d_]+(?:\.[a-f\d_]*)?|\.[a-f\d_]+)(?:p[+-]?\d+(?:_\d+)*)?i?(?!\w)/i,/(?:\b\d[\d_]*(?:\.[\d_]*)?|\B\.\d[\d_]*)(?:e[+-]?[\d_]+)?i?(?!\w)/i],operator:/[*\/%^!=]=?|\+[=+]?|-[=-]?|\|[=|]?|&(?:=|&|\^=?)?|>(?:>=?|=)?|<(?:<=?|=|-)?|:=|\.\.\./,builtin:/\b(?:append|bool|byte|cap|close|complex|complex(?:64|128)|copy|delete|error|float(?:32|64)|u?int(?:8|16|32|64)?|imag|len|make|new|panic|print(?:ln)?|real|recover|rune|string|uintptr)\b/}),Prism.languages.insertBefore("go","string",{char:{pattern:/'(?:\\.|[^'\\\r\n]){0,10}'/,greedy:!0}}),delete Prism.languages.go["class-name"]},57:function(){!function(e){function t(e){return RegExp("(^(?:"+e+"):[ \t]*(?![ \t]))[^]+","i")}e.languages.http={"request-line":{pattern:/^(?:CONNECT|DELETE|GET|HEAD|OPTIONS|PATCH|POST|PRI|PUT|SEARCH|TRACE)\s(?:https?:\/\/|\/)\S*\sHTTP\/[\d.]+/m,inside:{method:{pattern:/^[A-Z]+\b/,alias:"property"},"request-target":{pattern:/^(\s)(?:https?:\/\/|\/)\S*(?=\s)/,lookbehind:!0,alias:"url",inside:e.languages.uri},"http-version":{pattern:/^(\s)HTTP\/[\d.]+/,lookbehind:!0,alias:"property"}}},"response-status":{pattern:/^HTTP\/[\d.]+ \d+ .+/m,inside:{"http-version":{pattern:/^HTTP\/[\d.]+/,alias:"property"},"status-code":{pattern:/^(\s)\d+(?=\s)/,lookbehind:!0,alias:"number"},"reason-phrase":{pattern:/^(\s).+/,lookbehind:!0,alias:"string"}}},header:{pattern:/^[\w-]+:.+(?:(?:\r\n?|\n)[ \t].+)*/m,inside:{"header-value":[{pattern:t(/Content-Security-Policy/.source),lookbehind:!0,alias:["csp","languages-csp"],inside:e.languages.csp},{pattern:t(/Public-Key-Pins(?:-Report-Only)?/.source),lookbehind:!0,alias:["hpkp","languages-hpkp"],inside:e.languages.hpkp},{pattern:t(/Strict-Transport-Security/.source),lookbehind:!0,alias:["hsts","languages-hsts"],inside:e.languages.hsts},{pattern:t(/[^:]+/.source),lookbehind:!0}],"header-name":{pattern:/^[^:]+/,alias:"keyword"},punctuation:/^:/}}};var n,r=e.languages,i={"application/javascript":r.javascript,"application/json":r.json||r.javascript,"application/xml":r.xml,"text/xml":r.xml,"text/html":r.html,"text/css":r.css,"text/plain":r.plain},o={"application/json":!0,"application/xml":!0};function a(e){var t=e.replace(/^[a-z]+\//,"");return"(?:"+e+"|\\w+/(?:[\\w.-]+\\+)+"+t+"(?![+\\w.-]))"}for(var s in i)if(i[s]){n=n||{};var l=o[s]?a(s):s;n[s.replace(/\//g,"-")]={pattern:RegExp("("+/content-type:\s*/.source+l+/(?:(?:\r\n?|\n)[\w-].*)*(?:\r(?:\n|(?!\n))|\n)/.source+")"+/[^ \t\w-][\s\S]*/.source,"i"),lookbehind:!0,inside:i[s]}}n&&e.languages.insertBefore("http","header",n)}(Prism)},2503:function(){!function(e){var t=/\b(?:abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|exports|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|module|native|new|non-sealed|null|open|opens|package|permits|private|protected|provides|public|record(?!\s*[(){}[\]<>=%~.:,;?+\-*/&|^])|requires|return|sealed|short|static|strictfp|super|switch|synchronized|this|throw|throws|to|transient|transitive|try|uses|var|void|volatile|while|with|yield)\b/,n=/(?:[a-z]\w*\s*\.\s*)*(?:[A-Z]\w*\s*\.\s*)*/.source,r={pattern:RegExp(/(^|[^\w.])/.source+n+/[A-Z](?:[\d_A-Z]*[a-z]\w*)?\b/.source),lookbehind:!0,inside:{namespace:{pattern:/^[a-z]\w*(?:\s*\.\s*[a-z]\w*)*(?:\s*\.)?/,inside:{punctuation:/\./}},punctuation:/\./}};e.languages.java=e.languages.extend("clike",{string:{pattern:/(^|[^\\])"(?:\\.|[^"\\\r\n])*"/,lookbehind:!0,greedy:!0},"class-name":[r,{pattern:RegExp(/(^|[^\w.])/.source+n+/[A-Z]\w*(?=\s+\w+\s*[;,=()]|\s*(?:\[[\s,]*\]\s*)?::\s*new\b)/.source),lookbehind:!0,inside:r.inside},{pattern:RegExp(/(\b(?:class|enum|extends|implements|instanceof|interface|new|record|throws)\s+)/.source+n+/[A-Z]\w*\b/.source),lookbehind:!0,inside:r.inside}],keyword:t,function:[e.languages.clike.function,{pattern:/(::\s*)[a-z_]\w*/,lookbehind:!0}],number:/\b0b[01][01_]*L?\b|\b0x(?:\.[\da-f_p+-]+|[\da-f_]+(?:\.[\da-f_p+-]+)?)\b|(?:\b\d[\d_]*(?:\.[\d_]*)?|\B\.\d[\d_]*)(?:e[+-]?\d[\d_]*)?[dfl]?/i,operator:{pattern:/(^|[^.])(?:<<=?|>>>?=?|->|--|\+\+|&&|\|\||::|[?:~]|[-+*/%&|^!=<>]=?)/m,lookbehind:!0},constant:/\b[A-Z][A-Z_\d]+\b/}),e.languages.insertBefore("java","string",{"triple-quoted-string":{pattern:/"""[ \t]*[\r\n](?:(?:"|"")?(?:\\.|[^"\\]))*"""/,greedy:!0,alias:"string"},char:{pattern:/'(?:\\.|[^'\\\r\n]){1,6}'/,greedy:!0}}),e.languages.insertBefore("java","class-name",{annotation:{pattern:/(^|[^.])@\w+(?:\s*\.\s*\w+)*/,lookbehind:!0,alias:"punctuation"},generics:{pattern:/<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&))*>)*>)*>)*>/,inside:{"class-name":r,keyword:t,punctuation:/[<>(),.:]/,operator:/[?&|]/}},import:[{pattern:RegExp(/(\bimport\s+)/.source+n+/(?:[A-Z]\w*|\*)(?=\s*;)/.source),lookbehind:!0,inside:{namespace:r.inside.namespace,punctuation:/\./,operator:/\*/,"class-name":/\w+/}},{pattern:RegExp(/(\bimport\s+static\s+)/.source+n+/(?:\w+|\*)(?=\s*;)/.source),lookbehind:!0,alias:"static",inside:{namespace:r.inside.namespace,static:/\b\w+$/,punctuation:/\./,operator:/\*/,"class-name":/\w+/}}],namespace:{pattern:RegExp(/(\b(?:exports|import(?:\s+static)?|module|open|opens|package|provides|requires|to|transitive|uses|with)\s+)(?!)[a-z]\w*(?:\.[a-z]\w*)*\.?/.source.replace(//g,(function(){return t.source}))),lookbehind:!0,inside:{punctuation:/\./}}})}(Prism)},6841:function(){Prism.languages.lua={comment:/^#!.+|--(?:\[(=*)\[[\s\S]*?\]\1\]|.*)/m,string:{pattern:/(["'])(?:(?!\1)[^\\\r\n]|\\z(?:\r\n|\s)|\\(?:\r\n|[^z]))*\1|\[(=*)\[[\s\S]*?\]\2\]/,greedy:!0},number:/\b0x[a-f\d]+(?:\.[a-f\d]*)?(?:p[+-]?\d+)?\b|\b\d+(?:\.\B|(?:\.\d*)?(?:e[+-]?\d+)?\b)|\B\.\d+(?:e[+-]?\d+)?\b/i,keyword:/\b(?:and|break|do|else|elseif|end|false|for|function|goto|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/,function:/(?!\d)\w+(?=\s*(?:[({]))/,operator:[/[-+*%^&|#]|\/\/?|<[<=]?|>[>=]?|[=~]=?/,{pattern:/(^|[^.])\.\.(?!\.)/,lookbehind:!0}],punctuation:/[\[\](){},;]|\.+|:+/}},6854:function(){!function(e){function t(e,t){return"___"+e.toUpperCase()+t+"___"}Object.defineProperties(e.languages["markup-templating"]={},{buildPlaceholders:{value:function(n,r,i,o){if(n.language===r){var a=n.tokenStack=[];n.code=n.code.replace(i,(function(e){if("function"==typeof o&&!o(e))return e;for(var i,s=a.length;-1!==n.code.indexOf(i=t(r,s));)++s;return a[s]=e,i})),n.grammar=e.languages.markup}}},tokenizePlaceholders:{value:function(n,r){if(n.language===r&&n.tokenStack){n.grammar=e.languages[r];var i=0,o=Object.keys(n.tokenStack);!function a(s){for(var l=0;l=o.length);l++){var c=s[l];if("string"==typeof c||c.content&&"string"==typeof c.content){var u=o[i],p=n.tokenStack[u],d="string"==typeof c?c:c.content,f=t(r,u),h=d.indexOf(f);if(h>-1){++i;var m=d.substring(0,h),g=new e.Token(r,e.tokenize(p,n.grammar),"language-"+r,p),y=d.substring(h+f.length),b=[];m&&b.push.apply(b,a([m])),b.push(g),y&&b.push.apply(b,a([y])),"string"==typeof c?s.splice.apply(s,[l,1].concat(b)):c.content=b}}else c.content&&a(c.content)}return s}(n.tokens)}}}})}(Prism)},4335:function(){Prism.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",(function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(e,t){var n={};n["language-"+t]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[t]},n.cdata=/^$/i;var r={"included-cdata":{pattern://i,inside:n}};r["language-"+t]={pattern:/[\s\S]+/,inside:Prism.languages[t]};var i={};i[e]={pattern:RegExp(/(<__[^>]*>)(?:))*\]\]>|(?!)/.source.replace(/__/g,(function(){return e})),"i"),lookbehind:!0,greedy:!0,inside:r},Prism.languages.insertBefore("markup","cdata",i)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(e,t){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp(/(^|["'\s])/.source+"(?:"+e+")"+/\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))/.source,"i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[t,"language-"+t],inside:Prism.languages[t]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml},1426:function(){Prism.languages.objectivec=Prism.languages.extend("c",{string:{pattern:/@?"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"/,greedy:!0},keyword:/\b(?:asm|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|in|inline|int|long|register|return|self|short|signed|sizeof|static|struct|super|switch|typedef|typeof|union|unsigned|void|volatile|while)\b|(?:@interface|@end|@implementation|@protocol|@class|@public|@protected|@private|@property|@try|@catch|@finally|@throw|@synthesize|@dynamic|@selector)\b/,operator:/-[->]?|\+\+?|!=?|<>?=?|==?|&&?|\|\|?|[~^%?*\/@]/}),delete Prism.languages.objectivec["class-name"],Prism.languages.objc=Prism.languages.objectivec},8246:function(){!function(e){var t=/(?:\((?:[^()\\]|\\[\s\S])*\)|\{(?:[^{}\\]|\\[\s\S])*\}|\[(?:[^[\]\\]|\\[\s\S])*\]|<(?:[^<>\\]|\\[\s\S])*>)/.source;e.languages.perl={comment:[{pattern:/(^\s*)=\w[\s\S]*?=cut.*/m,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\$])#.*/,lookbehind:!0,greedy:!0}],string:[{pattern:RegExp(/\b(?:q|qq|qw|qx)(?![a-zA-Z0-9])\s*/.source+"(?:"+[/([^a-zA-Z0-9\s{(\[<])(?:(?!\1)[^\\]|\\[\s\S])*\1/.source,/([a-zA-Z0-9])(?:(?!\2)[^\\]|\\[\s\S])*\2/.source,t].join("|")+")"),greedy:!0},{pattern:/("|`)(?:(?!\1)[^\\]|\\[\s\S])*\1/,greedy:!0},{pattern:/'(?:[^'\\\r\n]|\\.)*'/,greedy:!0}],regex:[{pattern:RegExp(/\b(?:m|qr)(?![a-zA-Z0-9])\s*/.source+"(?:"+[/([^a-zA-Z0-9\s{(\[<])(?:(?!\1)[^\\]|\\[\s\S])*\1/.source,/([a-zA-Z0-9])(?:(?!\2)[^\\]|\\[\s\S])*\2/.source,t].join("|")+")"+/[msixpodualngc]*/.source),greedy:!0},{pattern:RegExp(/(^|[^-])\b(?:s|tr|y)(?![a-zA-Z0-9])\s*/.source+"(?:"+[/([^a-zA-Z0-9\s{(\[<])(?:(?!\2)[^\\]|\\[\s\S])*\2(?:(?!\2)[^\\]|\\[\s\S])*\2/.source,/([a-zA-Z0-9])(?:(?!\3)[^\\]|\\[\s\S])*\3(?:(?!\3)[^\\]|\\[\s\S])*\3/.source,t+/\s*/.source+t].join("|")+")"+/[msixpodualngcer]*/.source),lookbehind:!0,greedy:!0},{pattern:/\/(?:[^\/\\\r\n]|\\.)*\/[msixpodualngc]*(?=\s*(?:$|[\r\n,.;})&|\-+*~<>!?^]|(?:and|cmp|eq|ge|gt|le|lt|ne|not|or|x|xor)\b))/,greedy:!0}],variable:[/[&*$@%]\{\^[A-Z]+\}/,/[&*$@%]\^[A-Z_]/,/[&*$@%]#?(?=\{)/,/[&*$@%]#?(?:(?:::)*'?(?!\d)[\w$]+(?![\w$]))+(?:::)*/,/[&*$@%]\d+/,/(?!%=)[$@%][!"#$%&'()*+,\-.\/:;<=>?@[\\\]^_`{|}~]/],filehandle:{pattern:/<(?![<=])\S*?>|\b_\b/,alias:"symbol"},"v-string":{pattern:/v\d+(?:\.\d+)*|\d+(?:\.\d+){2,}/,alias:"string"},function:{pattern:/(\bsub[ \t]+)\w+/,lookbehind:!0},keyword:/\b(?:any|break|continue|default|delete|die|do|else|elsif|eval|for|foreach|given|goto|if|last|local|my|next|our|package|print|redo|require|return|say|state|sub|switch|undef|unless|until|use|when|while)\b/,number:/\b(?:0x[\dA-Fa-f](?:_?[\dA-Fa-f])*|0b[01](?:_?[01])*|(?:(?:\d(?:_?\d)*)?\.)?\d(?:_?\d)*(?:[Ee][+-]?\d+)?)\b/,operator:/-[rwxoRWXOezsfdlpSbctugkTBMAC]\b|\+[+=]?|-[-=>]?|\*\*?=?|\/\/?=?|=[=~>]?|~[~=]?|\|\|?=?|&&?=?|<(?:=>?|<=?)?|>>?=?|![~=]?|[%^]=?|\.(?:=|\.\.?)?|[\\?]|\bx(?:=|\b)|\b(?:and|cmp|eq|ge|gt|le|lt|ne|not|or|xor)\b/,punctuation:/[{}[\];(),:]/}}(Prism)},9945:function(){!function(e){var t=/\/\*[\s\S]*?\*\/|\/\/.*|#(?!\[).*/,n=[{pattern:/\b(?:false|true)\b/i,alias:"boolean"},{pattern:/(::\s*)\b[a-z_]\w*\b(?!\s*\()/i,greedy:!0,lookbehind:!0},{pattern:/(\b(?:case|const)\s+)\b[a-z_]\w*(?=\s*[;=])/i,greedy:!0,lookbehind:!0},/\b(?:null)\b/i,/\b[A-Z_][A-Z0-9_]*\b(?!\s*\()/],r=/\b0b[01]+(?:_[01]+)*\b|\b0o[0-7]+(?:_[0-7]+)*\b|\b0x[\da-f]+(?:_[\da-f]+)*\b|(?:\b\d+(?:_\d+)*\.?(?:\d+(?:_\d+)*)?|\B\.\d+)(?:e[+-]?\d+)?/i,i=/|\?\?=?|\.{3}|\??->|[!=]=?=?|::|\*\*=?|--|\+\+|&&|\|\||<<|>>|[?~]|[/^|%*&<>.+-]=?/,o=/[{}\[\](),:;]/;e.languages.php={delimiter:{pattern:/\?>$|^<\?(?:php(?=\s)|=)?/i,alias:"important"},comment:t,variable:/\$+(?:\w+\b|(?=\{))/,package:{pattern:/(namespace\s+|use\s+(?:function\s+)?)(?:\\?\b[a-z_]\w*)+\b(?!\\)/i,lookbehind:!0,inside:{punctuation:/\\/}},"class-name-definition":{pattern:/(\b(?:class|enum|interface|trait)\s+)\b[a-z_]\w*(?!\\)\b/i,lookbehind:!0,alias:"class-name"},"function-definition":{pattern:/(\bfunction\s+)[a-z_]\w*(?=\s*\()/i,lookbehind:!0,alias:"function"},keyword:[{pattern:/(\(\s*)\b(?:array|bool|boolean|float|int|integer|object|string)\b(?=\s*\))/i,alias:"type-casting",greedy:!0,lookbehind:!0},{pattern:/([(,?]\s*)\b(?:array(?!\s*\()|bool|callable|(?:false|null)(?=\s*\|)|float|int|iterable|mixed|object|self|static|string)\b(?=\s*\$)/i,alias:"type-hint",greedy:!0,lookbehind:!0},{pattern:/(\)\s*:\s*(?:\?\s*)?)\b(?:array(?!\s*\()|bool|callable|(?:false|null)(?=\s*\|)|float|int|iterable|mixed|never|object|self|static|string|void)\b/i,alias:"return-type",greedy:!0,lookbehind:!0},{pattern:/\b(?:array(?!\s*\()|bool|float|int|iterable|mixed|object|string|void)\b/i,alias:"type-declaration",greedy:!0},{pattern:/(\|\s*)(?:false|null)\b|\b(?:false|null)(?=\s*\|)/i,alias:"type-declaration",greedy:!0,lookbehind:!0},{pattern:/\b(?:parent|self|static)(?=\s*::)/i,alias:"static-context",greedy:!0},{pattern:/(\byield\s+)from\b/i,lookbehind:!0},/\bclass\b/i,{pattern:/((?:^|[^\s>:]|(?:^|[^-])>|(?:^|[^:]):)\s*)\b(?:abstract|and|array|as|break|callable|case|catch|clone|const|continue|declare|default|die|do|echo|else|elseif|empty|enddeclare|endfor|endforeach|endif|endswitch|endwhile|enum|eval|exit|extends|final|finally|fn|for|foreach|function|global|goto|if|implements|include|include_once|instanceof|insteadof|interface|isset|list|match|namespace|never|new|or|parent|print|private|protected|public|readonly|require|require_once|return|self|static|switch|throw|trait|try|unset|use|var|while|xor|yield|__halt_compiler)\b/i,lookbehind:!0}],"argument-name":{pattern:/([(,]\s*)\b[a-z_]\w*(?=\s*:(?!:))/i,lookbehind:!0},"class-name":[{pattern:/(\b(?:extends|implements|instanceof|new(?!\s+self|\s+static))\s+|\bcatch\s*\()\b[a-z_]\w*(?!\\)\b/i,greedy:!0,lookbehind:!0},{pattern:/(\|\s*)\b[a-z_]\w*(?!\\)\b/i,greedy:!0,lookbehind:!0},{pattern:/\b[a-z_]\w*(?!\\)\b(?=\s*\|)/i,greedy:!0},{pattern:/(\|\s*)(?:\\?\b[a-z_]\w*)+\b/i,alias:"class-name-fully-qualified",greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}},{pattern:/(?:\\?\b[a-z_]\w*)+\b(?=\s*\|)/i,alias:"class-name-fully-qualified",greedy:!0,inside:{punctuation:/\\/}},{pattern:/(\b(?:extends|implements|instanceof|new(?!\s+self\b|\s+static\b))\s+|\bcatch\s*\()(?:\\?\b[a-z_]\w*)+\b(?!\\)/i,alias:"class-name-fully-qualified",greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}},{pattern:/\b[a-z_]\w*(?=\s*\$)/i,alias:"type-declaration",greedy:!0},{pattern:/(?:\\?\b[a-z_]\w*)+(?=\s*\$)/i,alias:["class-name-fully-qualified","type-declaration"],greedy:!0,inside:{punctuation:/\\/}},{pattern:/\b[a-z_]\w*(?=\s*::)/i,alias:"static-context",greedy:!0},{pattern:/(?:\\?\b[a-z_]\w*)+(?=\s*::)/i,alias:["class-name-fully-qualified","static-context"],greedy:!0,inside:{punctuation:/\\/}},{pattern:/([(,?]\s*)[a-z_]\w*(?=\s*\$)/i,alias:"type-hint",greedy:!0,lookbehind:!0},{pattern:/([(,?]\s*)(?:\\?\b[a-z_]\w*)+(?=\s*\$)/i,alias:["class-name-fully-qualified","type-hint"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}},{pattern:/(\)\s*:\s*(?:\?\s*)?)\b[a-z_]\w*(?!\\)\b/i,alias:"return-type",greedy:!0,lookbehind:!0},{pattern:/(\)\s*:\s*(?:\?\s*)?)(?:\\?\b[a-z_]\w*)+\b(?!\\)/i,alias:["class-name-fully-qualified","return-type"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}}],constant:n,function:{pattern:/(^|[^\\\w])\\?[a-z_](?:[\w\\]*\w)?(?=\s*\()/i,lookbehind:!0,inside:{punctuation:/\\/}},property:{pattern:/(->\s*)\w+/,lookbehind:!0},number:r,operator:i,punctuation:o};var a={pattern:/\{\$(?:\{(?:\{[^{}]+\}|[^{}]+)\}|[^{}])+\}|(^|[^\\{])\$+(?:\w+(?:\[[^\r\n\[\]]+\]|->\w+)?)/,lookbehind:!0,inside:e.languages.php},s=[{pattern:/<<<'([^']+)'[\r\n](?:.*[\r\n])*?\1;/,alias:"nowdoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<<'[^']+'|[a-z_]\w*;$/i,alias:"symbol",inside:{punctuation:/^<<<'?|[';]$/}}}},{pattern:/<<<(?:"([^"]+)"[\r\n](?:.*[\r\n])*?\1;|([a-z_]\w*)[\r\n](?:.*[\r\n])*?\2;)/i,alias:"heredoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<<(?:"[^"]+"|[a-z_]\w*)|[a-z_]\w*;$/i,alias:"symbol",inside:{punctuation:/^<<<"?|[";]$/}},interpolation:a}},{pattern:/`(?:\\[\s\S]|[^\\`])*`/,alias:"backtick-quoted-string",greedy:!0},{pattern:/'(?:\\[\s\S]|[^\\'])*'/,alias:"single-quoted-string",greedy:!0},{pattern:/"(?:\\[\s\S]|[^\\"])*"/,alias:"double-quoted-string",greedy:!0,inside:{interpolation:a}}];e.languages.insertBefore("php","variable",{string:s,attribute:{pattern:/#\[(?:[^"'\/#]|\/(?![*/])|\/\/.*$|#(?!\[).*$|\/\*(?:[^*]|\*(?!\/))*\*\/|"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*')+\](?=\s*[a-z$#])/im,greedy:!0,inside:{"attribute-content":{pattern:/^(#\[)[\s\S]+(?=\]$)/,lookbehind:!0,inside:{comment:t,string:s,"attribute-class-name":[{pattern:/([^:]|^)\b[a-z_]\w*(?!\\)\b/i,alias:"class-name",greedy:!0,lookbehind:!0},{pattern:/([^:]|^)(?:\\?\b[a-z_]\w*)+/i,alias:["class-name","class-name-fully-qualified"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}}],constant:n,number:r,operator:i,punctuation:o}},delimiter:{pattern:/^#\[|\]$/,alias:"punctuation"}}}}),e.hooks.add("before-tokenize",(function(t){/<\?/.test(t.code)&&e.languages["markup-templating"].buildPlaceholders(t,"php",/<\?(?:[^"'/#]|\/(?![*/])|("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|(?:\/\/|#(?!\[))(?:[^?\n\r]|\?(?!>))*(?=$|\?>|[\r\n])|#\[|\/\*(?:[^*]|\*(?!\/))*(?:\*\/|$))*?(?:\?>|$)/g)})),e.hooks.add("after-tokenize",(function(t){e.languages["markup-templating"].tokenizePlaceholders(t,"php")}))}(Prism)},366:function(){Prism.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0,greedy:!0},"string-interpolation":{pattern:/(?:f|fr|rf)(?:("""|''')[\s\S]*?\1|("|')(?:\\.|(?!\2)[^\\\r\n])*\2)/i,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^{])(?:\{\{)*)\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}])+\})+\})+\}/,lookbehind:!0,inside:{"format-spec":{pattern:/(:)[^:(){}]+(?=\}$)/,lookbehind:!0},"conversion-option":{pattern:/![sra](?=[:}]$)/,alias:"punctuation"},rest:null}},string:/[\s\S]+/}},"triple-quoted-string":{pattern:/(?:[rub]|br|rb)?("""|''')[\s\S]*?\1/i,greedy:!0,alias:"string"},string:{pattern:/(?:[rub]|br|rb)?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i,greedy:!0},function:{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)\w+/i,lookbehind:!0},decorator:{pattern:/(^[\t ]*)@\w+(?:\.\w+)*/m,lookbehind:!0,alias:["annotation","punctuation"],inside:{punctuation:/\./}},keyword:/\b(?:_(?=\s*:)|and|as|assert|async|await|break|case|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|match|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\b/,builtin:/\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b/,boolean:/\b(?:False|None|True)\b/,number:/\b0(?:b(?:_?[01])+|o(?:_?[0-7])+|x(?:_?[a-f0-9])+)\b|(?:\b\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\B\.\d+(?:_\d+)*)(?:e[+-]?\d+(?:_\d+)*)?j?(?!\w)/i,operator:/[-+%=]=?|!=|:=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,punctuation:/[{}[\];(),.:]/},Prism.languages.python["string-interpolation"].inside.interpolation.inside.rest=Prism.languages.python,Prism.languages.py=Prism.languages.python},2939:function(){Prism.languages.q={string:/"(?:\\.|[^"\\\r\n])*"/,comment:[{pattern:/([\t )\]}])\/.*/,lookbehind:!0,greedy:!0},{pattern:/(^|\r?\n|\r)\/[\t ]*(?:(?:\r?\n|\r)(?:.*(?:\r?\n|\r(?!\n)))*?(?:\\(?=[\t ]*(?:\r?\n|\r))|$)|\S.*)/,lookbehind:!0,greedy:!0},{pattern:/^\\[\t ]*(?:\r?\n|\r)[\s\S]+/m,greedy:!0},{pattern:/^#!.+/m,greedy:!0}],symbol:/`(?::\S+|[\w.]*)/,datetime:{pattern:/0N[mdzuvt]|0W[dtz]|\d{4}\.\d\d(?:m|\.\d\d(?:T(?:\d\d(?::\d\d(?::\d\d(?:[.:]\d\d\d)?)?)?)?)?[dz]?)|\d\d:\d\d(?::\d\d(?:[.:]\d\d\d)?)?[uvt]?/,alias:"number"},number:/\b(?![01]:)(?:0N[hje]?|0W[hj]?|0[wn]|0x[\da-fA-F]+|\d+(?:\.\d*)?(?:e[+-]?\d+)?[hjfeb]?)/,keyword:/\\\w+\b|\b(?:abs|acos|aj0?|all|and|any|asc|asin|asof|atan|attr|avgs?|binr?|by|ceiling|cols|cor|cos|count|cov|cross|csv|cut|delete|deltas|desc|dev|differ|distinct|div|do|dsave|ej|enlist|eval|except|exec|exit|exp|fby|fills|first|fkeys|flip|floor|from|get|getenv|group|gtime|hclose|hcount|hdel|hopen|hsym|iasc|identity|idesc|if|ij|in|insert|inter|inv|keys?|last|like|list|ljf?|load|log|lower|lsq|ltime|ltrim|mavg|maxs?|mcount|md5|mdev|med|meta|mins?|mmax|mmin|mmu|mod|msum|neg|next|not|null|or|over|parse|peach|pj|plist|prds?|prev|prior|rand|rank|ratios|raze|read0|read1|reciprocal|reval|reverse|rload|rotate|rsave|rtrim|save|scan|scov|sdev|select|set|setenv|show|signum|sin|sqrt|ssr?|string|sublist|sums?|sv|svar|system|tables|tan|til|trim|txf|type|uj|ungroup|union|update|upper|upsert|value|var|views?|vs|wavg|where|while|within|wj1?|wsum|ww|xasc|xbar|xcols?|xdesc|xexp|xgroup|xkey|xlog|xprev|xrank)\b/,adverb:{pattern:/['\/\\]:?|\beach\b/,alias:"function"},verb:{pattern:/(?:\B\.\B|\b[01]:|<[=>]?|>=?|[:+\-*%,!?~=|$&#@^]):?|\b_\b:?/,alias:"operator"},punctuation:/[(){}\[\];.]/}},9385:function(){!function(e){e.languages.ruby=e.languages.extend("clike",{comment:{pattern:/#.*|^=begin\s[\s\S]*?^=end/m,greedy:!0},"class-name":{pattern:/(\b(?:class|module)\s+|\bcatch\s+\()[\w.\\]+|\b[A-Z_]\w*(?=\s*\.\s*new\b)/,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:BEGIN|END|alias|and|begin|break|case|class|def|define_method|defined|do|each|else|elsif|end|ensure|extend|for|if|in|include|module|new|next|nil|not|or|prepend|private|protected|public|raise|redo|require|rescue|retry|return|self|super|then|throw|undef|unless|until|when|while|yield)\b/,operator:/\.{2,3}|&\.|===||[!=]?~|(?:&&|\|\||<<|>>|\*\*|[+\-*/%<>!^&|=])=?|[?:]/,punctuation:/[(){}[\].,;]/}),e.languages.insertBefore("ruby","operator",{"double-colon":{pattern:/::/,alias:"punctuation"}});var t={pattern:/((?:^|[^\\])(?:\\{2})*)#\{(?:[^{}]|\{[^{}]*\})*\}/,lookbehind:!0,inside:{content:{pattern:/^(#\{)[\s\S]+(?=\}$)/,lookbehind:!0,inside:e.languages.ruby},delimiter:{pattern:/^#\{|\}$/,alias:"punctuation"}}};delete e.languages.ruby.function;var n="(?:"+[/([^a-zA-Z0-9\s{(\[<=])(?:(?!\1)[^\\]|\\[\s\S])*\1/.source,/\((?:[^()\\]|\\[\s\S]|\((?:[^()\\]|\\[\s\S])*\))*\)/.source,/\{(?:[^{}\\]|\\[\s\S]|\{(?:[^{}\\]|\\[\s\S])*\})*\}/.source,/\[(?:[^\[\]\\]|\\[\s\S]|\[(?:[^\[\]\\]|\\[\s\S])*\])*\]/.source,/<(?:[^<>\\]|\\[\s\S]|<(?:[^<>\\]|\\[\s\S])*>)*>/.source].join("|")+")",r=/(?:"(?:\\.|[^"\\\r\n])*"|(?:\b[a-zA-Z_]\w*|[^\s\0-\x7F]+)[?!]?|\$.)/.source;e.languages.insertBefore("ruby","keyword",{"regex-literal":[{pattern:RegExp(/%r/.source+n+/[egimnosux]{0,6}/.source),greedy:!0,inside:{interpolation:t,regex:/[\s\S]+/}},{pattern:/(^|[^/])\/(?!\/)(?:\[[^\r\n\]]+\]|\\.|[^[/\\\r\n])+\/[egimnosux]{0,6}(?=\s*(?:$|[\r\n,.;})#]))/,lookbehind:!0,greedy:!0,inside:{interpolation:t,regex:/[\s\S]+/}}],variable:/[@$]+[a-zA-Z_]\w*(?:[?!]|\b)/,symbol:[{pattern:RegExp(/(^|[^:]):/.source+r),lookbehind:!0,greedy:!0},{pattern:RegExp(/([\r\n{(,][ \t]*)/.source+r+/(?=:(?!:))/.source),lookbehind:!0,greedy:!0}],"method-definition":{pattern:/(\bdef\s+)\w+(?:\s*\.\s*\w+)?/,lookbehind:!0,inside:{function:/\b\w+$/,keyword:/^self\b/,"class-name":/^\w+/,punctuation:/\./}}}),e.languages.insertBefore("ruby","string",{"string-literal":[{pattern:RegExp(/%[qQiIwWs]?/.source+n),greedy:!0,inside:{interpolation:t,string:/[\s\S]+/}},{pattern:/("|')(?:#\{[^}]+\}|#(?!\{)|\\(?:\r\n|[\s\S])|(?!\1)[^\\#\r\n])*\1/,greedy:!0,inside:{interpolation:t,string:/[\s\S]+/}},{pattern:/<<[-~]?([a-z_]\w*)[\r\n](?:.*[\r\n])*?[\t ]*\1/i,alias:"heredoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<[-~]?[a-z_]\w*|\b[a-z_]\w*$/i,inside:{symbol:/\b\w+/,punctuation:/^<<[-~]?/}},interpolation:t,string:/[\s\S]+/}},{pattern:/<<[-~]?'([a-z_]\w*)'[\r\n](?:.*[\r\n])*?[\t ]*\1/i,alias:"heredoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<[-~]?'[a-z_]\w*'|\b[a-z_]\w*$/i,inside:{symbol:/\b\w+/,punctuation:/^<<[-~]?'|'$/}},string:/[\s\S]+/}}],"command-literal":[{pattern:RegExp(/%x/.source+n),greedy:!0,inside:{interpolation:t,command:{pattern:/[\s\S]+/,alias:"string"}}},{pattern:/`(?:#\{[^}]+\}|#(?!\{)|\\(?:\r\n|[\s\S])|[^\\`#\r\n])*`/,greedy:!0,inside:{interpolation:t,command:{pattern:/[\s\S]+/,alias:"string"}}}]}),delete e.languages.ruby.string,e.languages.insertBefore("ruby","number",{builtin:/\b(?:Array|Bignum|Binding|Class|Continuation|Dir|Exception|FalseClass|File|Fixnum|Float|Hash|IO|Integer|MatchData|Method|Module|NilClass|Numeric|Object|Proc|Range|Regexp|Stat|String|Struct|Symbol|TMS|Thread|ThreadGroup|Time|TrueClass)\b/,constant:/\b[A-Z][A-Z0-9_]*(?:[?!]|\b)/}),e.languages.rb=e.languages.ruby}(Prism)},2886:function(){Prism.languages.scala=Prism.languages.extend("java",{"triple-quoted-string":{pattern:/"""[\s\S]*?"""/,greedy:!0,alias:"string"},string:{pattern:/("|')(?:\\.|(?!\1)[^\\\r\n])*\1/,greedy:!0},keyword:/<-|=>|\b(?:abstract|case|catch|class|def|derives|do|else|enum|extends|extension|final|finally|for|forSome|given|if|implicit|import|infix|inline|lazy|match|new|null|object|opaque|open|override|package|private|protected|return|sealed|self|super|this|throw|trait|transparent|try|type|using|val|var|while|with|yield)\b/,number:/\b0x(?:[\da-f]*\.)?[\da-f]+|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e\d+)?[dfl]?/i,builtin:/\b(?:Any|AnyRef|AnyVal|Boolean|Byte|Char|Double|Float|Int|Long|Nothing|Short|String|Unit)\b/,symbol:/'[^\d\s\\]\w*/}),Prism.languages.insertBefore("scala","triple-quoted-string",{"string-interpolation":{pattern:/\b[a-z]\w*(?:"""(?:[^$]|\$(?:[^{]|\{(?:[^{}]|\{[^{}]*\})*\}))*?"""|"(?:[^$"\r\n]|\$(?:[^{]|\{(?:[^{}]|\{[^{}]*\})*\}))*")/i,greedy:!0,inside:{id:{pattern:/^\w+/,greedy:!0,alias:"function"},escape:{pattern:/\\\$"|\$[$"]/,greedy:!0,alias:"symbol"},interpolation:{pattern:/\$(?:\w+|\{(?:[^{}]|\{[^{}]*\})*\})/,greedy:!0,inside:{punctuation:/^\$\{?|\}$/,expression:{pattern:/[\s\S]+/,inside:Prism.languages.scala}}},string:/[\s\S]+/}}}),delete Prism.languages.scala["class-name"],delete Prism.languages.scala.function,delete Prism.languages.scala.constant},5266:function(){Prism.languages.sql={comment:{pattern:/(^|[^\\])(?:\/\*[\s\S]*?\*\/|(?:--|\/\/|#).*)/,lookbehind:!0},variable:[{pattern:/@(["'`])(?:\\[\s\S]|(?!\1)[^\\])+\1/,greedy:!0},/@[\w.$]+/],string:{pattern:/(^|[^@\\])("|')(?:\\[\s\S]|(?!\2)[^\\]|\2\2)*\2/,greedy:!0,lookbehind:!0},identifier:{pattern:/(^|[^@\\])`(?:\\[\s\S]|[^`\\]|``)*`/,greedy:!0,lookbehind:!0,inside:{punctuation:/^`|`$/}},function:/\b(?:AVG|COUNT|FIRST|FORMAT|LAST|LCASE|LEN|MAX|MID|MIN|MOD|NOW|ROUND|SUM|UCASE)(?=\s*\()/i,keyword:/\b(?:ACTION|ADD|AFTER|ALGORITHM|ALL|ALTER|ANALYZE|ANY|APPLY|AS|ASC|AUTHORIZATION|AUTO_INCREMENT|BACKUP|BDB|BEGIN|BERKELEYDB|BIGINT|BINARY|BIT|BLOB|BOOL|BOOLEAN|BREAK|BROWSE|BTREE|BULK|BY|CALL|CASCADED?|CASE|CHAIN|CHAR(?:ACTER|SET)?|CHECK(?:POINT)?|CLOSE|CLUSTERED|COALESCE|COLLATE|COLUMNS?|COMMENT|COMMIT(?:TED)?|COMPUTE|CONNECT|CONSISTENT|CONSTRAINT|CONTAINS(?:TABLE)?|CONTINUE|CONVERT|CREATE|CROSS|CURRENT(?:_DATE|_TIME|_TIMESTAMP|_USER)?|CURSOR|CYCLE|DATA(?:BASES?)?|DATE(?:TIME)?|DAY|DBCC|DEALLOCATE|DEC|DECIMAL|DECLARE|DEFAULT|DEFINER|DELAYED|DELETE|DELIMITERS?|DENY|DESC|DESCRIBE|DETERMINISTIC|DISABLE|DISCARD|DISK|DISTINCT|DISTINCTROW|DISTRIBUTED|DO|DOUBLE|DROP|DUMMY|DUMP(?:FILE)?|DUPLICATE|ELSE(?:IF)?|ENABLE|ENCLOSED|END|ENGINE|ENUM|ERRLVL|ERRORS|ESCAPED?|EXCEPT|EXEC(?:UTE)?|EXISTS|EXIT|EXPLAIN|EXTENDED|FETCH|FIELDS|FILE|FILLFACTOR|FIRST|FIXED|FLOAT|FOLLOWING|FOR(?: EACH ROW)?|FORCE|FOREIGN|FREETEXT(?:TABLE)?|FROM|FULL|FUNCTION|GEOMETRY(?:COLLECTION)?|GLOBAL|GOTO|GRANT|GROUP|HANDLER|HASH|HAVING|HOLDLOCK|HOUR|IDENTITY(?:COL|_INSERT)?|IF|IGNORE|IMPORT|INDEX|INFILE|INNER|INNODB|INOUT|INSERT|INT|INTEGER|INTERSECT|INTERVAL|INTO|INVOKER|ISOLATION|ITERATE|JOIN|KEYS?|KILL|LANGUAGE|LAST|LEAVE|LEFT|LEVEL|LIMIT|LINENO|LINES|LINESTRING|LOAD|LOCAL|LOCK|LONG(?:BLOB|TEXT)|LOOP|MATCH(?:ED)?|MEDIUM(?:BLOB|INT|TEXT)|MERGE|MIDDLEINT|MINUTE|MODE|MODIFIES|MODIFY|MONTH|MULTI(?:LINESTRING|POINT|POLYGON)|NATIONAL|NATURAL|NCHAR|NEXT|NO|NONCLUSTERED|NULLIF|NUMERIC|OFF?|OFFSETS?|ON|OPEN(?:DATASOURCE|QUERY|ROWSET)?|OPTIMIZE|OPTION(?:ALLY)?|ORDER|OUT(?:ER|FILE)?|OVER|PARTIAL|PARTITION|PERCENT|PIVOT|PLAN|POINT|POLYGON|PRECEDING|PRECISION|PREPARE|PREV|PRIMARY|PRINT|PRIVILEGES|PROC(?:EDURE)?|PUBLIC|PURGE|QUICK|RAISERROR|READS?|REAL|RECONFIGURE|REFERENCES|RELEASE|RENAME|REPEAT(?:ABLE)?|REPLACE|REPLICATION|REQUIRE|RESIGNAL|RESTORE|RESTRICT|RETURN(?:ING|S)?|REVOKE|RIGHT|ROLLBACK|ROUTINE|ROW(?:COUNT|GUIDCOL|S)?|RTREE|RULE|SAVE(?:POINT)?|SCHEMA|SECOND|SELECT|SERIAL(?:IZABLE)?|SESSION(?:_USER)?|SET(?:USER)?|SHARE|SHOW|SHUTDOWN|SIMPLE|SMALLINT|SNAPSHOT|SOME|SONAME|SQL|START(?:ING)?|STATISTICS|STATUS|STRIPED|SYSTEM_USER|TABLES?|TABLESPACE|TEMP(?:ORARY|TABLE)?|TERMINATED|TEXT(?:SIZE)?|THEN|TIME(?:STAMP)?|TINY(?:BLOB|INT|TEXT)|TOP?|TRAN(?:SACTIONS?)?|TRIGGER|TRUNCATE|TSEQUAL|TYPES?|UNBOUNDED|UNCOMMITTED|UNDEFINED|UNION|UNIQUE|UNLOCK|UNPIVOT|UNSIGNED|UPDATE(?:TEXT)?|USAGE|USE|USER|USING|VALUES?|VAR(?:BINARY|CHAR|CHARACTER|YING)|VIEW|WAITFOR|WARNINGS|WHEN|WHERE|WHILE|WITH(?: ROLLUP|IN)?|WORK|WRITE(?:TEXT)?|YEAR)\b/i,boolean:/\b(?:FALSE|NULL|TRUE)\b/i,number:/\b0x[\da-f]+\b|\b\d+(?:\.\d*)?|\B\.\d+\b/i,operator:/[-+*\/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?|\b(?:AND|BETWEEN|DIV|ILIKE|IN|IS|LIKE|NOT|OR|REGEXP|RLIKE|SOUNDS LIKE|XOR)\b/i,punctuation:/[;[\]()`,.]/}},874:function(){Prism.languages.swift={comment:{pattern:/(^|[^\\:])(?:\/\/.*|\/\*(?:[^/*]|\/(?!\*)|\*(?!\/)|\/\*(?:[^*]|\*(?!\/))*\*\/)*\*\/)/,lookbehind:!0,greedy:!0},"string-literal":[{pattern:RegExp(/(^|[^"#])/.source+"(?:"+/"(?:\\(?:\((?:[^()]|\([^()]*\))*\)|\r\n|[^(])|[^\\\r\n"])*"/.source+"|"+/"""(?:\\(?:\((?:[^()]|\([^()]*\))*\)|[^(])|[^\\"]|"(?!""))*"""/.source+")"+/(?!["#])/.source),lookbehind:!0,greedy:!0,inside:{interpolation:{pattern:/(\\\()(?:[^()]|\([^()]*\))*(?=\))/,lookbehind:!0,inside:null},"interpolation-punctuation":{pattern:/^\)|\\\($/,alias:"punctuation"},punctuation:/\\(?=[\r\n])/,string:/[\s\S]+/}},{pattern:RegExp(/(^|[^"#])(#+)/.source+"(?:"+/"(?:\\(?:#+\((?:[^()]|\([^()]*\))*\)|\r\n|[^#])|[^\\\r\n])*?"/.source+"|"+/"""(?:\\(?:#+\((?:[^()]|\([^()]*\))*\)|[^#])|[^\\])*?"""/.source+")\\2"),lookbehind:!0,greedy:!0,inside:{interpolation:{pattern:/(\\#+\()(?:[^()]|\([^()]*\))*(?=\))/,lookbehind:!0,inside:null},"interpolation-punctuation":{pattern:/^\)|\\#+\($/,alias:"punctuation"},string:/[\s\S]+/}}],directive:{pattern:RegExp(/#/.source+"(?:"+/(?:elseif|if)\b/.source+"(?:[ \t]*"+/(?:![ \t]*)?(?:\b\w+\b(?:[ \t]*\((?:[^()]|\([^()]*\))*\))?|\((?:[^()]|\([^()]*\))*\))(?:[ \t]*(?:&&|\|\|))?/.source+")+|"+/(?:else|endif)\b/.source+")"),alias:"property",inside:{"directive-name":/^#\w+/,boolean:/\b(?:false|true)\b/,number:/\b\d+(?:\.\d+)*\b/,operator:/!|&&|\|\||[<>]=?/,punctuation:/[(),]/}},literal:{pattern:/#(?:colorLiteral|column|dsohandle|file(?:ID|Literal|Path)?|function|imageLiteral|line)\b/,alias:"constant"},"other-directive":{pattern:/#\w+\b/,alias:"property"},attribute:{pattern:/@\w+/,alias:"atrule"},"function-definition":{pattern:/(\bfunc\s+)\w+/,lookbehind:!0,alias:"function"},label:{pattern:/\b(break|continue)\s+\w+|\b[a-zA-Z_]\w*(?=\s*:\s*(?:for|repeat|while)\b)/,lookbehind:!0,alias:"important"},keyword:/\b(?:Any|Protocol|Self|Type|actor|as|assignment|associatedtype|associativity|async|await|break|case|catch|class|continue|convenience|default|defer|deinit|didSet|do|dynamic|else|enum|extension|fallthrough|fileprivate|final|for|func|get|guard|higherThan|if|import|in|indirect|infix|init|inout|internal|is|isolated|lazy|left|let|lowerThan|mutating|none|nonisolated|nonmutating|open|operator|optional|override|postfix|precedencegroup|prefix|private|protocol|public|repeat|required|rethrows|return|right|safe|self|set|some|static|struct|subscript|super|switch|throw|throws|try|typealias|unowned|unsafe|var|weak|where|while|willSet)\b/,boolean:/\b(?:false|true)\b/,nil:{pattern:/\bnil\b/,alias:"constant"},"short-argument":/\$\d+\b/,omit:{pattern:/\b_\b/,alias:"keyword"},number:/\b(?:[\d_]+(?:\.[\de_]+)?|0x[a-f0-9_]+(?:\.[a-f0-9p_]+)?|0b[01_]+|0o[0-7_]+)\b/i,"class-name":/\b[A-Z](?:[A-Z_\d]*[a-z]\w*)?\b/,function:/\b[a-z_]\w*(?=\s*\()/i,constant:/\b(?:[A-Z_]{2,}|k[A-Z][A-Za-z_]+)\b/,operator:/[-+*/%=!<>&|^~?]+|\.[.\-+*/%=!<>&|^~?]+/,punctuation:/[{}[\]();,.:\\]/},Prism.languages.swift["string-literal"].forEach((function(e){e.inside.interpolation.inside=Prism.languages.swift}))},3358:function(){!function(e){var t=/[*&][^\s[\]{},]+/,n=/!(?:<[\w\-%#;/?:@&=+$,.!~*'()[\]]+>|(?:[a-zA-Z\d-]*!)?[\w\-%#;/?:@&=+$.~*'()]+)?/,r="(?:"+n.source+"(?:[ \t]+"+t.source+")?|"+t.source+"(?:[ \t]+"+n.source+")?)",i=/(?:[^\s\x00-\x08\x0e-\x1f!"#%&'*,\-:>?@[\]`{|}\x7f-\x84\x86-\x9f\ud800-\udfff\ufffe\uffff]|[?:-])(?:[ \t]*(?:(?![#:])|:))*/.source.replace(//g,(function(){return/[^\s\x00-\x08\x0e-\x1f,[\]{}\x7f-\x84\x86-\x9f\ud800-\udfff\ufffe\uffff]/.source})),o=/"(?:[^"\\\r\n]|\\.)*"|'(?:[^'\\\r\n]|\\.)*'/.source;function a(e,t){t=(t||"").replace(/m/g,"")+"m";var n=/([:\-,[{]\s*(?:\s<>[ \t]+)?)(?:<>)(?=[ \t]*(?:$|,|\]|\}|(?:[\r\n]\s*)?#))/.source.replace(/<>/g,(function(){return r})).replace(/<>/g,(function(){return e}));return RegExp(n,t)}e.languages.yaml={scalar:{pattern:RegExp(/([\-:]\s*(?:\s<>[ \t]+)?[|>])[ \t]*(?:((?:\r?\n|\r)[ \t]+)\S[^\r\n]*(?:\2[^\r\n]+)*)/.source.replace(/<>/g,(function(){return r}))),lookbehind:!0,alias:"string"},comment:/#.*/,key:{pattern:RegExp(/((?:^|[:\-,[{\r\n?])[ \t]*(?:<>[ \t]+)?)<>(?=\s*:\s)/.source.replace(/<>/g,(function(){return r})).replace(/<>/g,(function(){return"(?:"+i+"|"+o+")"}))),lookbehind:!0,greedy:!0,alias:"atrule"},directive:{pattern:/(^[ \t]*)%.+/m,lookbehind:!0,alias:"important"},datetime:{pattern:a(/\d{4}-\d\d?-\d\d?(?:[tT]|[ \t]+)\d\d?:\d{2}:\d{2}(?:\.\d*)?(?:[ \t]*(?:Z|[-+]\d\d?(?::\d{2})?))?|\d{4}-\d{2}-\d{2}|\d\d?:\d{2}(?::\d{2}(?:\.\d*)?)?/.source),lookbehind:!0,alias:"number"},boolean:{pattern:a(/false|true/.source,"i"),lookbehind:!0,alias:"important"},null:{pattern:a(/null|~/.source,"i"),lookbehind:!0,alias:"important"},string:{pattern:a(o),lookbehind:!0,greedy:!0},number:{pattern:a(/[+-]?(?:0x[\da-f]+|0o[0-7]+|(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?|\.inf|\.nan)/.source,"i"),lookbehind:!0},tag:n,important:t,punctuation:/---|[:[\]{}\-,|>?]|\.\.\./},e.languages.yml=e.languages.yaml}(Prism)},5660:function(e,t,n){var r=function(e){var t=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,n=0,r={},i={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(t){return t instanceof o?new o(t.type,e(t.content),t.alias):Array.isArray(t)?t.map(e):t.replace(/&/g,"&").replace(/=p.reach);S+=k.value.length,k=k.next){var O=k.value;if(t.length>e.length)return;if(!(O instanceof o)){var E,_=1;if(b){if(!(E=a(w,S,e,y))||E.index>=e.length)break;var A=E.index,j=E.index+E[0].length,C=S;for(C+=k.value.length;A>=C;)C+=(k=k.next).value.length;if(S=C-=k.value.length,k.value instanceof o)continue;for(var P=k;P!==t.tail&&(Cp.reach&&(p.reach=N);var $=k.prev;if(I&&($=c(t,$,I),S+=I.length),u(t,$,_),k=c(t,$,new o(d,g?i.tokenize(T,g):T,v,T)),R&&c(t,k,R),_>1){var L={cause:d+","+h,reach:N};s(e,t,n,k.prev,S,L),p&&L.reach>p.reach&&(p.reach=L.reach)}}}}}}function l(){var e={value:null,prev:null,next:null},t={value:null,prev:e,next:null};e.next=t,this.head=e,this.tail=t,this.length=0}function c(e,t,n){var r=t.next,i={value:n,prev:t,next:r};return t.next=i,r.prev=i,e.length++,i}function u(e,t,n){for(var r=t.next,i=0;i"+o.content+""},!e.document)return e.addEventListener?(i.disableWorkerMessageHandler||e.addEventListener("message",(function(t){var n=JSON.parse(t.data),r=n.language,o=n.code,a=n.immediateClose;e.postMessage(i.highlight(o,i.languages[r],r)),a&&e.close()}),!1),i):i;var p=i.util.currentScript();function d(){i.manual||i.highlightAll()}if(p&&(i.filename=p.src,p.hasAttribute("data-manual")&&(i.manual=!0)),!i.manual){var f=document.readyState;"loading"===f||"interactive"===f&&p&&p.defer?document.addEventListener("DOMContentLoaded",d):window.requestAnimationFrame?window.requestAnimationFrame(d):window.setTimeout(d,16)}return i}("undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{});e.exports&&(e.exports=r),void 0!==n.g&&(n.g.Prism=r),r.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},r.languages.markup.tag.inside["attr-value"].inside.entity=r.languages.markup.entity,r.languages.markup.doctype.inside["internal-subset"].inside=r.languages.markup,r.hooks.add("wrap",(function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))})),Object.defineProperty(r.languages.markup.tag,"addInlined",{value:function(e,t){var n={};n["language-"+t]={pattern:/(^$)/i,lookbehind:!0,inside:r.languages[t]},n.cdata=/^$/i;var i={"included-cdata":{pattern://i,inside:n}};i["language-"+t]={pattern:/[\s\S]+/,inside:r.languages[t]};var o={};o[e]={pattern:RegExp(/(<__[^>]*>)(?:))*\]\]>|(?!)/.source.replace(/__/g,(function(){return e})),"i"),lookbehind:!0,greedy:!0,inside:i},r.languages.insertBefore("markup","cdata",o)}}),Object.defineProperty(r.languages.markup.tag,"addAttribute",{value:function(e,t){r.languages.markup.tag.inside["special-attr"].push({pattern:RegExp(/(^|["'\s])/.source+"(?:"+e+")"+/\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))/.source,"i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[t,"language-"+t],inside:r.languages[t]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),r.languages.html=r.languages.markup,r.languages.mathml=r.languages.markup,r.languages.svg=r.languages.markup,r.languages.xml=r.languages.extend("markup",{}),r.languages.ssml=r.languages.xml,r.languages.atom=r.languages.xml,r.languages.rss=r.languages.xml,function(e){var t=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;e.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:"+/[^;{\s"']|\s+(?!\s)/.source+"|"+t.source+")*?"+/(?:;|(?=\s*\{))/.source),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+t.source+"|"+/(?:[^\\\r\n()"']|\\[\s\S])*/.source+")\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+t.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+t.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:t,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},e.languages.css.atrule.inside.rest=e.languages.css;var n=e.languages.markup;n&&(n.tag.addInlined("style","css"),n.tag.addAttribute("style","css"))}(r),r.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/},r.languages.javascript=r.languages.extend("clike",{"class-name":[r.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp(/(^|[^\w$])/.source+"(?:"+/NaN|Infinity/.source+"|"+/0[bB][01]+(?:_[01]+)*n?/.source+"|"+/0[oO][0-7]+(?:_[0-7]+)*n?/.source+"|"+/0[xX][\dA-Fa-f]+(?:_[\dA-Fa-f]+)*n?/.source+"|"+/\d+(?:_\d+)*n/.source+"|"+/(?:\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\.\d+(?:_\d+)*)(?:[Ee][+-]?\d+(?:_\d+)*)?/.source+")"+/(?![\w$])/.source),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),r.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,r.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp(/((?:^|[^$\w\xA0-\uFFFF."'\])\s]|\b(?:return|yield))\s*)/.source+/\//.source+"(?:"+/(?:\[(?:[^\]\\\r\n]|\\.)*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}/.source+"|"+/(?:\[(?:[^[\]\\\r\n]|\\.|\[(?:[^[\]\\\r\n]|\\.|\[(?:[^[\]\\\r\n]|\\.)*\])*\])*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}v[dgimyus]{0,7}/.source+")"+/(?=(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/.source),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:r.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:r.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:r.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:r.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:r.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),r.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:r.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),r.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),r.languages.markup&&(r.languages.markup.tag.addInlined("script","javascript"),r.languages.markup.tag.addAttribute(/on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)/.source,"javascript")),r.languages.js=r.languages.javascript,function(){if(void 0!==r&&"undefined"!=typeof document){Element.prototype.matches||(Element.prototype.matches=Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector);var e={js:"javascript",py:"python",rb:"ruby",ps1:"powershell",psm1:"powershell",sh:"bash",bat:"batch",h:"c",tex:"latex"},t="data-src-status",n="loading",i="loaded",o="pre[data-src]:not(["+t+'="'+i+'"]):not(['+t+'="'+n+'"])';r.hooks.add("before-highlightall",(function(e){e.selector+=", "+o})),r.hooks.add("before-sanity-check",(function(a){var s=a.element;if(s.matches(o)){a.code="",s.setAttribute(t,n);var l=s.appendChild(document.createElement("CODE"));l.textContent="Loading…";var c=s.getAttribute("data-src"),u=a.language;if("none"===u){var p=(/\.(\w+)$/.exec(c)||[,"none"])[1];u=e[p]||p}r.util.setLanguage(l,u),r.util.setLanguage(s,u);var d=r.plugins.autoloader;d&&d.loadLanguages(u),function(e,n,o){var a=new XMLHttpRequest;a.open("GET",e,!0),a.onreadystatechange=function(){4==a.readyState&&(a.status<400&&a.responseText?function(e){s.setAttribute(t,i);var n=function(e){var t=/^\s*(\d+)\s*(?:(,)\s*(?:(\d+)\s*)?)?$/.exec(e||"");if(t){var n=Number(t[1]),r=t[2],i=t[3];return r?i?[n,Number(i)]:[n,void 0]:[n,n]}}(s.getAttribute("data-range"));if(n){var o=e.split(/\r\n?|\n/g),a=n[0],c=null==n[1]?o.length:n[1];a<0&&(a+=o.length),a=Math.max(0,Math.min(a-1,o.length)),c<0&&(c+=o.length),c=Math.max(0,Math.min(c,o.length)),e=o.slice(a,c).join("\n"),s.hasAttribute("data-start")||s.setAttribute("data-start",String(a+1))}l.textContent=e,r.highlightElement(l)}(a.responseText):a.status>=400?o("✖ Error "+a.status+" while fetching file: "+a.statusText):o("✖ Error: File does not exist or is empty"))},a.send(null)}(c,0,(function(e){s.setAttribute(t,"failed"),l.textContent=e}))}})),r.plugins.fileHighlight={highlight:function(e){for(var t,n=(e||document).querySelectorAll(o),i=0;t=n[i++];)r.highlightElement(t)}};var a=!1;r.fileHighlight=function(){a||(console.warn("Prism.fileHighlight is deprecated. Use `Prism.plugins.fileHighlight.highlight` instead."),a=!0),r.plugins.fileHighlight.highlight.apply(this,arguments)}}}()},2703:function(e,t,n){"use strict";var r=n(414);function i(){}function o(){}o.resetWarningCache=i,e.exports=function(){function e(e,t,n,i,o,a){if(a!==r){var s=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw s.name="Invariant Violation",s}}function t(){return e}e.isRequired=e;var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:o,resetWarningCache:i};return n.PropTypes=n,n}},5697:function(e,t,n){e.exports=n(2703)()},414:function(e){"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},4448:function(e,t,n){"use strict";var r=n(7294),i=n(4142);function o(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n