feat: 合并Gin_Vue
This commit is contained in:
119
src/app.go
Normal file
119
src/app.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package src
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"ems.agt/src/framework/config"
|
||||
"ems.agt/src/framework/errorcatch"
|
||||
"ems.agt/src/framework/middleware"
|
||||
"ems.agt/src/framework/middleware/security"
|
||||
"ems.agt/src/modules/common"
|
||||
"ems.agt/src/modules/monitor"
|
||||
"ems.agt/src/modules/system"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 路由函数句柄,交给由 http.ListenAndServe(addr, router)
|
||||
func AppEngine() *gin.Engine {
|
||||
app := initAppEngine()
|
||||
|
||||
// 初始全局默认
|
||||
initDefeat(app)
|
||||
|
||||
// 初始模块路由
|
||||
initModulesRoute(app)
|
||||
|
||||
// 读取服务配置
|
||||
app.ForwardedByClientIP = config.Get("server.proxy").(bool)
|
||||
addr := fmt.Sprintf(":%d", config.Get("server.port").(int))
|
||||
|
||||
// 启动服务
|
||||
fmt.Printf("\nopen http://localhost%s \n\n", addr)
|
||||
return app
|
||||
}
|
||||
|
||||
// 运行服务程序 main.go
|
||||
//
|
||||
// func main() {
|
||||
// src.ConfigurationInit()
|
||||
// if err := src.RunServer(); err != nil {
|
||||
// src.ConfigurationClose()
|
||||
// }
|
||||
// }
|
||||
func RunServer() error {
|
||||
app := initAppEngine()
|
||||
|
||||
// 初始全局默认
|
||||
initDefeat(app)
|
||||
|
||||
// 初始模块路由
|
||||
initModulesRoute(app)
|
||||
|
||||
// 读取服务配置
|
||||
app.ForwardedByClientIP = config.Get("server.proxy").(bool)
|
||||
addr := fmt.Sprintf(":%d", config.Get("server.port").(int))
|
||||
|
||||
// 启动服务
|
||||
fmt.Printf("\nopen http://localhost%s \n\n", addr)
|
||||
return app.Run(addr)
|
||||
}
|
||||
|
||||
// 初始应用引擎
|
||||
func initAppEngine() *gin.Engine {
|
||||
var app *gin.Engine
|
||||
|
||||
// 禁止控制台日志输出的颜色
|
||||
gin.DisableConsoleColor()
|
||||
|
||||
// 根据运行环境注册引擎
|
||||
if config.Env() == "prod" {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
app = gin.New()
|
||||
app.Use(gin.Recovery())
|
||||
} else {
|
||||
app = gin.Default()
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
// 初始全局默认
|
||||
func initDefeat(app *gin.Engine) {
|
||||
// 全局中间件
|
||||
app.Use(errorcatch.ErrorCatch(), middleware.Report(), middleware.Cors(), security.Security())
|
||||
|
||||
// 静态目录-静态资源
|
||||
if v := config.Get("staticFile.default"); v != nil {
|
||||
fsMap := v.(map[string]any)
|
||||
prefix, dir := fsMap["prefix"], fsMap["dir"]
|
||||
if prefix != nil && dir != nil {
|
||||
app.StaticFS(prefix.(string), gin.Dir(dir.(string), true))
|
||||
}
|
||||
}
|
||||
|
||||
// 静态目录-上传资源
|
||||
if v := config.Get("staticFile.upload"); v != nil {
|
||||
fsMap := v.(map[string]any)
|
||||
prefix, dir := fsMap["prefix"], fsMap["dir"]
|
||||
if prefix != nil && dir != nil {
|
||||
app.StaticFS(prefix.(string), gin.Dir(dir.(string), true))
|
||||
}
|
||||
}
|
||||
|
||||
// 路由未找到时
|
||||
app.NoRoute(func(c *gin.Context) {
|
||||
c.JSON(404, gin.H{
|
||||
"code": 404,
|
||||
"msg": fmt.Sprintf("%s Not Found", c.Request.RequestURI),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 初始模块路由
|
||||
func initModulesRoute(app *gin.Engine) {
|
||||
|
||||
common.Setup(app)
|
||||
monitor.Setup(app)
|
||||
system.Setup(app)
|
||||
}
|
||||
35
src/configuration.go
Normal file
35
src/configuration.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package src
|
||||
|
||||
import (
|
||||
"ems.agt/src/framework/config"
|
||||
"ems.agt/src/framework/cron"
|
||||
"ems.agt/src/framework/datasource"
|
||||
"ems.agt/src/framework/logger"
|
||||
"ems.agt/src/framework/redis"
|
||||
)
|
||||
|
||||
// 配置中心初始加载
|
||||
func ConfigurationInit() {
|
||||
// 初始配置参数
|
||||
config.InitConfig()
|
||||
// 初始程序日志
|
||||
logger.InitLogger()
|
||||
// 连接数据库实例
|
||||
datasource.Connect()
|
||||
// 连接Redis实例
|
||||
redis.Connect()
|
||||
// 启动调度任务实例
|
||||
cron.StartCron()
|
||||
}
|
||||
|
||||
// 配置中心相关配置关闭连接
|
||||
func ConfigurationClose() {
|
||||
// 停止调度任务实例
|
||||
cron.StopCron()
|
||||
// 关闭Redis实例
|
||||
redis.Close()
|
||||
// 关闭数据库实例
|
||||
datasource.Close()
|
||||
// 关闭程序日志
|
||||
logger.Close()
|
||||
}
|
||||
163
src/framework/config/config.go
Normal file
163
src/framework/config/config.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
libConfig "ems.agt/src/lib_features/config"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
//go:embed config/*.yaml
|
||||
var configFiles embed.FS
|
||||
|
||||
// 初始化程序配置
|
||||
func InitConfig() {
|
||||
initFlag()
|
||||
initViper()
|
||||
}
|
||||
|
||||
// 指定参数绑定
|
||||
func initFlag() {
|
||||
// --env prod
|
||||
pflag.String("env", "prod", "Specify Run Environment Configuration local or prod")
|
||||
// --c /etc/restconf.yaml
|
||||
// -c /etc/restconf.yaml
|
||||
pConfig := pflag.StringP("config", "c", "./etc/restconf.yaml", "Specify Configuration File")
|
||||
// --version
|
||||
// -V
|
||||
pVersion := pflag.BoolP("version", "V", false, "Output program version")
|
||||
// --help
|
||||
pHelp := pflag.Bool("help", false, "Viewing Help Commands")
|
||||
|
||||
pflag.Parse()
|
||||
|
||||
// 参数固定输出
|
||||
if *pVersion {
|
||||
buildInfo := libConfig.BuildInfo()
|
||||
fmt.Println(buildInfo)
|
||||
os.Exit(1)
|
||||
}
|
||||
if *pHelp {
|
||||
pflag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 外层lib和features使用的配置
|
||||
libConfig.ConfigRead(*pConfig)
|
||||
|
||||
viper.BindPFlags(pflag.CommandLine)
|
||||
}
|
||||
|
||||
// 配置文件读取
|
||||
func initViper() {
|
||||
// 在当前工作目录中寻找配置
|
||||
// viper.AddConfigPath("config")
|
||||
// viper.AddConfigPath("src/config")
|
||||
// 如果配置文件名中没有扩展名,则需要设置Type
|
||||
viper.SetConfigType("yaml")
|
||||
|
||||
// 从 embed.FS 中读取默认配置文件内容
|
||||
configDefault, err := configFiles.ReadFile("config/config.default.yaml")
|
||||
if err != nil {
|
||||
log.Fatalf("ReadFile config default file: %s", err)
|
||||
return
|
||||
}
|
||||
// 设置默认配置文件内容到 viper
|
||||
err = viper.ReadConfig(bytes.NewReader(configDefault))
|
||||
if err != nil {
|
||||
log.Fatalf("NewReader config default file: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// // 配置文件的名称(无扩展名)
|
||||
// viper.SetConfigName("config.default")
|
||||
// // 读取默认配置文件
|
||||
// if err := viper.ReadInConfig(); err != nil {
|
||||
// log.Fatalf("fatal error config default file: %s", err)
|
||||
// }
|
||||
|
||||
env := viper.GetString("env")
|
||||
if env != "local" && env != "prod" {
|
||||
log.Fatalf("fatal error config env for local or prod : %s", env)
|
||||
}
|
||||
log.Printf("Current service environment operation configuration => %s \n", env)
|
||||
|
||||
// 加载运行配置文件合并相同配置
|
||||
if env == "prod" {
|
||||
// viper.SetConfigName("config.prod")
|
||||
// 从 embed.FS 中读取默认配置文件内容
|
||||
configProd, err := configFiles.ReadFile("config/config.prod.yaml")
|
||||
if err != nil {
|
||||
log.Fatalf("ReadFile config prod file: %s", err)
|
||||
return
|
||||
}
|
||||
// 设置默认配置文件内容到 viper
|
||||
err = viper.MergeConfig(bytes.NewReader(configProd))
|
||||
if err != nil {
|
||||
log.Fatalf("NewReader config prod file: %s", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// viper.SetConfigName("config.local")
|
||||
// 从 embed.FS 中读取默认配置文件内容
|
||||
configLocal, err := configFiles.ReadFile("config/config.local.yaml")
|
||||
if err != nil {
|
||||
log.Fatalf("ReadFile config local file: %s", err)
|
||||
return
|
||||
}
|
||||
// 设置默认配置文件内容到 viper
|
||||
err = viper.MergeConfig(bytes.NewReader(configLocal))
|
||||
if err != nil {
|
||||
log.Fatalf("NewReader config local file: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
// if err := viper.MergeInConfig(); err != nil {
|
||||
// log.Fatalf("fatal error config MergeInConfig: %s", err)
|
||||
// }
|
||||
|
||||
// 合并外层lib和features使用配置
|
||||
libConfig.ConfigInMerge()
|
||||
|
||||
// 记录程序开始运行的时间点
|
||||
viper.Set("runTime", time.Now())
|
||||
}
|
||||
|
||||
// Env 获取运行服务环境
|
||||
// local prod
|
||||
func Env() string {
|
||||
return viper.GetString("env")
|
||||
}
|
||||
|
||||
// RunTime 程序开始运行的时间
|
||||
func RunTime() time.Time {
|
||||
return viper.GetTime("runTime")
|
||||
}
|
||||
|
||||
// Get 获取配置信息
|
||||
//
|
||||
// Get("framework.name")
|
||||
func Get(key string) any {
|
||||
return viper.Get(key)
|
||||
}
|
||||
|
||||
// IsAdmin 用户是否为管理员
|
||||
func IsAdmin(userID string) bool {
|
||||
if userID == "" {
|
||||
return false
|
||||
}
|
||||
// 从本地配置获取user信息
|
||||
admins := Get("user.adminList").([]any)
|
||||
for _, s := range admins {
|
||||
if s.(string) == userID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
208
src/framework/config/config/config.default.yaml
Normal file
208
src/framework/config/config/config.default.yaml
Normal file
@@ -0,0 +1,208 @@
|
||||
# 项目信息
|
||||
framework:
|
||||
name: "ems_agt"
|
||||
version: "1.6.2"
|
||||
|
||||
# 应用服务配置
|
||||
server:
|
||||
# 服务端口
|
||||
port: 3040
|
||||
# 是否开启代理
|
||||
proxy: false
|
||||
|
||||
# 日志
|
||||
logger:
|
||||
fileDir: "/usr/local/omc/logs"
|
||||
fileName: "ems_agt.log"
|
||||
level: 2 # 日志记录的等级 0:silent<1:info<2:warn<3:error
|
||||
maxDay: 30 # 日志会保留 30 天
|
||||
maxSize: 10 # 调整按 10MB 大小的切割
|
||||
|
||||
# 静态文件配置, 相对项目根路径或填绝对路径
|
||||
staticFile:
|
||||
# 默认资源,dir目录需要预先创建
|
||||
default:
|
||||
prefix: "/static"
|
||||
dir: "/usr/local/omc/static"
|
||||
# 文件上传资源目录映射,与项目目录同级
|
||||
upload:
|
||||
prefix: "/upload"
|
||||
dir: "/usr/local/omc/upload"
|
||||
|
||||
# 文件上传
|
||||
upload:
|
||||
# 最大上传文件大小,默认为 10mb
|
||||
fileSize: 10
|
||||
# 文件扩展名白名单
|
||||
whitelist:
|
||||
# 图片
|
||||
- ".bmp"
|
||||
- ".webp"
|
||||
- ".gif"
|
||||
- ".jpg"
|
||||
- ".jpeg"
|
||||
- ".png"
|
||||
# word excel powerpoint
|
||||
- ".doc"
|
||||
- ".docx"
|
||||
- ".xls"
|
||||
- ".xlsx"
|
||||
- ".ppt"
|
||||
- ".pptx"
|
||||
# 文本文件
|
||||
- ".html"
|
||||
- ".htm"
|
||||
- ".txt"
|
||||
# pdf
|
||||
- ".pdf"
|
||||
# 压缩文件
|
||||
- ".zip"
|
||||
- ".gz"
|
||||
- ".tgz"
|
||||
- ".gzip"
|
||||
# 音视频格式
|
||||
- ".mp3"
|
||||
- ".mp4"
|
||||
- ".avi"
|
||||
- ".rmvb"
|
||||
|
||||
# cors 跨域
|
||||
cors:
|
||||
# 设置 Access-Control-Allow-Origin 的值,【默认值】会获取请求头上的 origin
|
||||
# 例如:http://mask-api.org
|
||||
# 如果请求设置了 credentials,则 origin 不能设置为 *
|
||||
origin: "*"
|
||||
# 设置 Access-Control-Allow-Credentials,【默认值】false
|
||||
credentials: true
|
||||
# 设置 Access-Control-Max-Age
|
||||
maxAge: 31536000
|
||||
# 允许跨域的方法,【默认值】为 GET,HEAD,PUT,POST,DELETE,PATCH
|
||||
allowMethods:
|
||||
- "OPTIONS"
|
||||
- "HEAD"
|
||||
- "GET"
|
||||
- "POST"
|
||||
- "PUT"
|
||||
- "DELETE"
|
||||
- "PATCH"
|
||||
# 设置 Access-Control-Allow-Headers 的值,【默认值】会获取请求头上的 Access-Control-Request-Headers
|
||||
allowHeaders:
|
||||
- "X-App-Code"
|
||||
- "X-App-Version"
|
||||
- "Authorization"
|
||||
- "Origin"
|
||||
- "X-Requested-With"
|
||||
- "Content-Type"
|
||||
- "Content-Language"
|
||||
- "Accept"
|
||||
- "Range"
|
||||
- "Accesstoken"
|
||||
- "Operationtype"
|
||||
# 设置 Access-Control-Expose-Headers 的值
|
||||
exposeHeaders:
|
||||
- "X-RepeatSubmit-Rest"
|
||||
|
||||
# security 安全
|
||||
security:
|
||||
csrf:
|
||||
enable: false
|
||||
type: "referer"
|
||||
# 允许调用的域名地址的,例如:http://<Referer地址>/mask-api
|
||||
refererWhiteList:
|
||||
- "127.0.0.1:3030"
|
||||
xframe:
|
||||
enable: true
|
||||
value: "SAMEORIGIN"
|
||||
csp:
|
||||
enable: true
|
||||
hsts:
|
||||
enable: false
|
||||
maxAge: 31536000
|
||||
includeSubdomains: false
|
||||
noopen:
|
||||
enable: false
|
||||
nosniff:
|
||||
enable: false
|
||||
xssProtection:
|
||||
enable: true
|
||||
value: "1; mode=block"
|
||||
|
||||
# JWT 令牌配置
|
||||
jwt:
|
||||
# 令牌算法 HS256 HS384 HS512
|
||||
algorithm: "HS512"
|
||||
# 令牌密钥
|
||||
secret: "217a0481c7f9cfe1cb547d32ee012b0f"
|
||||
# 令牌有效期(默认120分钟)
|
||||
expiresIn: 120
|
||||
# 验证令牌有效期,相差不足xx分钟,自动刷新缓存
|
||||
refreshIn: 20
|
||||
|
||||
# GORM 数据源
|
||||
gorm:
|
||||
dataSource:
|
||||
# 默认数据库实例
|
||||
default:
|
||||
type: "mysql"
|
||||
host: "127.0.0.1"
|
||||
port: 3306
|
||||
username: "<用户名>"
|
||||
password: "<密码>"
|
||||
database: "<数据库>"
|
||||
logging: false
|
||||
# 多个数据源时可以用这个指定默认的数据源
|
||||
defaultDataSourceName: "default"
|
||||
|
||||
# Redis 缓存数据
|
||||
redis:
|
||||
dataSource:
|
||||
default:
|
||||
port: 6379 # Redis port
|
||||
host: "127.0.0.1" # Redis host
|
||||
password: "<密码>"
|
||||
db: 0 # Redis db_num
|
||||
# 多个数据源时可以用这个指定默认的数据源
|
||||
defaultDataSourceName: "default"
|
||||
|
||||
# 用户配置
|
||||
user:
|
||||
# 密码
|
||||
password:
|
||||
# 密码最大错误次数
|
||||
maxRetryCount: 5
|
||||
# 密码锁定时间,单位分钟(默认10分钟)
|
||||
lockTime: 10
|
||||
# 管理员列表
|
||||
adminList:
|
||||
- "1"
|
||||
- "2"
|
||||
|
||||
# char 字符验证码配置
|
||||
charCaptcha:
|
||||
# 宽度
|
||||
width: 120
|
||||
# 高度
|
||||
height: 40
|
||||
# 干扰线条的数量
|
||||
noise: 4
|
||||
# 验证码的字符是否有颜色,默认没有,如果设定了背景,则默认有
|
||||
color: true
|
||||
# 验证码图片背景颜色
|
||||
background: "#fafafa"
|
||||
# 验证码长度
|
||||
size: 4
|
||||
# 验证码字符
|
||||
chars: "023456789abcdefghjkmnprstuvwxyz"
|
||||
|
||||
# math 数值计算码配置
|
||||
mathCaptcha:
|
||||
# 宽度
|
||||
width: 120
|
||||
# 高度
|
||||
height: 40
|
||||
# 干扰线条的数量
|
||||
noise: 4
|
||||
# 验证码的字符是否有颜色,默认没有,如果设定了背景,则默认有
|
||||
color: true
|
||||
# 验证码图片背景颜色
|
||||
background: "#fafafa"
|
||||
53
src/framework/config/config/config.local.yaml
Normal file
53
src/framework/config/config/config.local.yaml
Normal file
@@ -0,0 +1,53 @@
|
||||
# 应用服务配置
|
||||
server:
|
||||
port: 3040
|
||||
|
||||
# 日志
|
||||
logger:
|
||||
fileDir: "C:/usr/local/omc/logs"
|
||||
level: 0 # 输出最低等级
|
||||
|
||||
# 静态文件配置, 相对项目根路径或填绝对路径
|
||||
staticFile:
|
||||
default:
|
||||
dir: "C:/usr/local/omc/static"
|
||||
# 文件上传资源目录映射,与项目目录同级
|
||||
upload:
|
||||
dir: "C:/usr/local/omc/upload"
|
||||
|
||||
# security 安全
|
||||
security:
|
||||
csrf:
|
||||
refererWhiteList:
|
||||
- "localhost:3131"
|
||||
- "127.0.0.1:3131"
|
||||
|
||||
# GORM 数据源
|
||||
gorm:
|
||||
dataSource:
|
||||
default:
|
||||
type: "mysql"
|
||||
host: "192.168.0.229"
|
||||
port: 33066
|
||||
username: "root"
|
||||
password: "1000omc@kp!"
|
||||
database: "omc_db_dev"
|
||||
logging: true
|
||||
|
||||
# Redis 缓存数据,数据源声明全小写
|
||||
redis:
|
||||
dataSource:
|
||||
# OMC系统使用库
|
||||
default:
|
||||
port: 6379 # Redis port
|
||||
host: "192.168.0.229" # Redis host
|
||||
password: ""
|
||||
db: 10 # Redis db_num
|
||||
# UDM网元用户库
|
||||
udmuser:
|
||||
port: 6379 # Redis port
|
||||
host: "192.168.0.229"
|
||||
password: ""
|
||||
db: 0 # Redis db_num
|
||||
# 多个数据源时可以用这个指定默认的数据源
|
||||
defaultDataSourceName: "default"
|
||||
32
src/framework/config/config/config.prod.yaml
Normal file
32
src/framework/config/config/config.prod.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
# 应用服务配置
|
||||
server:
|
||||
port: 3040
|
||||
proxy: true
|
||||
|
||||
# security 安全
|
||||
security:
|
||||
csrf:
|
||||
# 允许调用的域名地址的,例如:http://<Referer地址>/
|
||||
refererWhiteList:
|
||||
- "127.0.0.1"
|
||||
- "<Referer地址>"
|
||||
|
||||
# GORM 数据源
|
||||
gorm:
|
||||
dataSource:
|
||||
default:
|
||||
type: "mysql"
|
||||
host: "<mysql地址>"
|
||||
port: 3306
|
||||
username: "<用户名>"
|
||||
password: "<密码>"
|
||||
database: "<数据库>"
|
||||
|
||||
# Redis 缓存数据
|
||||
redis:
|
||||
dataSource:
|
||||
default:
|
||||
port: 6379 # Redis port
|
||||
host: "<redis地址>"
|
||||
password: "<密码>"
|
||||
db: 0 # Redis db_num
|
||||
12
src/framework/constants/admin/admin.go
Normal file
12
src/framework/constants/admin/admin.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package admin
|
||||
|
||||
// 管理员常量信息
|
||||
|
||||
// 管理员-系统指定角色ID
|
||||
const ROLE_ID = "1"
|
||||
|
||||
// 管理员-系统指定角色KEY
|
||||
const ROLE_KEY = "admin"
|
||||
|
||||
// 管理员-系统指定权限
|
||||
const PERMISSION = "*:*:*"
|
||||
24
src/framework/constants/cachekey/cachekey.go
Normal file
24
src/framework/constants/cachekey/cachekey.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package cachekey
|
||||
|
||||
// 缓存的key常量
|
||||
|
||||
// 登录用户
|
||||
const LOGIN_TOKEN_KEY = "login_tokens:"
|
||||
|
||||
// 验证码
|
||||
const CAPTCHA_CODE_KEY = "captcha_codes:"
|
||||
|
||||
// 参数管理
|
||||
const SYS_CONFIG_KEY = "sys_config:"
|
||||
|
||||
// 字典管理
|
||||
const SYS_DICT_KEY = "sys_dict:"
|
||||
|
||||
// 防重提交
|
||||
const REPEAT_SUBMIT_KEY = "repeat_submit:"
|
||||
|
||||
// 限流
|
||||
const RATE_LIMIT_KEY = "rate_limit:"
|
||||
|
||||
// 登录账户密码错误次数
|
||||
const PWD_ERR_CNT_KEY = "pwd_err_cnt:"
|
||||
12
src/framework/constants/captcha/captcha.go
Normal file
12
src/framework/constants/captcha/captcha.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package captcha
|
||||
|
||||
// 验证码常量信息
|
||||
|
||||
// 验证码有效期,单位秒
|
||||
const EXPIRATION = 2 * 60
|
||||
|
||||
// 验证码类型-数值计算
|
||||
const TYPE_CHAR = "char"
|
||||
|
||||
// 验证码类型-字符验证
|
||||
const TYPE_MATH = "math"
|
||||
21
src/framework/constants/common/common.go
Normal file
21
src/framework/constants/common/common.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package common
|
||||
|
||||
// 通用常量信息
|
||||
|
||||
// www主域
|
||||
const WWW = "www."
|
||||
|
||||
// http请求
|
||||
const HTTP = "http://"
|
||||
|
||||
// https请求
|
||||
const HTTPS = "https://"
|
||||
|
||||
// 通用状态标识-正常/成功/是
|
||||
const STATUS_YES = "1"
|
||||
|
||||
// 通用状态标识-停用/失败/否
|
||||
const STATUS_NO = "0"
|
||||
|
||||
// 上下文信息-登录用户
|
||||
const CTX_LOGIN_USER = "loginuser"
|
||||
24
src/framework/constants/menu/menu.go
Normal file
24
src/framework/constants/menu/menu.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package menu
|
||||
|
||||
// 系统菜单常量信息
|
||||
|
||||
const (
|
||||
// 组件布局类型-基础布局组件标识
|
||||
COMPONENT_LAYOUT_BASIC = "BasicLayout"
|
||||
// 组件布局类型-空白布局组件标识
|
||||
COMPONENT_LAYOUT_BLANK = "BlankLayout"
|
||||
// 组件布局类型-内链接布局组件标识
|
||||
COMPONENT_LAYOUT_LINK = "LinkLayout"
|
||||
)
|
||||
|
||||
const (
|
||||
// 菜单类型-目录
|
||||
TYPE_DIR = "D"
|
||||
// 菜单类型-菜单
|
||||
TYPE_MENU = "M"
|
||||
// 菜单类型-按钮
|
||||
TYPE_BUTTON = "B"
|
||||
)
|
||||
|
||||
// 菜单内嵌地址标识-带/前缀
|
||||
const PATH_INLINE = "/inline"
|
||||
15
src/framework/constants/result/result.go
Normal file
15
src/framework/constants/result/result.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package result
|
||||
|
||||
// 响应结果常量信息
|
||||
|
||||
const (
|
||||
// 响应-code错误失败
|
||||
CODE_ERROR = 0
|
||||
// 响应-msg错误失败
|
||||
MSG_ERROR = "error"
|
||||
|
||||
// 响应-msg正常成功
|
||||
CODE_SUCCESS = 1
|
||||
// 响应-code正常成功
|
||||
MSG_SUCCESS = "success"
|
||||
)
|
||||
29
src/framework/constants/roledatascope/roledatascope.go
Normal file
29
src/framework/constants/roledatascope/roledatascope.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package roledatascope
|
||||
|
||||
// 系统角色数据范围常量
|
||||
|
||||
const (
|
||||
// 全部数据权限
|
||||
ALL = "1"
|
||||
|
||||
// 自定数据权限
|
||||
CUSTOM = "2"
|
||||
|
||||
// 部门数据权限
|
||||
DEPT = "3"
|
||||
|
||||
// 部门及以下数据权限
|
||||
DEPT_AND_CHILD = "4"
|
||||
|
||||
// 仅本人数据权限
|
||||
SELF = "5"
|
||||
)
|
||||
|
||||
// 系统角色数据范围映射
|
||||
var RoleDataScope = map[string]string{
|
||||
ALL: "全部数据权限",
|
||||
CUSTOM: "自定数据权限",
|
||||
DEPT: "部门数据权限",
|
||||
DEPT_AND_CHILD: "部门及以下数据权限",
|
||||
SELF: "仅本人数据权限",
|
||||
}
|
||||
21
src/framework/constants/token/token.go
Normal file
21
src/framework/constants/token/token.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package token
|
||||
|
||||
// 令牌常量信息
|
||||
|
||||
// 令牌-数据响应字段
|
||||
const RESPONSE_FIELD = "access_token"
|
||||
|
||||
// 令牌-请求头标识前缀
|
||||
const HEADER_PREFIX = "Bearer "
|
||||
|
||||
// 令牌-请求头标识
|
||||
const HEADER_KEY = "Authorization"
|
||||
|
||||
// 令牌-JWT唯一标识字段
|
||||
const JWT_UUID = "login_key"
|
||||
|
||||
// 令牌-JWT标识用户主键字段
|
||||
const JWT_KEY = "user_id"
|
||||
|
||||
// 令牌-JWT标识用户登录账号字段
|
||||
const JWT_NAME = "user_name"
|
||||
37
src/framework/constants/uploadsubpath/uploadsubpath.go
Normal file
37
src/framework/constants/uploadsubpath/uploadsubpath.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package uploadsubpath
|
||||
|
||||
// 文件上传-子路径类型常量
|
||||
|
||||
const (
|
||||
// 默认
|
||||
DEFAULT = "default"
|
||||
|
||||
// 头像
|
||||
AVATART = "avatar"
|
||||
|
||||
// 导入
|
||||
IMPORT = "import"
|
||||
|
||||
// 导出
|
||||
EXPORT = "export"
|
||||
|
||||
// 通用上传
|
||||
COMMON = "common"
|
||||
|
||||
// 下载
|
||||
DOWNLOAD = "download"
|
||||
|
||||
// 切片
|
||||
CHUNK = "chunk"
|
||||
)
|
||||
|
||||
// 子路径类型映射
|
||||
var UploadSubpath = map[string]string{
|
||||
DEFAULT: "默认",
|
||||
AVATART: "头像",
|
||||
IMPORT: "导入",
|
||||
EXPORT: "导出",
|
||||
COMMON: "通用上传",
|
||||
DOWNLOAD: "下载",
|
||||
CHUNK: "切片",
|
||||
}
|
||||
202
src/framework/cron/cron.go
Normal file
202
src/framework/cron/cron.go
Normal file
@@ -0,0 +1,202 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
// 定义内部调度任务实例
|
||||
var c *cron.Cron
|
||||
|
||||
// 任务队列
|
||||
var queueMap map[string]Queue
|
||||
|
||||
// StartCron 启动调度任务实例
|
||||
func StartCron() {
|
||||
queueMap = make(map[string]Queue)
|
||||
c = cron.New(cron.WithSeconds())
|
||||
c.Start()
|
||||
}
|
||||
|
||||
// StopCron 停止调度任务实例
|
||||
func StopCron() {
|
||||
c.Stop()
|
||||
}
|
||||
|
||||
// CreateQueue 创建队列注册处理器
|
||||
func CreateQueue(name string, processor QueueProcessor) Queue {
|
||||
queue := Queue{
|
||||
Name: name,
|
||||
Processor: processor,
|
||||
Job: &[]*QueueJob{},
|
||||
}
|
||||
queueMap[name] = queue
|
||||
return queue
|
||||
}
|
||||
|
||||
// GetQueue 通过名称获取队列实例
|
||||
func GetQueue(name string) Queue {
|
||||
if v, ok := queueMap[name]; ok {
|
||||
return v
|
||||
}
|
||||
return Queue{}
|
||||
}
|
||||
|
||||
// QueueNames 获取注册的队列名称
|
||||
func QueueNames() []string {
|
||||
keys := make([]string, 0, len(queueMap))
|
||||
for k := range queueMap {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// Queue 任务队列
|
||||
type Queue struct {
|
||||
Name string // 队列名
|
||||
Processor QueueProcessor
|
||||
Job *[]*QueueJob
|
||||
}
|
||||
|
||||
// QueueProcessor 队列处理函数接口
|
||||
type QueueProcessor interface {
|
||||
// Execute 实际执行函数
|
||||
Execute(data any) any
|
||||
}
|
||||
|
||||
// RunJob 运行任务,data是传入的数据
|
||||
func (q *Queue) RunJob(data any, opts JobOptions) int {
|
||||
job := &QueueJob{
|
||||
Status: Waiting,
|
||||
Data: data,
|
||||
Opts: opts,
|
||||
queueName: q.Name,
|
||||
queueProcessor: &q.Processor,
|
||||
}
|
||||
|
||||
// 非重复任务立即执行
|
||||
if opts.Cron == "" {
|
||||
// 获取执行的任务
|
||||
currentJob := job.GetJob(false)
|
||||
if currentJob.Status == Active {
|
||||
return Active
|
||||
}
|
||||
// 从切片 jobs 中删除指定索引位置的元素
|
||||
for i, v := range *q.Job {
|
||||
if v.cid == 0 {
|
||||
jobs := *q.Job
|
||||
jobs = append(jobs[:i], jobs[i+1:]...)
|
||||
*q.Job = jobs
|
||||
break
|
||||
}
|
||||
}
|
||||
go job.Run()
|
||||
} else {
|
||||
// 移除已存的任务ID
|
||||
q.RemoveJob(opts.JobId)
|
||||
// 添加新任务
|
||||
cid, err := c.AddJob(opts.Cron, job)
|
||||
if err != nil {
|
||||
newLog.Error(err, "err")
|
||||
job.Status = Failed
|
||||
}
|
||||
job.cid = cid
|
||||
}
|
||||
|
||||
*q.Job = append(*q.Job, job)
|
||||
newLog.Info("RunJob", job.cid, opts.JobId, job.Status)
|
||||
return job.Status
|
||||
}
|
||||
|
||||
// RemoveJob 移除任务
|
||||
func (q *Queue) RemoveJob(jobId string) bool {
|
||||
for i, v := range *q.Job {
|
||||
if jobId == v.Opts.JobId {
|
||||
newLog.Info("RemoveJob", v.cid, jobId, v.Status)
|
||||
c.Remove(v.cid)
|
||||
// 从切片 jobs 中删除指定索引位置的元素
|
||||
jobs := *q.Job
|
||||
jobs = append(jobs[:i], jobs[i+1:]...)
|
||||
*q.Job = jobs
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Status 任务执行状态
|
||||
const (
|
||||
Waiting = iota
|
||||
Active
|
||||
Completed
|
||||
Failed
|
||||
)
|
||||
|
||||
// JobOptions 任务参数信息
|
||||
type JobOptions struct {
|
||||
JobId string // 执行任务编号
|
||||
Cron string // 重复任务cron表达式
|
||||
}
|
||||
|
||||
// QueueJob 队列内部执行任务
|
||||
type QueueJob struct {
|
||||
Status int // 任务执行状态
|
||||
Timestamp int64 // 执行时间
|
||||
Data any // 执行任务时传入的参数
|
||||
Opts JobOptions
|
||||
|
||||
cid cron.EntryID // 执行ID
|
||||
|
||||
queueName string //队列名
|
||||
queueProcessor *QueueProcessor
|
||||
}
|
||||
|
||||
// GetJob 获取当前执行任务
|
||||
func (job *QueueJob) GetJob(repeat bool) *QueueJob {
|
||||
q := GetQueue(job.queueName)
|
||||
for _, v := range *q.Job {
|
||||
if repeat && v.Opts.JobId == job.Opts.JobId {
|
||||
return v
|
||||
}
|
||||
if !repeat && v.cid == 0 {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return job
|
||||
}
|
||||
|
||||
// Run 实现的接口函数
|
||||
func (s QueueJob) Run() {
|
||||
// 检查当前任务
|
||||
job := s.GetJob(s.cid != 0)
|
||||
|
||||
// Active 状态不执行
|
||||
if job.Status == Active {
|
||||
return
|
||||
}
|
||||
|
||||
// panics 异常收集
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err, ok := r.(error)
|
||||
if !ok {
|
||||
err = fmt.Errorf("%v", r)
|
||||
}
|
||||
job.Status = Failed
|
||||
newLog.Error(err, "failed", job)
|
||||
}
|
||||
}()
|
||||
|
||||
// 开始执行
|
||||
job.Status = Active
|
||||
job.Timestamp = time.Now().UnixMilli()
|
||||
newLog.Info("run", job.cid, job.Opts.JobId)
|
||||
|
||||
// 获取队列处理器接口实现
|
||||
processor := *job.queueProcessor
|
||||
result := processor.Execute(job.Data)
|
||||
job.Status = Completed
|
||||
newLog.Completed(result, "completed", job)
|
||||
}
|
||||
179
src/framework/cron/cron_test.go
Normal file
179
src/framework/cron/cron_test.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/logger"
|
||||
)
|
||||
|
||||
// 参考文章:
|
||||
// https://blog.csdn.net/zjbyough/article/details/113853582
|
||||
// https://mp.weixin.qq.com/s/Ak7RBv1NuS-VBeDNo8_fww
|
||||
func init() {
|
||||
StartCron()
|
||||
}
|
||||
|
||||
// 简单示例 队列任务处理
|
||||
var NewSimple = &Simple{}
|
||||
|
||||
type Simple struct{}
|
||||
|
||||
func (s *Simple) Execute(data any) any {
|
||||
logger.Infof("执行=> %+v ", data)
|
||||
// 实现任务处理逻辑
|
||||
return data
|
||||
}
|
||||
|
||||
func TestSimple(t *testing.T) {
|
||||
|
||||
simple := CreateQueue("simple", NewSimple)
|
||||
simple.RunJob(map[string]string{
|
||||
"ok": "ok",
|
||||
"data": "data",
|
||||
}, JobOptions{
|
||||
JobId: "101",
|
||||
})
|
||||
|
||||
simpleC := CreateQueue("simple", NewSimple)
|
||||
simpleC.RunJob(map[string]string{
|
||||
"corn": "*/5 * * * * *",
|
||||
"id": "102",
|
||||
}, JobOptions{
|
||||
JobId: "102",
|
||||
Cron: "*/5 * * * * *",
|
||||
})
|
||||
|
||||
// simpleC.RunJob(map[string]string{
|
||||
// "corn": "*/15 * * * * *",
|
||||
// "id": "103",
|
||||
// }, JobOptions{
|
||||
// JobId: "103",
|
||||
// Cron: "*/15 * * * * *",
|
||||
// })
|
||||
|
||||
// simpleC.RemoveJob("102")
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
// Foo 队列任务处理
|
||||
var NewFooProcessor = &FooProcessor{
|
||||
progress: 0,
|
||||
count: 0,
|
||||
}
|
||||
|
||||
type FooProcessor struct {
|
||||
progress int
|
||||
count int
|
||||
}
|
||||
|
||||
func (s *FooProcessor) Execute(data any) any {
|
||||
logger.Infof("执行 %d %d => %+v ", s.count, s.progress, data)
|
||||
s.count++
|
||||
|
||||
// 实现任务处理逻辑
|
||||
i := 0
|
||||
s.progress = i
|
||||
for i < 10 {
|
||||
// 获取任务进度
|
||||
progress := s.progress
|
||||
logger.Infof("data: %v => 任务进度:%d", data, progress)
|
||||
// 延迟响应
|
||||
time.Sleep(time.Second * 2)
|
||||
i++
|
||||
// 改变任务进度
|
||||
s.progress = i
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func TestFoo(t *testing.T) {
|
||||
|
||||
foo := CreateQueue("foo", NewFooProcessor)
|
||||
foo.RunJob(map[string]string{
|
||||
"data": "2",
|
||||
}, JobOptions{
|
||||
JobId: "2",
|
||||
})
|
||||
|
||||
fooC := CreateQueue("foo", NewFooProcessor)
|
||||
fooC.RunJob(map[string]string{
|
||||
"corn": "*/5 * * * * *",
|
||||
}, JobOptions{
|
||||
JobId: "3",
|
||||
Cron: "*/5 * * * * *",
|
||||
})
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
// Bar 队列任务处理
|
||||
var NewBarProcessor = &BarProcessor{
|
||||
progress: 0,
|
||||
count: 0,
|
||||
}
|
||||
|
||||
type BarProcessor struct {
|
||||
progress int
|
||||
count int
|
||||
}
|
||||
|
||||
func (s *BarProcessor) Execute(data any) any {
|
||||
logger.Infof("执行 %d %d => %+v ", s.count, s.progress, data)
|
||||
s.count++
|
||||
|
||||
// 实现任务处理逻辑
|
||||
i := 0
|
||||
s.progress = i
|
||||
for i < 5 {
|
||||
// 获取任务进度
|
||||
progress := s.progress
|
||||
logger.Infof("data: %v => 任务进度:%d", data, progress)
|
||||
// 延迟响应
|
||||
time.Sleep(time.Second * 2)
|
||||
// 程序中途执行错误
|
||||
if i == 3 {
|
||||
// arr := [1]int{1}
|
||||
// arr[i] = 3
|
||||
// fmt.Println(arr)
|
||||
// return "i = 3"
|
||||
panic("程序中途执行错误")
|
||||
}
|
||||
i++
|
||||
// 改变任务进度
|
||||
s.progress = i
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func TestBar(t *testing.T) {
|
||||
|
||||
bar := CreateQueue("bar", NewBarProcessor)
|
||||
bar.RunJob(map[string]string{
|
||||
"data": "wdf",
|
||||
}, JobOptions{
|
||||
JobId: "81923",
|
||||
})
|
||||
|
||||
barC := CreateQueue("bar", NewBarProcessor)
|
||||
barC.RunJob(map[string]string{
|
||||
"corn": "*/5 * * * * *",
|
||||
}, JobOptions{
|
||||
JobId: "789",
|
||||
Cron: "*/5 * * * * *",
|
||||
})
|
||||
|
||||
// barDB := CreateQueue("barDB", NewBarProcessor)
|
||||
// barDB.RunJob(JobData{
|
||||
// SysJob: model.SysJob{
|
||||
// JobID: "9123",
|
||||
// JobName: "测试任务",
|
||||
// },
|
||||
// }, JobOptions{
|
||||
// JobId: "9123",
|
||||
// })
|
||||
|
||||
select {}
|
||||
}
|
||||
112
src/framework/cron/log.go
Normal file
112
src/framework/cron/log.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/constants/common"
|
||||
"ems.agt/src/modules/monitor/model"
|
||||
"ems.agt/src/modules/monitor/repository"
|
||||
)
|
||||
|
||||
// 实例任务执行日志收集
|
||||
var newLog = cronlog{}
|
||||
|
||||
// cronlog 任务执行日志收集
|
||||
type cronlog struct{}
|
||||
|
||||
// Info 任务普通信息收集
|
||||
func (s cronlog) Info(msg string, keysAndValues ...any) {
|
||||
// logger.Infof("Info msg: %v ====> kv: %v", msg, keysAndValues)
|
||||
|
||||
}
|
||||
|
||||
// Error 任务异常错误收集
|
||||
func (s cronlog) Error(err error, msg string, keysAndValues ...any) {
|
||||
// logger.Errorf("Error: %v -> msg: %v ====> kv: %v", err, msg, keysAndValues)
|
||||
// logger.Errorf("k0: %v", keysAndValues[0].(*QueueJob))
|
||||
|
||||
// 指定的错误收集
|
||||
if msg == "failed" {
|
||||
// 任务对象
|
||||
job := keysAndValues[0].(*QueueJob)
|
||||
|
||||
// 结果信息序列化字符串
|
||||
jsonByte, _ := json.Marshal(map[string]any{
|
||||
"name": "failed",
|
||||
"message": err.Error(),
|
||||
})
|
||||
jobMsg := string(jsonByte)
|
||||
if len(jobMsg) > 500 {
|
||||
jobMsg = jobMsg[:500]
|
||||
}
|
||||
|
||||
// 读取任务信息创建日志对象
|
||||
if data, ok := job.Data.(JobData); ok {
|
||||
duration := time.Since(time.UnixMilli(job.Timestamp))
|
||||
sysJob := data.SysJob
|
||||
if sysJob.JobID == job.Opts.JobId {
|
||||
sysJobLog := model.SysJobLog{
|
||||
JobName: sysJob.JobName,
|
||||
JobGroup: sysJob.JobGroup,
|
||||
InvokeTarget: sysJob.InvokeTarget,
|
||||
TargetParams: sysJob.TargetParams,
|
||||
Status: common.STATUS_NO,
|
||||
JobMsg: jobMsg,
|
||||
CostTime: duration.Milliseconds(),
|
||||
}
|
||||
// 插入数据
|
||||
repository.NewSysJobLogImpl.InsertJobLog(sysJobLog)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Completed 任务完成return的结果收集
|
||||
func (s cronlog) Completed(result any, msg string, keysAndValues ...any) {
|
||||
// logger.Infof("Completed: %v -> msg: %v ====> kv: %v", result, msg, keysAndValues)
|
||||
// logger.Infof("k0: %v", keysAndValues[0].(*QueueJob))
|
||||
|
||||
// 指定的完成收集
|
||||
if msg == "completed" {
|
||||
// 任务对象
|
||||
job := keysAndValues[0].(*QueueJob)
|
||||
|
||||
// 结果信息序列化字符串
|
||||
jsonByte, _ := json.Marshal(map[string]any{
|
||||
"name": "completed",
|
||||
"message": result,
|
||||
})
|
||||
jobMsg := string(jsonByte)
|
||||
if len(jobMsg) > 500 {
|
||||
jobMsg = jobMsg[:500]
|
||||
}
|
||||
|
||||
// 读取任务信息创建日志对象
|
||||
if data, ok := job.Data.(JobData); ok {
|
||||
duration := time.Since(time.UnixMilli(job.Timestamp))
|
||||
sysJob := data.SysJob
|
||||
if sysJob.JobID == job.Opts.JobId {
|
||||
sysJobLog := model.SysJobLog{
|
||||
JobName: sysJob.JobName,
|
||||
JobGroup: sysJob.JobGroup,
|
||||
InvokeTarget: sysJob.InvokeTarget,
|
||||
TargetParams: sysJob.TargetParams,
|
||||
Status: common.STATUS_YES,
|
||||
JobMsg: jobMsg,
|
||||
CostTime: duration.Milliseconds(),
|
||||
}
|
||||
// 插入数据
|
||||
repository.NewSysJobLogImpl.InsertJobLog(sysJobLog)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// JobData 调度任务日志收集结构体,执行任务时传入的接收参数
|
||||
type JobData struct {
|
||||
// 触发执行cron重复多次
|
||||
Repeat bool
|
||||
// 定时任务调度表记录信息
|
||||
SysJob model.SysJob
|
||||
}
|
||||
161
src/framework/datasource/datasource.go
Normal file
161
src/framework/datasource/datasource.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package datasource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/config"
|
||||
"ems.agt/src/framework/logger"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
gormLog "gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
// 数据库连接实例
|
||||
var dbMap = make(map[string]*gorm.DB)
|
||||
|
||||
type dialectInfo struct {
|
||||
dialector gorm.Dialector
|
||||
logging bool
|
||||
}
|
||||
|
||||
// 载入数据库连接
|
||||
func loadDialect() map[string]dialectInfo {
|
||||
dialects := make(map[string]dialectInfo, 0)
|
||||
|
||||
// 读取数据源配置
|
||||
datasource := config.Get("gorm.datasource").(map[string]any)
|
||||
for key, value := range datasource {
|
||||
item := value.(map[string]any)
|
||||
// 数据库类型对应的数据库连接
|
||||
switch item["type"] {
|
||||
case "mysql":
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
item["username"],
|
||||
item["password"],
|
||||
item["host"],
|
||||
item["port"],
|
||||
item["database"],
|
||||
)
|
||||
dialects[key] = dialectInfo{
|
||||
dialector: mysql.Open(dsn),
|
||||
logging: item["logging"].(bool),
|
||||
}
|
||||
default:
|
||||
logger.Fatalf("%s: %v\n Not Load DB Config Type", key, item)
|
||||
}
|
||||
}
|
||||
|
||||
return dialects
|
||||
}
|
||||
|
||||
// 载入连接日志配置
|
||||
func loadLogger() gormLog.Interface {
|
||||
newLogger := gormLog.New(
|
||||
log.New(os.Stdout, "[GORM] ", log.LstdFlags), // 将日志输出到控制台
|
||||
gormLog.Config{
|
||||
SlowThreshold: time.Second, // Slow SQL 阈值
|
||||
LogLevel: gormLog.Info, // 日志级别 Silent不输出任何日志
|
||||
ParameterizedQueries: false, // 参数化查询SQL 用实际值带入?的执行语句
|
||||
Colorful: false, // 彩色日志输出
|
||||
},
|
||||
)
|
||||
return newLogger
|
||||
}
|
||||
|
||||
// 连接数据库实例
|
||||
func Connect() {
|
||||
// 遍历进行连接数据库实例
|
||||
for key, info := range loadDialect() {
|
||||
opts := &gorm.Config{}
|
||||
// 是否需要日志输出
|
||||
if info.logging {
|
||||
opts.Logger = loadLogger()
|
||||
}
|
||||
// 创建连接
|
||||
db, err := gorm.Open(info.dialector, opts)
|
||||
if err != nil {
|
||||
logger.Fatalf("failed error db connect: %s", err)
|
||||
}
|
||||
// 获取底层 SQL 数据库连接
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
logger.Fatalf("failed error underlying SQL database: %v", err)
|
||||
}
|
||||
// 测试数据库连接
|
||||
err = sqlDB.Ping()
|
||||
if err != nil {
|
||||
logger.Fatalf("failed error ping database: %v", err)
|
||||
}
|
||||
logger.Infof("database %s connection is successful.", key)
|
||||
dbMap[key] = db
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭数据库实例
|
||||
func Close() {
|
||||
for _, db := range dbMap {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if err := sqlDB.Close(); err != nil {
|
||||
logger.Errorf("fatal error db close: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取默认数据源
|
||||
func DefaultDB() *gorm.DB {
|
||||
source := config.Get("gorm.defaultDataSourceName").(string)
|
||||
return dbMap[source]
|
||||
}
|
||||
|
||||
// 获取数据源
|
||||
func DB(source string) *gorm.DB {
|
||||
return dbMap[source]
|
||||
}
|
||||
|
||||
// RawDB 原生查询语句
|
||||
func RawDB(source string, sql string, parameters []any) ([]map[string]any, error) {
|
||||
// 数据源
|
||||
db := DefaultDB()
|
||||
if source != "" {
|
||||
db = DB(source)
|
||||
}
|
||||
|
||||
// 使用正则表达式替换连续的空白字符为单个空格
|
||||
fmtSql := regexp.MustCompile(`\s+`).ReplaceAllString(sql, " ")
|
||||
|
||||
// logger.Infof("sql=> %v", fmtSql)
|
||||
// logger.Infof("parameters=> %v", parameters)
|
||||
|
||||
// 查询结果
|
||||
var rows []map[string]any
|
||||
res := db.Raw(fmtSql, parameters...).Scan(&rows)
|
||||
if res.Error != nil {
|
||||
return nil, res.Error
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
// ExecDB 原生执行语句
|
||||
func ExecDB(source string, sql string, parameters []any) (int64, error) {
|
||||
// 数据源
|
||||
db := DefaultDB()
|
||||
if source != "" {
|
||||
db = DB(source)
|
||||
}
|
||||
// 使用正则表达式替换连续的空白字符为单个空格
|
||||
fmtSql := regexp.MustCompile(`\s+`).ReplaceAllString(sql, " ")
|
||||
// 执行结果
|
||||
res := db.Exec(fmtSql, parameters...)
|
||||
if res.Error != nil {
|
||||
return 0, res.Error
|
||||
}
|
||||
return res.RowsAffected, nil
|
||||
}
|
||||
40
src/framework/errorcatch/errorcatch.go
Normal file
40
src/framework/errorcatch/errorcatch.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package errorcatch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"ems.agt/src/framework/config"
|
||||
"ems.agt/src/framework/logger"
|
||||
"ems.agt/src/framework/vo/result"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ErrorCatch 全局异常捕获
|
||||
func ErrorCatch() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
defer func() {
|
||||
// 在这里处理 Panic 异常,例如记录日志或返回错误信息给客户端
|
||||
if err := recover(); err != nil {
|
||||
logger.Errorf("发生了 Panic 异常: %v", err)
|
||||
|
||||
// 返回错误响应给客户端
|
||||
if config.Env() == "prod" {
|
||||
c.JSON(500, result.ErrMsg("服务器内部错误"))
|
||||
} else {
|
||||
// 通过实现 error 接口的 Error() 方法自定义错误类型进行捕获
|
||||
switch v := err.(type) {
|
||||
case error:
|
||||
c.JSON(500, result.ErrMsg(v.Error()))
|
||||
default:
|
||||
c.JSON(500, result.ErrMsg(fmt.Sprint(err)))
|
||||
}
|
||||
}
|
||||
|
||||
c.Abort() // 停止执行后续的处理函数
|
||||
}
|
||||
}()
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
49
src/framework/logger/logger.go
Normal file
49
src/framework/logger/logger.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var logWriter *Logger
|
||||
|
||||
// 初始程序日志
|
||||
func InitLogger() {
|
||||
env := viper.GetString("env")
|
||||
conf := viper.GetStringMap("logger")
|
||||
fileDir := conf["filedir"].(string)
|
||||
fileName := conf["filename"].(string)
|
||||
level := conf["level"].(int)
|
||||
maxDay := conf["maxday"].(int)
|
||||
maxSize := conf["maxsize"].(int)
|
||||
|
||||
newLog, err := NewLogger(env, fileDir, fileName, level, maxDay, maxSize)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to initialize logger: %v", err)
|
||||
}
|
||||
|
||||
logWriter = newLog
|
||||
}
|
||||
|
||||
// 关闭程序日志写入
|
||||
func Close() {
|
||||
logWriter.Close()
|
||||
}
|
||||
|
||||
func Infof(format string, v ...any) {
|
||||
logWriter.Infof(format, v...)
|
||||
}
|
||||
|
||||
func Warnf(format string, v ...any) {
|
||||
logWriter.Warnf(format, v...)
|
||||
}
|
||||
|
||||
func Errorf(format string, v ...any) {
|
||||
logWriter.Errorf(format, v...)
|
||||
}
|
||||
|
||||
// Fatalf 抛出错误并退出程序
|
||||
func Fatalf(format string, v ...any) {
|
||||
log.Fatalf(format, v...)
|
||||
}
|
||||
190
src/framework/logger/writer.go
Normal file
190
src/framework/logger/writer.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 日志器对象
|
||||
type Logger struct {
|
||||
env string // 运行环境
|
||||
filePath string // 文件路径
|
||||
fileName string // 文件名
|
||||
level int // 日志等级标识
|
||||
maxDay int // 保留最长天数
|
||||
maxSize int64 // 文件最大空间
|
||||
fileHandle *os.File // 文件实例
|
||||
logger *log.Logger // 日志实例
|
||||
logLevelMap map[int]string // 日志等级标识名
|
||||
logDay int // 日志当前日
|
||||
}
|
||||
|
||||
const (
|
||||
LOG_LEVEL_SILENT = iota
|
||||
LOG_LEVEL_INFO
|
||||
LOG_LEVEL_WARN
|
||||
LOG_LEVEL_ERROR
|
||||
)
|
||||
|
||||
// NewLogger 实例日志器对象
|
||||
func NewLogger(env, fileDir, fileName string, level, maxDay, maxSize int) (*Logger, error) {
|
||||
logFilePath := filepath.Join(fileDir, fileName)
|
||||
if err := os.MkdirAll(filepath.Dir(logFilePath), 0750); err != nil {
|
||||
return nil, fmt.Errorf("failed to mkdir logger dir: %v", err)
|
||||
}
|
||||
fileHandle, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open log file: %v", err)
|
||||
}
|
||||
|
||||
writer := io.Writer(fileHandle)
|
||||
if env == "local" {
|
||||
writer = io.MultiWriter(fileHandle, os.Stderr)
|
||||
}
|
||||
|
||||
logger := log.New(writer, "", log.LstdFlags|log.Lshortfile)
|
||||
|
||||
logLevelMap := map[int]string{
|
||||
LOG_LEVEL_INFO: "INFO",
|
||||
LOG_LEVEL_WARN: "WARN",
|
||||
LOG_LEVEL_ERROR: "ERROR",
|
||||
}
|
||||
|
||||
stdLogger := &Logger{
|
||||
env: env,
|
||||
filePath: fileDir,
|
||||
fileName: fileName,
|
||||
level: level,
|
||||
maxDay: maxDay,
|
||||
maxSize: int64(maxSize * 1024 * 1024),
|
||||
fileHandle: fileHandle,
|
||||
logger: logger,
|
||||
logLevelMap: logLevelMap,
|
||||
logDay: time.Now().Day(),
|
||||
}
|
||||
|
||||
go stdLogger.checkFile()
|
||||
|
||||
return stdLogger, nil
|
||||
}
|
||||
|
||||
// checkFile 检查文件分割,自定时调用
|
||||
func (l *Logger) checkFile() {
|
||||
fileInfo, err := l.fileHandle.Stat()
|
||||
if err != nil {
|
||||
l.logger.Printf("Failed to get log file info: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
currTime := time.Now()
|
||||
if l.logDay != currTime.Day() {
|
||||
l.logDay = currTime.Day()
|
||||
l.rotateFile(currTime.AddDate(0, 0, -1).Format("2006_01_02"))
|
||||
// 移除超过保存最长天数的文件
|
||||
l.removeOldFile(currTime.AddDate(0, 0, -l.maxDay))
|
||||
} else if fileInfo.Size() >= l.maxSize {
|
||||
l.rotateFile(currTime.Format("2006_01_02_150405"))
|
||||
} else if time.Since(fileInfo.ModTime()).Hours() > 24 {
|
||||
l.rotateFile(fileInfo.ModTime().Format("2006_01_02"))
|
||||
}
|
||||
|
||||
time.AfterFunc(1*time.Minute, l.checkFile)
|
||||
}
|
||||
|
||||
// rotateFile 检查文件大小进行分割
|
||||
func (l *Logger) rotateFile(timeFormat string) {
|
||||
l.fileHandle.Close()
|
||||
|
||||
newFileName := fmt.Sprintf("%s.%s", l.fileName, timeFormat)
|
||||
newFilePath := filepath.Join(l.filePath, newFileName)
|
||||
oldfilePath := filepath.Join(l.filePath, l.fileName)
|
||||
|
||||
// 重命名
|
||||
os.Rename(oldfilePath, newFilePath)
|
||||
|
||||
// 新文件句柄
|
||||
fileHandle, err := os.OpenFile(oldfilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
l.logger.Printf("Failed to open log file: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
l.fileHandle = fileHandle
|
||||
|
||||
// 重新设置 logger 的 writer
|
||||
writer := io.Writer(l.fileHandle)
|
||||
if l.env == "local" {
|
||||
writer = io.MultiWriter(l.fileHandle, os.Stderr)
|
||||
}
|
||||
l.logger.SetOutput(writer)
|
||||
}
|
||||
|
||||
// RemoveOldFile 删除旧文件
|
||||
func (l *Logger) removeOldFile(oldFileDate time.Time) {
|
||||
// 遍历目标文件夹中的文件
|
||||
files, err := os.ReadDir(l.filePath)
|
||||
if err != nil {
|
||||
l.Errorf("logger RemoveOldFile ReadDir err: %v", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
idx := strings.LastIndex(file.Name(), ".log.")
|
||||
if idx == -1 {
|
||||
continue
|
||||
}
|
||||
dateStr := file.Name()[idx+5 : idx+15]
|
||||
|
||||
// 解析日期字符串
|
||||
fileDate, err := time.Parse("2006_01_02", dateStr)
|
||||
if err != nil {
|
||||
l.Errorf("logger RemoveOldFile Parse err: %v", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
// 判断文件日期是否在给定日期之前
|
||||
if fileDate.Before(oldFileDate) {
|
||||
// 删除旧文件
|
||||
err := os.Remove(filepath.Join(l.filePath, file.Name()))
|
||||
if err != nil {
|
||||
l.Errorf("logger RemoveOldFile Remove err: %v", err.Error())
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writeLog 写入chan
|
||||
func (l *Logger) writeLog(level int, format string, args ...interface{}) {
|
||||
if level < l.level {
|
||||
return
|
||||
}
|
||||
|
||||
logMsg := fmt.Sprintf("[%s] %s\n", l.logLevelMap[level], fmt.Sprintf(format, args...))
|
||||
l.logger.Output(4, logMsg)
|
||||
}
|
||||
|
||||
func (l *Logger) Infof(format string, args ...interface{}) {
|
||||
l.writeLog(LOG_LEVEL_INFO, format, args...)
|
||||
}
|
||||
|
||||
func (l *Logger) Warnf(format string, args ...interface{}) {
|
||||
l.writeLog(LOG_LEVEL_WARN, format, args...)
|
||||
}
|
||||
|
||||
func (l *Logger) Errorf(format string, args ...interface{}) {
|
||||
l.writeLog(LOG_LEVEL_ERROR, format, args...)
|
||||
}
|
||||
|
||||
// Close 日志关闭
|
||||
func (l *Logger) Close() {
|
||||
err := l.fileHandle.Close()
|
||||
if err != nil {
|
||||
l.logger.Printf("Failed to close log file: %v\n", err)
|
||||
}
|
||||
}
|
||||
182
src/framework/middleware/collectlogs/operate_log.go
Normal file
182
src/framework/middleware/collectlogs/operate_log.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package collectlogs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/constants/common"
|
||||
"ems.agt/src/framework/utils/ctx"
|
||||
"ems.agt/src/framework/utils/parse"
|
||||
"ems.agt/src/framework/vo/result"
|
||||
"ems.agt/src/modules/system/model"
|
||||
"ems.agt/src/modules/system/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
// 业务操作类型-其它
|
||||
BUSINESS_TYPE_OTHER = "0"
|
||||
|
||||
// 业务操作类型-新增
|
||||
BUSINESS_TYPE_INSERT = "1"
|
||||
|
||||
// 业务操作类型-修改
|
||||
BUSINESS_TYPE_UPDATE = "2"
|
||||
|
||||
// 业务操作类型-删除
|
||||
BUSINESS_TYPE_DELETE = "3"
|
||||
|
||||
// 业务操作类型-授权
|
||||
BUSINESS_TYPE_GRANT = "4"
|
||||
|
||||
// 业务操作类型-导出
|
||||
BUSINESS_TYPE_EXPORT = "5"
|
||||
|
||||
// 业务操作类型-导入
|
||||
BUSINESS_TYPE_IMPORT = "6"
|
||||
|
||||
// 业务操作类型-强退
|
||||
BUSINESS_TYPE_FORCE = "7"
|
||||
|
||||
// 业务操作类型-清空数据
|
||||
BUSINESS_TYPE_CLEAN = "8"
|
||||
)
|
||||
|
||||
const (
|
||||
// 操作人类别-其它
|
||||
OPERATOR_TYPE_OTHER = "0"
|
||||
|
||||
// 操作人类别-后台用户
|
||||
OPERATOR_TYPE_MANAGE = "1"
|
||||
|
||||
// 操作人类别-手机端用户
|
||||
OPERATOR_TYPE_MOBILE = "2"
|
||||
)
|
||||
|
||||
// Option 操作日志参数
|
||||
type Options struct {
|
||||
Title string `json:"title"` // 标题
|
||||
BusinessType string `json:"businessType"` // 类型,默认常量 BUSINESS_TYPE_OTHER
|
||||
OperatorType string `json:"operatorType"` // 操作人类别,默认常量 OPERATOR_TYPE_OTHER
|
||||
IsSaveRequestData bool `json:"isSaveRequestData"` // 是否保存请求的参数
|
||||
IsSaveResponseData bool `json:"isSaveResponseData"` // 是否保存响应的参数
|
||||
}
|
||||
|
||||
// OptionNew 操作日志参数默认值
|
||||
//
|
||||
// 标题 "title":"--"
|
||||
//
|
||||
// 类型 "businessType": BUSINESS_TYPE_OTHER
|
||||
//
|
||||
// 注意之后JSON反序列使用:c.ShouldBindBodyWith(¶ms, binding.JSON)
|
||||
func OptionNew(title, businessType string) Options {
|
||||
return Options{
|
||||
Title: title,
|
||||
BusinessType: businessType,
|
||||
OperatorType: OPERATOR_TYPE_OTHER,
|
||||
IsSaveRequestData: true,
|
||||
IsSaveResponseData: true,
|
||||
}
|
||||
}
|
||||
|
||||
// 敏感属性字段进行掩码
|
||||
var maskProperties []string = []string{
|
||||
"password",
|
||||
"oldPassword",
|
||||
"newPassword",
|
||||
"confirmPassword",
|
||||
}
|
||||
|
||||
// OperateLog 访问操作日志记录
|
||||
//
|
||||
// 请在用户身份授权认证校验后使用以便获取登录用户信息
|
||||
func OperateLog(options Options) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Set("startTime", time.Now())
|
||||
|
||||
// 函数名
|
||||
funcName := c.HandlerName()
|
||||
lastDotIndex := strings.LastIndex(funcName, "/")
|
||||
funcName = funcName[lastDotIndex+1:]
|
||||
|
||||
// 解析ip地址
|
||||
ipaddr, location := ctx.IPAddrLocation(c)
|
||||
|
||||
// 获取登录用户信息
|
||||
loginUser, err := ctx.LoginUser(c)
|
||||
if err != nil {
|
||||
c.JSON(401, result.CodeMsg(401, "无效身份授权"))
|
||||
c.Abort() // 停止执行后续的处理函数
|
||||
return
|
||||
}
|
||||
|
||||
// 操作日志记录
|
||||
operLog := model.SysLogOperate{
|
||||
Title: options.Title,
|
||||
BusinessType: options.BusinessType,
|
||||
OperatorType: options.OperatorType,
|
||||
Method: funcName,
|
||||
OperURL: c.Request.RequestURI,
|
||||
RequestMethod: c.Request.Method,
|
||||
OperIP: ipaddr,
|
||||
OperLocation: location,
|
||||
OperName: loginUser.User.UserName,
|
||||
DeptName: loginUser.User.Dept.DeptName,
|
||||
}
|
||||
|
||||
if loginUser.User.UserType == "sys" {
|
||||
operLog.OperatorType = OPERATOR_TYPE_MANAGE
|
||||
}
|
||||
|
||||
// 是否需要保存request,参数和值
|
||||
if options.IsSaveRequestData {
|
||||
params := ctx.RequestParamsMap(c)
|
||||
for k, v := range params {
|
||||
// 敏感属性字段进行掩码
|
||||
for _, s := range maskProperties {
|
||||
if s == k {
|
||||
params[k] = parse.SafeContent(v.(string))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
jsonStr, _ := json.Marshal(params)
|
||||
paramsStr := string(jsonStr)
|
||||
if len(paramsStr) > 2000 {
|
||||
paramsStr = paramsStr[:2000]
|
||||
}
|
||||
operLog.OperParam = paramsStr
|
||||
}
|
||||
|
||||
// 调用下一个处理程序
|
||||
c.Next()
|
||||
|
||||
// 响应状态
|
||||
status := c.Writer.Status()
|
||||
if status == 200 {
|
||||
operLog.Status = common.STATUS_YES
|
||||
} else {
|
||||
operLog.Status = common.STATUS_NO
|
||||
}
|
||||
|
||||
// 是否需要保存response,参数和值
|
||||
if options.IsSaveResponseData {
|
||||
contentDisposition := c.Writer.Header().Get("Content-Disposition")
|
||||
contentType := c.Writer.Header().Get("Content-Type")
|
||||
content := contentType + contentDisposition
|
||||
msg := fmt.Sprintf(`{"status":"%d","size":"%d","content-type":"%s"}`, status, c.Writer.Size(), content)
|
||||
operLog.OperMsg = msg
|
||||
}
|
||||
|
||||
// 日志记录时间
|
||||
duration := time.Since(c.GetTime("startTime"))
|
||||
operLog.CostTime = duration.Milliseconds()
|
||||
operLog.OperTime = time.Now().UnixMilli()
|
||||
|
||||
// 保存操作记录到数据库
|
||||
service.NewSysLogOperateImpl.InsertSysLogOperate(operLog)
|
||||
}
|
||||
}
|
||||
83
src/framework/middleware/cors.go
Normal file
83
src/framework/middleware/cors.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"ems.agt/src/framework/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Cors 跨域
|
||||
func Cors() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 设置Vary头部
|
||||
c.Header("Vary", "Origin")
|
||||
c.Header("Keep-Alive", "timeout=5")
|
||||
|
||||
requestOrigin := c.GetHeader("Origin")
|
||||
if requestOrigin == "" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
origin := requestOrigin
|
||||
if v := config.Get("cors.origin"); v != nil {
|
||||
origin = v.(string)
|
||||
}
|
||||
c.Header("Access-Control-Allow-Origin", origin)
|
||||
|
||||
if v := config.Get("cors.credentials"); v != nil && v.(bool) {
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
|
||||
// OPTIONS
|
||||
if method := c.Request.Method; method == "OPTIONS" {
|
||||
requestMethod := c.GetHeader("Access-Control-Request-Method")
|
||||
if requestMethod == "" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 响应最大时间值
|
||||
if v := config.Get("cors.maxAge"); v != nil && v.(int) > 10000 {
|
||||
c.Header("Access-Control-Max-Age", fmt.Sprint(v))
|
||||
}
|
||||
|
||||
// 允许方法
|
||||
if v := config.Get("cors.allowMethods"); v != nil {
|
||||
var allowMethods = make([]string, 0)
|
||||
for _, s := range v.([]any) {
|
||||
allowMethods = append(allowMethods, s.(string))
|
||||
}
|
||||
c.Header("Access-Control-Allow-Methods", strings.Join(allowMethods, ","))
|
||||
} else {
|
||||
c.Header("Access-Control-Allow-Methods", "GET,HEAD,PUT,POST,DELETE,PATCH")
|
||||
}
|
||||
|
||||
// 允许请求头
|
||||
if v := config.Get("cors.allowHeaders"); v != nil {
|
||||
var allowHeaders = make([]string, 0)
|
||||
for _, s := range v.([]any) {
|
||||
allowHeaders = append(allowHeaders, s.(string))
|
||||
}
|
||||
c.Header("Access-Control-Allow-Headers", strings.Join(allowHeaders, ","))
|
||||
}
|
||||
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
|
||||
// 暴露请求头
|
||||
if v := config.Get("cors.exposeHeaders"); v != nil {
|
||||
var exposeHeaders = make([]string, 0)
|
||||
for _, s := range v.([]any) {
|
||||
exposeHeaders = append(exposeHeaders, s.(string))
|
||||
}
|
||||
c.Header("Access-Control-Expose-Headers", strings.Join(exposeHeaders, ","))
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
180
src/framework/middleware/pre_authorize.go
Normal file
180
src/framework/middleware/pre_authorize.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
AdminConstants "ems.agt/src/framework/constants/admin"
|
||||
commonConstants "ems.agt/src/framework/constants/common"
|
||||
ctxUtils "ems.agt/src/framework/utils/ctx"
|
||||
tokenUtils "ems.agt/src/framework/utils/token"
|
||||
"ems.agt/src/framework/vo/result"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// PreAuthorize 用户身份授权认证校验
|
||||
//
|
||||
// 只需含有其中角色 "hasRoles": {"xxx"},
|
||||
//
|
||||
// 只需含有其中权限 "hasPerms": {"xxx"},
|
||||
//
|
||||
// 同时匹配其中角色 "matchRoles": {"xxx"},
|
||||
//
|
||||
// 同时匹配其中权限 "matchPerms": {"xxx"},
|
||||
func PreAuthorize(options map[string][]string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 获取请求头标识信息
|
||||
tokenStr := ctxUtils.Authorization(c)
|
||||
if tokenStr == "" {
|
||||
c.JSON(401, result.CodeMsg(401, "无效身份授权"))
|
||||
c.Abort() // 停止执行后续的处理函数
|
||||
return
|
||||
}
|
||||
|
||||
// 验证令牌
|
||||
claims, err := tokenUtils.Verify(tokenStr)
|
||||
if err != nil {
|
||||
c.JSON(401, result.CodeMsg(401, err.Error()))
|
||||
c.Abort() // 停止执行后续的处理函数
|
||||
return
|
||||
}
|
||||
|
||||
// 获取缓存的用户信息
|
||||
loginUser := tokenUtils.LoginUser(claims)
|
||||
if loginUser.UserID == "" {
|
||||
c.JSON(401, result.CodeMsg(401, "无效身份授权"))
|
||||
c.Abort() // 停止执行后续的处理函数
|
||||
return
|
||||
}
|
||||
|
||||
// 检查刷新有效期后存入上下文
|
||||
tokenUtils.RefreshIn(&loginUser)
|
||||
c.Set(commonConstants.CTX_LOGIN_USER, loginUser)
|
||||
|
||||
// 登录用户角色权限校验
|
||||
if options != nil {
|
||||
var roles []string
|
||||
for _, item := range loginUser.User.Roles {
|
||||
roles = append(roles, item.RoleKey)
|
||||
}
|
||||
perms := loginUser.Permissions
|
||||
verifyOk := verifyRolePermission(roles, perms, options)
|
||||
if !verifyOk {
|
||||
msg := fmt.Sprintf("无权访问 %s %s", c.Request.Method, c.Request.RequestURI)
|
||||
c.JSON(403, result.CodeMsg(403, msg))
|
||||
c.Abort() // 停止执行后续的处理函数
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 调用下一个处理程序
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// verifyRolePermission 校验角色权限是否满足
|
||||
//
|
||||
// roles 角色字符数组
|
||||
//
|
||||
// perms 权限字符数组
|
||||
//
|
||||
// options 参数
|
||||
func verifyRolePermission(roles, perms []string, options map[string][]string) bool {
|
||||
// 直接放行 管理员角色或任意权限
|
||||
if contains(roles, AdminConstants.ROLE_KEY) || contains(perms, AdminConstants.PERMISSION) {
|
||||
return true
|
||||
}
|
||||
opts := make([]bool, 4)
|
||||
|
||||
// 只需含有其中角色
|
||||
hasRole := false
|
||||
if arr, ok := options["hasRoles"]; ok && len(arr) > 0 {
|
||||
hasRole = some(roles, arr)
|
||||
opts[0] = true
|
||||
}
|
||||
|
||||
// 只需含有其中权限
|
||||
hasPerms := false
|
||||
if arr, ok := options["hasPerms"]; ok && len(arr) > 0 {
|
||||
hasPerms = some(perms, arr)
|
||||
opts[1] = true
|
||||
}
|
||||
|
||||
// 同时匹配其中角色
|
||||
matchRoles := false
|
||||
if arr, ok := options["matchRoles"]; ok && len(arr) > 0 {
|
||||
matchRoles = every(roles, arr)
|
||||
opts[2] = true
|
||||
}
|
||||
|
||||
// 同时匹配其中权限
|
||||
matchPerms := false
|
||||
if arr, ok := options["matchPerms"]; ok && len(arr) > 0 {
|
||||
matchPerms = every(perms, arr)
|
||||
opts[3] = true
|
||||
}
|
||||
|
||||
// 同时判断 含有其中
|
||||
if opts[0] && opts[1] {
|
||||
return hasRole || hasPerms
|
||||
}
|
||||
// 同时判断 匹配其中
|
||||
if opts[2] && opts[3] {
|
||||
return matchRoles && matchPerms
|
||||
}
|
||||
// 同时判断 含有其中且匹配其中
|
||||
if opts[0] && opts[3] {
|
||||
return hasRole && matchPerms
|
||||
}
|
||||
if opts[1] && opts[2] {
|
||||
return hasPerms && matchRoles
|
||||
}
|
||||
|
||||
return hasRole || hasPerms || matchRoles || matchPerms
|
||||
}
|
||||
|
||||
// contains 检查字符串数组中是否包含指定的字符串
|
||||
func contains(arr []string, target string) bool {
|
||||
for _, str := range arr {
|
||||
if str == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// some 检查字符串数组中含有其中一项
|
||||
func some(origin []string, target []string) bool {
|
||||
has := false
|
||||
for _, t := range target {
|
||||
for _, o := range origin {
|
||||
if t == o {
|
||||
has = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if has {
|
||||
break
|
||||
}
|
||||
}
|
||||
return has
|
||||
}
|
||||
|
||||
// every 检查字符串数组中同时包含所有项
|
||||
func every(origin []string, target []string) bool {
|
||||
match := true
|
||||
for _, t := range target {
|
||||
found := false
|
||||
for _, o := range origin {
|
||||
if t == o {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
return match
|
||||
}
|
||||
101
src/framework/middleware/rate_limit.go
Normal file
101
src/framework/middleware/rate_limit.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/constants/cachekey"
|
||||
"ems.agt/src/framework/redis"
|
||||
"ems.agt/src/framework/utils/ctx"
|
||||
"ems.agt/src/framework/utils/ip2region"
|
||||
"ems.agt/src/framework/vo/result"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
// 默认策略全局限流
|
||||
LIMIT_GLOBAL = 1
|
||||
|
||||
// 根据请求者IP进行限流
|
||||
LIMIT_IP = 2
|
||||
|
||||
// 根据用户ID进行限流
|
||||
LIMIT_USER = 3
|
||||
)
|
||||
|
||||
// LimitOption 请求限流参数
|
||||
type LimitOption struct {
|
||||
Time int64 `json:"time"` // 限流时间,单位秒
|
||||
Count int64 `json:"count"` // 限流次数
|
||||
Type int64 `json:"type"` // 限流条件类型,默认LIMIT_GLOBAL
|
||||
}
|
||||
|
||||
// RateLimit 请求限流
|
||||
//
|
||||
// 示例参数:middleware.LimitOption{ Time: 5, Count: 10, Type: middleware.LIMIT_IP }
|
||||
//
|
||||
// 参数表示:5秒内,最多请求10次,限制类型为 IP
|
||||
//
|
||||
// 使用 USER 时,请在用户身份授权认证校验后使用
|
||||
// 以便获取登录用户信息,无用户信息时默认为 GLOBAL
|
||||
func RateLimit(option LimitOption) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 初始可选参数数据
|
||||
if option.Time < 5 {
|
||||
option.Time = 5
|
||||
}
|
||||
if option.Count < 10 {
|
||||
option.Count = 10
|
||||
}
|
||||
if option.Type == 0 {
|
||||
option.Type = LIMIT_GLOBAL
|
||||
}
|
||||
|
||||
// 获取执行函数名称
|
||||
funcName := c.HandlerName()
|
||||
lastDotIndex := strings.LastIndex(funcName, "/")
|
||||
funcName = funcName[lastDotIndex+1:]
|
||||
// 生成限流key
|
||||
var limitKey string = cachekey.RATE_LIMIT_KEY + funcName
|
||||
|
||||
// 用户
|
||||
if option.Type == LIMIT_USER {
|
||||
loginUser, err := ctx.LoginUser(c)
|
||||
if err != nil {
|
||||
c.JSON(401, result.Err(map[string]any{
|
||||
"code": 401,
|
||||
"msg": err.Error(),
|
||||
}))
|
||||
c.Abort() // 停止执行后续的处理函数
|
||||
return
|
||||
}
|
||||
limitKey = cachekey.RATE_LIMIT_KEY + loginUser.UserID + ":" + funcName
|
||||
}
|
||||
|
||||
// IP
|
||||
if option.Type == LIMIT_IP {
|
||||
clientIP := ip2region.ClientIP(c.ClientIP())
|
||||
limitKey = cachekey.RATE_LIMIT_KEY + clientIP + ":" + funcName
|
||||
}
|
||||
|
||||
// 在Redis查询并记录请求次数
|
||||
rateCount, _ := redis.RateLimit("", limitKey, option.Time, option.Count)
|
||||
rateTime, _ := redis.GetExpire("", limitKey)
|
||||
|
||||
// 设置响应头中的限流声明字段
|
||||
c.Header("X-RateLimit-Limit", fmt.Sprintf("%d", option.Count)) // 总请求数限制
|
||||
c.Header("X-RateLimit-Remaining", fmt.Sprintf("%d", option.Count-rateCount)) // 剩余可用请求数
|
||||
c.Header("X-RateLimit-Reset", fmt.Sprintf("%d", time.Now().Unix()+int64(rateTime))) // 重置时间戳
|
||||
|
||||
if rateCount >= option.Count {
|
||||
c.JSON(200, result.ErrMsg("访问过于频繁,请稍候再试"))
|
||||
c.Abort() // 停止执行后续的处理函数
|
||||
return
|
||||
}
|
||||
|
||||
// 调用下一个处理程序
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
84
src/framework/middleware/repeat/repeat.go
Normal file
84
src/framework/middleware/repeat/repeat.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package repeat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/constants/cachekey"
|
||||
"ems.agt/src/framework/logger"
|
||||
"ems.agt/src/framework/redis"
|
||||
"ems.agt/src/framework/utils/ctx"
|
||||
"ems.agt/src/framework/utils/ip2region"
|
||||
"ems.agt/src/framework/vo/result"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// repeatParam 重复提交参数的类型定义
|
||||
type repeatParam struct {
|
||||
Time int64 `json:"time"`
|
||||
Params string `json:"params"`
|
||||
}
|
||||
|
||||
// RepeatSubmit 防止表单重复提交,小于间隔时间视为重复提交
|
||||
//
|
||||
// 间隔时间(单位秒) 默认:5
|
||||
//
|
||||
// 注意之后JSON反序列使用:c.ShouldBindBodyWith(¶ms, binding.JSON)
|
||||
func RepeatSubmit(interval int64) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if interval < 5 {
|
||||
interval = 5
|
||||
}
|
||||
|
||||
// 提交参数
|
||||
params := ctx.RequestParamsMap(c)
|
||||
paramsJSONByte, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
logger.Errorf("RepeatSubmit params json marshal err: %v", err)
|
||||
}
|
||||
paramsJSONStr := string(paramsJSONByte)
|
||||
|
||||
// 唯一标识(指定key + 客户端IP + 请求地址)
|
||||
clientIP := ip2region.ClientIP(c.ClientIP())
|
||||
repeatKey := cachekey.REPEAT_SUBMIT_KEY + clientIP + ":" + c.Request.RequestURI
|
||||
|
||||
// 在Redis查询并记录请求次数
|
||||
repeatStr, _ := redis.Get("", repeatKey)
|
||||
if repeatStr != "" {
|
||||
var rp repeatParam
|
||||
err := json.Unmarshal([]byte(repeatStr), &rp)
|
||||
if err != nil {
|
||||
logger.Errorf("RepeatSubmit repeatStr json unmarshal err: %v", err)
|
||||
}
|
||||
compareTime := time.Now().Unix() - rp.Time
|
||||
compareParams := rp.Params == paramsJSONStr
|
||||
|
||||
// 设置重复提交声明响应头(毫秒)
|
||||
c.Header("X-RepeatSubmit-Rest", strconv.FormatInt(time.Now().Add(time.Duration(compareTime)*time.Second).UnixNano()/int64(time.Millisecond), 10))
|
||||
|
||||
// 小于间隔时间且参数内容一致
|
||||
if compareTime < interval && compareParams {
|
||||
c.JSON(200, result.ErrMsg("不允许重复提交,请稍候再试"))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 当前请求参数
|
||||
rp := repeatParam{
|
||||
Time: time.Now().Unix(),
|
||||
Params: paramsJSONStr,
|
||||
}
|
||||
rpJSON, err := json.Marshal(rp)
|
||||
if err != nil {
|
||||
logger.Errorf("RepeatSubmit rp json marshal err: %v", err)
|
||||
}
|
||||
// 保存请求时间和参数
|
||||
redis.SetByExpire("", repeatKey, string(rpJSON), time.Duration(interval)*time.Second)
|
||||
|
||||
// 调用下一个处理程序
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
23
src/framework/middleware/report.go
Normal file
23
src/framework/middleware/report.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Report 请求响应日志
|
||||
func Report() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
|
||||
// 调用下一个处理程序
|
||||
c.Next()
|
||||
|
||||
// 计算请求处理时间,并打印日志
|
||||
duration := time.Since(start)
|
||||
logger.Infof("%s %s report end=> %v", c.Request.Method, c.Request.RequestURI, duration)
|
||||
}
|
||||
}
|
||||
22
src/framework/middleware/security/csp.go
Normal file
22
src/framework/middleware/security/csp.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"ems.agt/src/framework/config"
|
||||
"ems.agt/src/framework/utils/generate"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// TODO
|
||||
// csp 这将帮助防止跨站脚本攻击(XSS)。
|
||||
// HTTP 响应头 Content-Security-Policy 允许站点管理者控制指定的页面加载哪些资源。
|
||||
func csp(c *gin.Context) {
|
||||
enable := false
|
||||
if v := config.Get("security.csp.enable"); v != nil {
|
||||
enable = v.(bool)
|
||||
}
|
||||
|
||||
if enable {
|
||||
c.Header("x-csp-nonce", generate.Code(8))
|
||||
}
|
||||
}
|
||||
37
src/framework/middleware/security/hsts.go
Normal file
37
src/framework/middleware/security/hsts.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"ems.agt/src/framework/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// hsts 是一个安全功能 HTTP Strict Transport Security(通常简称为 HSTS )
|
||||
// 它告诉浏览器只能通过 HTTPS 访问当前资源,而不是 HTTP。
|
||||
func hsts(c *gin.Context) {
|
||||
enable := false
|
||||
if v := config.Get("security.hsts.enable"); v != nil {
|
||||
enable = v.(bool)
|
||||
}
|
||||
|
||||
maxAge := 365 * 24 * 3600
|
||||
if v := config.Get("security.hsts.maxAge"); v != nil {
|
||||
maxAge = v.(int)
|
||||
}
|
||||
|
||||
includeSubdomains := false
|
||||
if v := config.Get("security.hsts.includeSubdomains"); v != nil {
|
||||
includeSubdomains = v.(bool)
|
||||
}
|
||||
|
||||
str := fmt.Sprintf("max-age=%d", maxAge)
|
||||
if includeSubdomains {
|
||||
str += "; includeSubdomains"
|
||||
}
|
||||
|
||||
if enable {
|
||||
c.Header("strict-transport-security", str)
|
||||
}
|
||||
}
|
||||
20
src/framework/middleware/security/noopen.go
Normal file
20
src/framework/middleware/security/noopen.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"ems.agt/src/framework/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// noopen 用于指定 IE 8 以上版本的用户不打开文件而直接保存文件。
|
||||
// 在下载对话框中不显式“打开”选项。
|
||||
func noopen(c *gin.Context) {
|
||||
enable := false
|
||||
if v := config.Get("security.noopen.enable"); v != nil {
|
||||
enable = v.(bool)
|
||||
}
|
||||
|
||||
if enable {
|
||||
c.Header("x-download-options", "noopen")
|
||||
}
|
||||
}
|
||||
26
src/framework/middleware/security/nosniff.go
Normal file
26
src/framework/middleware/security/nosniff.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"ems.agt/src/framework/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// nosniff 用于防止 XSS 等跨站脚本攻击
|
||||
// 如果从 script 或 stylesheet 读入的文件的 MIME 类型与指定 MIME 类型不匹配,不允许读取该文件。
|
||||
func nosniff(c *gin.Context) {
|
||||
// 排除状态码范围
|
||||
status := c.Writer.Status()
|
||||
if status >= 300 && status <= 308 {
|
||||
return
|
||||
}
|
||||
|
||||
enable := false
|
||||
if v := config.Get("security.nosniff.enable"); v != nil {
|
||||
enable = v.(bool)
|
||||
}
|
||||
|
||||
if enable {
|
||||
c.Header("x-content-type-options", "nosniff")
|
||||
}
|
||||
}
|
||||
74
src/framework/middleware/security/referer.go
Normal file
74
src/framework/middleware/security/referer.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"ems.agt/src/framework/config"
|
||||
"ems.agt/src/framework/vo/result"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// referer 配置 referer 的 host 部分
|
||||
func referer(c *gin.Context) {
|
||||
enable := false
|
||||
if v := config.Get("security.csrf.enable"); v != nil {
|
||||
enable = v.(bool)
|
||||
}
|
||||
|
||||
// csrf 校验类型
|
||||
okType := false
|
||||
if v := config.Get("security.csrf.type"); v != nil {
|
||||
vType := v.(string)
|
||||
if vType == "all" || vType == "any" || vType == "referer" {
|
||||
okType = true
|
||||
}
|
||||
}
|
||||
if !okType {
|
||||
return
|
||||
}
|
||||
|
||||
// 忽略请求方法
|
||||
method := c.Request.Method
|
||||
ignoreMethods := []string{"GET", "HEAD", "OPTIONS", "TRACE"}
|
||||
for _, ignore := range ignoreMethods {
|
||||
if ignore == method {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
referer := c.GetHeader("Referer")
|
||||
if referer == "" {
|
||||
c.AbortWithStatusJSON(200, result.ErrMsg("无效 Referer 未知"))
|
||||
return
|
||||
}
|
||||
|
||||
// 获取host
|
||||
u, err := url.Parse(referer)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(200, result.ErrMsg("无效 Referer 未知"))
|
||||
return
|
||||
}
|
||||
host := u.Host
|
||||
|
||||
// 允许的来源白名单
|
||||
refererWhiteList := make([]string, 0)
|
||||
if v := config.Get("security.csrf.refererWhiteList"); v != nil {
|
||||
for _, s := range v.([]any) {
|
||||
refererWhiteList = append(refererWhiteList, s.(string))
|
||||
}
|
||||
}
|
||||
|
||||
if enable && okType {
|
||||
ok := false
|
||||
for _, domain := range refererWhiteList {
|
||||
if domain == host {
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
c.AbortWithStatusJSON(200, result.ErrMsg("无效 Referer "+host))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/framework/middleware/security/security.go
Normal file
23
src/framework/middleware/security/security.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Security 安全
|
||||
func Security() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 拦截,判断是否有效Referer
|
||||
referer(c)
|
||||
|
||||
// 无拦截,仅仅设置响应头
|
||||
xframe(c)
|
||||
csp(c)
|
||||
hsts(c)
|
||||
noopen(c)
|
||||
nosniff(c)
|
||||
xssProtection(c)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
26
src/framework/middleware/security/xframe.go
Normal file
26
src/framework/middleware/security/xframe.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"ems.agt/src/framework/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// xframe 用来配置 X-Frame-Options 响应头
|
||||
// 用来给浏览器指示允许一个页面可否在 frame, iframe, embed 或者 object 中展现的标记。
|
||||
// 站点可以通过确保网站没有被嵌入到别人的站点里面,从而避免 clickjacking 攻击。
|
||||
func xframe(c *gin.Context) {
|
||||
enable := false
|
||||
if v := config.Get("security.xframe.enable"); v != nil {
|
||||
enable = v.(bool)
|
||||
}
|
||||
|
||||
value := "sameorigin"
|
||||
if v := config.Get("security.xframe.value"); v != nil {
|
||||
value = v.(string)
|
||||
}
|
||||
|
||||
if enable {
|
||||
c.Header("x-frame-options", value)
|
||||
}
|
||||
}
|
||||
24
src/framework/middleware/security/xss_protection.go
Normal file
24
src/framework/middleware/security/xss_protection.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"ems.agt/src/framework/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// xssProtection 用于启用浏览器的XSS过滤功能,以防止 XSS 跨站脚本攻击。
|
||||
func xssProtection(c *gin.Context) {
|
||||
enable := false
|
||||
if v := config.Get("security.xssProtection.enable"); v != nil {
|
||||
enable = v.(bool)
|
||||
}
|
||||
|
||||
value := "1; mode=block"
|
||||
if v := config.Get("security.xssProtection.value"); v != nil {
|
||||
value = v.(string)
|
||||
}
|
||||
|
||||
if enable {
|
||||
c.Header("x-xss-protection", value)
|
||||
}
|
||||
}
|
||||
358
src/framework/redis/redis.go
Normal file
358
src/framework/redis/redis.go
Normal file
@@ -0,0 +1,358 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/config"
|
||||
"ems.agt/src/framework/logger"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// Redis连接实例
|
||||
var rdbMap = make(map[string]*redis.Client)
|
||||
|
||||
// 声明定义限流脚本命令
|
||||
var rateLimitCommand = redis.NewScript(`
|
||||
local key = KEYS[1]
|
||||
local time = tonumber(ARGV[1])
|
||||
local count = tonumber(ARGV[2])
|
||||
local current = redis.call('get', key);
|
||||
if current and tonumber(current) >= count then
|
||||
return tonumber(current);
|
||||
end
|
||||
current = redis.call('incr', key)
|
||||
if tonumber(current) == 1 then
|
||||
redis.call('expire', key, time)
|
||||
end
|
||||
return tonumber(current);`)
|
||||
|
||||
// 连接Redis实例
|
||||
func Connect() {
|
||||
ctx := context.Background()
|
||||
// 读取数据源配置
|
||||
datasource := config.Get("redis.dataSource").(map[string]any)
|
||||
for k, v := range datasource {
|
||||
client := v.(map[string]any)
|
||||
// 创建连接
|
||||
address := fmt.Sprintf("%s:%d", client["host"], client["port"])
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: address,
|
||||
Password: client["password"].(string),
|
||||
DB: client["db"].(int),
|
||||
})
|
||||
// 测试数据库连接
|
||||
pong, err := rdb.Ping(ctx).Result()
|
||||
if err != nil {
|
||||
logger.Fatalf("Ping redis %s is %v", k, err)
|
||||
}
|
||||
logger.Infof("redis %s %s %d connection is successful.", k, pong, client["db"].(int))
|
||||
rdbMap[k] = rdb
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭Redis实例
|
||||
func Close() {
|
||||
for _, rdb := range rdbMap {
|
||||
if err := rdb.Close(); err != nil {
|
||||
logger.Errorf("fatal error db close: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取默认实例
|
||||
func DefaultRDB() *redis.Client {
|
||||
source := config.Get("redis.defaultDataSourceName").(string)
|
||||
return rdbMap[source]
|
||||
}
|
||||
|
||||
// 获取实例
|
||||
func RDB(source string) *redis.Client {
|
||||
return rdbMap[source]
|
||||
}
|
||||
|
||||
// Info 获取redis服务信息
|
||||
func Info(source string) map[string]map[string]string {
|
||||
// 数据源
|
||||
rdb := DefaultRDB()
|
||||
if source != "" {
|
||||
rdb = RDB(source)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
info, err := rdb.Info(ctx).Result()
|
||||
if err != nil {
|
||||
return map[string]map[string]string{}
|
||||
}
|
||||
infoObj := make(map[string]map[string]string)
|
||||
lines := strings.Split(info, "\r\n")
|
||||
label := ""
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "#") {
|
||||
label = strings.Fields(line)[len(strings.Fields(line))-1]
|
||||
label = strings.ToLower(label)
|
||||
infoObj[label] = make(map[string]string)
|
||||
continue
|
||||
}
|
||||
kvArr := strings.Split(line, ":")
|
||||
if len(kvArr) >= 2 {
|
||||
key := strings.TrimSpace(kvArr[0])
|
||||
value := strings.TrimSpace(kvArr[len(kvArr)-1])
|
||||
infoObj[label][key] = value
|
||||
}
|
||||
}
|
||||
return infoObj
|
||||
}
|
||||
|
||||
// KeySize 获取redis当前连接可用键Key总数信息
|
||||
func KeySize(source string) int64 {
|
||||
// 数据源
|
||||
rdb := DefaultRDB()
|
||||
if source != "" {
|
||||
rdb = RDB(source)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
size, err := rdb.DBSize(ctx).Result()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
// CommandStats 获取redis命令状态信息
|
||||
func CommandStats(source string) []map[string]string {
|
||||
// 数据源
|
||||
rdb := DefaultRDB()
|
||||
if source != "" {
|
||||
rdb = RDB(source)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
commandstats, err := rdb.Info(ctx, "commandstats").Result()
|
||||
if err != nil {
|
||||
return []map[string]string{}
|
||||
}
|
||||
statsObjArr := make([]map[string]string, 0)
|
||||
lines := strings.Split(commandstats, "\r\n")
|
||||
for _, line := range lines {
|
||||
if !strings.HasPrefix(line, "cmdstat_") {
|
||||
continue
|
||||
}
|
||||
kvArr := strings.Split(line, ":")
|
||||
key := kvArr[0]
|
||||
valueStr := kvArr[len(kvArr)-1]
|
||||
statsObj := make(map[string]string)
|
||||
statsObj["name"] = key[8:]
|
||||
statsObj["value"] = valueStr[6:strings.Index(valueStr, ",usec=")]
|
||||
statsObjArr = append(statsObjArr, statsObj)
|
||||
}
|
||||
return statsObjArr
|
||||
}
|
||||
|
||||
// 获取键的剩余有效时间(秒)
|
||||
func GetExpire(source string, key string) (float64, error) {
|
||||
// 数据源
|
||||
rdb := DefaultRDB()
|
||||
if source != "" {
|
||||
rdb = RDB(source)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ttl, err := rdb.TTL(ctx, key).Result()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return ttl.Seconds(), nil
|
||||
}
|
||||
|
||||
// 获得缓存数据的key列表
|
||||
func GetKeys(source string, pattern string) ([]string, error) {
|
||||
// 数据源
|
||||
rdb := DefaultRDB()
|
||||
if source != "" {
|
||||
rdb = RDB(source)
|
||||
}
|
||||
|
||||
// 初始化变量
|
||||
var keys []string
|
||||
var cursor uint64 = 0
|
||||
ctx := context.Background()
|
||||
// 循环遍历获取匹配的键
|
||||
for {
|
||||
// 使用 SCAN 命令获取匹配的键
|
||||
batchKeys, nextCursor, err := rdb.Scan(ctx, cursor, pattern, 100).Result()
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to scan keys: %v", err)
|
||||
return keys, err
|
||||
}
|
||||
cursor = nextCursor
|
||||
keys = append(keys, batchKeys...)
|
||||
// 当 cursor 为 0,表示遍历完成
|
||||
if cursor == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// 批量获得缓存数据
|
||||
func GetBatch(source string, keys []string) ([]any, error) {
|
||||
if len(keys) == 0 {
|
||||
return []any{}, fmt.Errorf("not keys")
|
||||
}
|
||||
|
||||
// 数据源
|
||||
rdb := DefaultRDB()
|
||||
if source != "" {
|
||||
rdb = RDB(source)
|
||||
}
|
||||
|
||||
// 获取缓存数据
|
||||
result, err := rdb.MGet(context.Background(), keys...).Result()
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to get batch data: %v", err)
|
||||
return []any{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 获得缓存数据
|
||||
func Get(source, key string) (string, error) {
|
||||
// 数据源
|
||||
rdb := DefaultRDB()
|
||||
if source != "" {
|
||||
rdb = RDB(source)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
value, err := rdb.Get(ctx, key).Result()
|
||||
if err == redis.Nil || err != nil {
|
||||
return "", err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// 获得缓存数据Hash
|
||||
func GetHash(source, key string) (map[string]string, error) {
|
||||
// 数据源
|
||||
rdb := DefaultRDB()
|
||||
if source != "" {
|
||||
rdb = RDB(source)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
value, err := rdb.HGetAll(ctx, key).Result()
|
||||
if err == redis.Nil || err != nil {
|
||||
return map[string]string{}, err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// 判断是否存在
|
||||
func Has(source string, keys ...string) (bool, error) {
|
||||
// 数据源
|
||||
rdb := DefaultRDB()
|
||||
if source != "" {
|
||||
rdb = RDB(source)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
exists, err := rdb.Exists(ctx, keys...).Result()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return exists >= 1, nil
|
||||
}
|
||||
|
||||
// 设置缓存数据
|
||||
func Set(source, key string, value any) (bool, error) {
|
||||
// 数据源
|
||||
rdb := DefaultRDB()
|
||||
if source != "" {
|
||||
rdb = RDB(source)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
err := rdb.Set(ctx, key, value, 0).Err()
|
||||
if err != nil {
|
||||
logger.Errorf("redis Set err %v", err)
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 设置缓存数据与过期时间
|
||||
func SetByExpire(source, key string, value any, expiration time.Duration) (bool, error) {
|
||||
// 数据源
|
||||
rdb := DefaultRDB()
|
||||
if source != "" {
|
||||
rdb = RDB(source)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
err := rdb.Set(ctx, key, value, expiration).Err()
|
||||
if err != nil {
|
||||
logger.Errorf("redis SetByExpire err %v", err)
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 删除单个
|
||||
func Del(source string, key string) (bool, error) {
|
||||
// 数据源
|
||||
rdb := DefaultRDB()
|
||||
if source != "" {
|
||||
rdb = RDB(source)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
err := rdb.Del(ctx, key).Err()
|
||||
if err != nil {
|
||||
logger.Errorf("redis Del err %v", err)
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 删除多个
|
||||
func DelKeys(source string, keys []string) (bool, error) {
|
||||
if len(keys) == 0 {
|
||||
return false, fmt.Errorf("no keys")
|
||||
}
|
||||
|
||||
// 数据源
|
||||
rdb := DefaultRDB()
|
||||
if source != "" {
|
||||
rdb = RDB(source)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
err := rdb.Del(ctx, keys...).Err()
|
||||
if err != nil {
|
||||
logger.Errorf("redis DelKeys err %v", err)
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 限流查询并记录
|
||||
func RateLimit(source, limitKey string, time, count int64) (int64, error) {
|
||||
// 数据源
|
||||
rdb := DefaultRDB()
|
||||
if source != "" {
|
||||
rdb = RDB(source)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := rateLimitCommand.Run(ctx, rdb, []string{limitKey}, time, count).Result()
|
||||
if err != nil {
|
||||
logger.Errorf("redis RateLimit err %v", err)
|
||||
return 0, err
|
||||
}
|
||||
return result.(int64), err
|
||||
}
|
||||
20
src/framework/utils/crypto/crypto.go
Normal file
20
src/framework/utils/crypto/crypto.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// BcryptHash Bcrypt密码加密
|
||||
func BcryptHash(originStr string) string {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(originStr), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(hash)
|
||||
}
|
||||
|
||||
// BcryptCompare Bcrypt密码匹配检查
|
||||
func BcryptCompare(originStr, hashStr string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashStr), []byte(originStr))
|
||||
return err == nil
|
||||
}
|
||||
199
src/framework/utils/ctx/ctx.go
Normal file
199
src/framework/utils/ctx/ctx.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package ctx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"ems.agt/src/framework/config"
|
||||
"ems.agt/src/framework/constants/common"
|
||||
"ems.agt/src/framework/constants/roledatascope"
|
||||
"ems.agt/src/framework/constants/token"
|
||||
"ems.agt/src/framework/utils/ip2region"
|
||||
"ems.agt/src/framework/utils/ua"
|
||||
"ems.agt/src/framework/vo"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
)
|
||||
|
||||
// QueryMap 查询参数转换Map
|
||||
func QueryMap(c *gin.Context) map[string]any {
|
||||
queryValues := c.Request.URL.Query()
|
||||
queryParams := make(map[string]any)
|
||||
for key, values := range queryValues {
|
||||
queryParams[key] = values[0]
|
||||
}
|
||||
return queryParams
|
||||
}
|
||||
|
||||
// BodyJSONMap JSON参数转换Map
|
||||
func BodyJSONMap(c *gin.Context) map[string]any {
|
||||
params := make(map[string]any)
|
||||
c.ShouldBindBodyWith(¶ms, binding.JSON)
|
||||
return params
|
||||
}
|
||||
|
||||
// RequestParamsMap 请求参数转换Map
|
||||
func RequestParamsMap(c *gin.Context) map[string]any {
|
||||
params := make(map[string]any)
|
||||
// json
|
||||
if strings.HasPrefix(c.ContentType(), "application/json") {
|
||||
c.ShouldBindBodyWith(¶ms, binding.JSON)
|
||||
}
|
||||
|
||||
// 表单
|
||||
bodyParams := c.Request.PostForm
|
||||
for key, value := range bodyParams {
|
||||
params[key] = value[0]
|
||||
}
|
||||
|
||||
// 查询
|
||||
queryParams := c.Request.URL.Query()
|
||||
for key, value := range queryParams {
|
||||
params[key] = value[0]
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
// IPAddrLocation 解析ip地址
|
||||
func IPAddrLocation(c *gin.Context) (string, string) {
|
||||
ip := ip2region.ClientIP(c.ClientIP())
|
||||
location := ip2region.RealAddressByIp(ip)
|
||||
return ip, location
|
||||
}
|
||||
|
||||
// Authorization 解析请求头
|
||||
func Authorization(c *gin.Context) string {
|
||||
authHeader := c.GetHeader(token.HEADER_KEY)
|
||||
if authHeader == "" {
|
||||
return ""
|
||||
}
|
||||
// 拆分 Authorization 请求头,提取 JWT 令牌部分
|
||||
arr := strings.Split(authHeader, token.HEADER_PREFIX)
|
||||
if len(arr) == 2 && arr[1] == "" {
|
||||
return ""
|
||||
}
|
||||
return arr[1]
|
||||
}
|
||||
|
||||
// UaOsBrowser 解析请求用户代理信息
|
||||
func UaOsBrowser(c *gin.Context) (string, string) {
|
||||
userAgent := c.GetHeader("user-agent")
|
||||
uaInfo := ua.Info(userAgent)
|
||||
|
||||
browser := "未知 未知"
|
||||
bName, bVersion := uaInfo.Browser()
|
||||
if bName != "" && bVersion != "" {
|
||||
browser = bName + " " + bVersion
|
||||
}
|
||||
|
||||
os := "未知 未知"
|
||||
bos := uaInfo.OS()
|
||||
if bos != "" {
|
||||
os = bos
|
||||
}
|
||||
return os, browser
|
||||
}
|
||||
|
||||
// LoginUser 登录用户信息
|
||||
func LoginUser(c *gin.Context) (vo.LoginUser, error) {
|
||||
value, exists := c.Get(common.CTX_LOGIN_USER)
|
||||
if exists {
|
||||
return value.(vo.LoginUser), nil
|
||||
}
|
||||
return vo.LoginUser{}, fmt.Errorf("无效登录用户信息")
|
||||
}
|
||||
|
||||
// LoginUserToUserID 登录用户信息-用户ID
|
||||
func LoginUserToUserID(c *gin.Context) string {
|
||||
value, exists := c.Get(common.CTX_LOGIN_USER)
|
||||
if exists {
|
||||
loginUser := value.(vo.LoginUser)
|
||||
return loginUser.UserID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// LoginUserToUserName 登录用户信息-用户名称
|
||||
func LoginUserToUserName(c *gin.Context) string {
|
||||
value, exists := c.Get(common.CTX_LOGIN_USER)
|
||||
if exists {
|
||||
loginUser := value.(vo.LoginUser)
|
||||
return loginUser.User.UserName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// LoginUserToDataScopeSQL 登录用户信息-角色数据范围过滤SQL字符串
|
||||
func LoginUserToDataScopeSQL(c *gin.Context, deptAlias string, userAlias string) string {
|
||||
dataScopeSQL := ""
|
||||
// 登录用户信息
|
||||
loginUser, err := LoginUser(c)
|
||||
if err != nil {
|
||||
return dataScopeSQL
|
||||
}
|
||||
userInfo := loginUser.User
|
||||
|
||||
// 如果是管理员,则不过滤数据
|
||||
if config.IsAdmin(userInfo.UserID) {
|
||||
return dataScopeSQL
|
||||
}
|
||||
// 无用户角色
|
||||
if len(userInfo.Roles) <= 0 {
|
||||
return dataScopeSQL
|
||||
}
|
||||
|
||||
// 记录角色权限范围定义添加过, 非自定数据权限不需要重复拼接SQL
|
||||
var scopeKeys []string
|
||||
var conditions []string
|
||||
for _, role := range userInfo.Roles {
|
||||
dataScope := role.DataScope
|
||||
|
||||
if roledatascope.ALL == dataScope {
|
||||
break
|
||||
}
|
||||
|
||||
if roledatascope.CUSTOM != dataScope {
|
||||
hasKey := false
|
||||
for _, key := range scopeKeys {
|
||||
if key == dataScope {
|
||||
hasKey = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasKey {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if roledatascope.CUSTOM == dataScope {
|
||||
sql := fmt.Sprintf(`%s.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = '%s' )`, deptAlias, role.RoleID)
|
||||
conditions = append(conditions, sql)
|
||||
}
|
||||
|
||||
if roledatascope.DEPT_AND_CHILD == dataScope {
|
||||
sql := fmt.Sprintf(`%s.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = '%s' or find_in_set('%s' , ancestors ) )`, deptAlias, userInfo.DeptID, userInfo.DeptID)
|
||||
conditions = append(conditions, sql)
|
||||
}
|
||||
|
||||
if roledatascope.SELF == dataScope {
|
||||
// 数据权限为仅本人且没有userAlias别名不查询任何数据
|
||||
if userAlias == "" {
|
||||
sql := fmt.Sprintf(`%s.dept_id = '0'`, deptAlias)
|
||||
conditions = append(conditions, sql)
|
||||
} else {
|
||||
sql := fmt.Sprintf(`%s.user_id = '%s'`, userAlias, userInfo.UserID)
|
||||
conditions = append(conditions, sql)
|
||||
}
|
||||
}
|
||||
|
||||
// 记录角色范围
|
||||
scopeKeys = append(scopeKeys, dataScope)
|
||||
}
|
||||
|
||||
// 构建查询条件语句
|
||||
if len(conditions) > 0 {
|
||||
dataScopeSQL = fmt.Sprintf(" AND ( %s ) ", strings.Join(conditions, " OR "))
|
||||
}
|
||||
return dataScopeSQL
|
||||
}
|
||||
69
src/framework/utils/date/date.go
Normal file
69
src/framework/utils/date/date.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package date
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
// 年 列如:2022
|
||||
YYYY = "2006"
|
||||
// 年-月 列如:2022-12
|
||||
YYYY_MM = "2006-01"
|
||||
// 年-月-日 列如:2022-12-30
|
||||
YYYY_MM_DD = "2006-01-02"
|
||||
// 年月日时分秒 列如:20221230010159
|
||||
YYYYMMDDHHMMSS = "20060102150405"
|
||||
// 年-月-日 时:分:秒 列如:2022-12-30 01:01:59
|
||||
YYYY_MM_DD_HH_MM_SS = "2006-01-02 15:04:05"
|
||||
)
|
||||
|
||||
// 格式时间字符串
|
||||
//
|
||||
// dateStr 时间字符串
|
||||
//
|
||||
// formatStr 时间格式 默认YYYY-MM-DD HH:mm:ss
|
||||
func ParseStrToDate(dateStr, formatStr string) time.Time {
|
||||
t, err := time.Parse(formatStr, dateStr)
|
||||
if err != nil {
|
||||
logger.Infof("utils ParseStrToDate err %v", err)
|
||||
return time.Time{}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// 格式时间
|
||||
//
|
||||
// date 可转的Date对象
|
||||
//
|
||||
// formatStr 时间格式 默认YYYY-MM-DD HH:mm:ss
|
||||
func ParseDateToStr(date any, formatStr string) string {
|
||||
t, ok := date.(time.Time)
|
||||
if !ok {
|
||||
switch v := date.(type) {
|
||||
case int64:
|
||||
if v == 0 {
|
||||
return ""
|
||||
}
|
||||
t = time.UnixMilli(v)
|
||||
case string:
|
||||
parsedTime, err := time.Parse(formatStr, v)
|
||||
if err != nil {
|
||||
logger.Infof("utils ParseDateToStr err %v", err)
|
||||
return ""
|
||||
}
|
||||
t = parsedTime
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return t.Format(formatStr)
|
||||
}
|
||||
|
||||
// 格式时间成日期路径
|
||||
//
|
||||
// 年/月 列如:2022/12
|
||||
func ParseDatePath(date time.Time) string {
|
||||
return date.Format("2006/01")
|
||||
}
|
||||
154
src/framework/utils/file/excel.go
Normal file
154
src/framework/utils/file/excel.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/constants/uploadsubpath"
|
||||
"ems.agt/src/framework/logger"
|
||||
"ems.agt/src/framework/utils/date"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// TransferExeclUploadFile 表格文件上传保存
|
||||
//
|
||||
// file 上传文件对象
|
||||
func TransferExeclUploadFile(file *multipart.FileHeader) (string, error) {
|
||||
// 上传文件检查
|
||||
err := isAllowWrite(file.Filename, []string{".xls", ".xlsx"}, file.Size)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// 上传资源路径
|
||||
_, dir := resourceUpload()
|
||||
// 新文件名称并组装文件地址
|
||||
filePath := filepath.Join(uploadsubpath.IMPORT, date.ParseDatePath(time.Now()))
|
||||
fileName := generateFileName(file.Filename)
|
||||
writePathFile := filepath.Join(dir, filePath, fileName)
|
||||
// 存入新文件路径
|
||||
err = transferToNewFile(file, writePathFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.ToSlash(writePathFile), nil
|
||||
}
|
||||
|
||||
// 表格读取数据
|
||||
//
|
||||
// filePath 文件路径地址
|
||||
//
|
||||
// sheetName 工作簿名称, 空字符默认Sheet1
|
||||
func ReadSheet(filePath, sheetName string) ([]map[string]string, error) {
|
||||
data := make([]map[string]string, 0)
|
||||
// 打开 Excel 文件
|
||||
f, err := excelize.OpenFile(filePath)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
logger.Errorf("工作表文件关闭失败 : %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 检查工作簿是否存在
|
||||
if sheetName == "" {
|
||||
sheetName = "Sheet1"
|
||||
}
|
||||
if visible, _ := f.GetSheetVisible(sheetName); !visible {
|
||||
return data, fmt.Errorf("读取工作簿 %s 失败", sheetName)
|
||||
}
|
||||
|
||||
// 获取工作簿记录
|
||||
rows, err := f.GetRows(sheetName)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
|
||||
for i, row := range rows {
|
||||
// 跳过第一行
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
// 遍历每个单元格
|
||||
rowData := map[string]string{}
|
||||
for columnIndex, cellValue := range row {
|
||||
columnName, _ := excelize.ColumnNumberToName(columnIndex + 1)
|
||||
rowData[columnName] = cellValue
|
||||
}
|
||||
|
||||
data = append(data, rowData)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// 表格写入数据
|
||||
//
|
||||
// headerCells 第一行表头标题 "A1":"?"
|
||||
//
|
||||
// dataCells 从第二行开始的数据 "A2":"?"
|
||||
//
|
||||
// fileName 文件名称
|
||||
//
|
||||
// sheetName 工作簿名称, 空字符默认Sheet1
|
||||
func WriteSheet(headerCells map[string]string, dataCells []map[string]any, fileName, sheetName string) (string, error) {
|
||||
f := excelize.NewFile()
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
logger.Errorf("工作表文件关闭失败 : %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 创建一个工作表
|
||||
if sheetName == "" {
|
||||
sheetName = "Sheet1"
|
||||
}
|
||||
index, err := f.NewSheet(sheetName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建工作表失败 %v", err)
|
||||
}
|
||||
// 设置工作簿的默认工作表
|
||||
f.SetActiveSheet(index)
|
||||
|
||||
// 首个和最后key名称
|
||||
firstKey := "A"
|
||||
lastKey := "B"
|
||||
|
||||
// 第一行表头标题
|
||||
for key, title := range headerCells {
|
||||
f.SetCellValue(sheetName, key, title)
|
||||
if key[:1] > lastKey {
|
||||
lastKey = key[:1]
|
||||
}
|
||||
}
|
||||
|
||||
// 设置工作表上宽度为 20
|
||||
f.SetColWidth(sheetName, firstKey, lastKey, 20)
|
||||
|
||||
// 从第二行开始的数据
|
||||
for _, cell := range dataCells {
|
||||
for key, value := range cell {
|
||||
f.SetCellValue(sheetName, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// 上传资源路径
|
||||
_, dir := resourceUpload()
|
||||
filePath := filepath.Join(uploadsubpath.EXPORT, date.ParseDatePath(time.Now()))
|
||||
saveFilePath := filepath.Join(dir, filePath, fileName)
|
||||
|
||||
// 创建文件目录
|
||||
if err := os.MkdirAll(filepath.Dir(saveFilePath), 0750); err != nil {
|
||||
return "", fmt.Errorf("创建保存文件失败 %v", err)
|
||||
}
|
||||
|
||||
// 根据指定路径保存文件
|
||||
if err := f.SaveAs(saveFilePath); err != nil {
|
||||
return "", fmt.Errorf("保存工作表失败 %v", err)
|
||||
}
|
||||
return saveFilePath, nil
|
||||
}
|
||||
297
src/framework/utils/file/file.go
Normal file
297
src/framework/utils/file/file.go
Normal file
@@ -0,0 +1,297 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/config"
|
||||
"ems.agt/src/framework/constants/uploadsubpath"
|
||||
"ems.agt/src/framework/logger"
|
||||
"ems.agt/src/framework/utils/date"
|
||||
"ems.agt/src/framework/utils/generate"
|
||||
"ems.agt/src/framework/utils/parse"
|
||||
"ems.agt/src/framework/utils/regular"
|
||||
)
|
||||
|
||||
/**最大文件名长度 */
|
||||
const DEFAULT_FILE_NAME_LENGTH = 100
|
||||
|
||||
// 文件上传路径 prefix, dir
|
||||
func resourceUpload() (string, string) {
|
||||
upload := config.Get("staticFile.upload").(map[string]any)
|
||||
dir, err := filepath.Abs(upload["dir"].(string))
|
||||
if err != nil {
|
||||
logger.Errorf("file resourceUpload err %v", err)
|
||||
}
|
||||
return upload["prefix"].(string), dir
|
||||
}
|
||||
|
||||
// 最大上传文件大小
|
||||
func uploadFileSize() int64 {
|
||||
fileSize := 1 * 1024 * 1024
|
||||
size := config.Get("upload.fileSize").(int)
|
||||
if size > 1 {
|
||||
fileSize = size * 1024 * 1024
|
||||
}
|
||||
return int64(fileSize)
|
||||
}
|
||||
|
||||
// 文件上传扩展名白名单
|
||||
func uploadWhiteList() []string {
|
||||
arr := config.Get("upload.whitelist").([]any)
|
||||
strings := make([]string, len(arr))
|
||||
for i, val := range arr {
|
||||
if str, ok := val.(string); ok {
|
||||
strings[i] = str
|
||||
}
|
||||
}
|
||||
return strings
|
||||
}
|
||||
|
||||
// 生成文件名称 fileName_随机值.extName
|
||||
//
|
||||
// fileName 原始文件名称含后缀,如:logo.png
|
||||
func generateFileName(fileName string) string {
|
||||
fileExt := filepath.Ext(fileName)
|
||||
// 替换掉后缀和特殊字符保留文件名
|
||||
newFileName := regular.Replace(fileName, fileExt, "")
|
||||
newFileName = regular.Replace(newFileName, `[<>:"\\|?*]+`, "")
|
||||
newFileName = strings.TrimSpace(newFileName)
|
||||
return fmt.Sprintf("%s_%s%s", newFileName, generate.Code(6), fileExt)
|
||||
}
|
||||
|
||||
// 检查文件允许写入本地
|
||||
//
|
||||
// fileName 原始文件名称含后缀,如:midway1_logo_iipc68.png
|
||||
//
|
||||
// allowExts 允许上传拓展类型,['.png']
|
||||
func isAllowWrite(fileName string, allowExts []string, fileSize int64) error {
|
||||
// 判断上传文件名称长度
|
||||
if len(fileName) > DEFAULT_FILE_NAME_LENGTH {
|
||||
return fmt.Errorf("上传文件名称长度限制最长为 %d", DEFAULT_FILE_NAME_LENGTH)
|
||||
}
|
||||
|
||||
// 最大上传文件大小
|
||||
maxFileSize := uploadFileSize()
|
||||
if fileSize > maxFileSize {
|
||||
return fmt.Errorf("最大上传文件大小 %s", parse.Bit(float64(maxFileSize)))
|
||||
}
|
||||
|
||||
// 判断文件拓展是否为允许的拓展类型
|
||||
fileExt := filepath.Ext(fileName)
|
||||
hasExt := false
|
||||
if len(allowExts) == 0 {
|
||||
allowExts = uploadWhiteList()
|
||||
}
|
||||
for _, ext := range allowExts {
|
||||
if ext == fileExt {
|
||||
hasExt = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasExt {
|
||||
return fmt.Errorf("上传文件类型不支持,仅支持以下类型:%s", strings.Join(allowExts, "、"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查文件允许本地读取
|
||||
//
|
||||
// filePath 文件存放资源路径,URL相对地址
|
||||
func isAllowRead(filePath string) error {
|
||||
// 禁止目录上跳级别
|
||||
if strings.Contains(filePath, "..") {
|
||||
return fmt.Errorf("禁止目录上跳级别")
|
||||
}
|
||||
|
||||
// 检查允许下载的文件规则
|
||||
fileExt := filepath.Ext(filePath)
|
||||
hasExt := false
|
||||
for _, ext := range uploadWhiteList() {
|
||||
if ext == fileExt {
|
||||
hasExt = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasExt {
|
||||
return fmt.Errorf("非法下载的文件规则:%s", fileExt)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TransferUploadFile 上传资源文件转存
|
||||
//
|
||||
// subPath 子路径,默认 UploadSubPath.DEFAULT
|
||||
//
|
||||
// allowExts 允许上传拓展类型(含“.”),如 ['.png','.jpg']
|
||||
func TransferUploadFile(file *multipart.FileHeader, subPath string, allowExts []string) (string, error) {
|
||||
// 上传文件检查
|
||||
err := isAllowWrite(file.Filename, allowExts, file.Size)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// 上传资源路径
|
||||
prefix, dir := resourceUpload()
|
||||
// 新文件名称并组装文件地址
|
||||
fileName := generateFileName(file.Filename)
|
||||
filePath := filepath.Join(subPath, date.ParseDatePath(time.Now()))
|
||||
writePathFile := filepath.Join(dir, filePath, fileName)
|
||||
// 存入新文件路径
|
||||
err = transferToNewFile(file, writePathFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
urlPath := filepath.Join(prefix, filePath, fileName)
|
||||
return filepath.ToSlash(urlPath), nil
|
||||
}
|
||||
|
||||
// ReadUploadFileStream 上传资源文件读取
|
||||
//
|
||||
// filePath 文件存放资源路径,URL相对地址 如:/upload/common/2023/06/xxx.png
|
||||
//
|
||||
// headerRange 断点续传范围区间,bytes=0-12131
|
||||
func ReadUploadFileStream(filePath, headerRange string) (map[string]any, error) {
|
||||
// 读取文件检查
|
||||
err := isAllowRead(filePath)
|
||||
if err != nil {
|
||||
return map[string]any{}, err
|
||||
}
|
||||
// 上传资源路径
|
||||
prefix, dir := resourceUpload()
|
||||
fileAsbPath := strings.Replace(filePath, prefix, dir, 1)
|
||||
|
||||
// 响应结果
|
||||
result := map[string]any{
|
||||
"range": "",
|
||||
"chunkSize": 0,
|
||||
"fileSize": 0,
|
||||
"data": nil,
|
||||
}
|
||||
|
||||
// 文件大小
|
||||
fileSize := getFileSize(fileAsbPath)
|
||||
if fileSize <= 0 {
|
||||
return result, nil
|
||||
}
|
||||
result["fileSize"] = fileSize
|
||||
|
||||
if headerRange != "" {
|
||||
partsStr := strings.Replace(headerRange, "bytes=", "", 1)
|
||||
parts := strings.Split(partsStr, "-")
|
||||
start, err := strconv.ParseInt(parts[0], 10, 64)
|
||||
if err != nil || start > fileSize {
|
||||
start = 0
|
||||
}
|
||||
end, err := strconv.ParseInt(parts[1], 10, 64)
|
||||
if err != nil || end > fileSize {
|
||||
end = fileSize - 1
|
||||
}
|
||||
if start > end {
|
||||
start = end
|
||||
}
|
||||
|
||||
// 分片结果
|
||||
result["range"] = fmt.Sprintf("bytes %d-%d/%d", start, end, fileSize)
|
||||
result["chunkSize"] = end - start + 1
|
||||
byteArr, err := getFileStream(fileAsbPath, start, end)
|
||||
if err != nil {
|
||||
return map[string]any{}, errors.New("读取文件失败")
|
||||
}
|
||||
result["data"] = byteArr
|
||||
return result, nil
|
||||
}
|
||||
|
||||
byteArr, err := getFileStream(fileAsbPath, 0, fileSize)
|
||||
if err != nil {
|
||||
return map[string]any{}, errors.New("读取文件失败")
|
||||
}
|
||||
result["data"] = byteArr
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// TransferChunkUploadFile 上传资源切片文件转存
|
||||
//
|
||||
// file 上传文件对象
|
||||
//
|
||||
// index 切片文件序号
|
||||
//
|
||||
// identifier 切片文件目录标识符
|
||||
func TransferChunkUploadFile(file *multipart.FileHeader, index, identifier string) (string, error) {
|
||||
// 上传文件检查
|
||||
err := isAllowWrite(file.Filename, []string{}, file.Size)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// 上传资源路径
|
||||
prefix, dir := resourceUpload()
|
||||
// 新文件名称并组装文件地址
|
||||
filePath := filepath.Join(uploadsubpath.CHUNK, date.ParseDatePath(time.Now()), identifier)
|
||||
writePathFile := filepath.Join(dir, filePath, index)
|
||||
// 存入新文件路径
|
||||
err = transferToNewFile(file, writePathFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
urlPath := filepath.Join(prefix, filePath, index)
|
||||
return filepath.ToSlash(urlPath), nil
|
||||
}
|
||||
|
||||
// 上传资源切片文件检查
|
||||
//
|
||||
// identifier 切片文件目录标识符
|
||||
//
|
||||
// originalFileName 原始文件名称,如logo.png
|
||||
func ChunkCheckFile(identifier, originalFileName string) ([]string, error) {
|
||||
// 读取文件检查
|
||||
err := isAllowWrite(originalFileName, []string{}, 0)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
// 上传资源路径
|
||||
_, dir := resourceUpload()
|
||||
dirPath := path.Join(uploadsubpath.CHUNK, date.ParseDatePath(time.Now()), identifier)
|
||||
readPath := path.Join(dir, dirPath)
|
||||
fileList, err := getDirFileNameList(readPath)
|
||||
if err != nil {
|
||||
return []string{}, errors.New("读取文件失败")
|
||||
}
|
||||
return fileList, nil
|
||||
}
|
||||
|
||||
// 上传资源切片文件检查
|
||||
//
|
||||
// identifier 切片文件目录标识符
|
||||
//
|
||||
// originalFileName 原始文件名称,如logo.png
|
||||
//
|
||||
// subPath 子路径,默认 DEFAULT
|
||||
func ChunkMergeFile(identifier, originalFileName, subPath string) (string, error) {
|
||||
// 读取文件检查
|
||||
err := isAllowWrite(originalFileName, []string{}, 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// 上传资源路径
|
||||
prefix, dir := resourceUpload()
|
||||
// 切片存放目录
|
||||
dirPath := path.Join(uploadsubpath.CHUNK, date.ParseDatePath(time.Now()), identifier)
|
||||
readPath := path.Join(dir, dirPath)
|
||||
// 组合存放文件路径
|
||||
fileName := generateFileName(originalFileName)
|
||||
filePath := path.Join(subPath, date.ParseDatePath(time.Now()))
|
||||
writePath := path.Join(dir, filePath)
|
||||
err = mergeToNewFile(readPath, writePath, fileName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
urlPath := filepath.Join(prefix, filePath, fileName)
|
||||
return filepath.ToSlash(urlPath), nil
|
||||
}
|
||||
185
src/framework/utils/file/utils.go
Normal file
185
src/framework/utils/file/utils.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"ems.agt/src/framework/logger"
|
||||
)
|
||||
|
||||
// transferToNewFile 读取目标文件转移到新路径下
|
||||
//
|
||||
// readFilePath 读取目标文件
|
||||
//
|
||||
// writePath 写入路径
|
||||
//
|
||||
// fileName 文件名称
|
||||
func transferToNewFile(file *multipart.FileHeader, dst string) error {
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
if err = os.MkdirAll(filepath.Dir(dst), 0750); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, src)
|
||||
return err
|
||||
}
|
||||
|
||||
// mergeToNewFile 将多个文件合并成一个文件并删除合并前的切片目录文件
|
||||
//
|
||||
// dirPath 读取要合并文件的目录
|
||||
//
|
||||
// writePath 写入路径
|
||||
//
|
||||
// fileName 文件名称
|
||||
func mergeToNewFile(dirPath string, writePath string, fileName string) error {
|
||||
// 读取目录下所有文件并排序,注意文件名称是否数值
|
||||
fileNameList, err := getDirFileNameList(dirPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取合并目标文件失败: %v", err)
|
||||
}
|
||||
if len(fileNameList) <= 0 {
|
||||
return fmt.Errorf("读取合并目标文件失败")
|
||||
}
|
||||
|
||||
// 排序
|
||||
sort.Slice(fileNameList, func(i, j int) bool {
|
||||
numI, _ := strconv.Atoi(fileNameList[i])
|
||||
numJ, _ := strconv.Atoi(fileNameList[j])
|
||||
return numI < numJ
|
||||
})
|
||||
|
||||
// 写入到新路径文件
|
||||
newFilePath := filepath.Join(writePath, fileName)
|
||||
if err = os.MkdirAll(filepath.Dir(newFilePath), 0750); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 转移完成后删除切片目录
|
||||
defer os.Remove(dirPath)
|
||||
|
||||
// 打开新路径文件
|
||||
outputFile, err := os.Create(newFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file: %v", err)
|
||||
}
|
||||
defer outputFile.Close()
|
||||
|
||||
// 逐个读取文件后进行流拷贝数据块
|
||||
for _, fileName := range fileNameList {
|
||||
chunkPath := filepath.Join(dirPath, fileName)
|
||||
// 拷贝结束后删除切片
|
||||
defer os.Remove(chunkPath)
|
||||
// 打开切片文件
|
||||
inputFile, err := os.Open(chunkPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file: %v", err)
|
||||
}
|
||||
defer inputFile.Close()
|
||||
// 拷贝文件流
|
||||
_, err = io.Copy(outputFile, inputFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy file data: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getFileSize 读取文件大小
|
||||
func getFileSize(filePath string) int64 {
|
||||
// 获取文件信息
|
||||
fileInfo, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed stat %s: %v", filePath, err)
|
||||
return 0
|
||||
}
|
||||
// 获取文件大小(字节数)
|
||||
return fileInfo.Size()
|
||||
}
|
||||
|
||||
// 读取文件流用于返回下载
|
||||
//
|
||||
// filePath 文件路径
|
||||
// startOffset, endOffset 分片块读取区间,根据文件切片的块范围
|
||||
func getFileStream(filePath string, startOffset, endOffset int64) ([]byte, error) {
|
||||
// 打开文件
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 获取文件的大小
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fileSize := fileInfo.Size()
|
||||
|
||||
// 确保起始和结束偏移量在文件范围内
|
||||
if startOffset > fileSize {
|
||||
startOffset = 0
|
||||
}
|
||||
if endOffset >= fileSize {
|
||||
endOffset = fileSize - 1
|
||||
}
|
||||
|
||||
// 计算切片的大小
|
||||
chunkSize := endOffset - startOffset + 1
|
||||
|
||||
// 创建 SectionReader
|
||||
reader := io.NewSectionReader(file, startOffset, chunkSize)
|
||||
|
||||
// 创建一个缓冲区来存储读取的数据
|
||||
buffer := make([]byte, chunkSize)
|
||||
|
||||
// 读取数据到缓冲区
|
||||
_, err = reader.Read(buffer)
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buffer, nil
|
||||
}
|
||||
|
||||
// 获取文件目录下所有文件名称,不含目录名称
|
||||
//
|
||||
// filePath 文件路径
|
||||
func getDirFileNameList(dirPath string) ([]string, error) {
|
||||
fileNames := []string{}
|
||||
|
||||
dir, err := os.Open(dirPath)
|
||||
if err != nil {
|
||||
return fileNames, nil
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
fileInfos, err := dir.Readdir(-1)
|
||||
if err != nil {
|
||||
return fileNames, err
|
||||
}
|
||||
|
||||
for _, fileInfo := range fileInfos {
|
||||
if fileInfo.Mode().IsRegular() {
|
||||
fileNames = append(fileNames, fileInfo.Name())
|
||||
}
|
||||
}
|
||||
|
||||
return fileNames, nil
|
||||
}
|
||||
43
src/framework/utils/generate/generate.go
Normal file
43
src/framework/utils/generate/generate.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/logger"
|
||||
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
)
|
||||
|
||||
// 生成随机Code
|
||||
// 包含数字、小写字母
|
||||
// 不保证长度满足
|
||||
func Code(size int) string {
|
||||
str, err := gonanoid.Generate("0123456789abcdefghijklmnopqrstuvwxyz", size)
|
||||
if err != nil {
|
||||
logger.Infof("%d : %v", size, err)
|
||||
return ""
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// 生成随机字符串
|
||||
// 包含数字、大小写字母、下划线、横杠
|
||||
// 不保证长度满足
|
||||
func String(size int) string {
|
||||
str, err := gonanoid.New(size)
|
||||
if err != nil {
|
||||
logger.Infof("%d : %v", size, err)
|
||||
return ""
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// 随机数 纯数字0-9
|
||||
func Number(size int) int {
|
||||
source := rand.NewSource(time.Now().UnixNano())
|
||||
random := rand.New(source)
|
||||
min := int64(0)
|
||||
max := int64(9 * int(size))
|
||||
return int(random.Int63n(max-min+1) + min)
|
||||
}
|
||||
238
src/framework/utils/ip2region/binding.go
Normal file
238
src/framework/utils/ip2region/binding.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package ip2region
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderInfoLength = 256
|
||||
VectorIndexRows = 256
|
||||
VectorIndexCols = 256
|
||||
VectorIndexSize = 8
|
||||
SegmentIndexBlockSize = 14
|
||||
)
|
||||
|
||||
// --- Index policy define
|
||||
|
||||
type IndexPolicy int
|
||||
|
||||
const (
|
||||
VectorIndexPolicy IndexPolicy = 1
|
||||
BTreeIndexPolicy IndexPolicy = 2
|
||||
)
|
||||
|
||||
func (i IndexPolicy) String() string {
|
||||
switch i {
|
||||
case VectorIndexPolicy:
|
||||
return "VectorIndex"
|
||||
case BTreeIndexPolicy:
|
||||
return "BtreeIndex"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// --- Header define
|
||||
|
||||
type Header struct {
|
||||
// data []byte
|
||||
Version uint16
|
||||
IndexPolicy IndexPolicy
|
||||
CreatedAt uint32
|
||||
StartIndexPtr uint32
|
||||
EndIndexPtr uint32
|
||||
}
|
||||
|
||||
func NewHeader(input []byte) (*Header, error) {
|
||||
if len(input) < 16 {
|
||||
return nil, fmt.Errorf("invalid input buffer")
|
||||
}
|
||||
|
||||
return &Header{
|
||||
Version: binary.LittleEndian.Uint16(input),
|
||||
IndexPolicy: IndexPolicy(binary.LittleEndian.Uint16(input[2:])),
|
||||
CreatedAt: binary.LittleEndian.Uint32(input[4:]),
|
||||
StartIndexPtr: binary.LittleEndian.Uint32(input[8:]),
|
||||
EndIndexPtr: binary.LittleEndian.Uint32(input[12:]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// --- searcher implementation
|
||||
|
||||
type Searcher struct {
|
||||
handle *os.File
|
||||
|
||||
ioCount int
|
||||
|
||||
// use it only when this feature enabled.
|
||||
// Preload the vector index will reduce the number of IO operations
|
||||
// thus speedup the search process
|
||||
vectorIndex []byte
|
||||
|
||||
// content buffer.
|
||||
// running with the whole xdb file cached
|
||||
contentBuff []byte
|
||||
}
|
||||
|
||||
func baseNew(dbFile string, vIndex []byte, cBuff []byte) (*Searcher, error) {
|
||||
var err error
|
||||
|
||||
// content buff first
|
||||
if cBuff != nil {
|
||||
return &Searcher{
|
||||
vectorIndex: nil,
|
||||
contentBuff: cBuff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// open the xdb binary file
|
||||
handle, err := os.OpenFile(dbFile, os.O_RDONLY, 0600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Searcher{
|
||||
handle: handle,
|
||||
vectorIndex: vIndex,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewWithFileOnly(dbFile string) (*Searcher, error) {
|
||||
return baseNew(dbFile, nil, nil)
|
||||
}
|
||||
|
||||
func NewWithVectorIndex(dbFile string, vIndex []byte) (*Searcher, error) {
|
||||
return baseNew(dbFile, vIndex, nil)
|
||||
}
|
||||
|
||||
func NewWithBuffer(cBuff []byte) (*Searcher, error) {
|
||||
return baseNew("", nil, cBuff)
|
||||
}
|
||||
|
||||
func (s *Searcher) Close() {
|
||||
if s.handle != nil {
|
||||
err := s.handle.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetIOCount return the global io count for the last search
|
||||
func (s *Searcher) GetIOCount() int {
|
||||
return s.ioCount
|
||||
}
|
||||
|
||||
// SearchByStr find the region for the specified ip string
|
||||
func (s *Searcher) SearchByStr(str string) (string, error) {
|
||||
ip, err := CheckIP(str)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return s.Search(ip)
|
||||
}
|
||||
|
||||
// Search find the region for the specified long ip
|
||||
func (s *Searcher) Search(ip uint32) (string, error) {
|
||||
// reset the global ioCount
|
||||
s.ioCount = 0
|
||||
|
||||
// locate the segment index block based on the vector index
|
||||
var il0 = (ip >> 24) & 0xFF
|
||||
var il1 = (ip >> 16) & 0xFF
|
||||
var idx = il0*VectorIndexCols*VectorIndexSize + il1*VectorIndexSize
|
||||
var sPtr, ePtr = uint32(0), uint32(0)
|
||||
if s.vectorIndex != nil {
|
||||
sPtr = binary.LittleEndian.Uint32(s.vectorIndex[idx:])
|
||||
ePtr = binary.LittleEndian.Uint32(s.vectorIndex[idx+4:])
|
||||
} else if s.contentBuff != nil {
|
||||
sPtr = binary.LittleEndian.Uint32(s.contentBuff[HeaderInfoLength+idx:])
|
||||
ePtr = binary.LittleEndian.Uint32(s.contentBuff[HeaderInfoLength+idx+4:])
|
||||
} else {
|
||||
// read the vector index block
|
||||
var buff = make([]byte, VectorIndexSize)
|
||||
err := s.read(int64(HeaderInfoLength+idx), buff)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read vector index block at %d: %w", HeaderInfoLength+idx, err)
|
||||
}
|
||||
|
||||
sPtr = binary.LittleEndian.Uint32(buff)
|
||||
ePtr = binary.LittleEndian.Uint32(buff[4:])
|
||||
}
|
||||
|
||||
// fmt.Printf("sPtr=%d, ePtr=%d", sPtr, ePtr)
|
||||
|
||||
// binary search the segment index to get the region
|
||||
var dataLen, dataPtr = 0, uint32(0)
|
||||
var buff = make([]byte, SegmentIndexBlockSize)
|
||||
var l, h = 0, int((ePtr - sPtr) / SegmentIndexBlockSize)
|
||||
for l <= h {
|
||||
m := (l + h) >> 1
|
||||
p := sPtr + uint32(m*SegmentIndexBlockSize)
|
||||
err := s.read(int64(p), buff)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read segment index at %d: %w", p, err)
|
||||
}
|
||||
|
||||
// decode the data step by step to reduce the unnecessary operations
|
||||
sip := binary.LittleEndian.Uint32(buff)
|
||||
if ip < sip {
|
||||
h = m - 1
|
||||
} else {
|
||||
eip := binary.LittleEndian.Uint32(buff[4:])
|
||||
if ip > eip {
|
||||
l = m + 1
|
||||
} else {
|
||||
dataLen = int(binary.LittleEndian.Uint16(buff[8:]))
|
||||
dataPtr = binary.LittleEndian.Uint32(buff[10:])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//fmt.Printf("dataLen: %d, dataPtr: %d", dataLen, dataPtr)
|
||||
if dataLen == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// load and return the region data
|
||||
var regionBuff = make([]byte, dataLen)
|
||||
err := s.read(int64(dataPtr), regionBuff)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read region at %d: %w", dataPtr, err)
|
||||
}
|
||||
|
||||
return string(regionBuff), nil
|
||||
}
|
||||
|
||||
// do the data read operation based on the setting.
|
||||
// content buffer first or will read from the file.
|
||||
// this operation will invoke the Seek for file based read.
|
||||
func (s *Searcher) read(offset int64, buff []byte) error {
|
||||
if s.contentBuff != nil {
|
||||
cLen := copy(buff, s.contentBuff[offset:])
|
||||
if cLen != len(buff) {
|
||||
return fmt.Errorf("incomplete read: readed bytes should be %d", len(buff))
|
||||
}
|
||||
} else {
|
||||
_, err := s.handle.Seek(offset, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("seek to %d: %w", offset, err)
|
||||
}
|
||||
|
||||
s.ioCount++
|
||||
rLen, err := s.handle.Read(buff)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handle read: %w", err)
|
||||
}
|
||||
|
||||
if rLen != len(buff) {
|
||||
return fmt.Errorf("incomplete read: readed bytes should be %d", len(buff))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
89
src/framework/utils/ip2region/ip2region.go
Normal file
89
src/framework/utils/ip2region/ip2region.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package ip2region
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/logger"
|
||||
)
|
||||
|
||||
// 网络地址(内网)
|
||||
const LOCAT_HOST = "127.0.0.1"
|
||||
|
||||
// 全局查询对象
|
||||
var searcher *Searcher
|
||||
|
||||
//go:embed ip2region.xdb
|
||||
var ip2regionDB embed.FS
|
||||
|
||||
func init() {
|
||||
// 从 dbPath 加载整个 xdb 到内存
|
||||
buf, err := ip2regionDB.ReadFile("ip2region.xdb")
|
||||
if err != nil {
|
||||
logger.Fatalf("failed error load xdb from : %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 用全局的 cBuff 创建完全基于内存的查询对象。
|
||||
base, err := NewWithBuffer(buf)
|
||||
if err != nil {
|
||||
logger.Errorf("failed error create searcher with content: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 赋值到全局查询对象
|
||||
searcher = base
|
||||
}
|
||||
|
||||
// RegionSearchByIp 查询IP所在地
|
||||
//
|
||||
// 国家|区域|省份|城市|ISP
|
||||
func RegionSearchByIp(ip string) (string, int, int64) {
|
||||
ip = ClientIP(ip)
|
||||
if ip == LOCAT_HOST {
|
||||
return "0|0|0|内网IP|内网IP", 0, 0
|
||||
}
|
||||
tStart := time.Now()
|
||||
region, err := searcher.SearchByStr(ip)
|
||||
if err != nil {
|
||||
logger.Errorf("failed to SearchIP(%s): %s\n", ip, err)
|
||||
return "0|0|0|0|0", 0, 0
|
||||
}
|
||||
return region, 0, time.Since(tStart).Milliseconds()
|
||||
}
|
||||
|
||||
// RealAddressByIp 地址IP所在地
|
||||
//
|
||||
// 218.4.167.70 江苏省 苏州市
|
||||
func RealAddressByIp(ip string) string {
|
||||
ip = ClientIP(ip)
|
||||
if ip == LOCAT_HOST {
|
||||
return "内网IP"
|
||||
}
|
||||
region, err := searcher.SearchByStr(ip)
|
||||
if err != nil {
|
||||
logger.Errorf("failed to SearchIP(%s): %s\n", ip, err)
|
||||
return "未知"
|
||||
}
|
||||
parts := strings.Split(region, "|")
|
||||
province := parts[2]
|
||||
city := parts[3]
|
||||
if province == "0" && city != "0" {
|
||||
return city
|
||||
}
|
||||
return province + " " + city
|
||||
}
|
||||
|
||||
// ClientIP 处理客户端IP地址显示iPv4
|
||||
//
|
||||
// 转换 ip2region.ClientIP(c.ClientIP())
|
||||
func ClientIP(ip string) string {
|
||||
if strings.HasPrefix(ip, "::ffff:") {
|
||||
ip = strings.Replace(ip, "::ffff:", "", 1)
|
||||
}
|
||||
if ip == LOCAT_HOST || ip == "::1" {
|
||||
return LOCAT_HOST
|
||||
}
|
||||
return ip
|
||||
}
|
||||
BIN
src/framework/utils/ip2region/ip2region.xdb
Normal file
BIN
src/framework/utils/ip2region/ip2region.xdb
Normal file
Binary file not shown.
175
src/framework/utils/ip2region/util.go
Normal file
175
src/framework/utils/ip2region/util.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package ip2region
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var shiftIndex = []int{24, 16, 8, 0}
|
||||
|
||||
func CheckIP(ip string) (uint32, error) {
|
||||
var ps = strings.Split(strings.TrimSpace(ip), ".")
|
||||
if len(ps) != 4 {
|
||||
return 0, fmt.Errorf("invalid ip address `%s`", ip)
|
||||
}
|
||||
|
||||
var val = uint32(0)
|
||||
for i, s := range ps {
|
||||
d, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("the %dth part `%s` is not an integer", i, s)
|
||||
}
|
||||
|
||||
if d < 0 || d > 255 {
|
||||
return 0, fmt.Errorf("the %dth part `%s` should be an integer bettween 0 and 255", i, s)
|
||||
}
|
||||
|
||||
val |= uint32(d) << shiftIndex[i]
|
||||
}
|
||||
|
||||
// convert the ip to integer
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func Long2IP(ip uint32) string {
|
||||
return fmt.Sprintf("%d.%d.%d.%d", (ip>>24)&0xFF, (ip>>16)&0xFF, (ip>>8)&0xFF, ip&0xFF)
|
||||
}
|
||||
|
||||
func MidIP(sip uint32, eip uint32) uint32 {
|
||||
return uint32((uint64(sip) + uint64(eip)) >> 1)
|
||||
}
|
||||
|
||||
// LoadHeader load the header info from the specified handle
|
||||
func LoadHeader(handle *os.File) (*Header, error) {
|
||||
_, err := handle.Seek(0, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("seek to the header: %w", err)
|
||||
}
|
||||
|
||||
var buff = make([]byte, HeaderInfoLength)
|
||||
rLen, err := handle.Read(buff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rLen != len(buff) {
|
||||
return nil, fmt.Errorf("incomplete read: readed bytes should be %d", len(buff))
|
||||
}
|
||||
|
||||
return NewHeader(buff)
|
||||
}
|
||||
|
||||
// LoadHeaderFromFile load header info from the specified db file path
|
||||
func LoadHeaderFromFile(dbFile string) (*Header, error) {
|
||||
handle, err := os.OpenFile(dbFile, os.O_RDONLY, 0600)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open xdb file `%s`: %w", dbFile, err)
|
||||
}
|
||||
|
||||
defer func(handle *os.File) {
|
||||
_ = handle.Close()
|
||||
}(handle)
|
||||
|
||||
header, err := LoadHeader(handle)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return header, nil
|
||||
}
|
||||
|
||||
// LoadHeaderFromBuff wrap the header info from the content buffer
|
||||
func LoadHeaderFromBuff(cBuff []byte) (*Header, error) {
|
||||
return NewHeader(cBuff[0:256])
|
||||
}
|
||||
|
||||
// LoadVectorIndex util function to load the vector index from the specified file handle
|
||||
func LoadVectorIndex(handle *os.File) ([]byte, error) {
|
||||
// load all the vector index block
|
||||
_, err := handle.Seek(HeaderInfoLength, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("seek to vector index: %w", err)
|
||||
}
|
||||
|
||||
var buff = make([]byte, VectorIndexRows*VectorIndexCols*VectorIndexSize)
|
||||
rLen, err := handle.Read(buff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rLen != len(buff) {
|
||||
return nil, fmt.Errorf("incomplete read: readed bytes should be %d", len(buff))
|
||||
}
|
||||
|
||||
return buff, nil
|
||||
}
|
||||
|
||||
// LoadVectorIndexFromFile load vector index from a specified file path
|
||||
func LoadVectorIndexFromFile(dbFile string) ([]byte, error) {
|
||||
handle, err := os.OpenFile(dbFile, os.O_RDONLY, 0600)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open xdb file `%s`: %w", dbFile, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = handle.Close()
|
||||
}()
|
||||
|
||||
vIndex, err := LoadVectorIndex(handle)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vIndex, nil
|
||||
}
|
||||
|
||||
// LoadContent load the whole xdb content from the specified file handle
|
||||
func LoadContent(handle *os.File) ([]byte, error) {
|
||||
// get file size
|
||||
fi, err := handle.Stat()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stat: %w", err)
|
||||
}
|
||||
|
||||
size := fi.Size()
|
||||
|
||||
// seek to the head of the file
|
||||
_, err = handle.Seek(0, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("seek to get xdb file length: %w", err)
|
||||
}
|
||||
|
||||
var buff = make([]byte, size)
|
||||
rLen, err := handle.Read(buff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rLen != len(buff) {
|
||||
return nil, fmt.Errorf("incomplete read: readed bytes should be %d", len(buff))
|
||||
}
|
||||
|
||||
return buff, nil
|
||||
}
|
||||
|
||||
// LoadContentFromFile load the whole xdb content from the specified db file path
|
||||
func LoadContentFromFile(dbFile string) ([]byte, error) {
|
||||
handle, err := os.OpenFile(dbFile, os.O_RDONLY, 0600)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open xdb file `%s`: %w", dbFile, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = handle.Close()
|
||||
}()
|
||||
|
||||
cBuff, err := LoadContent(handle)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cBuff, nil
|
||||
}
|
||||
|
||||
167
src/framework/utils/parse/parse.go
Normal file
167
src/framework/utils/parse/parse.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
// Number 解析数值型
|
||||
func Number(str any) int64 {
|
||||
switch str := str.(type) {
|
||||
case string:
|
||||
if str == "" {
|
||||
return 0
|
||||
}
|
||||
num, err := strconv.ParseInt(str, 10, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return num
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
||||
return reflect.ValueOf(str).Int()
|
||||
case float32, float64:
|
||||
return int64(reflect.ValueOf(str).Float())
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Boolean 解析布尔型
|
||||
func Boolean(str any) bool {
|
||||
switch str := str.(type) {
|
||||
case string:
|
||||
if str == "" || str == "false" || str == "0" {
|
||||
return false
|
||||
}
|
||||
// 尝试将字符串解析为数字
|
||||
if num, err := strconv.ParseFloat(str, 64); err == nil {
|
||||
return num != 0
|
||||
}
|
||||
return true
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
||||
num := reflect.ValueOf(str).Int()
|
||||
return num != 0
|
||||
case float32, float64:
|
||||
num := reflect.ValueOf(str).Float()
|
||||
return num != 0
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertToCamelCase 字符串转换驼峰形式
|
||||
//
|
||||
// 字符串 dict/inline/data/:dictId 结果 DictInlineDataDictId
|
||||
func ConvertToCamelCase(str string) string {
|
||||
if len(str) == 0 {
|
||||
return str
|
||||
}
|
||||
reg := regexp.MustCompile(`[-_:/]\w`)
|
||||
result := reg.ReplaceAllStringFunc(str, func(match string) string {
|
||||
return strings.ToUpper(string(match[1]))
|
||||
})
|
||||
|
||||
words := strings.Fields(result)
|
||||
for i, word := range words {
|
||||
str := word[1:]
|
||||
str = strings.ReplaceAll(str, "/", "")
|
||||
words[i] = strings.ToUpper(word[:1]) + str
|
||||
}
|
||||
|
||||
return strings.Join(words, "")
|
||||
}
|
||||
|
||||
// Bit 比特位为单位
|
||||
func Bit(bit float64) string {
|
||||
var GB, MB, KB string
|
||||
|
||||
if bit > float64(1<<30) {
|
||||
GB = fmt.Sprintf("%0.2f", bit/(1<<30))
|
||||
}
|
||||
|
||||
if bit > float64(1<<20) && bit < (1<<30) {
|
||||
MB = fmt.Sprintf("%.2f", bit/(1<<20))
|
||||
}
|
||||
|
||||
if bit > float64(1<<10) && bit < (1<<20) {
|
||||
KB = fmt.Sprintf("%.2f", bit/(1<<10))
|
||||
}
|
||||
|
||||
if GB != "" {
|
||||
return GB + "GB"
|
||||
} else if MB != "" {
|
||||
return MB + "MB"
|
||||
} else if KB != "" {
|
||||
return KB + "KB"
|
||||
} else {
|
||||
return fmt.Sprintf("%vB", bit)
|
||||
}
|
||||
}
|
||||
|
||||
// CronExpression 解析 Cron 表达式,返回下一次执行的时间戳(毫秒)
|
||||
//
|
||||
// 【*/5 * * * * ?】 6个参数
|
||||
func CronExpression(expression string) int64 {
|
||||
specParser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
|
||||
schedule, err := specParser.Parse(expression)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return schedule.Next(time.Now()).UnixMilli()
|
||||
}
|
||||
|
||||
// SafeContent 内容值进行安全掩码
|
||||
func SafeContent(value string) string {
|
||||
if len(value) < 3 {
|
||||
return strings.Repeat("*", len(value))
|
||||
} else if len(value) < 6 {
|
||||
return string(value[0]) + strings.Repeat("*", len(value)-1)
|
||||
} else if len(value) < 10 {
|
||||
return string(value[0]) + strings.Repeat("*", len(value)-2) + string(value[len(value)-1])
|
||||
} else if len(value) < 15 {
|
||||
return value[:2] + strings.Repeat("*", len(value)-4) + value[len(value)-2:]
|
||||
} else {
|
||||
return value[:3] + strings.Repeat("*", len(value)-6) + value[len(value)-3:]
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveDuplicates 数组内字符串去重
|
||||
func RemoveDuplicates(ids []string) []string {
|
||||
uniqueIDs := make(map[string]bool)
|
||||
uniqueIDSlice := make([]string, 0)
|
||||
|
||||
for _, id := range ids {
|
||||
_, ok := uniqueIDs[id]
|
||||
if !ok && id != "" {
|
||||
uniqueIDs[id] = true
|
||||
uniqueIDSlice = append(uniqueIDSlice, id)
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueIDSlice
|
||||
}
|
||||
|
||||
// Color 解析颜色 #fafafa
|
||||
func Color(colorStr string) *color.RGBA {
|
||||
// 去除 # 号
|
||||
colorStr = colorStr[1:]
|
||||
|
||||
// 将颜色字符串拆分为 R、G、B 分量
|
||||
r, _ := strconv.ParseInt(colorStr[0:2], 16, 0)
|
||||
g, _ := strconv.ParseInt(colorStr[2:4], 16, 0)
|
||||
b, _ := strconv.ParseInt(colorStr[4:6], 16, 0)
|
||||
|
||||
return &color.RGBA{
|
||||
R: uint8(r),
|
||||
G: uint8(g),
|
||||
B: uint8(b),
|
||||
A: 255, // 不透明
|
||||
}
|
||||
}
|
||||
86
src/framework/utils/regular/regular.go
Normal file
86
src/framework/utils/regular/regular.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package regular
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/dlclark/regexp2"
|
||||
)
|
||||
|
||||
// Replace 正则替换
|
||||
func Replace(originStr, pattern, repStr string) string {
|
||||
regex := regexp.MustCompile(pattern)
|
||||
return regex.ReplaceAllString(originStr, repStr)
|
||||
}
|
||||
|
||||
// 判断是否为有效用户名格式
|
||||
//
|
||||
// 用户名不能以数字开头,可包含大写小写字母,数字,且不少于5位
|
||||
func ValidUsername(username string) bool {
|
||||
if username == "" {
|
||||
return false
|
||||
}
|
||||
pattern := `^[a-zA-Z][a-z0-9A-Z]{5,}`
|
||||
match, err := regexp.MatchString(pattern, username)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
// 判断是否为有效密码格式
|
||||
//
|
||||
// 密码至少包含大小写字母、数字、特殊符号,且不少于6位
|
||||
func ValidPassword(password string) bool {
|
||||
if password == "" {
|
||||
return false
|
||||
}
|
||||
pattern := `^(?![A-Za-z0-9]+$)(?![a-z0-9\W]+$)(?![A-Za-z\W]+$)(?![A-Z0-9\W]+$)[a-zA-Z0-9\W]{6,}$`
|
||||
re := regexp2.MustCompile(pattern, 0)
|
||||
match, err := re.MatchString(password)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
// 判断是否为有效手机号格式,1开头的11位手机号
|
||||
func ValidMobile(mobile string) bool {
|
||||
if mobile == "" {
|
||||
return false
|
||||
}
|
||||
pattern := `^1[3|4|5|6|7|8|9][0-9]\d{8}$`
|
||||
match, err := regexp.MatchString(pattern, mobile)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
// 判断是否为有效邮箱格式
|
||||
func ValidEmail(email string) bool {
|
||||
if email == "" {
|
||||
return false
|
||||
}
|
||||
pattern := `^(([^<>()\\.,;:\s@"]+(\.[^<>()\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+\.)+[a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}))$`
|
||||
re := regexp2.MustCompile(pattern, 0)
|
||||
match, err := re.MatchString(email)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
// 判断是否为http(s)://开头
|
||||
//
|
||||
// link 网络链接
|
||||
func ValidHttp(link string) bool {
|
||||
if link == "" {
|
||||
return false
|
||||
}
|
||||
pattern := `^http(s)?:\/\/+`
|
||||
match, err := regexp.MatchString(pattern, link)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return match
|
||||
}
|
||||
132
src/framework/utils/repo/repo.go
Normal file
132
src/framework/utils/repo/repo.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"ems.agt/src/framework/utils/parse"
|
||||
)
|
||||
|
||||
// PageNumSize 分页页码记录数
|
||||
func PageNumSize(pageNum, pageSize any) (int64, int64) {
|
||||
// 记录起始索引
|
||||
num := parse.Number(pageNum)
|
||||
if num > 5000 {
|
||||
num = 5000
|
||||
}
|
||||
if num < 1 {
|
||||
num = 1
|
||||
}
|
||||
|
||||
// 显示记录数
|
||||
size := parse.Number(pageSize)
|
||||
if size > 50000 {
|
||||
size = 50000
|
||||
}
|
||||
if size < 0 {
|
||||
size = 10
|
||||
}
|
||||
return num - 1, size
|
||||
}
|
||||
|
||||
// SetFieldValue 判断结构体内是否存在指定字段并设置值
|
||||
func SetFieldValue(obj any, fieldName string, value any) {
|
||||
// 获取结构体的反射值
|
||||
userValue := reflect.ValueOf(obj)
|
||||
|
||||
// 获取字段的反射值
|
||||
fieldValue := userValue.Elem().FieldByName(fieldName)
|
||||
|
||||
// 检查字段是否存在
|
||||
if fieldValue.IsValid() && fieldValue.CanSet() {
|
||||
// 获取字段的类型
|
||||
fieldType := fieldValue.Type()
|
||||
|
||||
// 转换传入的值类型为字段类型
|
||||
switch fieldType.Kind() {
|
||||
case reflect.String:
|
||||
if value == nil {
|
||||
fieldValue.SetString("")
|
||||
} else {
|
||||
fieldValue.SetString(fmt.Sprintf("%v", value))
|
||||
}
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
intValue, err := strconv.ParseInt(fmt.Sprintf("%v", value), 10, 64)
|
||||
if err != nil {
|
||||
intValue = 0
|
||||
}
|
||||
fieldValue.SetInt(intValue)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
uintValue, err := strconv.ParseUint(fmt.Sprintf("%v", value), 10, 64)
|
||||
if err != nil {
|
||||
uintValue = 0
|
||||
}
|
||||
fieldValue.SetUint(uintValue)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
floatValue, err := strconv.ParseFloat(fmt.Sprintf("%v", value), 64)
|
||||
if err != nil {
|
||||
floatValue = 0
|
||||
}
|
||||
fieldValue.SetFloat(floatValue)
|
||||
default:
|
||||
// 设置字段的值
|
||||
fieldValue.Set(reflect.ValueOf(value).Convert(fieldValue.Type()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertIdsSlice 将 []string 转换为 []any
|
||||
func ConvertIdsSlice(ids []string) []any {
|
||||
// 将 []string 转换为 []any
|
||||
arr := make([]any, len(ids))
|
||||
for i, v := range ids {
|
||||
arr[i] = v
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// 查询-参数值的占位符
|
||||
func KeyPlaceholderByQuery(sum int) string {
|
||||
placeholders := make([]string, sum)
|
||||
for i := 0; i < sum; i++ {
|
||||
placeholders[i] = "?"
|
||||
}
|
||||
return strings.Join(placeholders, ",")
|
||||
}
|
||||
|
||||
// 插入-参数映射键值占位符 keys, placeholder, values
|
||||
func KeyPlaceholderValueByInsert(params map[string]any) ([]string, string, []any) {
|
||||
// 参数映射的键
|
||||
keys := make([]string, len(params))
|
||||
// 参数映射的值
|
||||
values := make([]any, len(params))
|
||||
sum := 0
|
||||
for k, v := range params {
|
||||
keys[sum] = k
|
||||
values[sum] = v
|
||||
sum++
|
||||
}
|
||||
// 参数值的占位符
|
||||
placeholders := make([]string, sum)
|
||||
for i := 0; i < sum; i++ {
|
||||
placeholders[i] = "?"
|
||||
}
|
||||
return keys, strings.Join(placeholders, ","), values
|
||||
}
|
||||
|
||||
// 更新-参数映射键值占位符 keys, values
|
||||
func KeyValueByUpdate(params map[string]any) ([]string, []any) {
|
||||
// 参数映射的键
|
||||
keys := make([]string, len(params))
|
||||
// 参数映射的值
|
||||
values := make([]any, len(params))
|
||||
sum := 0
|
||||
for k, v := range params {
|
||||
keys[sum] = k + "=?"
|
||||
values[sum] = v
|
||||
sum++
|
||||
}
|
||||
return keys, values
|
||||
}
|
||||
151
src/framework/utils/token/token.go
Normal file
151
src/framework/utils/token/token.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/config"
|
||||
cachekeyConstants "ems.agt/src/framework/constants/cachekey"
|
||||
tokenConstants "ems.agt/src/framework/constants/token"
|
||||
"ems.agt/src/framework/logger"
|
||||
redisCahe "ems.agt/src/framework/redis"
|
||||
"ems.agt/src/framework/utils/generate"
|
||||
"ems.agt/src/framework/vo"
|
||||
|
||||
jwt "github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// Remove 清除登录用户信息UUID
|
||||
func Remove(tokenStr string) string {
|
||||
claims, err := Verify(tokenStr)
|
||||
if err != nil {
|
||||
logger.Errorf("token verify err %v", err)
|
||||
return ""
|
||||
}
|
||||
// 清除缓存KEY
|
||||
uuid := claims[tokenConstants.JWT_UUID].(string)
|
||||
tokenKey := cachekeyConstants.LOGIN_TOKEN_KEY + uuid
|
||||
hasKey, _ := redisCahe.Has("", tokenKey)
|
||||
if hasKey {
|
||||
redisCahe.Del("", tokenKey)
|
||||
}
|
||||
return claims[tokenConstants.JWT_NAME].(string)
|
||||
}
|
||||
|
||||
// Create 令牌生成
|
||||
func Create(loginUser *vo.LoginUser, ilobArgs ...string) string {
|
||||
// 生成用户唯一tokne32位
|
||||
loginUser.UUID = generate.Code(32)
|
||||
|
||||
// 设置请求用户登录客户端
|
||||
loginUser.IPAddr = ilobArgs[0]
|
||||
loginUser.LoginLocation = ilobArgs[1]
|
||||
loginUser.OS = ilobArgs[2]
|
||||
loginUser.Browser = ilobArgs[3]
|
||||
|
||||
// 设置用户令牌有效期并存入缓存
|
||||
Cache(loginUser)
|
||||
|
||||
// 令牌算法 HS256 HS384 HS512
|
||||
algorithm := config.Get("jwt.algorithm").(string)
|
||||
var method *jwt.SigningMethodHMAC
|
||||
switch algorithm {
|
||||
case "HS512":
|
||||
method = jwt.SigningMethodHS512
|
||||
case "HS384":
|
||||
method = jwt.SigningMethodHS384
|
||||
case "HS256":
|
||||
default:
|
||||
method = jwt.SigningMethodHS256
|
||||
}
|
||||
// 生成令牌负荷绑定uuid标识
|
||||
jwtToken := jwt.NewWithClaims(method, jwt.MapClaims{
|
||||
tokenConstants.JWT_UUID: loginUser.UUID,
|
||||
tokenConstants.JWT_KEY: loginUser.UserID,
|
||||
tokenConstants.JWT_NAME: loginUser.User.UserName,
|
||||
"exp": loginUser.ExpireTime,
|
||||
"ait": loginUser.LoginTime,
|
||||
})
|
||||
|
||||
// 生成令牌设置密钥
|
||||
secret := config.Get("jwt.secret").(string)
|
||||
tokenStr, err := jwtToken.SignedString([]byte(secret))
|
||||
if err != nil {
|
||||
logger.Infof("jwt sign err : %v", err)
|
||||
return ""
|
||||
}
|
||||
return tokenStr
|
||||
}
|
||||
|
||||
// Cache 缓存登录用户信息
|
||||
func Cache(loginUser *vo.LoginUser) {
|
||||
// 计算配置的有效期
|
||||
expTime := config.Get("jwt.expiresIn").(int)
|
||||
expTimestamp := time.Duration(expTime) * time.Minute
|
||||
iatTimestamp := time.Now().UnixMilli()
|
||||
loginUser.LoginTime = iatTimestamp
|
||||
loginUser.ExpireTime = iatTimestamp + expTimestamp.Milliseconds()
|
||||
// 根据登录标识将loginUser缓存
|
||||
tokenKey := cachekeyConstants.LOGIN_TOKEN_KEY + loginUser.UUID
|
||||
jsonBytes, err := json.Marshal(loginUser)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
redisCahe.SetByExpire("", tokenKey, string(jsonBytes), expTimestamp)
|
||||
}
|
||||
|
||||
// RefreshIn 验证令牌有效期,相差不足xx分钟,自动刷新缓存
|
||||
func RefreshIn(loginUser *vo.LoginUser) {
|
||||
// 相差不足xx分钟,自动刷新缓存
|
||||
refreshTime := config.Get("jwt.refreshIn").(int)
|
||||
refreshTimestamp := time.Duration(refreshTime) * time.Minute
|
||||
// 过期时间
|
||||
expireTimestamp := loginUser.ExpireTime
|
||||
currentTimestamp := time.Now().UnixMilli()
|
||||
if expireTimestamp-currentTimestamp <= refreshTimestamp.Milliseconds() {
|
||||
Cache(loginUser)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify 校验令牌是否有效
|
||||
func Verify(tokenString string) (jwt.MapClaims, error) {
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
|
||||
// 判断加密算法是预期的加密算法
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); ok {
|
||||
secret := config.Get("jwt.secret").(string)
|
||||
return []byte(secret), nil
|
||||
}
|
||||
return nil, jwt.ErrSignatureInvalid
|
||||
})
|
||||
if err != nil {
|
||||
logger.Errorf("token String Verify : %v", err)
|
||||
return nil, errors.New("无效身份授权")
|
||||
}
|
||||
// 如果解析负荷成功并通过签名校验
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
return nil, errors.New("token valid error")
|
||||
}
|
||||
|
||||
// LoginUser 缓存的登录用户信息
|
||||
func LoginUser(claims jwt.MapClaims) vo.LoginUser {
|
||||
uuid := claims[tokenConstants.JWT_UUID].(string)
|
||||
tokenKey := cachekeyConstants.LOGIN_TOKEN_KEY + uuid
|
||||
hasKey, _ := redisCahe.Has("", tokenKey)
|
||||
var loginUser vo.LoginUser
|
||||
if hasKey {
|
||||
loginUserStr, _ := redisCahe.Get("", tokenKey)
|
||||
if loginUserStr == "" {
|
||||
return loginUser
|
||||
}
|
||||
err := json.Unmarshal([]byte(loginUserStr), &loginUser)
|
||||
if err != nil {
|
||||
logger.Errorf("loginuser info json err : %v", err)
|
||||
return loginUser
|
||||
}
|
||||
return loginUser
|
||||
}
|
||||
return loginUser
|
||||
}
|
||||
8
src/framework/utils/ua/ua.go
Normal file
8
src/framework/utils/ua/ua.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package ua
|
||||
|
||||
import "github.com/mssola/user_agent"
|
||||
|
||||
// 获取user-agent信息
|
||||
func Info(userAgent string) *user_agent.UserAgent {
|
||||
return user_agent.New(userAgent)
|
||||
}
|
||||
39
src/framework/vo/loginuser.go
Normal file
39
src/framework/vo/loginuser.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package vo
|
||||
|
||||
import systemModel "ems.agt/src/modules/system/model"
|
||||
|
||||
// LoginUser 登录用户身份权限信息对象
|
||||
type LoginUser struct {
|
||||
// UserID 用户ID
|
||||
UserID string `json:"userId"`
|
||||
|
||||
// DeptID 部门ID
|
||||
DeptID string `json:"deptId"`
|
||||
|
||||
// UUID 用户唯一标识
|
||||
UUID string `json:"uuid"`
|
||||
|
||||
// LoginTime 登录时间时间戳
|
||||
LoginTime int64 `json:"loginTime"`
|
||||
|
||||
// ExpireTime 过期时间时间戳
|
||||
ExpireTime int64 `json:"expireTime"`
|
||||
|
||||
// IPAddr 登录IP地址 x.x.x.x
|
||||
IPAddr string `json:"ipaddr"`
|
||||
|
||||
// LoginLocation 登录地点 xx xx
|
||||
LoginLocation string `json:"loginLocation"`
|
||||
|
||||
// Browser 浏览器类型
|
||||
Browser string `json:"browser"`
|
||||
|
||||
// OS 操作系统
|
||||
OS string `json:"os"`
|
||||
|
||||
// Permissions 权限列表
|
||||
Permissions []string `json:"permissions"`
|
||||
|
||||
// User 用户信息
|
||||
User systemModel.SysUser `json:"user"`
|
||||
}
|
||||
71
src/framework/vo/result/result.go
Normal file
71
src/framework/vo/result/result.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package result
|
||||
|
||||
import (
|
||||
"ems.agt/src/framework/constants/result"
|
||||
)
|
||||
|
||||
// CodeMsg 响应结果
|
||||
func CodeMsg(code int, msg string) map[string]any {
|
||||
args := make(map[string]any)
|
||||
args["code"] = code
|
||||
args["msg"] = msg
|
||||
return args
|
||||
}
|
||||
|
||||
// 响应成功结果 map[string]any{}
|
||||
func Ok(v map[string]any) map[string]any {
|
||||
args := make(map[string]any)
|
||||
args["code"] = result.CODE_SUCCESS
|
||||
args["msg"] = result.MSG_SUCCESS
|
||||
// v合并到args
|
||||
for key, value := range v {
|
||||
args[key] = value
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// 响应成功结果信息
|
||||
func OkMsg(msg string) map[string]any {
|
||||
args := make(map[string]any)
|
||||
args["code"] = result.CODE_SUCCESS
|
||||
args["msg"] = msg
|
||||
return args
|
||||
}
|
||||
|
||||
// 响应成功结果数据
|
||||
func OkData(data any) map[string]any {
|
||||
args := make(map[string]any)
|
||||
args["code"] = result.CODE_SUCCESS
|
||||
args["msg"] = result.MSG_SUCCESS
|
||||
args["data"] = data
|
||||
return args
|
||||
}
|
||||
|
||||
// 响应失败结果 map[string]any{}
|
||||
func Err(v map[string]any) map[string]any {
|
||||
args := make(map[string]any)
|
||||
args["code"] = result.CODE_ERROR
|
||||
args["msg"] = result.MSG_ERROR
|
||||
// v合并到args
|
||||
for key, value := range v {
|
||||
args[key] = value
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// 响应失败结果信息
|
||||
func ErrMsg(msg string) map[string]any {
|
||||
args := make(map[string]any)
|
||||
args["code"] = result.CODE_ERROR
|
||||
args["msg"] = msg
|
||||
return args
|
||||
}
|
||||
|
||||
// 响应失败结果数据
|
||||
func ErrData(data any) map[string]any {
|
||||
args := make(map[string]any)
|
||||
args["code"] = result.CODE_ERROR
|
||||
args["msg"] = result.MSG_ERROR
|
||||
args["data"] = data
|
||||
return args
|
||||
}
|
||||
17
src/framework/vo/router.go
Normal file
17
src/framework/vo/router.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package vo
|
||||
|
||||
// Router 路由信息对象
|
||||
type Router struct {
|
||||
// 路由名字 英文首字母大写
|
||||
Name string `json:"name"`
|
||||
// 路由地址
|
||||
Path string `json:"path"`
|
||||
// 其他元素
|
||||
Meta RouterMeta `json:"meta"`
|
||||
// 组件地址
|
||||
Component string `json:"component"`
|
||||
// 重定向地址
|
||||
Redirect string `json:"redirect"`
|
||||
// 子路由
|
||||
Children []Router `json:"children,omitempty"`
|
||||
}
|
||||
17
src/framework/vo/router_meta.go
Normal file
17
src/framework/vo/router_meta.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package vo
|
||||
|
||||
// RouterMeta 路由元信息对象
|
||||
type RouterMeta struct {
|
||||
// 设置该菜单在侧边栏和面包屑中展示的名字
|
||||
Title string `json:"title"`
|
||||
// 设置该菜单的图标
|
||||
Icon string `json:"icon"`
|
||||
// 设置为true,则不会被 <keep-alive>缓存
|
||||
Cache bool `json:"cache"`
|
||||
// 内链地址(http(s)://开头), 打开目标位置 '_blank' | '_self' | ''
|
||||
Target string `json:"target"`
|
||||
// 在菜单中隐藏子节点
|
||||
HideChildInMenu bool `json:"hideChildInMenu"`
|
||||
// 在菜单中隐藏自己和子节点
|
||||
HideInMenu bool `json:"hideInMenu"`
|
||||
}
|
||||
51
src/framework/vo/treeselect.go
Normal file
51
src/framework/vo/treeselect.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package vo
|
||||
|
||||
import systemModel "ems.agt/src/modules/system/model"
|
||||
|
||||
// TreeSelect 树结构实体类
|
||||
type TreeSelect struct {
|
||||
// ID 节点ID
|
||||
ID string `json:"id"`
|
||||
|
||||
// Label 节点名称
|
||||
Label string `json:"label"`
|
||||
|
||||
// Children 子节点
|
||||
Children []TreeSelect `json:"children"`
|
||||
}
|
||||
|
||||
// SysMenuTreeSelect 使用给定的 SysMenu 对象解析为 TreeSelect 对象
|
||||
func SysMenuTreeSelect(sysMenu systemModel.SysMenu) TreeSelect {
|
||||
t := TreeSelect{}
|
||||
t.ID = sysMenu.MenuID
|
||||
t.Label = sysMenu.MenuName
|
||||
|
||||
if len(sysMenu.Children) > 0 {
|
||||
for _, menu := range sysMenu.Children {
|
||||
child := SysMenuTreeSelect(menu)
|
||||
t.Children = append(t.Children, child)
|
||||
}
|
||||
} else {
|
||||
t.Children = []TreeSelect{}
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// SysDeptTreeSelect 使用给定的 SysDept 对象解析为 TreeSelect 对象
|
||||
func SysDeptTreeSelect(sysDept systemModel.SysDept) TreeSelect {
|
||||
t := TreeSelect{}
|
||||
t.ID = sysDept.DeptID
|
||||
t.Label = sysDept.DeptName
|
||||
|
||||
if len(sysDept.Children) > 0 {
|
||||
for _, dept := range sysDept.Children {
|
||||
child := SysDeptTreeSelect(dept)
|
||||
t.Children = append(t.Children, child)
|
||||
}
|
||||
} else {
|
||||
t.Children = []TreeSelect{}
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
32
src/lib_features/account/account.go
Normal file
32
src/lib_features/account/account.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package libfeatures
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"ems.agt/lib/dborm"
|
||||
"ems.agt/lib/oauth"
|
||||
libConfig "ems.agt/restagent/config"
|
||||
"ems.agt/src/framework/logger"
|
||||
"ems.agt/src/framework/redis"
|
||||
)
|
||||
|
||||
// SessionToken 设置登录会话-兼容旧登录方式
|
||||
func SessionToken(username, sourceAddr string) bool {
|
||||
token, _ := redis.Get("", "session_token")
|
||||
if token != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
token = oauth.GenRandToken("omc") // Generate new token to session ID
|
||||
affected, err := dborm.XormInsertSession(username, sourceAddr, token, libConfig.GetExpiresFromConfig(), libConfig.GetYamlConfig().Auth.Session)
|
||||
if err != nil {
|
||||
logger.Errorf("SessionToken XormInsertSession err %v", err)
|
||||
}
|
||||
if affected == 1 {
|
||||
// 过期时间单位秒 配置1800是半小时
|
||||
expireTime := time.Duration(int64(libConfig.GetExpiresFromConfig())) * time.Second
|
||||
redis.SetByExpire("", "session_token", token, expireTime)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
53
src/lib_features/config/config.go
Normal file
53
src/lib_features/config/config.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package libfeatures
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
libConf "ems.agt/lib/core/conf"
|
||||
libGlobal "ems.agt/lib/global"
|
||||
libConfig "ems.agt/restagent/config"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// BuildInfo 程序-V查看编译版本号信息
|
||||
func BuildInfo() string {
|
||||
return fmt.Sprintf("OMC restagent version: %s\n%s\n%s\n\n", libGlobal.Version, libGlobal.BuildTime, libGlobal.GoVer)
|
||||
}
|
||||
|
||||
// ConfigRead 指定配置文件读取
|
||||
func ConfigRead(configFile string) {
|
||||
// 外层lib和features使用的配置
|
||||
libConfig.ReadConfig(configFile)
|
||||
uriPrefix := libConfig.GetYamlConfig().OMC.UriPrefix
|
||||
if uriPrefix != "" {
|
||||
libConfig.UriPrefix = uriPrefix
|
||||
}
|
||||
if libConfig.GetYamlConfig().TestConfig.Enabled {
|
||||
libConfig.ReadTestConfigYaml(libConfig.GetYamlConfig().TestConfig.File)
|
||||
}
|
||||
// 外层lib和features使用配置
|
||||
libConf.InitConfig(configFile)
|
||||
}
|
||||
|
||||
// 配置文件读取进行内部参数合并
|
||||
func ConfigInMerge() {
|
||||
// 合并外层lib和features使用配置
|
||||
for key, value := range libConf.AllSettings() {
|
||||
// 跳过配置
|
||||
if key == "testconfig" || key == "rest" || key == "logger" {
|
||||
continue
|
||||
}
|
||||
// 数据库配置
|
||||
if key == "database" {
|
||||
item := value.(map[string]any)
|
||||
defaultItem := viper.GetStringMap("gorm.datasource.default")
|
||||
defaultItem["host"] = item["host"]
|
||||
defaultItem["port"] = item["port"]
|
||||
defaultItem["username"] = item["user"]
|
||||
defaultItem["password"] = item["password"]
|
||||
defaultItem["database"] = item["name"]
|
||||
continue
|
||||
}
|
||||
viper.Set(key, value)
|
||||
}
|
||||
}
|
||||
5
src/lib_features/readme.md
Normal file
5
src/lib_features/readme.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 外层 lib 和 features 粘合层
|
||||
|
||||
- config.go 配置合并: restagent.yaml 文件内容,主要是数据库配置
|
||||
- account.go 登录会话生成 token
|
||||
- session.go 中间件方式设置请求头 token 值
|
||||
23
src/lib_features/session/session.go
Normal file
23
src/lib_features/session/session.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"ems.agt/src/framework/redis"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SessionHeader 旧登录方式token头
|
||||
func SessionHeader() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 读取登录生成的会话token
|
||||
token, err := redis.Get("", "session_token")
|
||||
if err == nil {
|
||||
c.Request.Header.Set("Accesstoken", token)
|
||||
}
|
||||
|
||||
// Accesstoken: omc-ce4d0a86-8515-ad51-3249-4913c95f8e34
|
||||
// 调用下一个处理程序
|
||||
c.Next()
|
||||
|
||||
}
|
||||
}
|
||||
85
src/modules/common/common.go
Normal file
85
src/modules/common/common.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"ems.agt/src/framework/logger"
|
||||
"ems.agt/src/framework/middleware"
|
||||
"ems.agt/src/modules/common/controller"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 模块路由注册
|
||||
func Setup(router *gin.Engine) {
|
||||
logger.Infof("开始加载 ====> common 模块路由")
|
||||
|
||||
// 路由主页
|
||||
indexGroup := router.Group("/")
|
||||
indexGroup.GET("",
|
||||
middleware.RateLimit(middleware.LimitOption{
|
||||
Time: 300,
|
||||
Count: 10,
|
||||
Type: middleware.LIMIT_IP,
|
||||
}),
|
||||
controller.NewIndex.Handler,
|
||||
)
|
||||
|
||||
// 验证码操作处理
|
||||
indexGroup.GET("/captchaImage",
|
||||
middleware.RateLimit(middleware.LimitOption{
|
||||
Time: 300,
|
||||
Count: 60,
|
||||
Type: middleware.LIMIT_IP,
|
||||
}),
|
||||
controller.NewCaptcha.Image,
|
||||
)
|
||||
|
||||
// 账号身份操作处理
|
||||
{
|
||||
indexGroup.POST("/login",
|
||||
middleware.RateLimit(middleware.LimitOption{
|
||||
Time: 300,
|
||||
Count: 10,
|
||||
Type: middleware.LIMIT_IP,
|
||||
}),
|
||||
controller.NewAccount.Login,
|
||||
)
|
||||
indexGroup.GET("/getInfo", middleware.PreAuthorize(nil), controller.NewAccount.Info)
|
||||
indexGroup.GET("/getRouters", middleware.PreAuthorize(nil), controller.NewAccount.Router)
|
||||
indexGroup.POST("/logout",
|
||||
middleware.RateLimit(middleware.LimitOption{
|
||||
Time: 300,
|
||||
Count: 5,
|
||||
Type: middleware.LIMIT_IP,
|
||||
}),
|
||||
controller.NewAccount.Logout,
|
||||
)
|
||||
}
|
||||
|
||||
// 账号注册操作处理
|
||||
{
|
||||
indexGroup.POST("/register",
|
||||
middleware.RateLimit(middleware.LimitOption{
|
||||
Time: 300,
|
||||
Count: 10,
|
||||
Type: middleware.LIMIT_IP,
|
||||
}),
|
||||
controller.NewRegister.UserName,
|
||||
)
|
||||
}
|
||||
|
||||
// 通用请求
|
||||
commonGroup := router.Group("/common")
|
||||
{
|
||||
commonGroup.GET("/hash", middleware.PreAuthorize(nil), controller.NewCommont.Hash)
|
||||
}
|
||||
|
||||
// 文件操作处理
|
||||
fileGroup := router.Group("/file")
|
||||
{
|
||||
fileGroup.GET("/download/:filePath", middleware.PreAuthorize(nil), controller.NewFile.Download)
|
||||
fileGroup.POST("/upload", middleware.PreAuthorize(nil), controller.NewFile.Upload)
|
||||
fileGroup.POST("/chunkCheck", middleware.PreAuthorize(nil), controller.NewFile.ChunkCheck)
|
||||
fileGroup.POST("/chunkUpload", middleware.PreAuthorize(nil), controller.NewFile.ChunkUpload)
|
||||
fileGroup.POST("/chunkMerge", middleware.PreAuthorize(nil), controller.NewFile.ChunkMerge)
|
||||
}
|
||||
}
|
||||
144
src/modules/common/controller/account.go
Normal file
144
src/modules/common/controller/account.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"ems.agt/src/framework/config"
|
||||
commonConstants "ems.agt/src/framework/constants/common"
|
||||
tokenConstants "ems.agt/src/framework/constants/token"
|
||||
ctxUtils "ems.agt/src/framework/utils/ctx"
|
||||
tokenUtils "ems.agt/src/framework/utils/token"
|
||||
"ems.agt/src/framework/vo/result"
|
||||
libAccount "ems.agt/src/lib_features/account"
|
||||
commonModel "ems.agt/src/modules/common/model"
|
||||
commonService "ems.agt/src/modules/common/service"
|
||||
systemService "ems.agt/src/modules/system/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 实例化控制层 AccountController 结构体
|
||||
var NewAccount = &AccountController{
|
||||
accountService: commonService.NewAccountImpl,
|
||||
sysLogLoginService: systemService.NewSysLogLoginImpl,
|
||||
}
|
||||
|
||||
// 账号身份操作处理
|
||||
//
|
||||
// PATH /
|
||||
type AccountController struct {
|
||||
// 账号身份操作服务
|
||||
accountService commonService.IAccount
|
||||
// 系统登录访问
|
||||
sysLogLoginService systemService.ISysLogLogin
|
||||
}
|
||||
|
||||
// 系统登录
|
||||
//
|
||||
// POST /login
|
||||
func (s *AccountController) Login(c *gin.Context) {
|
||||
var loginBody commonModel.LoginBody
|
||||
if err := c.ShouldBindJSON(&loginBody); err != nil {
|
||||
c.JSON(400, result.CodeMsg(400, "参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
// 当前请求信息
|
||||
ipaddr, location := ctxUtils.IPAddrLocation(c)
|
||||
os, browser := ctxUtils.UaOsBrowser(c)
|
||||
|
||||
// 校验验证码
|
||||
err := s.accountService.ValidateCaptcha(
|
||||
loginBody.Code,
|
||||
loginBody.UUID,
|
||||
)
|
||||
// 根据错误信息,创建系统访问记录
|
||||
if err != nil {
|
||||
msg := err.Error() + " " + loginBody.Code
|
||||
s.sysLogLoginService.NewSysLogLogin(
|
||||
loginBody.Username, commonConstants.STATUS_NO, msg,
|
||||
ipaddr, location, os, browser,
|
||||
)
|
||||
c.JSON(200, result.ErrMsg(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// 登录用户信息
|
||||
loginUser, err := s.accountService.LoginByUsername(loginBody.Username, loginBody.Password)
|
||||
if err != nil {
|
||||
c.JSON(200, result.ErrMsg(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// 生成令牌,创建系统访问记录
|
||||
tokenStr := tokenUtils.Create(&loginUser, ipaddr, location, os, browser)
|
||||
if tokenStr == "" {
|
||||
c.JSON(200, result.Err(nil))
|
||||
return
|
||||
} else {
|
||||
s.sysLogLoginService.NewSysLogLogin(
|
||||
loginBody.Username, commonConstants.STATUS_YES, "登录成功",
|
||||
ipaddr, location, os, browser,
|
||||
)
|
||||
}
|
||||
|
||||
// 设置登录会话-兼容旧登录方式
|
||||
libAccount.SessionToken(loginBody.Username, ipaddr)
|
||||
|
||||
c.JSON(200, result.OkData(map[string]any{
|
||||
tokenConstants.RESPONSE_FIELD: tokenStr,
|
||||
}))
|
||||
}
|
||||
|
||||
// 登录用户信息
|
||||
//
|
||||
// GET /getInfo
|
||||
func (s *AccountController) Info(c *gin.Context) {
|
||||
loginUser, err := ctxUtils.LoginUser(c)
|
||||
if err != nil {
|
||||
c.JSON(401, result.CodeMsg(401, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// 角色权限集合,管理员拥有所有权限
|
||||
isAdmin := config.IsAdmin(loginUser.UserID)
|
||||
roles, perms := s.accountService.RoleAndMenuPerms(loginUser.UserID, isAdmin)
|
||||
|
||||
c.JSON(200, result.OkData(map[string]any{
|
||||
"user": loginUser.User,
|
||||
"roles": roles,
|
||||
"permissions": perms,
|
||||
}))
|
||||
}
|
||||
|
||||
// 登录用户路由信息
|
||||
//
|
||||
// GET /getRouters
|
||||
func (s *AccountController) Router(c *gin.Context) {
|
||||
userID := ctxUtils.LoginUserToUserID(c)
|
||||
|
||||
// 前端路由,管理员拥有所有
|
||||
isAdmin := config.IsAdmin(userID)
|
||||
buildMenus := s.accountService.RouteMenus(userID, isAdmin)
|
||||
c.JSON(200, result.OkData(buildMenus))
|
||||
}
|
||||
|
||||
// 系统登出
|
||||
//
|
||||
// POST /logout
|
||||
func (s *AccountController) Logout(c *gin.Context) {
|
||||
tokenStr := ctxUtils.Authorization(c)
|
||||
if tokenStr != "" {
|
||||
// 存在token时记录退出信息
|
||||
userName := tokenUtils.Remove(tokenStr)
|
||||
if userName != "" {
|
||||
// 当前请求信息
|
||||
ipaddr, location := ctxUtils.IPAddrLocation(c)
|
||||
os, browser := ctxUtils.UaOsBrowser(c)
|
||||
// 创建系统访问记录
|
||||
s.sysLogLoginService.NewSysLogLogin(
|
||||
userName, commonConstants.STATUS_NO, "退出成功",
|
||||
ipaddr, location, os, browser,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(200, result.OkMsg("退出成功"))
|
||||
}
|
||||
129
src/modules/common/controller/captcha.go
Normal file
129
src/modules/common/controller/captcha.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/config"
|
||||
"ems.agt/src/framework/constants/cachekey"
|
||||
"ems.agt/src/framework/constants/captcha"
|
||||
"ems.agt/src/framework/logger"
|
||||
"ems.agt/src/framework/redis"
|
||||
"ems.agt/src/framework/utils/parse"
|
||||
"ems.agt/src/framework/vo/result"
|
||||
systemService "ems.agt/src/modules/system/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mojocn/base64Captcha"
|
||||
)
|
||||
|
||||
// 实例化控制层 CaptchaController 结构体
|
||||
var NewCaptcha = &CaptchaController{
|
||||
sysConfigService: systemService.NewSysConfigImpl,
|
||||
}
|
||||
|
||||
// 验证码操作处理
|
||||
//
|
||||
// PATH /
|
||||
type CaptchaController struct {
|
||||
// 参数配置服务
|
||||
sysConfigService systemService.ISysConfig
|
||||
}
|
||||
|
||||
// 获取验证码
|
||||
//
|
||||
// GET /captchaImage
|
||||
func (s *CaptchaController) Image(c *gin.Context) {
|
||||
// 从数据库配置获取验证码开关 true开启,false关闭
|
||||
captchaEnabledStr := s.sysConfigService.SelectConfigValueByKey("sys.account.captchaEnabled")
|
||||
captchaEnabled := parse.Boolean(captchaEnabledStr)
|
||||
if !captchaEnabled {
|
||||
c.JSON(200, result.Ok(map[string]any{
|
||||
"captchaEnabled": captchaEnabled,
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
// 生成唯一标识
|
||||
verifyKey := ""
|
||||
data := map[string]any{
|
||||
"captchaEnabled": captchaEnabled,
|
||||
"uuid": "",
|
||||
"img": "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
|
||||
}
|
||||
|
||||
// 从数据库配置获取验证码类型 math 数值计算 char 字符验证
|
||||
captchaType := s.sysConfigService.SelectConfigValueByKey("sys.account.captchaType")
|
||||
if captchaType == captcha.TYPE_MATH {
|
||||
math := config.Get("mathCaptcha").(map[string]any)
|
||||
driverCaptcha := &base64Captcha.DriverMath{
|
||||
//Height png height in pixel.
|
||||
Height: math["height"].(int),
|
||||
// Width Captcha png width in pixel.
|
||||
Width: math["width"].(int),
|
||||
//NoiseCount text noise count.
|
||||
NoiseCount: math["noise"].(int),
|
||||
//ShowLineOptions := OptionShowHollowLine | OptionShowSlimeLine | OptionShowSineLine .
|
||||
ShowLineOptions: base64Captcha.OptionShowHollowLine,
|
||||
}
|
||||
if math["color"].(bool) {
|
||||
//BgColor captcha image background color (optional)
|
||||
driverCaptcha.BgColor = parse.Color(math["background"].(string))
|
||||
}
|
||||
// 验证码生成
|
||||
id, question, answer := driverCaptcha.GenerateIdQuestionAnswer()
|
||||
// 验证码表达式解析输出
|
||||
item, err := driverCaptcha.DrawCaptcha(question)
|
||||
if err != nil {
|
||||
logger.Infof("Generate Id Question Answer %s %s : %v", captchaType, question, err)
|
||||
} else {
|
||||
data["uuid"] = id
|
||||
data["img"] = item.EncodeB64string()
|
||||
expiration := captcha.EXPIRATION * time.Second
|
||||
verifyKey = cachekey.CAPTCHA_CODE_KEY + id
|
||||
redis.SetByExpire("", verifyKey, answer, expiration)
|
||||
}
|
||||
}
|
||||
if captchaType == captcha.TYPE_CHAR {
|
||||
char := config.Get("charCaptcha").(map[string]any)
|
||||
driverCaptcha := &base64Captcha.DriverString{
|
||||
//Height png height in pixel.
|
||||
Height: char["height"].(int),
|
||||
// Width Captcha png width in pixel.
|
||||
Width: char["width"].(int),
|
||||
//NoiseCount text noise count.
|
||||
NoiseCount: char["noise"].(int),
|
||||
//Length random string length.
|
||||
Length: char["size"].(int),
|
||||
//Source is a unicode which is the rand string from.
|
||||
Source: char["chars"].(string),
|
||||
//ShowLineOptions := OptionShowHollowLine | OptionShowSlimeLine | OptionShowSineLine .
|
||||
ShowLineOptions: base64Captcha.OptionShowHollowLine,
|
||||
}
|
||||
if char["color"].(bool) {
|
||||
//BgColor captcha image background color (optional)
|
||||
driverCaptcha.BgColor = parse.Color(char["background"].(string))
|
||||
}
|
||||
// 验证码生成
|
||||
id, question, answer := driverCaptcha.GenerateIdQuestionAnswer()
|
||||
// 验证码表达式解析输出
|
||||
item, err := driverCaptcha.DrawCaptcha(question)
|
||||
if err != nil {
|
||||
logger.Infof("Generate Id Question Answer %s %s : %v", captchaType, question, err)
|
||||
} else {
|
||||
data["uuid"] = id
|
||||
data["img"] = item.EncodeB64string()
|
||||
expiration := captcha.EXPIRATION * time.Second
|
||||
verifyKey = cachekey.CAPTCHA_CODE_KEY + id
|
||||
redis.SetByExpire("", verifyKey, answer, expiration)
|
||||
}
|
||||
}
|
||||
|
||||
// 本地开发下返回验证码结果,方便接口调试
|
||||
if config.Env() == "local" {
|
||||
text, _ := redis.Get("", verifyKey)
|
||||
data["text"] = text
|
||||
c.JSON(200, result.Ok(data))
|
||||
return
|
||||
}
|
||||
c.JSON(200, result.Ok(data))
|
||||
}
|
||||
20
src/modules/common/controller/common.go
Normal file
20
src/modules/common/controller/common.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 实例化控制层 CommontController 结构体
|
||||
var NewCommont = &CommontController{}
|
||||
|
||||
// 通用请求
|
||||
//
|
||||
// PATH /
|
||||
type CommontController struct{}
|
||||
|
||||
// 哈希加密
|
||||
//
|
||||
// GET /hash
|
||||
func (s *CommontController) Hash(c *gin.Context) {
|
||||
c.String(200, "commont Hash")
|
||||
}
|
||||
185
src/modules/common/controller/file.go
Normal file
185
src/modules/common/controller/file.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"ems.agt/src/framework/constants/uploadsubpath"
|
||||
"ems.agt/src/framework/utils/file"
|
||||
"ems.agt/src/framework/vo/result"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 实例化控制层 FileController 结构体
|
||||
var NewFile = &FileController{}
|
||||
|
||||
// 文件操作处理
|
||||
//
|
||||
// PATH /
|
||||
type FileController struct{}
|
||||
|
||||
// 下载文件
|
||||
//
|
||||
// GET /download/:filePath
|
||||
func (s *FileController) Download(c *gin.Context) {
|
||||
filePath := c.Param("filePath")
|
||||
if len(filePath) < 8 {
|
||||
c.JSON(400, result.CodeMsg(400, "参数错误"))
|
||||
return
|
||||
}
|
||||
// base64解析出地址
|
||||
decodedBytes, err := base64.StdEncoding.DecodeString(filePath)
|
||||
if err != nil {
|
||||
c.JSON(400, result.CodeMsg(400, err.Error()))
|
||||
return
|
||||
}
|
||||
routerPath := string(decodedBytes)
|
||||
// 地址文件名截取
|
||||
fileName := routerPath[strings.LastIndex(routerPath, "/")+1:]
|
||||
|
||||
// 响应头
|
||||
c.Writer.Header().Set("Content-Disposition", `attachment; filename="`+url.QueryEscape(fileName)+`"`)
|
||||
c.Writer.Header().Set("Accept-Ranges", "bytes")
|
||||
c.Writer.Header().Set("Content-Type", "application/octet-stream")
|
||||
|
||||
// 断点续传
|
||||
headerRange := c.GetHeader("Range")
|
||||
resultMap, err := file.ReadUploadFileStream(routerPath, headerRange)
|
||||
if err != nil {
|
||||
c.JSON(200, result.ErrMsg(err.Error()))
|
||||
return
|
||||
}
|
||||
if headerRange != "" {
|
||||
c.Writer.Header().Set("Content-Range", fmt.Sprint(resultMap["range"]))
|
||||
c.Writer.Header().Set("Content-Length", fmt.Sprint(resultMap["chunkSize"]))
|
||||
c.Status(206)
|
||||
} else {
|
||||
c.Writer.Header().Set("Content-Length", fmt.Sprint(resultMap["fileSize"]))
|
||||
c.Status(200)
|
||||
|
||||
}
|
||||
c.Writer.Write(resultMap["data"].([]byte))
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
//
|
||||
// POST /upload
|
||||
func (s *FileController) Upload(c *gin.Context) {
|
||||
// 上传的文件
|
||||
formFile, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(400, result.CodeMsg(400, "参数错误"))
|
||||
return
|
||||
}
|
||||
// 子路径
|
||||
subPath := c.PostForm("subPath")
|
||||
if _, ok := uploadsubpath.UploadSubpath[subPath]; !ok {
|
||||
c.JSON(400, result.CodeMsg(400, "参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
// 上传文件转存
|
||||
upFilePath, err := file.TransferUploadFile(formFile, subPath, nil)
|
||||
if err != nil {
|
||||
c.JSON(200, result.ErrMsg(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
newFileName := upFilePath[strings.LastIndex(upFilePath, "/")+1:]
|
||||
c.JSON(200, result.OkData(map[string]string{
|
||||
"url": "http://" + c.Request.Host + upFilePath,
|
||||
"fileName": upFilePath,
|
||||
"newFileName": newFileName,
|
||||
"originalFileName": formFile.Filename,
|
||||
}))
|
||||
}
|
||||
|
||||
// 切片文件检查
|
||||
//
|
||||
// POST /chunkCheck
|
||||
func (s *FileController) ChunkCheck(c *gin.Context) {
|
||||
var body struct {
|
||||
// 唯一标识
|
||||
Identifier string `json:"identifier" binding:"required"`
|
||||
// 文件名
|
||||
FileName string `json:"fileName" binding:"required"`
|
||||
}
|
||||
err := c.ShouldBindJSON(&body)
|
||||
if err != nil {
|
||||
c.JSON(400, result.CodeMsg(400, "参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
// 读取标识目录
|
||||
chunks, err := file.ChunkCheckFile(body.Identifier, body.FileName)
|
||||
if err != nil {
|
||||
c.JSON(200, result.ErrMsg(err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(200, result.OkData(chunks))
|
||||
}
|
||||
|
||||
// 切片文件合并
|
||||
//
|
||||
// POST /chunkMerge
|
||||
func (s *FileController) ChunkMerge(c *gin.Context) {
|
||||
var body struct {
|
||||
// 唯一标识
|
||||
Identifier string `json:"identifier" binding:"required"`
|
||||
// 文件名
|
||||
FileName string `json:"fileName" binding:"required"`
|
||||
// 子路径类型
|
||||
SubPath string `json:"subPath" binding:"required"`
|
||||
}
|
||||
err := c.ShouldBindJSON(&body)
|
||||
if err != nil {
|
||||
c.JSON(400, result.CodeMsg(400, "参数错误"))
|
||||
return
|
||||
}
|
||||
if _, ok := uploadsubpath.UploadSubpath[body.SubPath]; !ok {
|
||||
c.JSON(400, result.CodeMsg(400, "参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
// 切片文件合并
|
||||
mergeFilePath, err := file.ChunkMergeFile(body.Identifier, body.FileName, body.SubPath)
|
||||
if err != nil {
|
||||
c.JSON(200, result.ErrMsg(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
newFileName := mergeFilePath[strings.LastIndex(mergeFilePath, "/")+1:]
|
||||
c.JSON(200, result.OkData(map[string]string{
|
||||
"url": "http://" + c.Request.Host + mergeFilePath,
|
||||
"fileName": mergeFilePath,
|
||||
"newFileName": newFileName,
|
||||
"originalFileName": body.FileName,
|
||||
}))
|
||||
}
|
||||
|
||||
// 切片文件上传
|
||||
//
|
||||
// POST /chunkUpload
|
||||
func (s *FileController) ChunkUpload(c *gin.Context) {
|
||||
// 切片编号
|
||||
index := c.PostForm("index")
|
||||
// 切片唯一标识
|
||||
identifier := c.PostForm("identifier")
|
||||
// 上传的文件
|
||||
formFile, err := c.FormFile("file")
|
||||
if index == "" || identifier == "" || err != nil {
|
||||
c.JSON(400, result.CodeMsg(400, "参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
// 上传文件转存
|
||||
chunkFilePath, err := file.TransferChunkUploadFile(formFile, index, identifier)
|
||||
if err != nil {
|
||||
c.JSON(200, result.ErrMsg(err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(206, result.OkData(chunkFilePath))
|
||||
}
|
||||
28
src/modules/common/controller/index.go
Normal file
28
src/modules/common/controller/index.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"ems.agt/src/framework/config"
|
||||
"ems.agt/src/framework/vo/result"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 实例化控制层 IndexController 结构体
|
||||
var NewIndex = &IndexController{}
|
||||
|
||||
// 路由主页
|
||||
//
|
||||
// PATH /
|
||||
type IndexController struct{}
|
||||
|
||||
// 根路由
|
||||
//
|
||||
// GET /
|
||||
func (s *IndexController) Handler(c *gin.Context) {
|
||||
name := config.Get("framework.name").(string)
|
||||
version := config.Get("framework.version").(string)
|
||||
str := "欢迎使用%s后台管理框架,当前版本:%s,请通过前端管理地址访问。"
|
||||
c.JSON(200, result.OkMsg(fmt.Sprintf(str, name, version)))
|
||||
}
|
||||
88
src/modules/common/controller/register.go
Normal file
88
src/modules/common/controller/register.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
commonConstants "ems.agt/src/framework/constants/common"
|
||||
ctxUtils "ems.agt/src/framework/utils/ctx"
|
||||
"ems.agt/src/framework/utils/regular"
|
||||
"ems.agt/src/framework/vo/result"
|
||||
commonModel "ems.agt/src/modules/common/model"
|
||||
commonService "ems.agt/src/modules/common/service"
|
||||
systemService "ems.agt/src/modules/system/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 实例化控制层 RegisterController 结构体
|
||||
var NewRegister = &RegisterController{
|
||||
registerService: commonService.NewRegisterImpl,
|
||||
sysLogLoginService: systemService.NewSysLogLoginImpl,
|
||||
}
|
||||
|
||||
// 账号注册操作处理
|
||||
//
|
||||
// PATH /
|
||||
type RegisterController struct {
|
||||
// 账号注册操作服务
|
||||
registerService commonService.IRegister
|
||||
// 系统登录访问
|
||||
sysLogLoginService systemService.ISysLogLogin
|
||||
}
|
||||
|
||||
// 账号注册
|
||||
//
|
||||
// GET /captchaImage
|
||||
func (s *RegisterController) UserName(c *gin.Context) {
|
||||
var registerBody commonModel.RegisterBody
|
||||
if err := c.ShouldBindJSON(®isterBody); err != nil {
|
||||
c.JSON(400, result.ErrMsg("参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
// 判断必传参数
|
||||
if !regular.ValidUsername(registerBody.Username) {
|
||||
c.JSON(200, result.ErrMsg("账号不能以数字开头,可包含大写小写字母,数字,且不少于5位"))
|
||||
return
|
||||
}
|
||||
if !regular.ValidPassword(registerBody.Password) {
|
||||
c.JSON(200, result.ErrMsg("登录密码至少包含大小写字母、数字、特殊符号,且不少于6位"))
|
||||
return
|
||||
}
|
||||
if registerBody.Password != registerBody.ConfirmPassword {
|
||||
c.JSON(200, result.ErrMsg("用户确认输入密码不一致"))
|
||||
return
|
||||
}
|
||||
|
||||
// 当前请求信息
|
||||
ipaddr, location := ctxUtils.IPAddrLocation(c)
|
||||
os, browser := ctxUtils.UaOsBrowser(c)
|
||||
|
||||
// 校验验证码
|
||||
err := s.registerService.ValidateCaptcha(
|
||||
registerBody.Code,
|
||||
registerBody.UUID,
|
||||
)
|
||||
// 根据错误信息,创建系统访问记录
|
||||
if err != nil {
|
||||
msg := err.Error() + " " + registerBody.Code
|
||||
s.sysLogLoginService.NewSysLogLogin(
|
||||
registerBody.Username, commonConstants.STATUS_NO, msg,
|
||||
ipaddr, location, os, browser,
|
||||
)
|
||||
c.JSON(200, result.ErrMsg(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
infoStr := s.registerService.ByUserName(registerBody.Username, registerBody.Password, registerBody.UserType)
|
||||
if !strings.HasPrefix(infoStr, "注册") {
|
||||
msg := registerBody.Username + " 注册成功 " + infoStr
|
||||
s.sysLogLoginService.NewSysLogLogin(
|
||||
registerBody.Username, commonConstants.STATUS_NO, msg,
|
||||
ipaddr, location, os, browser,
|
||||
)
|
||||
c.JSON(200, result.OkMsg("注册成功"))
|
||||
return
|
||||
}
|
||||
c.JSON(200, result.ErrMsg(infoStr))
|
||||
}
|
||||
16
src/modules/common/model/login_body.go
Normal file
16
src/modules/common/model/login_body.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package model
|
||||
|
||||
// LoginBody 用户登录对象
|
||||
type LoginBody struct {
|
||||
// Username 用户名
|
||||
Username string `json:"username" binding:"required"`
|
||||
|
||||
// Password 用户密码
|
||||
Password string `json:"password" binding:"required"`
|
||||
|
||||
// Code 验证码
|
||||
Code string `json:"code"`
|
||||
|
||||
// UUID 验证码唯一标识
|
||||
UUID string `json:"uuid"`
|
||||
}
|
||||
22
src/modules/common/model/register_body.go
Normal file
22
src/modules/common/model/register_body.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package model
|
||||
|
||||
// RegisterBody 用户注册对象
|
||||
type RegisterBody struct {
|
||||
// Username 用户名
|
||||
Username string `json:"username" binding:"required"`
|
||||
|
||||
// Password 用户密码
|
||||
Password string `json:"password" binding:"required"`
|
||||
|
||||
// ConfirmPassword 用户确认密码
|
||||
ConfirmPassword string `json:"confirmPassword" binding:"required"`
|
||||
|
||||
// Code 验证码
|
||||
Code string `json:"code"`
|
||||
|
||||
// UUID 验证码唯一标识
|
||||
UUID string `json:"uuid"`
|
||||
|
||||
// UserType 标记用户类型
|
||||
UserType string `json:"userType"`
|
||||
}
|
||||
21
src/modules/common/service/account.go
Normal file
21
src/modules/common/service/account.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package service
|
||||
|
||||
import "ems.agt/src/framework/vo"
|
||||
|
||||
// 账号身份操作服务 服务层接口
|
||||
type IAccount interface {
|
||||
// ValidateCaptcha 校验验证码
|
||||
ValidateCaptcha(code, uuid string) error
|
||||
|
||||
// LoginByUsername 登录生成token
|
||||
LoginByUsername(username, password string) (vo.LoginUser, error)
|
||||
|
||||
// ClearLoginRecordCache 清除错误记录次数
|
||||
ClearLoginRecordCache(username string) bool
|
||||
|
||||
// RoleAndMenuPerms 角色和菜单数据权限
|
||||
RoleAndMenuPerms(userId string, isAdmin bool) ([]string, []string)
|
||||
|
||||
// RouteMenus 前端路由所需要的菜单
|
||||
RouteMenus(userId string, isAdmin bool) []vo.Router
|
||||
}
|
||||
166
src/modules/common/service/account.impl.go
Normal file
166
src/modules/common/service/account.impl.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/config"
|
||||
adminConstants "ems.agt/src/framework/constants/admin"
|
||||
"ems.agt/src/framework/constants/cachekey"
|
||||
"ems.agt/src/framework/constants/common"
|
||||
"ems.agt/src/framework/redis"
|
||||
"ems.agt/src/framework/utils/crypto"
|
||||
"ems.agt/src/framework/utils/parse"
|
||||
"ems.agt/src/framework/vo"
|
||||
systemService "ems.agt/src/modules/system/service"
|
||||
)
|
||||
|
||||
// 实例化服务层 AccountImpl 结构体
|
||||
var NewAccountImpl = &AccountImpl{
|
||||
sysUserService: systemService.NewSysUserImpl,
|
||||
sysConfigService: systemService.NewSysConfigImpl,
|
||||
sysRoleService: systemService.NewSysRoleImpl,
|
||||
sysMenuService: systemService.NewSysMenuImpl,
|
||||
}
|
||||
|
||||
// 账号身份操作服务 服务层处理
|
||||
type AccountImpl struct {
|
||||
// 用户信息服务
|
||||
sysUserService systemService.ISysUser
|
||||
// 参数配置服务
|
||||
sysConfigService systemService.ISysConfig
|
||||
// 角色服务
|
||||
sysRoleService systemService.ISysRole
|
||||
// 菜单服务
|
||||
sysMenuService systemService.ISysMenu
|
||||
}
|
||||
|
||||
// ValidateCaptcha 校验验证码
|
||||
func (s *AccountImpl) ValidateCaptcha(code, uuid string) error {
|
||||
// 验证码检查,从数据库配置获取验证码开关 true开启,false关闭
|
||||
captchaEnabledStr := s.sysConfigService.SelectConfigValueByKey("sys.account.captchaEnabled")
|
||||
if !parse.Boolean(captchaEnabledStr) {
|
||||
return nil
|
||||
}
|
||||
if code == "" || uuid == "" {
|
||||
return errors.New("验证码信息错误")
|
||||
}
|
||||
verifyKey := cachekey.CAPTCHA_CODE_KEY + uuid
|
||||
captcha, _ := redis.Get("", verifyKey)
|
||||
if captcha == "" {
|
||||
return errors.New("验证码已失效")
|
||||
}
|
||||
redis.Del("", verifyKey)
|
||||
if captcha != code {
|
||||
return errors.New("验证码错误")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoginByUsername 登录创建用户信息
|
||||
func (s *AccountImpl) LoginByUsername(username, password string) (vo.LoginUser, error) {
|
||||
loginUser := vo.LoginUser{}
|
||||
|
||||
// 检查密码重试次数
|
||||
retrykey, retryCount, lockTime, err := s.passwordRetryCount(username)
|
||||
if err != nil {
|
||||
return loginUser, err
|
||||
}
|
||||
|
||||
// 查询用户登录账号
|
||||
sysUser := s.sysUserService.SelectUserByUserName(username)
|
||||
if sysUser.UserName != username {
|
||||
return loginUser, errors.New("用户不存在或密码错误")
|
||||
}
|
||||
if sysUser.DelFlag == common.STATUS_YES {
|
||||
return loginUser, errors.New("对不起,您的账号已被删除")
|
||||
}
|
||||
if sysUser.Status == common.STATUS_NO {
|
||||
return loginUser, errors.New("对不起,您的账号已禁用")
|
||||
}
|
||||
|
||||
// 检验用户密码
|
||||
compareBool := crypto.BcryptCompare(password, sysUser.Password)
|
||||
if !compareBool {
|
||||
redis.SetByExpire("", retrykey, retryCount+1, lockTime)
|
||||
return loginUser, errors.New("用户不存在或密码错误")
|
||||
} else {
|
||||
// 清除错误记录次数
|
||||
s.ClearLoginRecordCache(username)
|
||||
}
|
||||
|
||||
// 登录用户信息
|
||||
loginUser.UserID = sysUser.UserID
|
||||
loginUser.DeptID = sysUser.DeptID
|
||||
loginUser.User = sysUser
|
||||
// 用户权限组标识
|
||||
isAdmin := config.IsAdmin(sysUser.UserID)
|
||||
if isAdmin {
|
||||
loginUser.Permissions = []string{adminConstants.PERMISSION}
|
||||
} else {
|
||||
perms := s.sysMenuService.SelectMenuPermsByUserId(sysUser.UserID)
|
||||
loginUser.Permissions = parse.RemoveDuplicates(perms)
|
||||
}
|
||||
return loginUser, nil
|
||||
}
|
||||
|
||||
// ClearLoginRecordCache 清除错误记录次数
|
||||
func (s *AccountImpl) ClearLoginRecordCache(username string) bool {
|
||||
cacheKey := cachekey.PWD_ERR_CNT_KEY + username
|
||||
hasKey, _ := redis.Has("", cacheKey)
|
||||
if hasKey {
|
||||
delOk, _ := redis.Del("", cacheKey)
|
||||
return delOk
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// passwordRetryCount 密码重试次数
|
||||
func (s *AccountImpl) passwordRetryCount(username string) (string, int64, time.Duration, error) {
|
||||
// 验证登录次数和错误锁定时间
|
||||
maxRetryCount := config.Get("user.password.maxRetryCount").(int)
|
||||
lockTime := config.Get("user.password.lockTime").(int)
|
||||
// 验证缓存记录次数
|
||||
retrykey := cachekey.PWD_ERR_CNT_KEY + username
|
||||
retryCount, err := redis.Get("", retrykey)
|
||||
if retryCount == "" || err != nil {
|
||||
retryCount = "0"
|
||||
}
|
||||
// 是否超过错误值
|
||||
if parse.Number(retryCount) >= int64(maxRetryCount) {
|
||||
msg := fmt.Sprintf("密码输入错误 %d 次,帐户锁定 %d 分钟", maxRetryCount, lockTime)
|
||||
return retrykey, int64(maxRetryCount), time.Duration(lockTime) * time.Minute, errors.New(msg)
|
||||
}
|
||||
return retrykey, int64(maxRetryCount), time.Duration(lockTime) * time.Minute, nil
|
||||
}
|
||||
|
||||
// RoleAndMenuPerms 角色和菜单数据权限
|
||||
func (s *AccountImpl) RoleAndMenuPerms(userId string, isAdmin bool) ([]string, []string) {
|
||||
if isAdmin {
|
||||
return []string{adminConstants.ROLE_KEY}, []string{adminConstants.PERMISSION}
|
||||
} else {
|
||||
// 角色key
|
||||
roleGroup := []string{}
|
||||
roles := s.sysRoleService.SelectRoleListByUserId(userId)
|
||||
for _, role := range roles {
|
||||
roleGroup = append(roleGroup, role.RoleKey)
|
||||
}
|
||||
// 菜单权限key
|
||||
perms := s.sysMenuService.SelectMenuPermsByUserId(userId)
|
||||
return parse.RemoveDuplicates(roleGroup), parse.RemoveDuplicates(perms)
|
||||
}
|
||||
}
|
||||
|
||||
// RouteMenus 前端路由所需要的菜单
|
||||
func (s *AccountImpl) RouteMenus(userId string, isAdmin bool) []vo.Router {
|
||||
var buildMenus []vo.Router
|
||||
if isAdmin {
|
||||
menus := s.sysMenuService.SelectMenuTreeByUserId("*")
|
||||
buildMenus = s.sysMenuService.BuildRouteMenus(menus, "")
|
||||
} else {
|
||||
menus := s.sysMenuService.SelectMenuTreeByUserId(userId)
|
||||
buildMenus = s.sysMenuService.BuildRouteMenus(menus, "")
|
||||
}
|
||||
return buildMenus
|
||||
}
|
||||
10
src/modules/common/service/register.go
Normal file
10
src/modules/common/service/register.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package service
|
||||
|
||||
// 账号注册操作处理 服务层接口
|
||||
type IRegister interface {
|
||||
// ValidateCaptcha 校验验证码
|
||||
ValidateCaptcha(code, uuid string) error
|
||||
|
||||
// ByUserName 账号注册
|
||||
ByUserName(username, password, userType string) string
|
||||
}
|
||||
100
src/modules/common/service/register.impl.go
Normal file
100
src/modules/common/service/register.impl.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"ems.agt/src/framework/constants/cachekey"
|
||||
"ems.agt/src/framework/constants/common"
|
||||
"ems.agt/src/framework/redis"
|
||||
"ems.agt/src/framework/utils/parse"
|
||||
systemModel "ems.agt/src/modules/system/model"
|
||||
systemService "ems.agt/src/modules/system/service"
|
||||
)
|
||||
|
||||
// 实例化服务层 RegisterImpl 结构体
|
||||
var NewRegisterImpl = &RegisterImpl{
|
||||
sysUserService: systemService.NewSysUserImpl,
|
||||
sysConfigService: systemService.NewSysConfigImpl,
|
||||
sysRoleService: systemService.NewSysRoleImpl,
|
||||
}
|
||||
|
||||
// 账号注册操作处理 服务层处理
|
||||
type RegisterImpl struct {
|
||||
// 用户信息服务
|
||||
sysUserService systemService.ISysUser
|
||||
// 参数配置服务
|
||||
sysConfigService systemService.ISysConfig
|
||||
// 角色服务
|
||||
sysRoleService systemService.ISysRole
|
||||
}
|
||||
|
||||
// ValidateCaptcha 校验验证码
|
||||
func (s *RegisterImpl) ValidateCaptcha(code, uuid string) error {
|
||||
// 验证码检查,从数据库配置获取验证码开关 true开启,false关闭
|
||||
captchaEnabledStr := s.sysConfigService.SelectConfigValueByKey("sys.account.captchaEnabled")
|
||||
if !parse.Boolean(captchaEnabledStr) {
|
||||
return nil
|
||||
}
|
||||
if code == "" || uuid == "" {
|
||||
return errors.New("验证码信息错误")
|
||||
}
|
||||
verifyKey := cachekey.CAPTCHA_CODE_KEY + uuid
|
||||
captcha, err := redis.Get("", verifyKey)
|
||||
if captcha == "" || err != nil {
|
||||
return errors.New("验证码已失效")
|
||||
}
|
||||
redis.Del("", verifyKey)
|
||||
if captcha != code {
|
||||
return errors.New("验证码错误")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ByUserName 账号注册
|
||||
func (s *RegisterImpl) ByUserName(username, password, userType string) string {
|
||||
// 检查用户登录账号是否唯一
|
||||
uniqueUserName := s.sysUserService.CheckUniqueUserName(username, "")
|
||||
if !uniqueUserName {
|
||||
return fmt.Sprintf("注册用户【%s】失败,注册账号已存在", username)
|
||||
}
|
||||
|
||||
sysUser := systemModel.SysUser{
|
||||
UserName: username,
|
||||
NickName: username, // 昵称使用名称账号
|
||||
Password: password, // 原始密码
|
||||
Status: common.STATUS_YES, // 账号状态激活
|
||||
DeptID: "100", // 归属部门为根节点
|
||||
CreateBy: "注册", // 创建来源
|
||||
}
|
||||
// 标记用户类型
|
||||
if userType == "" {
|
||||
sysUser.UserType = "sys"
|
||||
}
|
||||
// 新增用户的角色管理
|
||||
sysUser.RoleIDs = s.registerRoleInit(userType)
|
||||
// 新增用户的岗位管理
|
||||
sysUser.PostIDs = s.registerPostInit(userType)
|
||||
|
||||
insertId := s.sysUserService.InsertUser(sysUser)
|
||||
if insertId != "" {
|
||||
return insertId
|
||||
}
|
||||
return "注册失败,请联系系统管理人员"
|
||||
}
|
||||
|
||||
// registerRoleInit 注册初始角色
|
||||
func (s *RegisterImpl) registerRoleInit(userType string) []string {
|
||||
if userType == "sys" {
|
||||
return []string{}
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// registerPostInit 注册初始岗位
|
||||
func (s *RegisterImpl) registerPostInit(userType string) []string {
|
||||
if userType == "sys" {
|
||||
return []string{}
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
149
src/modules/monitor/controller/sys_cache.go
Normal file
149
src/modules/monitor/controller/sys_cache.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"ems.agt/src/framework/constants/cachekey"
|
||||
"ems.agt/src/framework/redis"
|
||||
"ems.agt/src/framework/vo/result"
|
||||
"ems.agt/src/modules/monitor/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 实例化控制层 SysCacheController 结构体
|
||||
var NewSysCache = &SysCacheController{}
|
||||
|
||||
// 缓存监控信息
|
||||
//
|
||||
// PATH /monitor/cache
|
||||
type SysCacheController struct{}
|
||||
|
||||
// Redis信息
|
||||
//
|
||||
// GET /
|
||||
func (s *SysCacheController) Info(c *gin.Context) {
|
||||
c.JSON(200, result.OkData(map[string]any{
|
||||
"info": redis.Info(""),
|
||||
"dbSize": redis.KeySize(""),
|
||||
"commandStats": redis.CommandStats(""),
|
||||
}))
|
||||
}
|
||||
|
||||
// 缓存名称列表
|
||||
//
|
||||
// GET /getNames
|
||||
func (s *SysCacheController) Names(c *gin.Context) {
|
||||
caches := []model.SysCache{
|
||||
model.NewSysCacheNames("用户信息", cachekey.LOGIN_TOKEN_KEY),
|
||||
model.NewSysCacheNames("配置信息", cachekey.SYS_CONFIG_KEY),
|
||||
model.NewSysCacheNames("数据字典", cachekey.SYS_DICT_KEY),
|
||||
model.NewSysCacheNames("验证码", cachekey.CAPTCHA_CODE_KEY),
|
||||
model.NewSysCacheNames("防重提交", cachekey.REPEAT_SUBMIT_KEY),
|
||||
model.NewSysCacheNames("限流处理", cachekey.RATE_LIMIT_KEY),
|
||||
model.NewSysCacheNames("密码错误次数", cachekey.PWD_ERR_CNT_KEY),
|
||||
}
|
||||
c.JSON(200, result.OkData(caches))
|
||||
}
|
||||
|
||||
// 缓存名称下键名列表
|
||||
//
|
||||
// GET /getKeys/:cacheName
|
||||
func (s *SysCacheController) Keys(c *gin.Context) {
|
||||
cacheName := c.Param("cacheName")
|
||||
if cacheName == "" {
|
||||
c.JSON(400, result.CodeMsg(400, "参数错误"))
|
||||
return
|
||||
}
|
||||
caches := []model.SysCache{}
|
||||
|
||||
// 遍历组装
|
||||
cacheKeys, _ := redis.GetKeys("", cacheName+":*")
|
||||
for _, key := range cacheKeys {
|
||||
caches = append(caches, model.NewSysCacheKeys(cacheName, key))
|
||||
}
|
||||
|
||||
c.JSON(200, result.OkData(caches))
|
||||
}
|
||||
|
||||
// 缓存内容
|
||||
//
|
||||
// GET /getValue/:cacheName/:cacheKey
|
||||
func (s *SysCacheController) Value(c *gin.Context) {
|
||||
cacheName := c.Param("cacheName")
|
||||
cacheKey := c.Param("cacheKey")
|
||||
if cacheName == "" || cacheKey == "" {
|
||||
c.JSON(400, result.CodeMsg(400, "参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
cacheValue, err := redis.Get("", cacheName+":"+cacheKey)
|
||||
if err != nil {
|
||||
c.JSON(200, result.ErrMsg(err.Error()))
|
||||
return
|
||||
}
|
||||
sysCache := model.NewSysCacheValue(cacheName, cacheKey, cacheValue)
|
||||
c.JSON(200, result.OkData(sysCache))
|
||||
}
|
||||
|
||||
// 删除缓存名称下键名列表
|
||||
//
|
||||
// DELETE /clearCacheName/:cacheName
|
||||
func (s *SysCacheController) ClearCacheName(c *gin.Context) {
|
||||
cacheName := c.Param("cacheName")
|
||||
if cacheName == "" {
|
||||
c.JSON(400, result.CodeMsg(400, "参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
cacheKeys, err := redis.GetKeys("", cacheName+":*")
|
||||
if err != nil {
|
||||
c.JSON(200, result.ErrMsg(err.Error()))
|
||||
return
|
||||
}
|
||||
ok, _ := redis.DelKeys("", cacheKeys)
|
||||
if ok {
|
||||
c.JSON(200, result.Ok(nil))
|
||||
return
|
||||
}
|
||||
c.JSON(200, result.Err(nil))
|
||||
}
|
||||
|
||||
// 删除缓存键名
|
||||
//
|
||||
// DELETE /clearCacheKey/:cacheName/:cacheKey
|
||||
func (s *SysCacheController) ClearCacheKey(c *gin.Context) {
|
||||
cacheName := c.Param("cacheName")
|
||||
cacheKey := c.Param("cacheKey")
|
||||
if cacheName == "" || cacheKey == "" {
|
||||
c.JSON(400, result.CodeMsg(400, "参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
ok, _ := redis.Del("", cacheName+":"+cacheKey)
|
||||
if ok {
|
||||
c.JSON(200, result.Ok(nil))
|
||||
return
|
||||
}
|
||||
c.JSON(200, result.Err(nil))
|
||||
}
|
||||
|
||||
// 安全清理缓存名称
|
||||
//
|
||||
// DELETE /clearCacheSafe
|
||||
func (s *SysCacheController) ClearCacheSafe(c *gin.Context) {
|
||||
caches := []model.SysCache{
|
||||
model.NewSysCacheNames("配置信息", cachekey.SYS_CONFIG_KEY),
|
||||
model.NewSysCacheNames("数据字典", cachekey.SYS_DICT_KEY),
|
||||
model.NewSysCacheNames("验证码", cachekey.CAPTCHA_CODE_KEY),
|
||||
model.NewSysCacheNames("防重提交", cachekey.REPEAT_SUBMIT_KEY),
|
||||
model.NewSysCacheNames("限流处理", cachekey.RATE_LIMIT_KEY),
|
||||
model.NewSysCacheNames("密码错误次数", cachekey.PWD_ERR_CNT_KEY),
|
||||
}
|
||||
for _, v := range caches {
|
||||
cacheKeys, err := redis.GetKeys("", v.CacheName+":*")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
redis.DelKeys("", cacheKeys)
|
||||
}
|
||||
c.JSON(200, result.Ok(nil))
|
||||
}
|
||||
338
src/modules/monitor/controller/sys_job.go
Normal file
338
src/modules/monitor/controller/sys_job.go
Normal file
@@ -0,0 +1,338 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/utils/ctx"
|
||||
"ems.agt/src/framework/utils/file"
|
||||
"ems.agt/src/framework/utils/parse"
|
||||
"ems.agt/src/framework/vo/result"
|
||||
"ems.agt/src/modules/monitor/model"
|
||||
"ems.agt/src/modules/monitor/service"
|
||||
systemService "ems.agt/src/modules/system/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
)
|
||||
|
||||
// 实例化控制层 SysJobLogController 结构体
|
||||
var NewSysJob = &SysJobController{
|
||||
sysJobService: service.NewSysJobImpl,
|
||||
sysDictDataService: systemService.NewSysDictDataImpl,
|
||||
}
|
||||
|
||||
// 调度任务信息
|
||||
//
|
||||
// PATH /monitor/job
|
||||
type SysJobController struct {
|
||||
// 调度任务服务
|
||||
sysJobService service.ISysJob
|
||||
// 字典数据服务
|
||||
sysDictDataService systemService.ISysDictData
|
||||
}
|
||||
|
||||
// 调度任务列表
|
||||
//
|
||||
// GET /list
|
||||
func (s *SysJobController) List(c *gin.Context) {
|
||||
querys := ctx.QueryMap(c)
|
||||
data := s.sysJobService.SelectJobPage(querys)
|
||||
c.JSON(200, result.Ok(data))
|
||||
}
|
||||
|
||||
// 调度任务信息
|
||||
//
|
||||
// GET /:jobId
|
||||
func (s *SysJobController) Info(c *gin.Context) {
|
||||
jobId := c.Param("jobId")
|
||||
if jobId == "" {
|
||||
c.JSON(400, result.CodeMsg(400, "参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
data := s.sysJobService.SelectJobById(jobId)
|
||||
if data.JobID == jobId {
|
||||
c.JSON(200, result.OkData(data))
|
||||
return
|
||||
}
|
||||
c.JSON(200, result.Err(nil))
|
||||
}
|
||||
|
||||
// 调度任务新增
|
||||
//
|
||||
// POST /
|
||||
func (s *SysJobController) Add(c *gin.Context) {
|
||||
var body model.SysJob
|
||||
err := c.ShouldBindBodyWith(&body, binding.JSON)
|
||||
if err != nil || body.JobID != "" {
|
||||
c.JSON(400, result.CodeMsg(400, "参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查cron表达式格式
|
||||
if parse.CronExpression(body.CronExpression) == 0 {
|
||||
msg := fmt.Sprintf("调度任务新增【%s】失败,Cron表达式不正确", body.JobName)
|
||||
c.JSON(200, result.ErrMsg(msg))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查任务调用传入参数是否json格式
|
||||
if body.TargetParams != "" {
|
||||
msg := fmt.Sprintf("调度任务新增【%s】失败,任务传入参数json字符串不正确", body.JobName)
|
||||
if len(body.TargetParams) < 7 {
|
||||
c.JSON(200, result.ErrMsg(msg))
|
||||
return
|
||||
}
|
||||
if !json.Valid([]byte(body.TargetParams)) {
|
||||
c.JSON(200, result.ErrMsg(msg))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 检查属性值唯一
|
||||
uniqueJob := s.sysJobService.CheckUniqueJobName(body.JobName, body.JobGroup, "")
|
||||
if !uniqueJob {
|
||||
msg := fmt.Sprintf("调度任务新增【%s】失败,同任务组内有相同任务名称", body.JobName)
|
||||
c.JSON(200, result.ErrMsg(msg))
|
||||
return
|
||||
}
|
||||
|
||||
body.CreateBy = ctx.LoginUserToUserName(c)
|
||||
insertId := s.sysJobService.InsertJob(body)
|
||||
if insertId != "" {
|
||||
c.JSON(200, result.Ok(nil))
|
||||
return
|
||||
}
|
||||
c.JSON(200, result.Err(nil))
|
||||
}
|
||||
|
||||
// 调度任务修改
|
||||
//
|
||||
// PUT /
|
||||
func (s *SysJobController) Edit(c *gin.Context) {
|
||||
var body model.SysJob
|
||||
err := c.ShouldBindBodyWith(&body, binding.JSON)
|
||||
if err != nil || body.JobID == "" {
|
||||
c.JSON(400, result.CodeMsg(400, "参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查cron表达式格式
|
||||
if parse.CronExpression(body.CronExpression) == 0 {
|
||||
msg := fmt.Sprintf("调度任务修改【%s】失败,Cron表达式不正确", body.JobName)
|
||||
c.JSON(200, result.ErrMsg(msg))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查任务调用传入参数是否json格式
|
||||
if body.TargetParams != "" {
|
||||
msg := fmt.Sprintf("调度任务修改【%s】失败,任务传入参数json字符串不正确", body.JobName)
|
||||
if len(body.TargetParams) < 7 {
|
||||
c.JSON(200, result.ErrMsg(msg))
|
||||
return
|
||||
}
|
||||
if !json.Valid([]byte(body.TargetParams)) {
|
||||
c.JSON(200, result.ErrMsg(msg))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 检查属性值唯一
|
||||
uniqueJob := s.sysJobService.CheckUniqueJobName(body.JobName, body.JobGroup, body.JobID)
|
||||
if !uniqueJob {
|
||||
msg := fmt.Sprintf("调度任务修改【%s】失败,同任务组内有相同任务名称", body.JobName)
|
||||
c.JSON(200, result.ErrMsg(msg))
|
||||
return
|
||||
}
|
||||
|
||||
body.UpdateBy = ctx.LoginUserToUserName(c)
|
||||
rows := s.sysJobService.UpdateJob(body)
|
||||
if rows > 0 {
|
||||
c.JSON(200, result.Ok(nil))
|
||||
return
|
||||
}
|
||||
c.JSON(200, result.Err(nil))
|
||||
}
|
||||
|
||||
// 调度任务删除
|
||||
//
|
||||
// DELETE /:jobIds
|
||||
func (s *SysJobController) Remove(c *gin.Context) {
|
||||
jobIds := c.Param("jobIds")
|
||||
if jobIds == "" {
|
||||
c.JSON(400, result.CodeMsg(400, "参数错误"))
|
||||
return
|
||||
}
|
||||
// 处理字符转id数组后去重
|
||||
ids := strings.Split(jobIds, ",")
|
||||
uniqueIDs := parse.RemoveDuplicates(ids)
|
||||
if len(uniqueIDs) <= 0 {
|
||||
c.JSON(200, result.Err(nil))
|
||||
return
|
||||
}
|
||||
rows, err := s.sysJobService.DeleteJobByIds(uniqueIDs)
|
||||
if err != nil {
|
||||
c.JSON(200, result.ErrMsg(err.Error()))
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf("删除成功:%d", rows)
|
||||
c.JSON(200, result.OkMsg(msg))
|
||||
}
|
||||
|
||||
// 调度任务修改状态
|
||||
//
|
||||
// PUT /changeStatus
|
||||
func (s *SysJobController) Status(c *gin.Context) {
|
||||
var body struct {
|
||||
// 任务ID
|
||||
JobId string `json:"jobId" binding:"required"`
|
||||
// 状态
|
||||
Status string `json:"status" binding:"required"`
|
||||
}
|
||||
err := c.ShouldBindBodyWith(&body, binding.JSON)
|
||||
if err != nil {
|
||||
c.JSON(400, result.CodeMsg(400, "参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否存在
|
||||
job := s.sysJobService.SelectJobById(body.JobId)
|
||||
if job.JobID != body.JobId {
|
||||
c.JSON(200, result.ErrMsg("没有权限访问调度任务数据!"))
|
||||
return
|
||||
}
|
||||
|
||||
// 与旧值相等不变更
|
||||
if job.Status == body.Status {
|
||||
c.JSON(200, result.ErrMsg("变更状态与旧值相等!"))
|
||||
return
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
job.Status = body.Status
|
||||
job.UpdateBy = ctx.LoginUserToUserName(c)
|
||||
ok := s.sysJobService.ChangeStatus(job)
|
||||
if ok {
|
||||
c.JSON(200, result.Ok(nil))
|
||||
return
|
||||
}
|
||||
c.JSON(200, result.Err(nil))
|
||||
}
|
||||
|
||||
// 调度任务立即执行一次
|
||||
//
|
||||
// PUT /run/:jobId
|
||||
func (s *SysJobController) Run(c *gin.Context) {
|
||||
jobId := c.Param("jobId")
|
||||
if jobId == "" {
|
||||
c.JSON(400, result.CodeMsg(400, "参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否存在
|
||||
job := s.sysJobService.SelectJobById(jobId)
|
||||
if job.JobID != jobId {
|
||||
c.JSON(200, result.ErrMsg("没有权限访问调度任务数据!"))
|
||||
return
|
||||
}
|
||||
|
||||
ok := s.sysJobService.RunQueueJob(job)
|
||||
if ok {
|
||||
c.JSON(200, result.Ok(nil))
|
||||
return
|
||||
}
|
||||
c.JSON(200, result.Err(nil))
|
||||
}
|
||||
|
||||
// 调度任务重置刷新队列
|
||||
//
|
||||
// PUT /resetQueueJob
|
||||
func (s *SysJobController) ResetQueueJob(c *gin.Context) {
|
||||
s.sysJobService.ResetQueueJob()
|
||||
c.JSON(200, result.Ok(nil))
|
||||
}
|
||||
|
||||
// 导出调度任务信息
|
||||
//
|
||||
// POST /export
|
||||
func (s *SysJobController) Export(c *gin.Context) {
|
||||
// 查询结果,根据查询条件结果,单页最大值限制
|
||||
querys := ctx.BodyJSONMap(c)
|
||||
data := s.sysJobService.SelectJobPage(querys)
|
||||
if data["total"].(int64) == 0 {
|
||||
c.JSON(200, result.ErrMsg("导出数据记录为空"))
|
||||
return
|
||||
}
|
||||
rows := data["rows"].([]model.SysJob)
|
||||
|
||||
// 导出文件名称
|
||||
fileName := fmt.Sprintf("job_export_%d_%d.xlsx", len(rows), time.Now().UnixMilli())
|
||||
// 第一行表头标题
|
||||
headerCells := map[string]string{
|
||||
"A1": "任务编号",
|
||||
"B1": "任务名称",
|
||||
"C1": "任务组名",
|
||||
"D1": "调用目标",
|
||||
"E1": "传入参数",
|
||||
"F1": "执行表达式",
|
||||
"G1": "出错策略",
|
||||
"H1": "并发执行",
|
||||
"I1": "任务状态",
|
||||
"J1": "备注说明",
|
||||
}
|
||||
// 读取任务组名字典数据
|
||||
dictSysJobGroup := s.sysDictDataService.SelectDictDataByType("sys_job_group")
|
||||
// 从第二行开始的数据
|
||||
dataCells := make([]map[string]any, 0)
|
||||
for i, row := range rows {
|
||||
idx := strconv.Itoa(i + 2)
|
||||
// 任务组名
|
||||
sysJobGroup := ""
|
||||
for _, v := range dictSysJobGroup {
|
||||
if row.JobGroup == v.DictValue {
|
||||
sysJobGroup = v.DictLabel
|
||||
break
|
||||
}
|
||||
}
|
||||
misfirePolicy := "放弃执行"
|
||||
if row.MisfirePolicy == "1" {
|
||||
misfirePolicy = "立即执行"
|
||||
} else if row.MisfirePolicy == "2" {
|
||||
misfirePolicy = "执行一次"
|
||||
}
|
||||
concurrent := "禁止"
|
||||
if row.Concurrent == "1" {
|
||||
concurrent = "允许"
|
||||
}
|
||||
// 状态
|
||||
statusValue := "失败"
|
||||
if row.Status == "1" {
|
||||
statusValue = "成功"
|
||||
}
|
||||
dataCells = append(dataCells, map[string]any{
|
||||
"A" + idx: row.JobID,
|
||||
"B" + idx: row.JobName,
|
||||
"C" + idx: sysJobGroup,
|
||||
"D" + idx: row.InvokeTarget,
|
||||
"E" + idx: row.TargetParams,
|
||||
"F" + idx: row.CronExpression,
|
||||
"G" + idx: misfirePolicy,
|
||||
"H" + idx: concurrent,
|
||||
"I" + idx: statusValue,
|
||||
"J" + idx: row.Remark,
|
||||
})
|
||||
}
|
||||
|
||||
// 导出数据表格
|
||||
saveFilePath, err := file.WriteSheet(headerCells, dataCells, fileName, "")
|
||||
if err != nil {
|
||||
c.JSON(200, result.ErrMsg(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.FileAttachment(saveFilePath, fileName)
|
||||
}
|
||||
167
src/modules/monitor/controller/sys_job_log.go
Normal file
167
src/modules/monitor/controller/sys_job_log.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/utils/ctx"
|
||||
"ems.agt/src/framework/utils/date"
|
||||
"ems.agt/src/framework/utils/file"
|
||||
"ems.agt/src/framework/utils/parse"
|
||||
"ems.agt/src/framework/vo/result"
|
||||
"ems.agt/src/modules/monitor/model"
|
||||
"ems.agt/src/modules/monitor/service"
|
||||
systemService "ems.agt/src/modules/system/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 实例化控制层 SysJobLogController 结构体
|
||||
var NewSysJobLog = &SysJobLogController{
|
||||
sysJobLogService: service.NewSysJobLogImpl,
|
||||
sysDictDataService: systemService.NewSysDictDataImpl,
|
||||
}
|
||||
|
||||
// 调度任务日志信息
|
||||
//
|
||||
// PATH /monitor/jobLog
|
||||
type SysJobLogController struct {
|
||||
// 调度任务日志服务
|
||||
sysJobLogService service.ISysJobLog
|
||||
// 字典数据服务
|
||||
sysDictDataService systemService.ISysDictData
|
||||
}
|
||||
|
||||
// 调度任务日志列表
|
||||
//
|
||||
// GET /list
|
||||
func (s *SysJobLogController) List(c *gin.Context) {
|
||||
// 查询参数转换map
|
||||
querys := ctx.QueryMap(c)
|
||||
list := s.sysJobLogService.SelectJobLogPage(querys)
|
||||
c.JSON(200, result.Ok(list))
|
||||
}
|
||||
|
||||
// 调度任务日志信息
|
||||
//
|
||||
// GET /:jobLogId
|
||||
func (s *SysJobLogController) Info(c *gin.Context) {
|
||||
jobLogId := c.Param("jobLogId")
|
||||
if jobLogId == "" {
|
||||
c.JSON(400, result.CodeMsg(400, "参数错误"))
|
||||
return
|
||||
}
|
||||
data := s.sysJobLogService.SelectJobLogById(jobLogId)
|
||||
if data.JobLogID == jobLogId {
|
||||
c.JSON(200, result.OkData(data))
|
||||
return
|
||||
}
|
||||
c.JSON(200, result.Err(nil))
|
||||
}
|
||||
|
||||
// 调度任务日志删除
|
||||
//
|
||||
// DELETE /:jobLogIds
|
||||
func (s *SysJobLogController) Remove(c *gin.Context) {
|
||||
jobLogIds := c.Param("jobLogIds")
|
||||
if jobLogIds == "" {
|
||||
c.JSON(400, result.CodeMsg(400, "参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
// 处理字符转id数组后去重
|
||||
ids := strings.Split(jobLogIds, ",")
|
||||
uniqueIDs := parse.RemoveDuplicates(ids)
|
||||
if len(uniqueIDs) <= 0 {
|
||||
c.JSON(200, result.Err(nil))
|
||||
return
|
||||
}
|
||||
rows := s.sysJobLogService.DeleteJobLogByIds(uniqueIDs)
|
||||
if rows > 0 {
|
||||
msg := fmt.Sprintf("删除成功:%d", rows)
|
||||
c.JSON(200, result.OkMsg(msg))
|
||||
return
|
||||
}
|
||||
c.JSON(200, result.Err(nil))
|
||||
}
|
||||
|
||||
// 调度任务日志清空
|
||||
//
|
||||
// DELETE /clean
|
||||
func (s *SysJobLogController) Clean(c *gin.Context) {
|
||||
err := s.sysJobLogService.CleanJobLog()
|
||||
if err != nil {
|
||||
c.JSON(200, result.ErrMsg(err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(200, result.Ok(nil))
|
||||
}
|
||||
|
||||
// 导出调度任务日志信息
|
||||
//
|
||||
// POST /export
|
||||
func (s *SysJobLogController) Export(c *gin.Context) {
|
||||
// 查询结果,根据查询条件结果,单页最大值限制
|
||||
querys := ctx.BodyJSONMap(c)
|
||||
data := s.sysJobLogService.SelectJobLogPage(querys)
|
||||
if data["total"].(int64) == 0 {
|
||||
c.JSON(200, result.ErrMsg("导出数据记录为空"))
|
||||
return
|
||||
}
|
||||
rows := data["rows"].([]model.SysJobLog)
|
||||
|
||||
// 导出文件名称
|
||||
fileName := fmt.Sprintf("jobLog_export_%d_%d.xlsx", len(rows), time.Now().UnixMilli())
|
||||
// 第一行表头标题
|
||||
headerCells := map[string]string{
|
||||
"A1": "日志序号",
|
||||
"B1": "任务名称",
|
||||
"C1": "任务组名",
|
||||
"D1": "调用目标",
|
||||
"E1": "传入参数",
|
||||
"F1": "日志信息",
|
||||
"G1": "执行状态",
|
||||
"H1": "记录时间",
|
||||
}
|
||||
// 读取任务组名字典数据
|
||||
dictSysJobGroup := s.sysDictDataService.SelectDictDataByType("sys_job_group")
|
||||
// 从第二行开始的数据
|
||||
dataCells := make([]map[string]any, 0)
|
||||
for i, row := range rows {
|
||||
idx := strconv.Itoa(i + 2)
|
||||
// 任务组名
|
||||
sysJobGroup := ""
|
||||
for _, v := range dictSysJobGroup {
|
||||
if row.JobGroup == v.DictValue {
|
||||
sysJobGroup = v.DictLabel
|
||||
break
|
||||
}
|
||||
}
|
||||
// 状态
|
||||
statusValue := "失败"
|
||||
if row.Status == "1" {
|
||||
statusValue = "成功"
|
||||
}
|
||||
dataCells = append(dataCells, map[string]any{
|
||||
"A" + idx: row.JobLogID,
|
||||
"B" + idx: row.JobName,
|
||||
"C" + idx: sysJobGroup,
|
||||
"D" + idx: row.InvokeTarget,
|
||||
"E" + idx: row.TargetParams,
|
||||
"F" + idx: row.JobMsg,
|
||||
"G" + idx: statusValue,
|
||||
"H" + idx: date.ParseDateToStr(row.CreateTime, date.YYYY_MM_DD_HH_MM_SS),
|
||||
})
|
||||
}
|
||||
|
||||
// 导出数据表格
|
||||
saveFilePath, err := file.WriteSheet(headerCells, dataCells, fileName, "")
|
||||
if err != nil {
|
||||
c.JSON(200, result.ErrMsg(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.FileAttachment(saveFilePath, fileName)
|
||||
}
|
||||
126
src/modules/monitor/controller/sys_user_online.go
Normal file
126
src/modules/monitor/controller/sys_user_online.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"ems.agt/src/framework/constants/cachekey"
|
||||
"ems.agt/src/framework/redis"
|
||||
"ems.agt/src/framework/vo"
|
||||
"ems.agt/src/framework/vo/result"
|
||||
"ems.agt/src/modules/monitor/model"
|
||||
"ems.agt/src/modules/monitor/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 实例化控制层 SysUserOnlineController 结构体
|
||||
var NewSysUserOnline = &SysUserOnlineController{
|
||||
sysUserOnlineService: service.NewSysUserOnlineImpl,
|
||||
}
|
||||
|
||||
// 在线用户监控
|
||||
//
|
||||
// PATH /monitor/online
|
||||
type SysUserOnlineController struct {
|
||||
// 在线用户服务
|
||||
sysUserOnlineService service.ISysUserOnline
|
||||
}
|
||||
|
||||
// 在线用户列表
|
||||
//
|
||||
// GET /list
|
||||
func (s *SysUserOnlineController) List(c *gin.Context) {
|
||||
ipaddr := c.Query("ipaddr")
|
||||
userName := c.Query("userName")
|
||||
|
||||
// 获取所有在线用户key
|
||||
keys, _ := redis.GetKeys("", cachekey.LOGIN_TOKEN_KEY+"*")
|
||||
|
||||
// 分批获取
|
||||
arr := make([]string, 0)
|
||||
for i := 0; i < len(keys); i += 20 {
|
||||
end := i + 20
|
||||
if end > len(keys) {
|
||||
end = len(keys)
|
||||
}
|
||||
chunk := keys[i:end]
|
||||
values, _ := redis.GetBatch("", chunk)
|
||||
for _, v := range values {
|
||||
arr = append(arr, v.(string))
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历字符串信息解析组合可用对象
|
||||
userOnlines := make([]model.SysUserOnline, 0)
|
||||
for _, str := range arr {
|
||||
if str == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var loginUser vo.LoginUser
|
||||
err := json.Unmarshal([]byte(str), &loginUser)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
onlineUser := s.sysUserOnlineService.LoginUserToUserOnline(loginUser)
|
||||
if onlineUser.TokenID != "" {
|
||||
userOnlines = append(userOnlines, onlineUser)
|
||||
}
|
||||
}
|
||||
|
||||
// 根据查询条件过滤
|
||||
filteredUserOnlines := make([]model.SysUserOnline, 0)
|
||||
if ipaddr != "" && userName != "" {
|
||||
for _, o := range userOnlines {
|
||||
if strings.Contains(o.IPAddr, ipaddr) && strings.Contains(o.UserName, userName) {
|
||||
filteredUserOnlines = append(filteredUserOnlines, o)
|
||||
}
|
||||
}
|
||||
} else if ipaddr != "" {
|
||||
for _, o := range userOnlines {
|
||||
if strings.Contains(o.IPAddr, ipaddr) {
|
||||
filteredUserOnlines = append(filteredUserOnlines, o)
|
||||
}
|
||||
}
|
||||
} else if userName != "" {
|
||||
for _, o := range userOnlines {
|
||||
if strings.Contains(o.UserName, userName) {
|
||||
filteredUserOnlines = append(filteredUserOnlines, o)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filteredUserOnlines = userOnlines
|
||||
}
|
||||
|
||||
// 按登录时间排序
|
||||
sort.Slice(filteredUserOnlines, func(i, j int) bool {
|
||||
return filteredUserOnlines[j].LoginTime > filteredUserOnlines[i].LoginTime
|
||||
})
|
||||
|
||||
c.JSON(200, result.Ok(map[string]any{
|
||||
"total": len(filteredUserOnlines),
|
||||
"rows": filteredUserOnlines,
|
||||
}))
|
||||
}
|
||||
|
||||
// 在线用户强制退出
|
||||
//
|
||||
// DELETE /:tokenId
|
||||
func (s *SysUserOnlineController) ForceLogout(c *gin.Context) {
|
||||
tokenId := c.Param("tokenId")
|
||||
if tokenId == "" || tokenId == "*" {
|
||||
c.JSON(400, result.CodeMsg(400, "参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
// 删除token
|
||||
ok, _ := redis.Del("", cachekey.LOGIN_TOKEN_KEY+tokenId)
|
||||
if ok {
|
||||
c.JSON(200, result.Ok(nil))
|
||||
return
|
||||
}
|
||||
c.JSON(200, result.Err(nil))
|
||||
}
|
||||
36
src/modules/monitor/controller/system_info.go
Normal file
36
src/modules/monitor/controller/system_info.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"ems.agt/src/framework/vo/result"
|
||||
"ems.agt/src/modules/monitor/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 实例化控制层 SystemInfoController 结构体
|
||||
var NewSystemInfo = &SystemInfoController{
|
||||
systemInfogService: service.NewSystemInfoImpl,
|
||||
}
|
||||
|
||||
// 服务器监控信息
|
||||
//
|
||||
// PATH /monitor/system-info
|
||||
type SystemInfoController struct {
|
||||
// 服务器系统相关信息服务
|
||||
systemInfogService service.ISystemInfo
|
||||
}
|
||||
|
||||
// 服务器信息
|
||||
//
|
||||
// GET /
|
||||
func (s *SystemInfoController) Info(c *gin.Context) {
|
||||
c.JSON(200, result.OkData(map[string]any{
|
||||
"project": s.systemInfogService.ProjectInfo(),
|
||||
"cpu": s.systemInfogService.CPUInfo(),
|
||||
"memory": s.systemInfogService.MemoryInfo(),
|
||||
"network": s.systemInfogService.NetworkInfo(),
|
||||
"time": s.systemInfogService.TimeInfo(),
|
||||
"system": s.systemInfogService.SystemInfo(),
|
||||
"disk": s.systemInfogService.DiskInfo(),
|
||||
}))
|
||||
}
|
||||
41
src/modules/monitor/model/sys_cache.go
Normal file
41
src/modules/monitor/model/sys_cache.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package model
|
||||
|
||||
import "strings"
|
||||
|
||||
// SysCache 缓存信息对象
|
||||
type SysCache struct {
|
||||
CacheName string `json:"cacheName"` // 缓存名称
|
||||
CacheKey string `json:"cacheKey"` // 缓存键名
|
||||
CacheValue string `json:"cacheValue"` // 缓存内容
|
||||
Remark string `json:"remark"` // 备注
|
||||
}
|
||||
|
||||
// NewSysCacheNames 创建新的缓存名称列表项实例
|
||||
func NewSysCacheNames(cacheName string, cacheKey string) SysCache {
|
||||
return SysCache{
|
||||
CacheName: cacheKey[:len(cacheKey)-1],
|
||||
CacheKey: "",
|
||||
CacheValue: "",
|
||||
Remark: cacheName,
|
||||
}
|
||||
}
|
||||
|
||||
// NewSysCacheKeys 创建新的缓存键名列表项实例
|
||||
func NewSysCacheKeys(cacheName string, cacheKey string) SysCache {
|
||||
return SysCache{
|
||||
CacheName: cacheName,
|
||||
CacheKey: strings.Replace(cacheKey, cacheName+":", "", 1),
|
||||
CacheValue: "",
|
||||
Remark: "",
|
||||
}
|
||||
}
|
||||
|
||||
// NewSysCacheValue 创建新的缓存键名内容项实例
|
||||
func NewSysCacheValue(cacheName string, cacheKey string, cacheValue string) SysCache {
|
||||
return SysCache{
|
||||
CacheName: cacheName,
|
||||
CacheKey: cacheKey,
|
||||
CacheValue: cacheValue,
|
||||
Remark: "",
|
||||
}
|
||||
}
|
||||
33
src/modules/monitor/model/sys_job.go
Normal file
33
src/modules/monitor/model/sys_job.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package model
|
||||
|
||||
// SysJob 调度任务信息表 sys_job
|
||||
type SysJob struct {
|
||||
// 任务ID
|
||||
JobID string `json:"jobId"`
|
||||
// 任务名称
|
||||
JobName string `json:"jobName" binding:"required"`
|
||||
// 任务组名
|
||||
JobGroup string `json:"jobGroup" binding:"required"`
|
||||
// 调用目标字符串
|
||||
InvokeTarget string `json:"invokeTarget" binding:"required"`
|
||||
// 调用目标传入参数
|
||||
TargetParams string `json:"targetParams"`
|
||||
// cron执行表达式
|
||||
CronExpression string `json:"cronExpression" binding:"required"`
|
||||
// 计划执行错误策略(1立即执行 2执行一次 3放弃执行)
|
||||
MisfirePolicy string `json:"misfirePolicy"`
|
||||
// 是否并发执行(0禁止 1允许)
|
||||
Concurrent string `json:"concurrent"`
|
||||
// 任务状态(0暂停 1正常)
|
||||
Status string `json:"status"`
|
||||
// 创建者
|
||||
CreateBy string `json:"createBy"`
|
||||
// 创建时间
|
||||
CreateTime int64 `json:"createTime"`
|
||||
// 更新者
|
||||
UpdateBy string `json:"updateBy"`
|
||||
// 更新时间
|
||||
UpdateTime int64 `json:"updateTime"`
|
||||
// 备注
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
23
src/modules/monitor/model/sys_job_log.go
Normal file
23
src/modules/monitor/model/sys_job_log.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package model
|
||||
|
||||
// SysJobLog 定时任务调度日志表 sys_job_log
|
||||
type SysJobLog struct {
|
||||
// 日志序号
|
||||
JobLogID string `json:"jobLogId"`
|
||||
// 任务名称
|
||||
JobName string `json:"jobName"`
|
||||
// 任务组名
|
||||
JobGroup string `json:"jobGroup"`
|
||||
// 调用目标字符串
|
||||
InvokeTarget string `json:"invokeTarget"`
|
||||
// 调用目标传入参数
|
||||
TargetParams string `json:"targetParams"`
|
||||
// 日志信息
|
||||
JobMsg string `json:"jobMsg"`
|
||||
// 执行状态(0失败 1正常)
|
||||
Status string `json:"status"`
|
||||
// 创建时间
|
||||
CreateTime int64 `json:"createTime"`
|
||||
// 消耗时间(毫秒)
|
||||
CostTime int64 `json:"costTime"`
|
||||
}
|
||||
21
src/modules/monitor/model/sys_user_online.go
Normal file
21
src/modules/monitor/model/sys_user_online.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package model
|
||||
|
||||
// SysUserOnline 当前在线会话对象
|
||||
type SysUserOnline struct {
|
||||
// 会话编号
|
||||
TokenID string `json:"tokenId"`
|
||||
// 部门名称
|
||||
DeptName string `json:"deptName"`
|
||||
// 用户名称
|
||||
UserName string `json:"userName"`
|
||||
// 登录IP地址
|
||||
IPAddr string `json:"ipaddr"`
|
||||
// 登录地址
|
||||
LoginLocation string `json:"loginLocation"`
|
||||
// 浏览器类型
|
||||
Browser string `json:"browser"`
|
||||
// 操作系统
|
||||
OS string `json:"os"`
|
||||
// 登录时间
|
||||
LoginTime int64 `json:"loginTime"`
|
||||
}
|
||||
160
src/modules/monitor/monitor.go
Normal file
160
src/modules/monitor/monitor.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"ems.agt/src/framework/logger"
|
||||
"ems.agt/src/framework/middleware"
|
||||
"ems.agt/src/framework/middleware/collectlogs"
|
||||
"ems.agt/src/framework/middleware/repeat"
|
||||
"ems.agt/src/modules/monitor/controller"
|
||||
"ems.agt/src/modules/monitor/processor"
|
||||
"ems.agt/src/modules/monitor/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Setup 模块路由注册
|
||||
func Setup(router *gin.Engine) {
|
||||
logger.Infof("开始加载 ====> monitor 模块路由")
|
||||
|
||||
// 启动时需要的初始参数
|
||||
InitLoad()
|
||||
|
||||
// 服务器服务信息
|
||||
router.GET("/monitor/system-info",
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:system:info"}}),
|
||||
controller.NewSystemInfo.Info,
|
||||
)
|
||||
|
||||
// 缓存服务信息
|
||||
sysCacheGroup := router.Group("/monitor/cache")
|
||||
{
|
||||
sysCacheGroup.GET("",
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:cache:info"}}),
|
||||
controller.NewSysCache.Info,
|
||||
)
|
||||
sysCacheGroup.GET("/getNames",
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:cache:list"}}),
|
||||
controller.NewSysCache.Names,
|
||||
)
|
||||
sysCacheGroup.GET("/getKeys/:cacheName",
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:cache:list"}}),
|
||||
controller.NewSysCache.Keys,
|
||||
)
|
||||
sysCacheGroup.GET("/getValue/:cacheName/:cacheKey",
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:cache:query"}}),
|
||||
controller.NewSysCache.Value,
|
||||
)
|
||||
sysCacheGroup.DELETE("/clearCacheName/:cacheName",
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:cache:remove"}}),
|
||||
controller.NewSysCache.ClearCacheName,
|
||||
)
|
||||
sysCacheGroup.DELETE("/clearCacheKey/:cacheName/:cacheKey",
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:cache:remove"}}),
|
||||
controller.NewSysCache.ClearCacheKey,
|
||||
)
|
||||
sysCacheGroup.DELETE("/clearCacheSafe",
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:cache:remove"}}),
|
||||
controller.NewSysCache.ClearCacheSafe,
|
||||
)
|
||||
}
|
||||
|
||||
// 调度任务日志信息
|
||||
sysJobLogGroup := router.Group("/monitor/jobLog")
|
||||
{
|
||||
sysJobLogGroup.GET("/list",
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:job:list"}}),
|
||||
controller.NewSysJobLog.List,
|
||||
)
|
||||
sysJobLogGroup.GET("/:jobLogId",
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:job:query"}}),
|
||||
controller.NewSysJobLog.Info,
|
||||
)
|
||||
sysJobLogGroup.DELETE("/:jobLogIds",
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:job:remove"}}),
|
||||
collectlogs.OperateLog(collectlogs.OptionNew("调度任务日志信息", collectlogs.BUSINESS_TYPE_DELETE)),
|
||||
controller.NewSysJobLog.Remove,
|
||||
)
|
||||
sysJobLogGroup.DELETE("/clean",
|
||||
repeat.RepeatSubmit(5),
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:job:remove"}}),
|
||||
collectlogs.OperateLog(collectlogs.OptionNew("调度任务日志信息", collectlogs.BUSINESS_TYPE_CLEAN)),
|
||||
controller.NewSysJobLog.Clean,
|
||||
)
|
||||
sysJobLogGroup.POST("/export",
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:job:export"}}),
|
||||
collectlogs.OperateLog(collectlogs.OptionNew("调度任务日志信息", collectlogs.BUSINESS_TYPE_EXPORT)),
|
||||
controller.NewSysJobLog.Export,
|
||||
)
|
||||
}
|
||||
|
||||
// 调度任务信息
|
||||
sysJobGroup := router.Group("/monitor/job")
|
||||
{
|
||||
sysJobGroup.GET("/list",
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:job:list"}}),
|
||||
controller.NewSysJob.List,
|
||||
)
|
||||
sysJobGroup.GET("/:jobId",
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:job:query"}}),
|
||||
controller.NewSysJob.Info,
|
||||
)
|
||||
sysJobGroup.POST("",
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:job:add"}}),
|
||||
collectlogs.OperateLog(collectlogs.OptionNew("调度任务信息", collectlogs.BUSINESS_TYPE_INSERT)),
|
||||
controller.NewSysJob.Add,
|
||||
)
|
||||
sysJobGroup.PUT("",
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:job:edit"}}),
|
||||
collectlogs.OperateLog(collectlogs.OptionNew("调度任务信息", collectlogs.BUSINESS_TYPE_UPDATE)),
|
||||
controller.NewSysJob.Edit,
|
||||
)
|
||||
sysJobGroup.DELETE("/:jobIds",
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:job:remove"}}),
|
||||
collectlogs.OperateLog(collectlogs.OptionNew("调度任务信息", collectlogs.BUSINESS_TYPE_DELETE)),
|
||||
controller.NewSysJob.Remove,
|
||||
)
|
||||
sysJobGroup.PUT("/changeStatus",
|
||||
repeat.RepeatSubmit(5),
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:job:changeStatus"}}),
|
||||
collectlogs.OperateLog(collectlogs.OptionNew("调度任务信息", collectlogs.BUSINESS_TYPE_UPDATE)),
|
||||
controller.NewSysJob.Status,
|
||||
)
|
||||
sysJobGroup.PUT("/run/:jobId",
|
||||
repeat.RepeatSubmit(10),
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:job:changeStatus"}}),
|
||||
collectlogs.OperateLog(collectlogs.OptionNew("调度任务信息", collectlogs.BUSINESS_TYPE_UPDATE)),
|
||||
controller.NewSysJob.Run,
|
||||
)
|
||||
sysJobGroup.PUT("/resetQueueJob",
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:job:changeStatus"}}),
|
||||
collectlogs.OperateLog(collectlogs.OptionNew("调度任务信息", collectlogs.BUSINESS_TYPE_CLEAN)),
|
||||
controller.NewSysJob.ResetQueueJob,
|
||||
)
|
||||
sysJobGroup.POST("/export",
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:job:export"}}),
|
||||
collectlogs.OperateLog(collectlogs.OptionNew("调度任务信息", collectlogs.BUSINESS_TYPE_EXPORT)),
|
||||
controller.NewSysJob.Export,
|
||||
)
|
||||
}
|
||||
|
||||
// 在线用户监控
|
||||
sysUserOnlineGroup := router.Group("/monitor/online")
|
||||
{
|
||||
sysUserOnlineGroup.GET("/list",
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:online:list"}}),
|
||||
controller.NewSysUserOnline.List,
|
||||
)
|
||||
sysUserOnlineGroup.DELETE("/:tokenId",
|
||||
middleware.PreAuthorize(map[string][]string{"hasPerms": {"monitor:online:forceLogout"}}),
|
||||
controller.NewSysUserOnline.ForceLogout,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// InitLoad 初始参数
|
||||
func InitLoad() {
|
||||
// 初始化定时任务处理
|
||||
processor.InitCronQueue()
|
||||
// 启动时,初始化调度任务
|
||||
service.NewSysJobImpl.ResetQueueJob()
|
||||
}
|
||||
60
src/modules/monitor/processor/bar/bar.go
Normal file
60
src/modules/monitor/processor/bar/bar.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package bar
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/cron"
|
||||
"ems.agt/src/framework/logger"
|
||||
)
|
||||
|
||||
var NewProcessor = &BarProcessor{
|
||||
progress: 0,
|
||||
count: 0,
|
||||
}
|
||||
|
||||
// bar 队列任务处理
|
||||
type BarProcessor struct {
|
||||
// 任务进度
|
||||
progress int
|
||||
// 执行次数
|
||||
count int
|
||||
}
|
||||
|
||||
func (s *BarProcessor) Execute(data any) any {
|
||||
logger.Infof("执行 %d 次,上次进度: %d ", s.count, s.progress)
|
||||
s.count++
|
||||
|
||||
options := data.(cron.JobData)
|
||||
sysJob := options.SysJob
|
||||
logger.Infof("重复 %v 任务ID %s", options.Repeat, sysJob.JobID)
|
||||
|
||||
// 实现任务处理逻辑
|
||||
i := 0
|
||||
s.progress = i
|
||||
for i < 5 {
|
||||
// 获取任务进度
|
||||
progress := s.progress
|
||||
logger.Infof("jonId: %s => 任务进度:%d", sysJob.JobID, progress)
|
||||
// 延迟响应
|
||||
time.Sleep(time.Second * 2)
|
||||
// 程序中途执行错误
|
||||
if i == 3 {
|
||||
// arr := [1]int{1}
|
||||
// arr[i] = 3
|
||||
// fmt.Println(arr)
|
||||
// return "i = 3"
|
||||
panic("程序中途执行错误")
|
||||
}
|
||||
i++
|
||||
// 改变任务进度
|
||||
s.progress = i
|
||||
}
|
||||
|
||||
// 返回结果,用于记录执行结果
|
||||
return map[string]any{
|
||||
"repeat": options.Repeat,
|
||||
"jobName": sysJob.JobName,
|
||||
"invokeTarget": sysJob.InvokeTarget,
|
||||
"targetParams": sysJob.TargetParams,
|
||||
}
|
||||
}
|
||||
52
src/modules/monitor/processor/foo/foo.go
Normal file
52
src/modules/monitor/processor/foo/foo.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package foo
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/cron"
|
||||
"ems.agt/src/framework/logger"
|
||||
)
|
||||
|
||||
var NewProcessor = &FooProcessor{
|
||||
progress: 0,
|
||||
count: 0,
|
||||
}
|
||||
|
||||
// foo 队列任务处理
|
||||
type FooProcessor struct {
|
||||
// 任务进度
|
||||
progress int
|
||||
// 执行次数
|
||||
count int
|
||||
}
|
||||
|
||||
func (s *FooProcessor) Execute(data any) any {
|
||||
logger.Infof("执行 %d 次,上次进度: %d ", s.count, s.progress)
|
||||
s.count++
|
||||
|
||||
options := data.(cron.JobData)
|
||||
sysJob := options.SysJob
|
||||
logger.Infof("重复 %v 任务ID %s", options.Repeat, sysJob.JobID)
|
||||
|
||||
// 实现任务处理逻辑
|
||||
i := 0
|
||||
s.progress = i
|
||||
for i < 20 {
|
||||
// 获取任务进度
|
||||
progress := s.progress
|
||||
logger.Infof("jonId: %s => 任务进度:%d", sysJob.JobID, progress)
|
||||
// 延迟响应
|
||||
time.Sleep(time.Second * 2)
|
||||
i++
|
||||
// 改变任务进度
|
||||
s.progress = i
|
||||
}
|
||||
|
||||
// 返回结果,用于记录执行结果
|
||||
return map[string]any{
|
||||
"repeat": options.Repeat,
|
||||
"jobName": sysJob.JobName,
|
||||
"invokeTarget": sysJob.InvokeTarget,
|
||||
"targetParams": sysJob.TargetParams,
|
||||
}
|
||||
}
|
||||
15
src/modules/monitor/processor/processor.go
Normal file
15
src/modules/monitor/processor/processor.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"ems.agt/src/framework/cron"
|
||||
"ems.agt/src/modules/monitor/processor/bar"
|
||||
"ems.agt/src/modules/monitor/processor/foo"
|
||||
"ems.agt/src/modules/monitor/processor/simple"
|
||||
)
|
||||
|
||||
// InitCronQueue 初始定时任务队列
|
||||
func InitCronQueue() {
|
||||
cron.CreateQueue("simple", simple.NewProcessor)
|
||||
cron.CreateQueue("foo", foo.NewProcessor)
|
||||
cron.CreateQueue("bar", bar.NewProcessor)
|
||||
}
|
||||
26
src/modules/monitor/processor/simple/simple.go
Normal file
26
src/modules/monitor/processor/simple/simple.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package simple
|
||||
|
||||
import (
|
||||
"ems.agt/src/framework/cron"
|
||||
"ems.agt/src/framework/logger"
|
||||
)
|
||||
|
||||
var NewProcessor = &simpleProcessor{}
|
||||
|
||||
// simple 队列任务处理
|
||||
type simpleProcessor struct{}
|
||||
|
||||
func (s *simpleProcessor) Execute(data any) any {
|
||||
options := data.(cron.JobData)
|
||||
|
||||
sysJob := options.SysJob
|
||||
logger.Infof("重复 %v 任务ID %s", options.Repeat, sysJob.JobID)
|
||||
|
||||
// 返回结果,用于记录执行结果
|
||||
return map[string]any{
|
||||
"repeat": options.Repeat,
|
||||
"jobName": sysJob.JobName,
|
||||
"invokeTarget": sysJob.InvokeTarget,
|
||||
"targetParams": sysJob.TargetParams,
|
||||
}
|
||||
}
|
||||
29
src/modules/monitor/repository/sys_job.go
Normal file
29
src/modules/monitor/repository/sys_job.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"ems.agt/src/modules/monitor/model"
|
||||
)
|
||||
|
||||
// ISysJob 调度任务表 数据层接口
|
||||
type ISysJob interface {
|
||||
// SelectJobPage 分页查询调度任务集合
|
||||
SelectJobPage(query map[string]any) map[string]any
|
||||
|
||||
// SelectJobList 查询调度任务集合
|
||||
SelectJobList(sysJob model.SysJob) []model.SysJob
|
||||
|
||||
// SelectJobByIds 通过调度ID查询调度任务信息
|
||||
SelectJobByIds(jobIds []string) []model.SysJob
|
||||
|
||||
// CheckUniqueJob 校验调度任务是否唯一
|
||||
CheckUniqueJob(sysJob model.SysJob) string
|
||||
|
||||
// InsertJob 新增调度任务信息
|
||||
InsertJob(sysJob model.SysJob) string
|
||||
|
||||
// UpdateJob 修改调度任务信息
|
||||
UpdateJob(sysJob model.SysJob) int64
|
||||
|
||||
// DeleteJobByIds 批量删除调度任务信息
|
||||
DeleteJobByIds(jobIds []string) int64
|
||||
}
|
||||
344
src/modules/monitor/repository/sys_job.impl.go
Normal file
344
src/modules/monitor/repository/sys_job.impl.go
Normal file
@@ -0,0 +1,344 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/datasource"
|
||||
"ems.agt/src/framework/logger"
|
||||
"ems.agt/src/framework/utils/parse"
|
||||
"ems.agt/src/framework/utils/repo"
|
||||
"ems.agt/src/modules/monitor/model"
|
||||
)
|
||||
|
||||
// 实例化数据层 SysJobImpl 结构体
|
||||
var NewSysJobImpl = &SysJobImpl{
|
||||
selectSql: `select job_id, job_name, job_group, invoke_target, target_params, cron_expression,
|
||||
misfire_policy, concurrent, status, create_by, create_time, remark from sys_job`,
|
||||
|
||||
resultMap: map[string]string{
|
||||
"job_id": "JobID",
|
||||
"job_name": "JobName",
|
||||
"job_group": "JobGroup",
|
||||
"invoke_target": "InvokeTarget",
|
||||
"target_params": "TargetParams",
|
||||
"cron_expression": "CronExpression",
|
||||
"misfire_policy": "MisfirePolicy",
|
||||
"concurrent": "Concurrent",
|
||||
"status": "Status",
|
||||
"create_by": "CreateBy",
|
||||
"create_time": "CreateTime",
|
||||
"update_by": "UpdateBy",
|
||||
"update_time": "UpdateTime",
|
||||
"remark": "Remark",
|
||||
},
|
||||
}
|
||||
|
||||
// SysJobImpl 调度任务表 数据层处理
|
||||
type SysJobImpl struct {
|
||||
// 查询视图对象SQL
|
||||
selectSql string
|
||||
// 结果字段与实体映射
|
||||
resultMap map[string]string
|
||||
}
|
||||
|
||||
// convertResultRows 将结果记录转实体结果组
|
||||
func (r *SysJobImpl) convertResultRows(rows []map[string]any) []model.SysJob {
|
||||
arr := make([]model.SysJob, 0)
|
||||
for _, row := range rows {
|
||||
sysJob := model.SysJob{}
|
||||
for key, value := range row {
|
||||
if keyMapper, ok := r.resultMap[key]; ok {
|
||||
repo.SetFieldValue(&sysJob, keyMapper, value)
|
||||
}
|
||||
}
|
||||
arr = append(arr, sysJob)
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// SelectJobPage 分页查询调度任务集合
|
||||
func (r *SysJobImpl) SelectJobPage(query map[string]any) map[string]any {
|
||||
// 查询条件拼接
|
||||
var conditions []string
|
||||
var params []any
|
||||
if v, ok := query["jobName"]; ok && v != "" {
|
||||
conditions = append(conditions, "job_name like concat(?, '%')")
|
||||
params = append(params, v)
|
||||
}
|
||||
if v, ok := query["jobGroup"]; ok && v != "" {
|
||||
conditions = append(conditions, "job_group = ?")
|
||||
params = append(params, v)
|
||||
}
|
||||
if v, ok := query["invokeTarget"]; ok && v != "" {
|
||||
conditions = append(conditions, "invoke_target like concat(?, '%')")
|
||||
params = append(params, v)
|
||||
}
|
||||
if v, ok := query["status"]; ok && v != "" {
|
||||
conditions = append(conditions, "status = ?")
|
||||
params = append(params, v)
|
||||
}
|
||||
|
||||
// 构建查询条件语句
|
||||
whereSql := ""
|
||||
if len(conditions) > 0 {
|
||||
whereSql += " where " + strings.Join(conditions, " and ")
|
||||
}
|
||||
|
||||
// 查询结果
|
||||
result := map[string]any{
|
||||
"total": 0,
|
||||
"rows": []model.SysJob{},
|
||||
}
|
||||
|
||||
// 查询数量 长度为0直接返回
|
||||
totalSql := "select count(1) as 'total' from sys_job"
|
||||
totalRows, err := datasource.RawDB("", totalSql+whereSql, params)
|
||||
if err != nil {
|
||||
logger.Errorf("total err => %v", err)
|
||||
return result
|
||||
}
|
||||
total := parse.Number(totalRows[0]["total"])
|
||||
if total == 0 {
|
||||
return result
|
||||
} else {
|
||||
result["total"] = total
|
||||
}
|
||||
|
||||
// 分页
|
||||
pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"])
|
||||
pageSql := " limit ?,? "
|
||||
params = append(params, pageNum*pageSize)
|
||||
params = append(params, pageSize)
|
||||
|
||||
// 查询数据
|
||||
querySql := r.selectSql + whereSql + pageSql
|
||||
results, err := datasource.RawDB("", querySql, params)
|
||||
if err != nil {
|
||||
logger.Errorf("query err => %v", err)
|
||||
return result
|
||||
}
|
||||
|
||||
// 转换实体
|
||||
result["rows"] = r.convertResultRows(results)
|
||||
return result
|
||||
}
|
||||
|
||||
// SelectJobList 查询调度任务集合
|
||||
func (r *SysJobImpl) SelectJobList(sysJob model.SysJob) []model.SysJob {
|
||||
// 查询条件拼接
|
||||
var conditions []string
|
||||
var params []any
|
||||
if sysJob.JobName != "" {
|
||||
conditions = append(conditions, "job_name like concat(?, '%')")
|
||||
params = append(params, sysJob.JobName)
|
||||
}
|
||||
if sysJob.JobGroup != "" {
|
||||
conditions = append(conditions, "job_group = ?")
|
||||
params = append(params, sysJob.JobGroup)
|
||||
}
|
||||
if sysJob.InvokeTarget != "" {
|
||||
conditions = append(conditions, "invoke_target like concat(?, '%')")
|
||||
params = append(params, sysJob.InvokeTarget)
|
||||
}
|
||||
if sysJob.Status != "" {
|
||||
conditions = append(conditions, "status = ?")
|
||||
params = append(params, sysJob.Status)
|
||||
}
|
||||
|
||||
// 构建查询条件语句
|
||||
whereSql := ""
|
||||
if len(conditions) > 0 {
|
||||
whereSql += " where " + strings.Join(conditions, " and ")
|
||||
}
|
||||
|
||||
// 查询数据
|
||||
querySql := r.selectSql + whereSql
|
||||
results, err := datasource.RawDB("", querySql, params)
|
||||
if err != nil {
|
||||
logger.Errorf("query err => %v", err)
|
||||
return []model.SysJob{}
|
||||
}
|
||||
|
||||
// 转换实体
|
||||
return r.convertResultRows(results)
|
||||
}
|
||||
|
||||
// SelectJobByIds 通过调度ID查询调度任务信息
|
||||
func (r *SysJobImpl) SelectJobByIds(jobIds []string) []model.SysJob {
|
||||
placeholder := repo.KeyPlaceholderByQuery(len(jobIds))
|
||||
querySql := r.selectSql + " where job_id in (" + placeholder + ")"
|
||||
parameters := repo.ConvertIdsSlice(jobIds)
|
||||
results, err := datasource.RawDB("", querySql, parameters)
|
||||
if err != nil {
|
||||
logger.Errorf("query err => %v", err)
|
||||
return []model.SysJob{}
|
||||
}
|
||||
// 转换实体
|
||||
return r.convertResultRows(results)
|
||||
}
|
||||
|
||||
// CheckUniqueJob 校验调度任务是否唯一
|
||||
func (r *SysJobImpl) CheckUniqueJob(sysJob model.SysJob) string {
|
||||
// 查询条件拼接
|
||||
var conditions []string
|
||||
var params []any
|
||||
if sysJob.JobName != "" {
|
||||
conditions = append(conditions, "job_name = ?")
|
||||
params = append(params, sysJob.JobName)
|
||||
}
|
||||
if sysJob.JobGroup != "" {
|
||||
conditions = append(conditions, "job_group = ?")
|
||||
params = append(params, sysJob.JobGroup)
|
||||
}
|
||||
|
||||
// 构建查询条件语句
|
||||
whereSql := ""
|
||||
if len(conditions) > 0 {
|
||||
whereSql += " where " + strings.Join(conditions, " and ")
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 查询数据
|
||||
querySql := "select job_id as 'str' from sys_job " + whereSql + " limit 1"
|
||||
results, err := datasource.RawDB("", querySql, params)
|
||||
if err != nil {
|
||||
logger.Errorf("query err %v", err)
|
||||
return ""
|
||||
}
|
||||
if len(results) > 0 {
|
||||
return fmt.Sprintf("%v", results[0]["str"])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// InsertJob 新增调度任务信息
|
||||
func (r *SysJobImpl) InsertJob(sysJob model.SysJob) string {
|
||||
// 参数拼接
|
||||
params := make(map[string]any)
|
||||
if sysJob.JobID != "" {
|
||||
params["job_id"] = sysJob.JobID
|
||||
}
|
||||
if sysJob.JobName != "" {
|
||||
params["job_name"] = sysJob.JobName
|
||||
}
|
||||
if sysJob.JobGroup != "" {
|
||||
params["job_group"] = sysJob.JobGroup
|
||||
}
|
||||
if sysJob.InvokeTarget != "" {
|
||||
params["invoke_target"] = sysJob.InvokeTarget
|
||||
}
|
||||
if sysJob.TargetParams != "" {
|
||||
params["target_params"] = sysJob.TargetParams
|
||||
}
|
||||
if sysJob.CronExpression != "" {
|
||||
params["cron_expression"] = sysJob.CronExpression
|
||||
}
|
||||
if sysJob.MisfirePolicy != "" {
|
||||
params["misfire_policy"] = sysJob.MisfirePolicy
|
||||
}
|
||||
if sysJob.Concurrent != "" {
|
||||
params["concurrent"] = sysJob.Concurrent
|
||||
}
|
||||
if sysJob.Status != "" {
|
||||
params["status"] = sysJob.Status
|
||||
}
|
||||
if sysJob.Remark != "" {
|
||||
params["remark"] = sysJob.Remark
|
||||
}
|
||||
if sysJob.CreateBy != "" {
|
||||
params["create_by"] = sysJob.CreateBy
|
||||
params["create_time"] = time.Now().UnixMilli()
|
||||
}
|
||||
|
||||
// 构建执行语句
|
||||
keys, placeholder, values := repo.KeyPlaceholderValueByInsert(params)
|
||||
sql := "insert into sys_job (" + strings.Join(keys, ",") + ")values(" + placeholder + ")"
|
||||
|
||||
db := datasource.DefaultDB()
|
||||
// 开启事务
|
||||
tx := db.Begin()
|
||||
// 执行插入
|
||||
err := tx.Exec(sql, values...).Error
|
||||
if err != nil {
|
||||
logger.Errorf("insert row : %v", err.Error())
|
||||
tx.Rollback()
|
||||
return ""
|
||||
}
|
||||
// 获取生成的自增 ID
|
||||
var insertedID string
|
||||
err = tx.Raw("select last_insert_id()").Row().Scan(&insertedID)
|
||||
if err != nil {
|
||||
logger.Errorf("insert last id : %v", err.Error())
|
||||
tx.Rollback()
|
||||
return ""
|
||||
}
|
||||
// 提交事务
|
||||
tx.Commit()
|
||||
return insertedID
|
||||
}
|
||||
|
||||
// UpdateJob 修改调度任务信息
|
||||
func (r *SysJobImpl) UpdateJob(sysJob model.SysJob) int64 {
|
||||
// 参数拼接
|
||||
params := make(map[string]any)
|
||||
if sysJob.JobName != "" {
|
||||
params["job_name"] = sysJob.JobName
|
||||
}
|
||||
if sysJob.JobGroup != "" {
|
||||
params["job_group"] = sysJob.JobGroup
|
||||
}
|
||||
if sysJob.InvokeTarget != "" {
|
||||
params["invoke_target"] = sysJob.InvokeTarget
|
||||
}
|
||||
if sysJob.TargetParams != "" {
|
||||
params["target_params"] = sysJob.TargetParams
|
||||
}
|
||||
if sysJob.CronExpression != "" {
|
||||
params["cron_expression"] = sysJob.CronExpression
|
||||
}
|
||||
if sysJob.MisfirePolicy != "" {
|
||||
params["misfire_policy"] = sysJob.MisfirePolicy
|
||||
}
|
||||
if sysJob.Concurrent != "" {
|
||||
params["concurrent"] = sysJob.Concurrent
|
||||
}
|
||||
if sysJob.Status != "" {
|
||||
params["status"] = sysJob.Status
|
||||
}
|
||||
if sysJob.Remark != "" {
|
||||
params["remark"] = sysJob.Remark
|
||||
}
|
||||
if sysJob.UpdateBy != "" {
|
||||
params["update_by"] = sysJob.UpdateBy
|
||||
params["update_time"] = time.Now().UnixMilli()
|
||||
}
|
||||
|
||||
// 构建执行语句
|
||||
keys, values := repo.KeyValueByUpdate(params)
|
||||
sql := "update sys_job set " + strings.Join(keys, ",") + " where job_id = ?"
|
||||
|
||||
// 执行更新
|
||||
values = append(values, sysJob.JobID)
|
||||
rows, err := datasource.ExecDB("", sql, values)
|
||||
if err != nil {
|
||||
logger.Errorf("update row : %v", err.Error())
|
||||
return 0
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// DeleteJobByIds 批量删除调度任务信息
|
||||
func (r *SysJobImpl) DeleteJobByIds(jobIds []string) int64 {
|
||||
placeholder := repo.KeyPlaceholderByQuery(len(jobIds))
|
||||
sql := "delete from sys_job where job_id in (" + placeholder + ")"
|
||||
parameters := repo.ConvertIdsSlice(jobIds)
|
||||
results, err := datasource.ExecDB("", sql, parameters)
|
||||
if err != nil {
|
||||
logger.Errorf("delete err => %v", err)
|
||||
return 0
|
||||
}
|
||||
return results
|
||||
}
|
||||
26
src/modules/monitor/repository/sys_job_log.go
Normal file
26
src/modules/monitor/repository/sys_job_log.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"ems.agt/src/modules/monitor/model"
|
||||
)
|
||||
|
||||
// 调度任务日志表 数据层接口
|
||||
type ISysJobLog interface {
|
||||
// 分页查询调度任务日志集合
|
||||
SelectJobLogPage(query map[string]any) map[string]any
|
||||
|
||||
// 查询调度任务日志集合
|
||||
SelectJobLogList(sysJobLog model.SysJobLog) []model.SysJobLog
|
||||
|
||||
// 通过调度ID查询调度任务日志信息
|
||||
SelectJobLogById(jobLogId string) model.SysJobLog
|
||||
|
||||
// 新增调度任务日志信息
|
||||
InsertJobLog(sysJobLog model.SysJobLog) string
|
||||
|
||||
// 批量删除调度任务日志信息
|
||||
DeleteJobLogByIds(jobLogId []string) int64
|
||||
|
||||
// 清空调度任务日志
|
||||
CleanJobLog() error
|
||||
}
|
||||
272
src/modules/monitor/repository/sys_job_log.impl.go
Normal file
272
src/modules/monitor/repository/sys_job_log.impl.go
Normal file
@@ -0,0 +1,272 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/datasource"
|
||||
"ems.agt/src/framework/logger"
|
||||
"ems.agt/src/framework/utils/date"
|
||||
"ems.agt/src/framework/utils/parse"
|
||||
"ems.agt/src/framework/utils/repo"
|
||||
"ems.agt/src/modules/monitor/model"
|
||||
)
|
||||
|
||||
// 实例化数据层 SysJobLogImpl 结构体
|
||||
var NewSysJobLogImpl = &SysJobLogImpl{
|
||||
selectSql: `select job_log_id, job_name, job_group, invoke_target,
|
||||
target_params, job_msg, status, create_time, cost_time from sys_job_log`,
|
||||
|
||||
resultMap: map[string]string{
|
||||
"job_log_id": "JobLogID",
|
||||
"job_name": "JobName",
|
||||
"job_group": "JobGroup",
|
||||
"invoke_target": "InvokeTarget",
|
||||
"target_params": "TargetParams",
|
||||
"job_msg": "JobMsg",
|
||||
"status": "Status",
|
||||
"create_time": "CreateTime",
|
||||
"cost_time": "CostTime",
|
||||
},
|
||||
}
|
||||
|
||||
// SysJobLogImpl 调度任务日志表 数据层处理
|
||||
type SysJobLogImpl struct {
|
||||
// 查询视图对象SQL
|
||||
selectSql string
|
||||
// 结果字段与实体映射
|
||||
resultMap map[string]string
|
||||
}
|
||||
|
||||
// convertResultRows 将结果记录转实体结果组
|
||||
func (r *SysJobLogImpl) convertResultRows(rows []map[string]any) []model.SysJobLog {
|
||||
arr := make([]model.SysJobLog, 0)
|
||||
for _, row := range rows {
|
||||
sysJobLog := model.SysJobLog{}
|
||||
for key, value := range row {
|
||||
if keyMapper, ok := r.resultMap[key]; ok {
|
||||
repo.SetFieldValue(&sysJobLog, keyMapper, value)
|
||||
}
|
||||
}
|
||||
arr = append(arr, sysJobLog)
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// 分页查询调度任务日志集合
|
||||
func (r *SysJobLogImpl) SelectJobLogPage(query map[string]any) map[string]any {
|
||||
// 查询条件拼接
|
||||
var conditions []string
|
||||
var params []any
|
||||
if v, ok := query["jobName"]; ok && v != "" {
|
||||
conditions = append(conditions, "job_name like concat(?, '%')")
|
||||
params = append(params, v)
|
||||
}
|
||||
if v, ok := query["jobGroup"]; ok && v != "" {
|
||||
conditions = append(conditions, "job_group = ?")
|
||||
params = append(params, v)
|
||||
}
|
||||
if v, ok := query["status"]; ok && v != "" {
|
||||
conditions = append(conditions, "status = ?")
|
||||
params = append(params, v)
|
||||
}
|
||||
if v, ok := query["invokeTarget"]; ok && v != "" {
|
||||
conditions = append(conditions, "invoke_target like concat(?, '%')")
|
||||
params = append(params, v)
|
||||
}
|
||||
beginTime, ok := query["beginTime"]
|
||||
if !ok {
|
||||
beginTime, ok = query["params[beginTime]"]
|
||||
}
|
||||
if ok && beginTime != "" {
|
||||
conditions = append(conditions, "create_time >= ?")
|
||||
beginDate := date.ParseStrToDate(beginTime.(string), date.YYYY_MM_DD)
|
||||
params = append(params, beginDate.UnixMilli())
|
||||
}
|
||||
endTime, ok := query["endTime"]
|
||||
if !ok {
|
||||
endTime, ok = query["params[endTime]"]
|
||||
}
|
||||
if ok && endTime != "" {
|
||||
conditions = append(conditions, "create_time <= ?")
|
||||
endDate := date.ParseStrToDate(endTime.(string), date.YYYY_MM_DD)
|
||||
params = append(params, endDate.UnixMilli())
|
||||
}
|
||||
|
||||
// 构建查询条件语句
|
||||
whereSql := ""
|
||||
if len(conditions) > 0 {
|
||||
whereSql += " where " + strings.Join(conditions, " and ")
|
||||
}
|
||||
|
||||
// 查询结果
|
||||
result := map[string]any{
|
||||
"total": 0,
|
||||
"rows": []model.SysJobLog{},
|
||||
}
|
||||
|
||||
// 查询数量 长度为0直接返回
|
||||
totalSql := "select count(1) as 'total' from sys_job_log"
|
||||
totalRows, err := datasource.RawDB("", totalSql+whereSql, params)
|
||||
if err != nil {
|
||||
logger.Errorf("total err => %v", err)
|
||||
return result
|
||||
}
|
||||
total := parse.Number(totalRows[0]["total"])
|
||||
if total == 0 {
|
||||
return result
|
||||
} else {
|
||||
result["total"] = total
|
||||
}
|
||||
|
||||
// 分页
|
||||
pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"])
|
||||
pageSql := " order by job_log_id desc limit ?,? "
|
||||
params = append(params, pageNum*pageSize)
|
||||
params = append(params, pageSize)
|
||||
|
||||
// 查询数据
|
||||
querySql := r.selectSql + whereSql + pageSql
|
||||
results, err := datasource.RawDB("", querySql, params)
|
||||
if err != nil {
|
||||
logger.Errorf("query err => %v", err)
|
||||
return result
|
||||
}
|
||||
|
||||
// 转换实体
|
||||
result["rows"] = r.convertResultRows(results)
|
||||
return result
|
||||
}
|
||||
|
||||
// 查询调度任务日志集合
|
||||
func (r *SysJobLogImpl) SelectJobLogList(sysJobLog model.SysJobLog) []model.SysJobLog {
|
||||
// 查询条件拼接
|
||||
var conditions []string
|
||||
var params []any
|
||||
if sysJobLog.JobName != "" {
|
||||
conditions = append(conditions, "job_name like concat(?, '%')")
|
||||
params = append(params, sysJobLog.JobName)
|
||||
}
|
||||
if sysJobLog.JobGroup != "" {
|
||||
conditions = append(conditions, "job_group = ?")
|
||||
params = append(params, sysJobLog.JobGroup)
|
||||
}
|
||||
if sysJobLog.Status != "" {
|
||||
conditions = append(conditions, "status = ?")
|
||||
params = append(params, sysJobLog.Status)
|
||||
}
|
||||
if sysJobLog.InvokeTarget != "" {
|
||||
conditions = append(conditions, "invoke_target like concat(?, '%')")
|
||||
params = append(params, sysJobLog.InvokeTarget)
|
||||
}
|
||||
|
||||
// 构建查询条件语句
|
||||
whereSql := ""
|
||||
if len(conditions) > 0 {
|
||||
whereSql += " where " + strings.Join(conditions, " and ")
|
||||
}
|
||||
|
||||
// 查询数据
|
||||
querySql := r.selectSql + whereSql
|
||||
results, err := datasource.RawDB("", querySql, params)
|
||||
if err != nil {
|
||||
logger.Errorf("query err => %v", err)
|
||||
return []model.SysJobLog{}
|
||||
}
|
||||
|
||||
// 转换实体
|
||||
return r.convertResultRows(results)
|
||||
}
|
||||
|
||||
// 通过调度ID查询调度任务日志信息
|
||||
func (r *SysJobLogImpl) SelectJobLogById(jobLogId string) model.SysJobLog {
|
||||
querySql := r.selectSql + " where job_log_id = ?"
|
||||
results, err := datasource.RawDB("", querySql, []any{jobLogId})
|
||||
if err != nil {
|
||||
logger.Errorf("query err => %v", err)
|
||||
return model.SysJobLog{}
|
||||
}
|
||||
// 转换实体
|
||||
rows := r.convertResultRows(results)
|
||||
if len(rows) > 0 {
|
||||
return rows[0]
|
||||
}
|
||||
return model.SysJobLog{}
|
||||
}
|
||||
|
||||
// 新增调度任务日志信息
|
||||
func (r *SysJobLogImpl) InsertJobLog(sysJobLog model.SysJobLog) string {
|
||||
// 参数拼接
|
||||
params := make(map[string]any)
|
||||
params["create_time"] = time.Now().UnixMilli()
|
||||
if sysJobLog.JobLogID != "" {
|
||||
params["job_log_id"] = sysJobLog.JobLogID
|
||||
}
|
||||
if sysJobLog.JobName != "" {
|
||||
params["job_name"] = sysJobLog.JobName
|
||||
}
|
||||
if sysJobLog.JobGroup != "" {
|
||||
params["job_group"] = sysJobLog.JobGroup
|
||||
}
|
||||
if sysJobLog.InvokeTarget != "" {
|
||||
params["invoke_target"] = sysJobLog.InvokeTarget
|
||||
}
|
||||
if sysJobLog.TargetParams != "" {
|
||||
params["target_params"] = sysJobLog.TargetParams
|
||||
}
|
||||
if sysJobLog.JobMsg != "" {
|
||||
params["job_msg"] = sysJobLog.JobMsg
|
||||
}
|
||||
if sysJobLog.Status != "" {
|
||||
params["status"] = sysJobLog.Status
|
||||
}
|
||||
if sysJobLog.CostTime > 0 {
|
||||
params["cost_time"] = sysJobLog.CostTime
|
||||
}
|
||||
|
||||
// 构建执行语句
|
||||
keys, placeholder, values := repo.KeyPlaceholderValueByInsert(params)
|
||||
sql := "insert into sys_job_log (" + strings.Join(keys, ",") + ")values(" + placeholder + ")"
|
||||
|
||||
db := datasource.DefaultDB()
|
||||
// 开启事务
|
||||
tx := db.Begin()
|
||||
// 执行插入
|
||||
err := tx.Exec(sql, values...).Error
|
||||
if err != nil {
|
||||
logger.Errorf("insert row : %v", err.Error())
|
||||
tx.Rollback()
|
||||
return ""
|
||||
}
|
||||
// 获取生成的自增 ID
|
||||
var insertedID string
|
||||
err = tx.Raw("select last_insert_id()").Row().Scan(&insertedID)
|
||||
if err != nil {
|
||||
logger.Errorf("insert last id : %v", err.Error())
|
||||
tx.Rollback()
|
||||
return ""
|
||||
}
|
||||
// 提交事务
|
||||
tx.Commit()
|
||||
return insertedID
|
||||
}
|
||||
|
||||
// 批量删除调度任务日志信息
|
||||
func (r *SysJobLogImpl) DeleteJobLogByIds(jobLogIds []string) int64 {
|
||||
placeholder := repo.KeyPlaceholderByQuery(len(jobLogIds))
|
||||
sql := "delete from sys_job_log where job_log_id in (" + placeholder + ")"
|
||||
parameters := repo.ConvertIdsSlice(jobLogIds)
|
||||
results, err := datasource.ExecDB("", sql, parameters)
|
||||
if err != nil {
|
||||
logger.Errorf("delete err => %v", err)
|
||||
return 0
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// 清空调度任务日志
|
||||
func (r *SysJobLogImpl) CleanJobLog() error {
|
||||
sql := "truncate table sys_job_log"
|
||||
_, err := datasource.ExecDB("", sql, []any{})
|
||||
return err
|
||||
}
|
||||
38
src/modules/monitor/service/sys_job.go
Normal file
38
src/modules/monitor/service/sys_job.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"ems.agt/src/modules/monitor/model"
|
||||
)
|
||||
|
||||
// ISysJob 调度任务信息 服务层接口
|
||||
type ISysJob interface {
|
||||
// SelectJobPage 分页查询调度任务集合
|
||||
SelectJobPage(query map[string]any) map[string]any
|
||||
|
||||
// SelectJobList 查询调度任务集合
|
||||
SelectJobList(sysJob model.SysJob) []model.SysJob
|
||||
|
||||
// SelectJobById 通过调度ID查询调度任务信息
|
||||
SelectJobById(jobId string) model.SysJob
|
||||
|
||||
// CheckUniqueJobName 校验调度任务名称和组是否唯一
|
||||
CheckUniqueJobName(jobName, jobGroup, jobId string) bool
|
||||
|
||||
// InsertJob 新增调度任务信息
|
||||
InsertJob(sysJob model.SysJob) string
|
||||
|
||||
// UpdateJob 修改调度任务信息
|
||||
UpdateJob(sysJob model.SysJob) int64
|
||||
|
||||
// DeleteJobByIds 批量删除调度任务信息
|
||||
DeleteJobByIds(jobIds []string) (int64, error)
|
||||
|
||||
// ChangeStatus 任务调度状态修改
|
||||
ChangeStatus(sysJob model.SysJob) bool
|
||||
|
||||
// RunQueueJob 立即运行一次调度任务
|
||||
RunQueueJob(sysJob model.SysJob) bool
|
||||
|
||||
// ResetQueueJob 重置初始调度任务
|
||||
ResetQueueJob()
|
||||
}
|
||||
190
src/modules/monitor/service/sys_job.impl.go
Normal file
190
src/modules/monitor/service/sys_job.impl.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"ems.agt/src/framework/constants/common"
|
||||
"ems.agt/src/framework/cron"
|
||||
"ems.agt/src/modules/monitor/model"
|
||||
"ems.agt/src/modules/monitor/repository"
|
||||
)
|
||||
|
||||
// 实例化服务层 SysJobImpl 结构体
|
||||
var NewSysJobImpl = &SysJobImpl{
|
||||
sysJobRepository: repository.NewSysJobImpl,
|
||||
}
|
||||
|
||||
// SysJobImpl 调度任务 服务层处理
|
||||
type SysJobImpl struct {
|
||||
// 调度任务数据信息
|
||||
sysJobRepository repository.ISysJob
|
||||
}
|
||||
|
||||
// SelectJobPage 分页查询调度任务集合
|
||||
func (r *SysJobImpl) SelectJobPage(query map[string]any) map[string]any {
|
||||
return r.sysJobRepository.SelectJobPage(query)
|
||||
}
|
||||
|
||||
// SelectJobList 查询调度任务集合
|
||||
func (r *SysJobImpl) SelectJobList(sysJob model.SysJob) []model.SysJob {
|
||||
return r.sysJobRepository.SelectJobList(sysJob)
|
||||
}
|
||||
|
||||
// SelectJobById 通过调度ID查询调度任务信息
|
||||
func (r *SysJobImpl) SelectJobById(jobId string) model.SysJob {
|
||||
if jobId == "" {
|
||||
return model.SysJob{}
|
||||
}
|
||||
jobs := r.sysJobRepository.SelectJobByIds([]string{jobId})
|
||||
if len(jobs) > 0 {
|
||||
return jobs[0]
|
||||
}
|
||||
return model.SysJob{}
|
||||
}
|
||||
|
||||
// CheckUniqueJobName 校验调度任务名称和组是否唯一
|
||||
func (r *SysJobImpl) CheckUniqueJobName(jobName, jobGroup, jobId string) bool {
|
||||
uniqueId := r.sysJobRepository.CheckUniqueJob(model.SysJob{
|
||||
JobName: jobName,
|
||||
JobGroup: jobGroup,
|
||||
})
|
||||
if uniqueId == jobId {
|
||||
return true
|
||||
}
|
||||
return uniqueId == ""
|
||||
}
|
||||
|
||||
// InsertJob 新增调度任务信息
|
||||
func (r *SysJobImpl) InsertJob(sysJob model.SysJob) string {
|
||||
insertId := r.sysJobRepository.InsertJob(sysJob)
|
||||
if insertId == "" && sysJob.Status == common.STATUS_YES {
|
||||
sysJob.JobID = insertId
|
||||
r.insertQueueJob(sysJob, true)
|
||||
}
|
||||
return insertId
|
||||
}
|
||||
|
||||
// UpdateJob 修改调度任务信息
|
||||
func (r *SysJobImpl) UpdateJob(sysJob model.SysJob) int64 {
|
||||
rows := r.sysJobRepository.UpdateJob(sysJob)
|
||||
if rows > 0 {
|
||||
//状态正常添加队列任务
|
||||
if sysJob.Status == common.STATUS_YES {
|
||||
r.insertQueueJob(sysJob, true)
|
||||
}
|
||||
// 状态禁用删除队列任务
|
||||
if sysJob.Status == common.STATUS_NO {
|
||||
r.deleteQueueJob(sysJob)
|
||||
}
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// DeleteJobByIds 批量删除调度任务信息
|
||||
func (r *SysJobImpl) DeleteJobByIds(jobIds []string) (int64, error) {
|
||||
// 检查是否存在
|
||||
jobs := r.sysJobRepository.SelectJobByIds(jobIds)
|
||||
if len(jobs) <= 0 {
|
||||
return 0, errors.New("没有权限访问调度任务数据!")
|
||||
}
|
||||
if len(jobs) == len(jobIds) {
|
||||
// 清除任务
|
||||
for _, job := range jobs {
|
||||
r.deleteQueueJob(job)
|
||||
}
|
||||
rows := r.sysJobRepository.DeleteJobByIds(jobIds)
|
||||
return rows, nil
|
||||
}
|
||||
return 0, errors.New("删除调度任务信息失败!")
|
||||
}
|
||||
|
||||
// ChangeStatus 任务调度状态修改
|
||||
func (r *SysJobImpl) ChangeStatus(sysJob model.SysJob) bool {
|
||||
// 更新状态
|
||||
newSysJob := model.SysJob{
|
||||
JobID: sysJob.JobID,
|
||||
Status: sysJob.Status,
|
||||
UpdateBy: sysJob.UpdateBy,
|
||||
}
|
||||
rows := r.sysJobRepository.UpdateJob(newSysJob)
|
||||
if rows > 0 {
|
||||
//状态正常添加队列任务
|
||||
if sysJob.Status == common.STATUS_YES {
|
||||
r.insertQueueJob(sysJob, true)
|
||||
}
|
||||
// 状态禁用删除队列任务
|
||||
if sysJob.Status == common.STATUS_NO {
|
||||
r.deleteQueueJob(sysJob)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ResetQueueJob 重置初始调度任务
|
||||
func (r *SysJobImpl) ResetQueueJob() {
|
||||
// 获取注册的队列名称
|
||||
queueNames := cron.QueueNames()
|
||||
if len(queueNames) == 0 {
|
||||
return
|
||||
}
|
||||
// 查询系统中定义状态为正常启用的任务
|
||||
sysJobs := r.sysJobRepository.SelectJobList(model.SysJob{
|
||||
Status: common.STATUS_YES,
|
||||
})
|
||||
for _, sysJob := range sysJobs {
|
||||
for _, name := range queueNames {
|
||||
if name == sysJob.InvokeTarget {
|
||||
r.insertQueueJob(sysJob, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RunQueueJob 立即运行一次调度任务
|
||||
func (r *SysJobImpl) RunQueueJob(sysJob model.SysJob) bool {
|
||||
return r.insertQueueJob(sysJob, false)
|
||||
}
|
||||
|
||||
// insertQueueJob 添加调度任务
|
||||
func (r *SysJobImpl) insertQueueJob(sysJob model.SysJob, repeat bool) bool {
|
||||
// 获取队列 Processor
|
||||
queue := cron.GetQueue(sysJob.InvokeTarget)
|
||||
if queue.Name != sysJob.InvokeTarget {
|
||||
return false
|
||||
}
|
||||
|
||||
// 给执行任务数据参数
|
||||
options := cron.JobData{
|
||||
Repeat: repeat,
|
||||
SysJob: sysJob,
|
||||
}
|
||||
|
||||
// 不是重复任务的情况,立即执行一次
|
||||
if !repeat {
|
||||
// 执行单次任务
|
||||
status := queue.RunJob(options, cron.JobOptions{
|
||||
JobId: sysJob.JobID,
|
||||
})
|
||||
// 执行中或等待中的都返回正常
|
||||
return status == cron.Active || status == cron.Waiting
|
||||
}
|
||||
|
||||
// 执行重复任务
|
||||
queue.RunJob(options, cron.JobOptions{
|
||||
JobId: sysJob.JobID,
|
||||
Cron: sysJob.CronExpression,
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// deleteQueueJob 删除调度任务
|
||||
func (r *SysJobImpl) deleteQueueJob(sysJob model.SysJob) bool {
|
||||
// 获取队列 Processor
|
||||
queue := cron.GetQueue(sysJob.InvokeTarget)
|
||||
if queue.Name != sysJob.InvokeTarget {
|
||||
return false
|
||||
}
|
||||
return queue.RemoveJob(sysJob.JobID)
|
||||
}
|
||||
23
src/modules/monitor/service/sys_job_log.go
Normal file
23
src/modules/monitor/service/sys_job_log.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"ems.agt/src/modules/monitor/model"
|
||||
)
|
||||
|
||||
// ISysJobLog 调度任务日志 服务层接口
|
||||
type ISysJobLog interface {
|
||||
// SelectJobLogPage 分页查询调度任务日志集合
|
||||
SelectJobLogPage(query map[string]any) map[string]any
|
||||
|
||||
// SelectJobLogList 查询调度任务日志集合
|
||||
SelectJobLogList(sysJobLog model.SysJobLog) []model.SysJobLog
|
||||
|
||||
// SelectJobLogById 通过调度ID查询调度任务日志信息
|
||||
SelectJobLogById(jobLogId string) model.SysJobLog
|
||||
|
||||
// DeleteJobLogByIds 批量删除调度任务日志信息
|
||||
DeleteJobLogByIds(jobLogIds []string) int64
|
||||
|
||||
// CleanJobLog 清空调度任务日志
|
||||
CleanJobLog() error
|
||||
}
|
||||
42
src/modules/monitor/service/sys_job_log.impl.go
Normal file
42
src/modules/monitor/service/sys_job_log.impl.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"ems.agt/src/modules/monitor/model"
|
||||
"ems.agt/src/modules/monitor/repository"
|
||||
)
|
||||
|
||||
// 实例化服务层 SysJobLogImpl 结构体
|
||||
var NewSysJobLogImpl = &SysJobLogImpl{
|
||||
sysJobLogRepository: repository.NewSysJobLogImpl,
|
||||
}
|
||||
|
||||
// SysJobLogImpl 调度任务日志 服务层处理
|
||||
type SysJobLogImpl struct {
|
||||
// 调度任务日志数据信息
|
||||
sysJobLogRepository repository.ISysJobLog
|
||||
}
|
||||
|
||||
// SelectJobLogPage 分页查询调度任务日志集合
|
||||
func (s *SysJobLogImpl) SelectJobLogPage(query map[string]any) map[string]any {
|
||||
return s.sysJobLogRepository.SelectJobLogPage(query)
|
||||
}
|
||||
|
||||
// SelectJobLogList 查询调度任务日志集合
|
||||
func (s *SysJobLogImpl) SelectJobLogList(sysJobLog model.SysJobLog) []model.SysJobLog {
|
||||
return s.sysJobLogRepository.SelectJobLogList(sysJobLog)
|
||||
}
|
||||
|
||||
// SelectJobLogById 通过调度ID查询调度任务日志信息
|
||||
func (s *SysJobLogImpl) SelectJobLogById(jobLogId string) model.SysJobLog {
|
||||
return s.sysJobLogRepository.SelectJobLogById(jobLogId)
|
||||
}
|
||||
|
||||
// DeleteJobLogByIds 批量删除调度任务日志信息
|
||||
func (s *SysJobLogImpl) DeleteJobLogByIds(jobLogIds []string) int64 {
|
||||
return s.sysJobLogRepository.DeleteJobLogByIds(jobLogIds)
|
||||
}
|
||||
|
||||
// CleanJobLog 清空调度任务日志
|
||||
func (s *SysJobLogImpl) CleanJobLog() error {
|
||||
return s.sysJobLogRepository.CleanJobLog()
|
||||
}
|
||||
12
src/modules/monitor/service/sys_user_online.go
Normal file
12
src/modules/monitor/service/sys_user_online.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"ems.agt/src/framework/vo"
|
||||
"ems.agt/src/modules/monitor/model"
|
||||
)
|
||||
|
||||
// ISysUserOnline 在线用户 服务层接口
|
||||
type ISysUserOnline interface {
|
||||
// LoginUserToUserOnline 设置在线用户信息
|
||||
LoginUserToUserOnline(loginUser vo.LoginUser) model.SysUserOnline
|
||||
}
|
||||
33
src/modules/monitor/service/sys_user_online.impl.go
Normal file
33
src/modules/monitor/service/sys_user_online.impl.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"ems.agt/src/framework/vo"
|
||||
"ems.agt/src/modules/monitor/model"
|
||||
)
|
||||
|
||||
// 实例化服务层 SysUserOnlineImpl 结构体
|
||||
var NewSysUserOnlineImpl = &SysUserOnlineImpl{}
|
||||
|
||||
// SysUserOnlineImpl 在线用户 服务层处理
|
||||
type SysUserOnlineImpl struct{}
|
||||
|
||||
// LoginUserToUserOnline 设置在线用户信息
|
||||
func (r *SysUserOnlineImpl) LoginUserToUserOnline(loginUser vo.LoginUser) model.SysUserOnline {
|
||||
if loginUser.UserID == "" {
|
||||
return model.SysUserOnline{}
|
||||
}
|
||||
|
||||
sysUserOnline := model.SysUserOnline{
|
||||
TokenID: loginUser.UUID,
|
||||
UserName: loginUser.User.UserName,
|
||||
IPAddr: loginUser.IPAddr,
|
||||
LoginLocation: loginUser.LoginLocation,
|
||||
Browser: loginUser.Browser,
|
||||
OS: loginUser.OS,
|
||||
LoginTime: loginUser.LoginTime,
|
||||
}
|
||||
if loginUser.User.DeptID != "" {
|
||||
sysUserOnline.DeptName = loginUser.User.Dept.DeptName
|
||||
}
|
||||
return sysUserOnline
|
||||
}
|
||||
25
src/modules/monitor/service/system_info.go
Normal file
25
src/modules/monitor/service/system_info.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package service
|
||||
|
||||
// ISystemInfo 服务器系统相关信息 服务层接口
|
||||
type ISystemInfo interface {
|
||||
// ProjectInfo 程序项目信息
|
||||
ProjectInfo() map[string]any
|
||||
|
||||
// SystemInfo 系统信息
|
||||
SystemInfo() map[string]any
|
||||
|
||||
// TimeInfo 系统时间信息
|
||||
TimeInfo() map[string]string
|
||||
|
||||
// MemoryInfo 内存信息
|
||||
MemoryInfo() map[string]any
|
||||
|
||||
// CPUInfo CPU信息
|
||||
CPUInfo() map[string]any
|
||||
|
||||
// NetworkInfo 网络信息
|
||||
NetworkInfo() map[string]string
|
||||
|
||||
// DiskInfo 磁盘信息
|
||||
DiskInfo() []map[string]string
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user