diff --git a/config/etc/default/restconf.yaml b/config/etc/default/restconf.yaml index 7a8a8e36..43a84b14 100644 --- a/config/etc/default/restconf.yaml +++ b/config/etc/default/restconf.yaml @@ -3,21 +3,21 @@ # duration: rotation time with xx hours, example: 1/12/24 hours # count: rotation count of log, default is 30 rotation logger: - file: /usr/local/omc/log/restagent.log + file: /usr/local/omc/log/restagent.log level: warn duration: 24 - count: 90 + count: 90 # rest agent listen ipv4/v6 and port, support multiple routines # ip: 0.0.0.0 or ::0, support IPv4/v6 -# clientAuthType: 0:NoClientCert (default), 1:RequestClientCert, 2:RequireAnyClientCert, +# clientAuthType: 0:NoClientCert (default), 1:RequestClientCert, 2:RequireAnyClientCert, # 3:VerifyClientCertIfGiven, 4:RequireAndVerifyClientCerts rest: - ipv4: 0.0.0.0 - ipv6: + ipv6: port: 33030 - ipv4: 0.0.0.0 - ipv6: + ipv6: port: 33443 schema: https clientAuthType: 0 @@ -28,7 +28,7 @@ rest: webServer: enabled: true rootDir: /usr/local/omc/htdocs/front - listen: + listen: - addr: :80 schema: http - addr: :443 @@ -38,15 +38,25 @@ webServer: certFile: /usr/local/omc/etc/certs/omc-server.crt keyFile: /usr/local/omc/etc/certs/omc-server.key +# data sources database: - type: mysql - user: root - password: 1000omc@kp! - host: 127.0.0.1 - port: 33066 - name: omc_db - connParam: charset=utf8mb4&collation=utf8mb4_general_ci&parseTime=True&interpolateParams=True - backup: /usr/local/omc/database + dataSource: + # Default database instance + default: + type: "mysql" + host: "127.0.0.1" + port: 33066 + username: "root" + password: "1000omc@kp!" + database: "omc_db" + logging: false + # Built-in lightweight database + lite: + type: "sqlite" + database: "/usr/local/omc/database/omc_db.sqlite" + logging: false + # used to specify the default data source for multiple data resourece + defaultDataSourceName: "default" # Redis data cache redis: @@ -85,9 +95,9 @@ ne: scpdir: /tmp licensedir: /usr/local/etc/{neType}/license # backup etc list of IMS, does not contain spaces - etcListIMS: '{*.yaml,mmtel,vars.cfg}' - etcListDefault: '{*.yaml,*.conf,*.cfg}' - # true/false to overwrite config file when dpkg ne software + etcListIMS: "{*.yaml,mmtel,vars.cfg}" + etcListDefault: "{*.yaml,*.conf,*.cfg}" + # true/false to overwrite config file when dpkg ne software dpkgOverwrite: false # dpkg timeout (second) dpkgTimeout: 180 @@ -115,7 +125,7 @@ omc: frontTraceDir: /usr/local/omc/htdocs/front/trace software: /usr/local/omc/software license: /usr/local/omc/license - gtpUri: gtp:192.168.2.119:32152 + gtpUri: gtp:192.168.2.119:32152 checkContentType: false testMode: false rbacMode: true @@ -126,21 +136,21 @@ omc: # Forward interface: # TLS Skip verify: true/false # email/sms -# smProxy: sms(Short Message Service)/smsc(SMS Centre) -# dataCoding: 0:GSM7BIT, 1:ASCII, 2:BINARY8BIT1, 3:LATIN1, +# smProxy: sms(Short Message Service)/smsc(SMS Centre) +# dataCoding: 0:GSM7BIT, 1:ASCII, 2:BINARY8BIT1, 3:LATIN1, # 4:BINARY8BIT2, 6:CYRILLIC, 7:HEBREW, 8:UCS2 -alarm: +alarm: alarmEmailForward: enable: true - emailList: + emailList: smtp: mail.smtp.com port: 25 user: smtpext@smtp.com password: "1000smtp@omc!" - tlsSkipVerify: true + tlsSkipVerify: true alarmSMSForward: enable: true - mobileList: + mobileList: smscAddr: "192.168.13.114:2775" systemID: "omc" password: "omc123" @@ -154,7 +164,7 @@ alarm: signName: xxx SMSC templateCode: 1000 smProxy: smsc - + # User authorized information # crypt: mysql/md5/bcrypt # token: true/false to check accessToken @@ -169,7 +179,7 @@ auth: publicKey: /usr/local/omc/etc/certs/omc privateKey: /usr/local/omc/etc/certs/omc -# Parameter for limit number +# Parameter for limit number # rmuid_maxnum: the max number of rmUID, default: 50 # alarmid_maxnum: the max number of AlarmID, default: 50 # pmid_maxnum: the max number of pmID, default: 50 @@ -186,4 +196,4 @@ params: testConfig: enabled: false - file: /usr/local/omc/etc/testconfig.yaml \ No newline at end of file + file: /usr/local/omc/etc/testconfig.yaml diff --git a/src/framework/config/config.go b/src/framework/config/config.go index 4b53516d..adad1415 100644 --- a/src/framework/config/config.go +++ b/src/framework/config/config.go @@ -15,13 +15,13 @@ import ( libGlobal "be.ems/lib/global" ) -var cfg *viper.Viper +var conf *viper.Viper // 初始化程序配置 -func InitConfig(configDir, assetsDir embed.FS) { - cfg = viper.New() +func InitConfig(configDir *embed.FS) { + conf = viper.New() initFlag() - initViper(configDir, assetsDir) + initViper(configDir) } // 指定参数绑定 @@ -50,63 +50,50 @@ func initFlag() { os.Exit(1) } - cfg.BindPFlags(pflag.CommandLine) + conf.BindPFlags(pflag.CommandLine) } // 配置文件读取 -func initViper(configDir, assetsDir embed.FS) { +func initViper(configDir *embed.FS) { // 如果配置文件名中没有扩展名,则需要设置Type - cfg.SetConfigType("yaml") - - // 从 embed.FS 中读取默认配置文件内容 - configDefault, err := configDir.ReadFile("config/config.default.yaml") + conf.SetConfigType("yaml") + // 读取默认配置文件 + configDefaultByte, err := configDir.ReadFile("config/config.default.yaml") if err != nil { - log.Fatalf("ReadFile config default file: %s", err) + log.Fatalf("config default file read error: %s", err) return } - // 设置默认配置文件内容到 viper - err = cfg.ReadConfig(bytes.NewReader(configDefault)) - if err != nil { - log.Fatalf("NewReader config default file: %s", err) + if err = conf.ReadConfig(bytes.NewReader(configDefaultByte)); err != nil { + log.Fatalf("config default file read error: %s", err) return } - // 加载运行环境配置 - env := cfg.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) + // 当期服务环境运行配置 => local + env := conf.GetString("env") + log.Printf("current service environment operation configuration => %s \n", env) // 加载运行配置文件合并相同配置 - envPath := "config/config.prod.yaml" - if env == "local" { - envPath = "config/config.local.yaml" - } - // 从 embed.FS 中读取默认配置文件内容 - configEnv, err := configDir.ReadFile(envPath) + envConfigPath := fmt.Sprintf("config/config.%s.yaml", env) + configEnvByte, err := configDir.ReadFile(envConfigPath) if err != nil { - log.Fatalf("ReadFile config local file: %s", err) + log.Fatalf("config env %s file read error: %s", env, err) return } - // 设置默认配置文件内容到 viper - err = cfg.MergeConfig(bytes.NewReader(configEnv)) - if err != nil { - log.Fatalf("NewReader config local file: %s", err) + if err = conf.MergeConfig(bytes.NewReader(configEnvByte)); err != nil { + log.Fatalf("config env %s file read error: %s", env, err) return } - // 合并外部使用配置 - configFile := cfg.GetString("config") - if configFile != "" { - configInMerge(configFile) + // 外部文件配置 + externalConfig := conf.GetString("config") + if externalConfig != "" { + // readExternalConfig(externalConfig) + // 处理旧配置,存在相同的配置项处理 + configInMerge(externalConfig) } // 记录程序开始运行的时间点 - cfg.Set("runTime", time.Now()) - - // 设置程序内全局资源访问 - cfg.Set("AssetsDir", assetsDir) + conf.Set("runTime", time.Now()) } // 配置文件读取进行内部参数合并 @@ -137,54 +124,62 @@ func configInMerge(configFile string) { if key == "testconfig" || key == "logger" { continue } - // 数据库配置 - if key == "database" { - item := value.(map[string]any) - defaultItem := cfg.GetStringMap("gorm.datasource.default") - defaultItem["type"] = item["type"] - defaultItem["host"] = item["host"] - defaultItem["port"] = item["port"] - defaultItem["username"] = item["user"] - defaultItem["password"] = item["password"] - defaultItem["database"] = item["name"] - continue - } - cfg.Set(key, value) + conf.Set(key, value) } } // Env 获取运行服务环境 // local prod func Env() string { - return cfg.GetString("env") + return conf.GetString("env") } // RunTime 程序开始运行的时间 func RunTime() time.Time { - return cfg.GetTime("runTime") + return conf.GetTime("runTime") } // Get 获取配置信息 // // Get("server.proxy") func Get(key string) any { - return cfg.Get(key) + return conf.Get(key) } // GetAssetsDirFS 访问程序内全局资源访问 -func GetAssetsDirFS() embed.FS { - return cfg.Get("AssetsDir").(embed.FS) +func GetAssetsDirFS() *embed.FS { + return conf.Get("AssetsDir").(*embed.FS) } -// IsAdmin 用户是否为管理员 -func IsAdmin(userID string) bool { - if userID == "" { +// SetAssetsDirFS 设置程序内全局资源访问 +func SetAssetsDirFS(assetsDir *embed.FS) { + conf.Set("AssetsDir", assetsDir) +} + +// readExternalConfig 读取外部文件配置 +func readExternalConfig(configPaht string) { + f, err := os.Open(configPaht) + if err != nil { + log.Fatalf("config external file read error: %s", err) + return + } + defer f.Close() + + if err = conf.MergeConfig(f); err != nil { + log.Fatalf("config external file read error: %s", err) + return + } +} + +// IsSystemUser 用户是否为系统管理员 +func IsSystemUser(userId int64) bool { + if userId <= 0 { return false } - // 从本地配置获取user信息 - admins := Get("user.adminList").([]any) - for _, s := range admins { - if fmt.Sprint(s) == userID { + // 从配置中获取系统管理员ID列表 + arr := Get("user.system").([]any) + for _, v := range arr { + if fmt.Sprint(v) == fmt.Sprint(userId) { return true } } diff --git a/src/framework/constants/admin/admin.go b/src/framework/constants/admin/admin.go deleted file mode 100644 index 1c158a8d..00000000 --- a/src/framework/constants/admin/admin.go +++ /dev/null @@ -1,12 +0,0 @@ -package admin - -// 系统管理员常量信息 - -// 系统管理员-系统指定角色ID -const ROLE_ID = "1" - -// 系统管理员-系统指定角色KEY -const ROLE_KEY = "system" - -// 系统管理员-系统指定权限 -const PERMISSION = "*:*:*" diff --git a/src/framework/constants/cache_key.go b/src/framework/constants/cache_key.go new file mode 100644 index 00000000..a6cac630 --- /dev/null +++ b/src/framework/constants/cache_key.go @@ -0,0 +1,25 @@ +package constants + +// 缓存的key常量 +const ( + // CACHE_LOGIN_TOKEN 登录用户 + CACHE_LOGIN_TOKEN = "login_tokens" + // CACHE_CAPTCHA_CODE 验证码 + CACHE_CAPTCHA_CODE = "captcha_codes" + // CACHE_SYS_CONFIG 参数管理 + CACHE_SYS_CONFIG = "sys_config" + // CACHE_SYS_DICT 字典管理 + CACHE_SYS_DICT = "sys_dict" + // CACHE_REPEAT_SUBMIT 防重提交 + CACHE_REPEAT_SUBMIT = "repeat_submit" + // CACHE_RATE_LIMIT 限流 + CACHE_RATE_LIMIT = "rate_limit" + // CACHE_PWD_ERR_COUNT 登录账户密码错误次数 + CACHE_PWD_ERR_COUNT = "pwd_err_count" + // CACHE_I18N 多语言 + CACHE_I18N = "i18n" + // CACHE_NE_INFO 网元信息管理 + CACHE_NE_INFO = "ne_info" + // CACHE_NE_DATA 网元数据管理 + CACHE_NE_DATA = "ne_data" +) diff --git a/src/framework/constants/cachekey/cachekey.go b/src/framework/constants/cachekey/cachekey.go deleted file mode 100644 index dd7c11fe..00000000 --- a/src/framework/constants/cachekey/cachekey.go +++ /dev/null @@ -1,30 +0,0 @@ -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:" - -// 网元信息管理 -const NE_KEY = "ne_info:" - -// 网元数据管理 -const NE_DATA_KEY = "ne_data:" diff --git a/src/framework/constants/captcha.go b/src/framework/constants/captcha.go new file mode 100644 index 00000000..ac377fef --- /dev/null +++ b/src/framework/constants/captcha.go @@ -0,0 +1,11 @@ +package constants + +// 验证码常量信息 +const ( + // CAPTCHA_EXPIRATION 验证码有效期,单位秒 + CAPTCHA_EXPIRATION = 2 * 60 + // CAPTCHA_TYPE_CHAR 验证码类型-数值计算 + CAPTCHA_TYPE_CHAR = "char" + // CAPTCHA_TYPE_MATH 验证码类型-字符验证 + CAPTCHA_TYPE_MATH = "math" +) diff --git a/src/framework/constants/captcha/captcha.go b/src/framework/constants/captcha/captcha.go deleted file mode 100644 index 816a65e4..00000000 --- a/src/framework/constants/captcha/captcha.go +++ /dev/null @@ -1,12 +0,0 @@ -package captcha - -// 验证码常量信息 - -// 验证码有效期,单位秒 -const EXPIRATION = 2 * 60 - -// 验证码类型-数值计算 -const TYPE_CHAR = "char" - -// 验证码类型-字符验证 -const TYPE_MATH = "math" diff --git a/src/framework/constants/common.go b/src/framework/constants/common.go new file mode 100644 index 00000000..f14ebece --- /dev/null +++ b/src/framework/constants/common.go @@ -0,0 +1,14 @@ +package constants + +const ( + // STATUS_YES 通用状态标识-正常/成功/是 + STATUS_YES = "1" + // STATUS_NO 通用状态标识-停用/失败/否 + STATUS_NO = "0" +) + +// CTX_LOGIN_USER 上下文信息-登录用户 +const CTX_LOGIN_USER = "ctx:login_user" + +// 启动-引导系统初始 +const LAUNCH_BOOTLOADER = "bootloader" diff --git a/src/framework/constants/common/common.go b/src/framework/constants/common/common.go deleted file mode 100644 index 08e0bcbc..00000000 --- a/src/framework/constants/common/common.go +++ /dev/null @@ -1,24 +0,0 @@ -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" - -// 启动-引导系统初始 -const LAUNCH_BOOTLOADER = "bootloader" diff --git a/src/framework/constants/menu.go b/src/framework/constants/menu.go new file mode 100644 index 00000000..b59e48bd --- /dev/null +++ b/src/framework/constants/menu.go @@ -0,0 +1,21 @@ +package constants + +// 系统菜单常量信息 +const ( + // MENU_COMPONENT_LAYOUT_BASIC 组件布局类型-基础布局组件标识 + MENU_COMPONENT_LAYOUT_BASIC = "BasicLayout" + // MENU_COMPONENT_LAYOUT_BLANK 组件布局类型-空白布局组件标识 + MENU_COMPONENT_LAYOUT_BLANK = "BlankLayout" + // MENU_COMPONENT_LAYOUT_LINK 组件布局类型-内链接布局组件标识 + MENU_COMPONENT_LAYOUT_LINK = "LinkLayout" + + // MENU_TYPE_DIR 菜单类型-目录 + MENU_TYPE_DIR = "D" + // MENU_TYPE_MENU 菜单类型-菜单 + MENU_TYPE_MENU = "M" + // MENU_TYPE_BUTTON 菜单类型-按钮 + MENU_TYPE_BUTTON = "B" + + // MENU_PATH_INLINE 菜单内嵌地址标识-带/前缀 + MENU_PATH_INLINE = "/inline" +) diff --git a/src/framework/constants/menu/menu.go b/src/framework/constants/menu/menu.go deleted file mode 100644 index 94913cbd..00000000 --- a/src/framework/constants/menu/menu.go +++ /dev/null @@ -1,24 +0,0 @@ -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" diff --git a/src/framework/constants/result/result.go b/src/framework/constants/result/result.go deleted file mode 100644 index 19761ce4..00000000 --- a/src/framework/constants/result/result.go +++ /dev/null @@ -1,20 +0,0 @@ -package result - -// 响应结果常量信息 - -const ( - // 响应-code错误失败 - CODE_ERROR = 0 - // 响应-msg错误失败 - MSG_ERROR = "error" - - // 响应-code正常成功 - CODE_SUCCESS = 1 - // 响应-msg正常成功 - MSG_SUCCESS = "success" - - // 响应-code加密数据 - CODE_ENCRYPT = 2 - // 响应-msg加密数据 - MSG_ENCRYPT = "encrypt" -) diff --git a/src/framework/constants/role_data_scope.go b/src/framework/constants/role_data_scope.go new file mode 100644 index 00000000..5b1e1666 --- /dev/null +++ b/src/framework/constants/role_data_scope.go @@ -0,0 +1,24 @@ +package constants + +// 系统角色数据范围常量 +const ( + // ROLE_SCOPE_ALL 全部数据权限 + ROLE_SCOPE_ALL = "1" + // ROLE_SCOPE_CUSTOM 自定数据权限 + ROLE_SCOPE_CUSTOM = "2" + // ROLE_SCOPE_DEPT 部门数据权限 + ROLE_SCOPE_DEPT = "3" + // ROLE_SCOPE_DEPT_CHILD 部门及以下数据权限 + ROLE_SCOPE_DEPT_CHILD = "4" + // ROLE_SCOPE_SELF 仅本人数据权限 + ROLE_SCOPE_SELF = "5" +) + +// ROLE_SCOPE_DATA 系统角色数据范围映射 +var ROLE_SCOPE_DATA = map[string]string{ + ROLE_SCOPE_ALL: "全部数据权限", + ROLE_SCOPE_CUSTOM: "自定数据权限", + ROLE_SCOPE_DEPT: "部门数据权限", + ROLE_SCOPE_DEPT_CHILD: "部门及以下数据权限", + ROLE_SCOPE_SELF: "仅本人数据权限", +} diff --git a/src/framework/constants/roledatascope/roledatascope.go b/src/framework/constants/roledatascope/roledatascope.go deleted file mode 100644 index c18cc893..00000000 --- a/src/framework/constants/roledatascope/roledatascope.go +++ /dev/null @@ -1,20 +0,0 @@ -package roledatascope - -// 系统角色数据范围常量 - -const ( - // 全部数据权限 - ALL = "1" - - // 自定数据权限 - CUSTOM = "2" - - // 部门数据权限 - DEPT = "3" - - // 部门及以下数据权限 - DEPT_AND_CHILD = "4" - - // 仅本人数据权限 - SELF = "5" -) diff --git a/src/framework/constants/system.go b/src/framework/constants/system.go new file mode 100644 index 00000000..c9546a5c --- /dev/null +++ b/src/framework/constants/system.go @@ -0,0 +1,12 @@ +package constants + +// 系统常量信息 + +// SYS_ROLE_SYSTEM_ID 系统管理员-系统指定角色ID +const SYS_ROLE_SYSTEM_ID = 1 + +// SYS_ROLE_SYSTEM_KEY 系统管理员-系统指定角色KEY +const SYS_ROLE_SYSTEM_KEY = "system" + +// SYS_PERMISSION_SYSTEM 系统管理员-系统指定权限 +const SYS_PERMISSION_SYSTEM = "*:*:*" diff --git a/src/framework/constants/token.go b/src/framework/constants/token.go new file mode 100644 index 00000000..70b192e3 --- /dev/null +++ b/src/framework/constants/token.go @@ -0,0 +1,24 @@ +package constants + +// 令牌常量信息 + +// HEADER_PREFIX 令牌-请求头标识前缀 +const HEADER_PREFIX = "Bearer " + +// HEADER_KEY 令牌-请求头标识 +const HEADER_KEY = "Authorization" + +// JWT_UUID 令牌-JWT唯一标识字段 +const JWT_UUID = "uuid" + +// JWT_USER_ID 令牌-JWT标识用户主键字段 +const JWT_USER_ID = "user_id" + +// JWT_USER_NAME 令牌-JWT标识用户登录账号字段 +const JWT_USER_NAME = "user_name" + +// NMS北向使用-数据响应字段和请求头授权 +const ACCESS_TOKEN = "accessToken" + +// WS请求使用-数据响应字段和请求头授权 +const ACCESS_TOKEN_QUERY = "access_token" diff --git a/src/framework/constants/token/token.go b/src/framework/constants/token/token.go deleted file mode 100644 index 04b8ee82..00000000 --- a/src/framework/constants/token/token.go +++ /dev/null @@ -1,24 +0,0 @@ -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" - -// NMS北向使用-数据响应字段和请求头授权 -const ACCESS_TOKEN = "accessToken" diff --git a/src/framework/constants/upload_sub_path.go b/src/framework/constants/upload_sub_path.go new file mode 100644 index 00000000..a8bdf1b3 --- /dev/null +++ b/src/framework/constants/upload_sub_path.go @@ -0,0 +1,30 @@ +package constants + +// 文件上传-子路径类型常量 +const ( + // UPLOAD_DEFAULT 默认 + UPLOAD_DEFAULT = "default" + // UPLOAD_AVATAR 头像 + UPLOAD_AVATAR = "avatar" + // UPLOAD_IMPORT 导入 + UPLOAD_IMPORT = "import" + // UPLOAD_EXPORT 导出 + UPLOAD_EXPORT = "export" + // UPLOAD_COMMON 通用上传 + UPLOAD_COMMON = "common" + // UPLOAD_DOWNLOAD 下载 + UPLOAD_DOWNLOAD = "download" + // UPLOAD_CHUNK 切片 + UPLOAD_CHUNK = "chunk" +) + +// UPLOAD_SUB_PATH 子路径类型映射 +var UPLOAD_SUB_PATH = map[string]string{ + UPLOAD_DEFAULT: "默认", + UPLOAD_AVATAR: "头像", + UPLOAD_IMPORT: "导入", + UPLOAD_EXPORT: "导出", + UPLOAD_COMMON: "通用上传", + UPLOAD_DOWNLOAD: "下载", + UPLOAD_CHUNK: "切片", +} diff --git a/src/framework/constants/uploadsubpath/uploadsubpath.go b/src/framework/constants/uploadsubpath/uploadsubpath.go deleted file mode 100644 index b4c181f4..00000000 --- a/src/framework/constants/uploadsubpath/uploadsubpath.go +++ /dev/null @@ -1,45 +0,0 @@ -package uploadsubpath - -// 文件上传-子路径类型常量 - -const ( - // 默认 - DEFAULT = "default" - - // 头像 - AVATART = "avatar" - - // 导入 - IMPORT = "import" - - // 导出 - EXPORT = "export" - - // 通用上传 - COMMON = "common" - - // 下载 - DOWNLOAD = "download" - - // 切片 - CHUNK = "chunk" - - // 软件包 - SOFTWARE = "software" - - // 授权文件 - LICENSE = "license" -) - -// 子路径类型映射 -var UploadSubpath = map[string]string{ - DEFAULT: "默认", - AVATART: "头像", - IMPORT: "导入", - EXPORT: "导出", - COMMON: "通用上传", - DOWNLOAD: "下载", - CHUNK: "切片", - SOFTWARE: "软件包", - LICENSE: "授权文件", -} diff --git a/src/framework/cron/log.go b/src/framework/cron/log.go index 22140a2d..9943f154 100644 --- a/src/framework/cron/log.go +++ b/src/framework/cron/log.go @@ -4,7 +4,7 @@ import ( "encoding/json" "time" - "be.ems/src/framework/constants/common" + "be.ems/src/framework/constants" "be.ems/src/modules/monitor/model" "be.ems/src/modules/monitor/repository" ) @@ -39,7 +39,7 @@ func (s cronlog) Error(err error, msg string, keysAndValues ...any) { Data: data, Result: err.Error(), } - jobLog.SaveLog(common.STATUS_NO) + jobLog.SaveLog(constants.STATUS_NO) } } } @@ -62,7 +62,7 @@ func (s cronlog) Completed(result any, msg string, keysAndValues ...any) { Data: data, Result: result, } - jobLog.SaveLog(common.STATUS_YES) + jobLog.SaveLog(constants.STATUS_YES) } } } @@ -80,13 +80,13 @@ func (jl *jobLogData) SaveLog(status string) { sysJob := jl.Data.SysJob // 任务日志不需要记录 - if sysJob.SaveLog == "" || sysJob.SaveLog == common.STATUS_NO { + if sysJob.SaveLog == "" || sysJob.SaveLog == constants.STATUS_NO { return } // 结果信息key的Name resultNmae := "failed" - if status == common.STATUS_YES { + if status == constants.STATUS_YES { resultNmae = "completed" } @@ -108,12 +108,12 @@ func (jl *jobLogData) SaveLog(status string) { JobGroup: sysJob.JobGroup, InvokeTarget: sysJob.InvokeTarget, TargetParams: sysJob.TargetParams, - Status: status, + StatusFlag: status, JobMsg: jobMsg, CostTime: duration.Milliseconds(), } // 插入数据 - repository.NewSysJobLogImpl.InsertJobLog(sysJobLog) + repository.NewSysJobLog.Insert(sysJobLog) } // JobData 调度任务日志收集结构体,执行任务时传入的接收参数 diff --git a/src/framework/datasource/datasource.go b/src/framework/database/db/db.go similarity index 69% rename from src/framework/datasource/datasource.go rename to src/framework/database/db/db.go index 0bebed8d..6bf98b3f 100644 --- a/src/framework/datasource/datasource.go +++ b/src/framework/database/db/db.go @@ -1,4 +1,4 @@ -package datasource +package db import ( "fmt" @@ -7,33 +7,40 @@ import ( "regexp" "time" - "be.ems/src/framework/config" - "be.ems/src/framework/logger" - "be.ems/src/framework/utils/parse" - + "github.com/glebarez/sqlite" "gorm.io/driver/mysql" "gorm.io/gorm" gormLog "gorm.io/gorm/logger" + + "be.ems/src/framework/config" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/parse" ) // 数据库连接实例 var dbMap = make(map[string]*gorm.DB) type dialectInfo struct { - dialector gorm.Dialector + dialectic gorm.Dialector logging bool } // 载入数据库连接 func loadDialect() map[string]dialectInfo { - dialects := make(map[string]dialectInfo, 0) + dialects := make(map[string]dialectInfo) // 读取数据源配置 - datasource := config.Get("gorm.datasource").(map[string]any) + datasource := config.Get("database.datasource").(map[string]any) for key, value := range datasource { item := value.(map[string]any) // 数据库类型对应的数据库连接 switch item["type"] { + case "sqlite": + dsn := fmt.Sprint(item["database"]) + dialects[key] = dialectInfo{ + dialectic: sqlite.Open(dsn), + logging: item["logging"].(bool), + } case "mysql": dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", item["username"], @@ -43,11 +50,11 @@ func loadDialect() map[string]dialectInfo { item["database"], ) dialects[key] = dialectInfo{ - dialector: mysql.Open(dsn), + dialectic: mysql.Open(dsn), logging: item["logging"].(bool), } default: - logger.Fatalf("%s: %v\n Not Load DB Config Type", key, item) + logger.Warnf("%s: %v\n Not Load DB Config Type", key, item) } } @@ -68,7 +75,7 @@ func loadLogger() gormLog.Interface { return newLogger } -// 连接数据库实例 +// Connect 连接数据库实例 func Connect() { // 遍历进行连接数据库实例 for key, info := range loadDialect() { @@ -78,7 +85,7 @@ func Connect() { opts.Logger = loadLogger() } // 创建连接 - db, err := gorm.Open(info.dialector, opts) + db, err := gorm.Open(info.dialectic, opts) if err != nil { logger.Fatalf("failed error db connect: %s", err) } @@ -92,12 +99,18 @@ func Connect() { if err != nil { logger.Fatalf("failed error ping database: %v", err) } + // SetMaxIdleConns 用于设置连接池中空闲连接的最大数量。 + sqlDB.SetMaxIdleConns(10) + // SetMaxOpenConns 设置打开数据库连接的最大数量。 + sqlDB.SetMaxOpenConns(100) + // SetConnMaxLifetime 设置了连接可复用的最大时间。 + sqlDB.SetConnMaxLifetime(time.Hour) logger.Infof("database %s connection is successful.", key) dbMap[key] = db } } -// 关闭数据库实例 +// Close 关闭数据库实例 func Close() { for _, db := range dbMap { sqlDB, err := db.DB() @@ -110,36 +123,41 @@ func Close() { } } -// 获取默认数据源 -func DefaultDB() *gorm.DB { - source := config.Get("gorm.defaultDataSourceName").(string) - return dbMap[source] -} - -// 获取数据源 +// DB 获取数据源 +// +// source-数据源 func DB(source string) *gorm.DB { + // 不指定时获取默认实例 if source == "" { source = config.Get("gorm.defaultDataSourceName").(string) } return dbMap[source] } -// RawDB 原生查询语句 -func RawDB(source string, sql string, parameters []any) ([]map[string]any, error) { - // 数据源 - db := DefaultDB() - if source != "" { - db = DB(source) +// Names 获取数据源名称列表 +func Names() []string { + var names []string + for key := range dbMap { + names = append(names, key) } + return names +} +// RawDB 原生语句查询 +// +// source-数据源 +// sql-预编译的SQL语句 +// parameters-预编译的SQL语句参数 +func RawDB(source string, sql string, parameters []any) ([]map[string]any, error) { + var rows []map[string]any + // 数据源 + db := DB(source) + if db == nil { + return rows, fmt.Errorf("not database 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 @@ -147,12 +165,16 @@ func RawDB(source string, sql string, parameters []any) ([]map[string]any, error return rows, nil } -// ExecDB 原生执行语句 +// ExecDB 原生语句执行 +// +// source-数据源 +// sql-预编译的SQL语句 +// parameters-预编译的SQL语句参数 func ExecDB(source string, sql string, parameters []any) (int64, error) { // 数据源 - db := DefaultDB() - if source != "" { - db = DB(source) + db := DB(source) + if db == nil { + return 0, fmt.Errorf("not database source") } // 使用正则表达式替换连续的空白字符为单个空格 fmtSql := regexp.MustCompile(`\s+`).ReplaceAllString(sql, " ") @@ -165,6 +187,9 @@ func ExecDB(source string, sql string, parameters []any) (int64, error) { } // PageNumSize 分页页码记录数 +// +// pageNum-页码 +// pageSize-记录数 func PageNumSize(pageNum, pageSize any) (int, int) { // 记录起始索引 num := parse.Number(pageNum) diff --git a/src/framework/redis/conn.go b/src/framework/database/redis/conn.go similarity index 100% rename from src/framework/redis/conn.go rename to src/framework/database/redis/conn.go diff --git a/src/framework/database/redis/expand.go b/src/framework/database/redis/expand.go new file mode 100644 index 00000000..8eba226e --- /dev/null +++ b/src/framework/database/redis/expand.go @@ -0,0 +1,139 @@ +package redis + +import ( + "context" + "errors" + "fmt" + "sync" + + "be.ems/src/framework/logger" + "github.com/redis/go-redis/v9" +) + +// 连接Redis实例 +func ConnectPush(source string, rdb *redis.Client) { + if rdb == nil { + delete(rdbMap, source) + return + } + rdbMap[source] = rdb +} + +// 批量获得缓存数据 [key]result +func GetHashBatch(source string, keys []string) (map[string]map[string]string, error) { + result := make(map[string]map[string]string, 0) + if len(keys) == 0 { + return result, fmt.Errorf("not keys") + } + + // 数据源 + rdb := RDB(source) + if rdb == nil { + return result, fmt.Errorf("redis not client") + } + + // 创建一个有限的并发控制信号通道 + sem := make(chan struct{}, 10) + var wg sync.WaitGroup + var mt sync.Mutex + batchSize := 1000 + total := len(keys) + if total < batchSize { + batchSize = total + } + + for i := 0; i < total; i += batchSize { + wg.Add(1) + go func(start int) { + ctx := context.Background() + // 并发控制,限制同时执行的 Goroutine 数量 + sem <- struct{}{} + defer func() { + <-sem + ctx.Done() + wg.Done() + }() + + // 检查索引是否越界 + end := start + batchSize + if end > total { + end = total + } + pipe := rdb.Pipeline() + for _, key := range keys[start:end] { + pipe.HGetAll(ctx, key) + } + + cmds, err := pipe.Exec(ctx) + if err != nil { + logger.Errorf("Failed to get hash batch exec err: %v", err) + return + } + + // 将结果添加到 result map 并发访问 + mt.Lock() + defer mt.Unlock() + + // 处理命令结果 + for _, cmd := range cmds { + if cmd.Err() != nil { + logger.Errorf("Failed to get hash batch cmds err: %v", cmd.Err()) + continue + } + // 将结果转换为 *redis.StringStringMapCmd 类型 + rcmd, ok := cmd.(*redis.MapStringStringCmd) + if !ok { + logger.Errorf("Failed to get hash batch type err: %v", cmd.Err()) + continue + } + + key := "-" + args := rcmd.Args() + if len(args) > 0 { + key = fmt.Sprint(args[1]) + } + + result[key] = rcmd.Val() + } + }(i) + } + + wg.Wait() + return result, nil +} + +// GetHash 获得缓存数据 +func GetHash(source, key, field string) (string, error) { + // 数据源 + rdb := RDB(source) + if rdb == nil { + return "", fmt.Errorf("redis not client") + } + + ctx := context.Background() + v, err := rdb.HGet(ctx, key, field).Result() + if errors.Is(err, redis.Nil) { + return "", fmt.Errorf("no key field") + } + if err != nil { + return "", err + } + return v, nil +} + +// SetHash 设置缓存数据 +func SetHash(source, key string, value map[string]any) error { + // 数据源 + rdb := RDB(source) + if rdb == nil { + return fmt.Errorf("redis not client") + } + + ctx := context.Background() + err := rdb.HSet(ctx, key, value).Err() + if err != nil { + logger.Errorf("redis HSet err %v", err) + return err + } + return nil +} diff --git a/src/framework/database/redis/redis.go b/src/framework/database/redis/redis.go new file mode 100644 index 00000000..1ffb3cff --- /dev/null +++ b/src/framework/database/redis/redis.go @@ -0,0 +1,346 @@ +package redis + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/redis/go-redis/v9" + + "be.ems/src/framework/config" + "be.ems/src/framework/logger" +) + +// Redis连接实例 +var rdbMap = make(map[string]*redis.Client) + +// Connect 连接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 %d %s connection is successful.", k, client["db"].(int), pong) + rdbMap[k] = rdb + } +} + +// Close 关闭Redis实例 +func Close() { + for _, rdb := range rdbMap { + if err := rdb.Close(); err != nil { + logger.Errorf("redis db close: %s", err) + } + } +} + +// RDB 获取实例 +func RDB(source string) *redis.Client { + // 不指定时获取默认实例 + if source == "" { + source = config.Get("redis.defaultDataSourceName").(string) + } + return rdbMap[source] +} + +// Info 获取redis服务信息 +func Info(source string) map[string]map[string]string { + infoObj := make(map[string]map[string]string) + // 数据源 + rdb := RDB(source) + if rdb == nil { + return infoObj + } + + ctx := context.Background() + info, err := rdb.Info(ctx).Result() + if err != nil { + return infoObj + } + + 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 := RDB(source) + if rdb == nil { + return 0 + } + + 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 { + statsObjArr := make([]map[string]string, 0) + // 数据源 + rdb := RDB(source) + if rdb == nil { + return statsObjArr + } + + ctx := context.Background() + commandstats, err := rdb.Info(ctx, "commandstats").Result() + if err != nil { + return statsObjArr + } + + 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 +} + +// GetExpire 获取键的剩余有效时间(秒) +func GetExpire(source string, key string) (int64, error) { + // 数据源 + rdb := RDB(source) + if rdb == nil { + return 0, fmt.Errorf("redis not client") + } + + ctx := context.Background() + ttl, err := rdb.TTL(ctx, key).Result() + if err != nil { + return 0, err + } + return int64(ttl.Seconds()), nil +} + +// GetKeys 获得缓存数据的key列表 +func GetKeys(source string, pattern string) ([]string, error) { + keys := make([]string, 0) + // 数据源 + rdb := RDB(source) + if rdb == nil { + return keys, fmt.Errorf("redis not client") + } + + // 游标 + var cursor uint64 = 0 + var count int64 = 100 + ctx := context.Background() + // 循环遍历获取匹配的键 + for { + // 使用 SCAN 命令获取匹配的键 + batchKeys, nextCursor, err := rdb.Scan(ctx, cursor, pattern, count).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 +} + +// GetBatch 批量获得缓存数据 +func GetBatch(source string, keys []string) ([]any, error) { + result := make([]any, 0) + if len(keys) == 0 { + return result, fmt.Errorf("not keys") + } + + // 数据源 + rdb := RDB(source) + if rdb == nil { + return result, fmt.Errorf("redis not client") + } + + // 获取缓存数据 + v, err := rdb.MGet(context.Background(), keys...).Result() + if err != nil || errors.Is(err, redis.Nil) { + logger.Errorf("failed to get batch data: %v", err) + return result, err + } + return v, nil +} + +// Get 获得缓存数据 +func Get(source, key string) (string, error) { + // 数据源 + rdb := RDB(source) + if rdb == nil { + return "", fmt.Errorf("redis not client") + } + + ctx := context.Background() + v, err := rdb.Get(ctx, key).Result() + if errors.Is(err, redis.Nil) { + return "", fmt.Errorf("no keys") + } + if err != nil { + return "", err + } + return v, nil +} + +// Has 判断是否存在 +func Has(source string, keys ...string) (int64, error) { + // 数据源 + rdb := RDB(source) + if rdb == nil { + return 0, fmt.Errorf("redis not client") + } + + ctx := context.Background() + exists, err := rdb.Exists(ctx, keys...).Result() + if err != nil { + return 0, err + } + return exists, nil +} + +// Set 设置缓存数据 +func Set(source, key string, value any) error { + // 数据源 + rdb := RDB(source) + if rdb == nil { + return fmt.Errorf("redis not client") + } + + ctx := context.Background() + err := rdb.Set(ctx, key, value, 0).Err() + if err != nil { + logger.Errorf("redis Set err %v", err) + return err + } + return nil +} + +// SetByExpire 设置缓存数据与过期时间 +func SetByExpire(source, key string, value any, expiration time.Duration) error { + // 数据源 + rdb := RDB(source) + if rdb == nil { + return fmt.Errorf("redis not client") + } + + ctx := context.Background() + err := rdb.Set(ctx, key, value, expiration).Err() + if err != nil { + logger.Errorf("redis SetByExpire err %v", err) + return err + } + return nil +} + +// Del 删除单个 +func Del(source string, key string) error { + // 数据源 + rdb := RDB(source) + if rdb == nil { + return fmt.Errorf("redis not client") + } + + ctx := context.Background() + if err := rdb.Del(ctx, key).Err(); err != nil { + logger.Errorf("redis Del err %v", err) + return err + } + return nil +} + +// DelKeys 删除多个 +func DelKeys(source string, keys []string) error { + if len(keys) == 0 { + return fmt.Errorf("no keys") + } + + // 数据源 + rdb := RDB(source) + if rdb == nil { + return fmt.Errorf("redis not client") + } + + ctx := context.Background() + if err := rdb.Del(ctx, keys...).Err(); err != nil { + logger.Errorf("redis DelKeys err %v", err) + return err + } + return nil +} + +// RateLimit 限流查询并记录 +func RateLimit(source, limitKey string, time, count int64) (int64, error) { + // 数据源 + rdb := RDB(source) + if rdb == nil { + return 0, fmt.Errorf("redis not client") + } + + ctx := context.Background() + result, err := rateLimitCommand.Run(ctx, rdb, []string{limitKey}, time, count).Result() + if err != nil { + logger.Errorf("redis lua script err %v", err) + return 0, err + } + return result.(int64), err +} + +// 声明定义限流脚本命令 +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);`) diff --git a/src/framework/errorcatch/errorcatch.go b/src/framework/errorcatch/errorcatch.go index 6e6f9e94..e18843e1 100644 --- a/src/framework/errorcatch/errorcatch.go +++ b/src/framework/errorcatch/errorcatch.go @@ -5,7 +5,7 @@ import ( "be.ems/src/framework/config" "be.ems/src/framework/logger" - "be.ems/src/framework/vo/result" + "be.ems/src/framework/resp" "github.com/gin-gonic/gin" ) @@ -20,14 +20,14 @@ func ErrorCatch() gin.HandlerFunc { // 返回错误响应给客户端 if config.Env() == "prod" { - c.JSON(500, result.CodeMsg(500, "Internal Server Errors")) + c.JSON(500, resp.CodeMsg(500, "Internal Server Errors")) } else { // 通过实现 error 接口的 Error() 方法自定义错误类型进行捕获 switch v := err.(type) { case error: - c.JSON(500, result.CodeMsg(500, v.Error())) + c.JSON(500, resp.CodeMsg(500, v.Error())) default: - c.JSON(500, result.CodeMsg(500, fmt.Sprint(err))) + c.JSON(500, resp.CodeMsg(500, fmt.Sprint(err))) } } diff --git a/src/framework/i18n/i18n.go b/src/framework/i18n/i18n.go index 657964fa..fbb70ccb 100644 --- a/src/framework/i18n/i18n.go +++ b/src/framework/i18n/i18n.go @@ -5,110 +5,54 @@ import ( "regexp" "strings" - systemService "be.ems/src/modules/system/service" + "be.ems/src/framework/constants" + "be.ems/src/framework/database/redis" ) -// localeItem 国际化数据项 -type localeItem struct { - Key string `json:"key"` - Value string `json:"value"` - Code string `json:"code"` -} - -// localeMap 国际化数据组 -var localeMap = make(map[string][]localeItem) - -// ClearLocaleData 清空国际化数据 -func ClearLocaleData() { - localeMap = make(map[string][]localeItem) -} - -// LoadLocaleData 加载国际化数据 -func LoadLocaleData(language string) []localeItem { - dictType := fmt.Sprintf("i18n_%s", language) - dictTypeList := systemService.NewSysDictType.DictDataCache(dictType) - localeData := []localeItem{} - for _, v := range dictTypeList { - localeData = append(localeData, localeItem{ - Key: v.DictLabel, - Value: v.DictValue, - Code: v.DictCode, - }) - } - localeMap[language] = localeData - return localeData -} - -// UpdateKeyValue 更新键对应的值 -func UpdateKeyValue(language, key, value string) bool { - arr, ok := localeMap[language] - if !ok || len(arr) == 0 { - arr = LoadLocaleData(language) - } - - code := "" - if key == "" { - return false - } - for _, v := range arr { - if v.Key == key { - code = v.Code - break - } - } - - // 更新字典数据 - sysDictDataService := systemService.NewSysDictData - item := sysDictDataService.SelectDictDataByCode(code) - if item.DictCode == code && item.DictLabel == key { - item.DictValue = value - row := sysDictDataService.UpdateDictData(item) - if row > 0 { - delete(localeMap, language) - return true - } - } - return false -} - // TFindKeyPrefix 翻译值查找键 值前缀匹配 func TFindKeyPrefix(language, keyPrefix, value string) string { key := value - if value == "" { + if key == "" { return key } - arr, ok := localeMap[language] - if !ok || len(arr) == 0 { - arr = LoadLocaleData(language) + + langKey := constants.CACHE_I18N + ":" + keyPrefix + "*" + prefixKeys, err := redis.GetKeys("", langKey) + if err != nil { + return key + } + mkv, err := redis.GetHashBatch("", prefixKeys) + if err != nil { + return key } - for _, v := range arr { - if strings.HasPrefix(v.Key, keyPrefix) && strings.HasPrefix(v.Value, value) { - key = v.Key - break + for k, m := range mkv { + // 跳过-号数据 i18n:menu.system.menu + + if v, ok := m[language]; ok { + if strings.HasPrefix(v, value) { + key = k[len(constants.CACHE_I18N)+1:] + break + } } } return key } // TKey 翻译键 +// language: zh-中文 en-英文 func TKey(language, key string) string { value := key if key == "" { return value } - arr, ok := localeMap[language] - if !ok || len(arr) == 0 { - arr = LoadLocaleData(language) - } - for _, v := range arr { - if v.Key == key { - value = v.Value - break - } + langKey := constants.CACHE_I18N + ":" + key + output, err := redis.GetHash("", langKey, language) + if err != nil { + return value } - return value + return output } // TTemplate 翻译模板字符串 diff --git a/src/framework/utils/ip2region/binding.go b/src/framework/ip2region/binding.go similarity index 100% rename from src/framework/utils/ip2region/binding.go rename to src/framework/ip2region/binding.go diff --git a/src/framework/utils/ip2region/ip2region.go b/src/framework/ip2region/ip2region.go similarity index 62% rename from src/framework/utils/ip2region/ip2region.go rename to src/framework/ip2region/ip2region.go index 0dc48f31..18afd863 100644 --- a/src/framework/utils/ip2region/ip2region.go +++ b/src/framework/ip2region/ip2region.go @@ -1,39 +1,36 @@ package ip2region import ( + "be.ems/src/framework/logger" + "embed" "strings" "time" - - "be.ems/src/framework/logger" ) -// 网络地址(内网) -const LOCAT_HOST = "127.0.0.1" +// LocalHost 网络地址(内网) +const LocalHost = "127.0.0.1" // 全局查询对象 var searcher *Searcher -//go:embed ip2region.xdb -var ip2regionDB embed.FS +// InitSearcher 初始化查询对象 +func InitSearcher(assetsDir *embed.FS) { + if searcher != nil { + return + } -func init() { - // 从 dbPath 加载整个 xdb 到内存 - buf, err := ip2regionDB.ReadFile("ip2region.xdb") + // 从 embed.FS 中读取内嵌文件 + fileBuff, err := assetsDir.ReadFile("src/assets/ip2region.xdb") if err != nil { logger.Fatalf("failed error load xdb from : %s\n", err) return } - - // 用全局的 cBuff 创建完全基于内存的查询对象。 - base, err := NewWithBuffer(buf) - if err != nil { + // 用全局的 fileBuff 创建完全基于内存的查询对象。 + if searcher, err = NewWithBuffer(fileBuff); err != nil { logger.Errorf("failed error create searcher with content: %s\n", err) return } - - // 赋值到全局查询对象 - searcher = base } // RegionSearchByIp 查询IP所在地 @@ -41,14 +38,13 @@ func init() { // 国家|区域|省份|城市|ISP func RegionSearchByIp(ip string) (string, int, int64) { ip = ClientIP(ip) - if ip == LOCAT_HOST { - // "0|0|0|内网IP|内网IP" + if ip == LocalHost { return "0|0|0|app.common.noIPregion|app.common.noIPregion", 0, 0 } tStart := time.Now() region, err := searcher.SearchByStr(ip) if err != nil { - logger.Errorf("failed to SearchIP(%s): %s\n", ip, err) + logger.Errorf("failed to RegionSearchByIp(%s): %s\n", ip, err) return "0|0|0|0|0", 0, 0 } return region, 0, time.Since(tStart).Milliseconds() @@ -59,21 +55,21 @@ func RegionSearchByIp(ip string) (string, int, int64) { // 218.4.167.70 江苏省 苏州市 func RealAddressByIp(ip string) string { ip = ClientIP(ip) - if ip == LOCAT_HOST { - return "app.common.noIPregion" // 内网IP + if ip == LocalHost { + return "app.common.noIPregion" } region, err := searcher.SearchByStr(ip) if err != nil { - logger.Errorf("failed to SearchIP(%s): %s\n", ip, err) - return "app.common.unknown" // 未知 + logger.Errorf("failed to RealAddressByIp(%s): %s\n", ip, err) + return "app.common.unknown" } parts := strings.Split(region, "|") province := parts[2] city := parts[3] + if province == "0" && city == "0" { + return "app.common.unknown" + } if province == "0" && city != "0" { - if city == "内网IP" { - return "app.common.noIPregion" // 内网IP - } return city } return province + " " + city @@ -86,8 +82,8 @@ 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 + if ip == LocalHost || ip == "::1" { + return LocalHost } return ip } diff --git a/src/framework/utils/ip2region/util.go b/src/framework/ip2region/util.go similarity index 99% rename from src/framework/utils/ip2region/util.go rename to src/framework/ip2region/util.go index cfef29ea..4b76c83a 100644 --- a/src/framework/utils/ip2region/util.go +++ b/src/framework/ip2region/util.go @@ -172,4 +172,3 @@ func LoadContentFromFile(dbFile string) ([]byte, error) { return cBuff, nil } - diff --git a/src/framework/middleware/collectlogs/operate_log.go b/src/framework/middleware/collectlogs/operate_log.go index 9528ace3..6af8b444 100644 --- a/src/framework/middleware/collectlogs/operate_log.go +++ b/src/framework/middleware/collectlogs/operate_log.go @@ -7,12 +7,11 @@ import ( "strings" "time" - "be.ems/src/framework/constants/common" - tokenConstants "be.ems/src/framework/constants/token" + "be.ems/src/framework/constants" "be.ems/src/framework/i18n" - "be.ems/src/framework/utils/ctx" + "be.ems/src/framework/reqctx" + "be.ems/src/framework/resp" "be.ems/src/framework/utils/parse" - "be.ems/src/framework/vo/result" "be.ems/src/modules/system/model" "be.ems/src/modules/system/service" @@ -91,7 +90,7 @@ func OptionNew(title, businessType string) Options { func OperateLog(options Options) gin.HandlerFunc { return func(c *gin.Context) { c.Set("startTime", time.Now()) - language := ctx.AcceptLanguage(c) + language := reqctx.AcceptLanguage(c) // 函数名 funcName := c.HandlerName() @@ -99,37 +98,31 @@ func OperateLog(options Options) gin.HandlerFunc { funcName = funcName[lastDotIndex+1:] // 解析ip地址 - ipaddr, location := ctx.IPAddrLocation(c) + ipaddr, location := reqctx.IPAddrLocation(c) // 获取登录用户信息 - loginUser, err := ctx.LoginUser(c) + loginUser, err := reqctx.LoginUser(c) if err != nil { - c.JSON(401, result.CodeMsg(401, i18n.TKey(language, err.Error()))) + c.JSON(401, resp.CodeMsg(401, i18n.TKey(language, err.Error()))) c.Abort() // 停止执行后续的处理函数 return } // 操作日志记录 operLog := model.SysLogOperate{ - Title: options.Title, - BusinessType: options.BusinessType, - OperatorType: options.OperatorType, - Method: funcName, - OperURL: c.Request.URL.Path, - 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 + Title: options.Title, + BusinessType: options.BusinessType, + OperaMethod: funcName, + OperaUrl: c.Request.URL.Path, + OperaUrlMethod: c.Request.Method, + OperaIp: ipaddr, + OperaLocation: location, + OperaBy: loginUser.User.UserName, } // 是否需要保存request,参数和值 if options.IsSaveRequestData { - params := ctx.RequestParamsMap(c) + params := reqctx.RequestParamsMap(c) // 敏感属性字段进行掩码 processSensitiveFields(params) jsonStr, _ := json.Marshal(params) @@ -137,7 +130,7 @@ func OperateLog(options Options) gin.HandlerFunc { if len(paramsStr) > 2000 { paramsStr = paramsStr[:2000] } - operLog.OperParam = paramsStr + operLog.OperaParam = paramsStr } // 调用下一个处理程序 @@ -146,9 +139,9 @@ func OperateLog(options Options) gin.HandlerFunc { // 响应状态 status := c.Writer.Status() if status == 200 { - operLog.Status = common.STATUS_YES + operLog.StatusFlag = constants.STATUS_YES } else { - operLog.Status = common.STATUS_NO + operLog.StatusFlag = constants.STATUS_NO } // 是否需要保存response,参数和值 @@ -157,16 +150,16 @@ func OperateLog(options Options) gin.HandlerFunc { 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 + operLog.OperaMsg = msg } // 日志记录时间 duration := time.Since(c.GetTime("startTime")) operLog.CostTime = duration.Milliseconds() - operLog.OperTime = time.Now().UnixMilli() + operLog.OperaTime = time.Now().UnixMilli() // 保存操作记录到数据库 - service.NewSysLogOperateImpl.InsertSysLogOperate(operLog) + service.NewSysLogOperate.Insert(operLog) } } @@ -179,8 +172,8 @@ var maskProperties []string = []string{ "oldPassword", "newPassword", "confirmPassword", - tokenConstants.RESPONSE_FIELD, - tokenConstants.ACCESS_TOKEN, + constants.ACCESS_TOKEN, + constants.ACCESS_TOKEN_QUERY, } // processSensitiveFields 处理敏感属性字段 diff --git a/src/framework/middleware/crypto_api.go b/src/framework/middleware/crypto_api.go index 661e9701..6c0ede4c 100644 --- a/src/framework/middleware/crypto_api.go +++ b/src/framework/middleware/crypto_api.go @@ -8,10 +8,11 @@ import ( "strings" "be.ems/src/framework/config" - constResult "be.ems/src/framework/constants/result" "be.ems/src/framework/logger" + "be.ems/src/framework/resp" "be.ems/src/framework/utils/crypto" "be.ems/src/framework/utils/parse" + "github.com/gin-gonic/gin" ) @@ -53,10 +54,7 @@ func CryptoApi(requestDecrypt, responseEncrypt bool) gin.HandlerFunc { // 是否存在data字段数据 if contentDe == "" { - c.JSON(400, map[string]any{ - "code": constResult.CODE_ERROR, - "msg": "decrypt not found field data", - }) + c.JSON(400, resp.ErrMsg("decrypt not found field data")) c.Abort() // 停止执行后续的处理函数 return } @@ -66,10 +64,7 @@ func CryptoApi(requestDecrypt, responseEncrypt bool) gin.HandlerFunc { dataBodyStr, err := crypto.AESDecryptBase64(contentDe, apiKey) if err != nil { logger.Errorf("CryptoApi decrypt err => %v", err) - c.JSON(400, map[string]any{ - "code": constResult.CODE_ERROR, - "msg": "decrypted data could not be parsed", - }) + c.JSON(400, resp.ErrMsg("decrypted data could not be parsed")) c.Abort() // 停止执行后续的处理函数 return } @@ -110,19 +105,19 @@ func CryptoApi(requestDecrypt, responseEncrypt bool) gin.HandlerFunc { codeV, codeOk := resBody["code"] dataV, dataOk := resBody["data"] if codeOk && dataOk { - if parse.Number(codeV) == constResult.CODE_SUCCESS { + if parse.Number(codeV) == resp.CODE_SUCCESS { byteBodyData, _ := json.Marshal(dataV) // 加密-原数据头加入标记16位长度iv终止符 apiKey := config.Get("aes.apiKey").(string) contentEn, err := crypto.AESEncryptBase64("=:)"+string(byteBodyData), apiKey) if err != nil { logger.Errorf("CryptoApi encrypt err => %v", err) - rbw.ReplaceWrite([]byte(fmt.Sprintf(`{"code":"%d","msg":"encrypt err"}`, constResult.CODE_ERROR))) + rbw.ReplaceWrite([]byte(fmt.Sprintf(`{"code":"%d","msg":"encrypt err"}`, resp.CODE_ERROR))) } else { // 响应加密 byteBody, _ := json.Marshal(map[string]any{ - "code": constResult.CODE_ENCRYPT, - "msg": constResult.MSG_ENCRYPT, + "code": resp.CODE_ENCRYPT, + "msg": resp.MSG_ENCRYPT, "data": contentEn, }) rbw.ReplaceWrite(byteBody) diff --git a/src/framework/middleware/operate_log.go b/src/framework/middleware/operate_log.go new file mode 100644 index 00000000..75542f40 --- /dev/null +++ b/src/framework/middleware/operate_log.go @@ -0,0 +1,195 @@ +package middleware + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" + "time" + + "github.com/gin-gonic/gin" + + "be.ems/src/framework/constants" + "be.ems/src/framework/reqctx" + "be.ems/src/framework/resp" + "be.ems/src/framework/utils/parse" + "be.ems/src/modules/system/model" + "be.ems/src/modules/system/service" +) + +const ( + // BUSINESS_TYPE_OTHER 业务操作类型-其它 + BUSINESS_TYPE_OTHER = "0" + + // BUSINESS_TYPE_INSERT 业务操作类型-新增 + BUSINESS_TYPE_INSERT = "1" + + // BUSINESS_TYPE_UPDATE 业务操作类型-修改 + BUSINESS_TYPE_UPDATE = "2" + + // BUSINESS_TYPE_DELETE 业务操作类型-删除 + BUSINESS_TYPE_DELETE = "3" + + // BUSINESS_TYPE_GRANT 业务操作类型-授权 + BUSINESS_TYPE_GRANT = "4" + + // BUSINESS_TYPE_EXPORT 业务操作类型-导出 + BUSINESS_TYPE_EXPORT = "5" + + // BUSINESS_TYPE_IMPORT 业务操作类型-导入 + BUSINESS_TYPE_IMPORT = "6" + + // BUSINESS_TYPE_FORCE 业务操作类型-强退 + BUSINESS_TYPE_FORCE = "7" + + // BUSINESS_TYPE_CLEAN 业务操作类型-清空数据 + BUSINESS_TYPE_CLEAN = "8" +) + +// Options Option 操作日志参数 +type Options struct { + Title string `json:"title"` // 标题 + BusinessType string `json:"businessType"` // 类型,默认常量 BUSINESS_TYPE_OTHER + IsSaveRequestData bool `json:"isSaveRequestData"` // 是否保存请求的参数 + IsSaveResponseData bool `json:"isSaveResponseData"` // 是否保存响应的参数 +} + +// OptionNew 操作日志参数默认值 +// +// 标题 "title":"--" +// +// 类型 "businessType": BUSINESS_TYPE_OTHER +// +// 注意之后JSON反序列使用:c.ShouldBindBodyWithJSON(¶ms) +func OptionNew(title, businessType string) Options { + return Options{ + Title: title, + BusinessType: businessType, + IsSaveRequestData: true, + IsSaveResponseData: true, + } +} + +// 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 := reqctx.IPAddrLocation(c) + + // 获取登录用户信息 + loginUser, err := reqctx.LoginUser(c) + if err != nil { + c.JSON(401, resp.CodeMsg(401, "无效身份授权")) + c.Abort() // 停止执行后续的处理函数 + return + } + + // 操作日志记录 + operaLog := model.SysLogOperate{ + Title: options.Title, + BusinessType: options.BusinessType, + OperaMethod: funcName, + OperaUrl: c.Request.RequestURI, + OperaUrlMethod: c.Request.Method, + OperaIp: ipaddr, + OperaLocation: location, + OperaBy: loginUser.User.UserName, + } + + // 是否需要保存request,参数和值 + if options.IsSaveRequestData { + params := reqctx.RequestParamsMap(c) + // 敏感属性字段进行掩码 + processSensitiveFields(params) + jsonStr, _ := json.Marshal(params) + paramsStr := string(jsonStr) + if len(paramsStr) > 2000 { + paramsStr = paramsStr[:2000] + } + operaLog.OperaParam = paramsStr + } + + // 调用下一个处理程序 + c.Next() + + // 响应状态 + status := c.Writer.Status() + if status == 200 { + operaLog.StatusFlag = constants.STATUS_YES + } else { + operaLog.StatusFlag = constants.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) + operaLog.OperaMsg = msg + } + + // 日志记录时间 + duration := time.Since(c.GetTime("startTime")) + operaLog.CostTime = duration.Milliseconds() + operaLog.OperaTime = time.Now().UnixMilli() + + // 保存操作记录到数据库 + service.NewSysLogOperate.Insert(operaLog) + } +} + +// 敏感属性字段进行掩码 +var maskProperties = []string{ + "password", + "oldPassword", + "newPassword", + "confirmPassword", +} + +// processSensitiveFields 处理敏感属性字段 +func processSensitiveFields(obj interface{}) { + val := reflect.ValueOf(obj) + + switch val.Kind() { + case reflect.Map: + for _, key := range val.MapKeys() { + value := val.MapIndex(key) + keyStr := key.Interface().(string) + + // 遍历是否敏感属性 + hasMaskKey := false + for _, v := range maskProperties { + if v == keyStr { + hasMaskKey = true + break + } + } + + if hasMaskKey { + valueStr := value.Interface().(string) + if len(valueStr) > 100 { + valueStr = valueStr[0:100] + } + val.SetMapIndex(key, reflect.ValueOf(parse.SafeContent(valueStr))) + } else { + processSensitiveFields(value.Interface()) + } + } + case reflect.Slice, reflect.Array: + for i := 0; i < val.Len(); i++ { + processSensitiveFields(val.Index(i).Interface()) + } + // default: + // logger.Infof("processSensitiveFields unhandled case %v", val.Kind()) + } +} diff --git a/src/framework/middleware/pre_authorize.go b/src/framework/middleware/pre_authorize.go index ea2878b5..a19b87d6 100644 --- a/src/framework/middleware/pre_authorize.go +++ b/src/framework/middleware/pre_authorize.go @@ -4,12 +4,11 @@ import ( "strings" "be.ems/src/framework/config" - AdminConstants "be.ems/src/framework/constants/admin" - commonConstants "be.ems/src/framework/constants/common" + "be.ems/src/framework/constants" "be.ems/src/framework/i18n" - ctxUtils "be.ems/src/framework/utils/ctx" - tokenUtils "be.ems/src/framework/utils/token" - "be.ems/src/framework/vo/result" + "be.ems/src/framework/reqctx" + "be.ems/src/framework/resp" + "be.ems/src/framework/token" "github.com/gin-gonic/gin" ) @@ -44,17 +43,17 @@ func PreAuthorize(options map[string][]string) gin.HandlerFunc { enable = v.(bool) } if !enable { - loginUser, _ := ctxUtils.LoginUser(c) - loginUser.UserID = "2" - loginUser.User.UserID = "2" + loginUser, _ := reqctx.LoginUser(c) + loginUser.UserId = 2 + loginUser.User.UserId = 2 loginUser.User.UserName = "admin" loginUser.User.NickName = "admin" - c.Set(commonConstants.CTX_LOGIN_USER, loginUser) + c.Set(constants.CTX_LOGIN_USER, loginUser) c.Next() return } - language := ctxUtils.AcceptLanguage(c) + language := reqctx.AcceptLanguage(c) requestURI := c.Request.RequestURI @@ -72,32 +71,32 @@ func PreAuthorize(options map[string][]string) gin.HandlerFunc { } // 获取请求头标识信息 - tokenStr := ctxUtils.Authorization(c) + tokenStr := reqctx.Authorization(c) if tokenStr == "" { - c.JSON(401, result.CodeMsg(401, i18n.TKey(language, "app.common.err401"))) + c.JSON(401, resp.CodeMsg(401, i18n.TKey(language, "app.common.err401"))) c.Abort() // 停止执行后续的处理函数 return } // 验证令牌 - claims, err := tokenUtils.Verify(tokenStr) + claims, err := token.Verify(tokenStr) if err != nil { - c.JSON(401, result.CodeMsg(401, err.Error())) + c.JSON(401, resp.CodeMsg(401, err.Error())) c.Abort() // 停止执行后续的处理函数 return } // 获取缓存的用户信息 - loginUser := tokenUtils.LoginUser(claims) - if loginUser.UserID == "" { - c.JSON(401, result.CodeMsg(401, i18n.TKey(language, "app.common.err401"))) + loginUser := token.Info(claims) + if loginUser.UserId <= 0 { + c.JSON(401, resp.CodeMsg(401, i18n.TKey(language, "app.common.err401"))) c.Abort() // 停止执行后续的处理函数 return } // 检查刷新有效期后存入上下文 - tokenUtils.RefreshIn(&loginUser) - c.Set(commonConstants.CTX_LOGIN_USER, loginUser) + token.RefreshIn(&loginUser) + c.Set(constants.CTX_LOGIN_USER, loginUser) // 登录用户角色权限校验 if options != nil { @@ -109,7 +108,7 @@ func PreAuthorize(options map[string][]string) gin.HandlerFunc { verifyOk := verifyRolePermission(roles, perms, options) if !verifyOk { msg := i18n.TTemplate(language, "app.common.err403", map[string]any{"method": c.Request.Method, "requestURI": requestURI}) - c.JSON(403, result.CodeMsg(403, msg)) + c.JSON(403, resp.CodeMsg(403, msg)) c.Abort() // 停止执行后续的处理函数 return } @@ -129,7 +128,7 @@ func PreAuthorize(options map[string][]string) gin.HandlerFunc { // options 参数 func verifyRolePermission(roles, perms []string, options map[string][]string) bool { // 直接放行 管理员角色或任意权限 - if contains(roles, AdminConstants.ROLE_KEY) || contains(perms, AdminConstants.PERMISSION) { + if contains(roles, constants.SYS_ROLE_SYSTEM_KEY) || contains(perms, constants.SYS_PERMISSION_SYSTEM) { return true } opts := make([]bool, 4) diff --git a/src/framework/middleware/rate_limit.go b/src/framework/middleware/rate_limit.go index 368028d6..7b650b48 100644 --- a/src/framework/middleware/rate_limit.go +++ b/src/framework/middleware/rate_limit.go @@ -5,31 +5,30 @@ import ( "strings" "time" - "be.ems/src/framework/constants/cachekey" - "be.ems/src/framework/i18n" - "be.ems/src/framework/redis" - "be.ems/src/framework/utils/ctx" - "be.ems/src/framework/utils/ip2region" - "be.ems/src/framework/vo/result" - "github.com/gin-gonic/gin" + + "be.ems/src/framework/constants" + "be.ems/src/framework/database/redis" + "be.ems/src/framework/ip2region" + "be.ems/src/framework/reqctx" + "be.ems/src/framework/resp" ) const ( - // 默认策略全局限流 + // LIMIT_GLOBAL 默认策略全局限流 LIMIT_GLOBAL = 1 - // 根据请求者IP进行限流 + // LIMIT_IP 根据请求者IP进行限流 LIMIT_IP = 2 - // 根据用户ID进行限流 + // LIMIT_USER 根据用户ID进行限流 LIMIT_USER = 3 ) // LimitOption 请求限流参数 type LimitOption struct { - Time int64 `json:"time"` // 限流时间,单位秒 - Count int64 `json:"count"` // 限流次数 + Time int64 `json:"time"` // 限流时间,单位秒 5 + Count int64 `json:"count"` // 限流次数,单位次 10 Type int64 `json:"type"` // 限流条件类型,默认LIMIT_GLOBAL } @@ -43,8 +42,6 @@ type LimitOption struct { // 以便获取登录用户信息,无用户信息时默认为 GLOBAL func RateLimit(option LimitOption) gin.HandlerFunc { return func(c *gin.Context) { - language := ctx.AcceptLanguage(c) - // 初始可选参数数据 if option.Time < 5 { option.Time = 5 @@ -61,40 +58,46 @@ func RateLimit(option LimitOption) gin.HandlerFunc { lastDotIndex := strings.LastIndex(funcName, "/") funcName = funcName[lastDotIndex+1:] // 生成限流key - var limitKey string = cachekey.RATE_LIMIT_KEY + funcName + limitKey := constants.CACHE_RATE_LIMIT + ":" + funcName // 用户 if option.Type == LIMIT_USER { - loginUser, err := ctx.LoginUser(c) + loginUser, err := reqctx.LoginUser(c) if err != nil { - c.JSON(401, result.Err(map[string]any{ - "code": 401, - "msg": i18n.TKey(language, err.Error()), - })) + c.JSON(401, resp.CodeMsg(40003, err.Error())) c.Abort() // 停止执行后续的处理函数 return } - limitKey = cachekey.RATE_LIMIT_KEY + loginUser.UserID + ":" + funcName + limitKey = fmt.Sprintf("%s:%d:%s", constants.CACHE_RATE_LIMIT, loginUser.UserId, funcName) } // IP if option.Type == LIMIT_IP { clientIP := ip2region.ClientIP(c.ClientIP()) - limitKey = cachekey.RATE_LIMIT_KEY + clientIP + ":" + funcName + limitKey = constants.CACHE_RATE_LIMIT + ":" + clientIP + ":" + funcName } // 在Redis查询并记录请求次数 - rateCount, _ := redis.RateLimit("", limitKey, option.Time, option.Count) - rateTime, _ := redis.GetExpire("", limitKey) + rateCount, err := redis.RateLimit("", limitKey, option.Time, option.Count) + if err != nil { + c.JSON(200, resp.CodeMsg(4013, "访问过于频繁,请稍候再试")) + c.Abort() // 停止执行后续的处理函数 + return + } + rateTime, err := redis.GetExpire("", limitKey) + if err != nil { + c.JSON(200, resp.CodeMsg(4013, "访问过于频繁,请稍候再试")) + c.Abort() // 停止执行后续的处理函数 + return + } // 设置响应头中的限流声明字段 - 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))) // 重置时间戳 + 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()+rateTime)) // 重置时间戳 if rateCount >= option.Count { - // 访问过于频繁,请稍候再试 - c.JSON(200, i18n.TKey(language, "app.common.rateLimitTip")) + c.JSON(200, resp.CodeMsg(4013, "访问过于频繁,请稍候再试")) c.Abort() // 停止执行后续的处理函数 return } diff --git a/src/framework/middleware/repeat/repeat.go b/src/framework/middleware/repeat/repeat.go index 1d08c594..78a09447 100644 --- a/src/framework/middleware/repeat/repeat.go +++ b/src/framework/middleware/repeat/repeat.go @@ -5,12 +5,12 @@ import ( "strconv" "time" - "be.ems/src/framework/constants/cachekey" + "be.ems/src/framework/constants" + "be.ems/src/framework/database/redis" + "be.ems/src/framework/ip2region" "be.ems/src/framework/logger" - "be.ems/src/framework/redis" - "be.ems/src/framework/utils/ctx" - "be.ems/src/framework/utils/ip2region" - "be.ems/src/framework/vo/result" + "be.ems/src/framework/reqctx" + "be.ems/src/framework/resp" "github.com/gin-gonic/gin" ) @@ -33,7 +33,7 @@ func RepeatSubmit(interval int64) gin.HandlerFunc { } // 提交参数 - params := ctx.RequestParamsMap(c) + params := reqctx.RequestParamsMap(c) paramsJSONByte, err := json.Marshal(params) if err != nil { logger.Errorf("RepeatSubmit params json marshal err: %v", err) @@ -42,7 +42,7 @@ func RepeatSubmit(interval int64) gin.HandlerFunc { // 唯一标识(指定key + 客户端IP + 请求地址) clientIP := ip2region.ClientIP(c.ClientIP()) - repeatKey := cachekey.REPEAT_SUBMIT_KEY + clientIP + ":" + c.Request.RequestURI + repeatKey := constants.CACHE_REPEAT_SUBMIT + clientIP + ":" + c.Request.RequestURI // 在Redis查询并记录请求次数 repeatStr, _ := redis.Get("", repeatKey) @@ -61,7 +61,7 @@ func RepeatSubmit(interval int64) gin.HandlerFunc { // 小于间隔时间且参数内容一致 if compareTime < interval && compareParams { // 不允许重复提交,请稍候再试 - c.JSON(200, result.ErrMsg("Duplicate submissions are not allowed. Please try again later")) + c.JSON(200, resp.ErrMsg("Duplicate submissions are not allowed. Please try again later")) c.Abort() return } diff --git a/src/framework/middleware/repeat_submit.go b/src/framework/middleware/repeat_submit.go new file mode 100644 index 00000000..79753319 --- /dev/null +++ b/src/framework/middleware/repeat_submit.go @@ -0,0 +1,84 @@ +package middleware + +import ( + "encoding/json" + "strconv" + "time" + + "github.com/gin-gonic/gin" + + "be.ems/src/framework/constants" + "be.ems/src/framework/database/redis" + "be.ems/src/framework/ip2region" + "be.ems/src/framework/logger" + "be.ems/src/framework/reqctx" + "be.ems/src/framework/resp" +) + +// repeatParam 重复提交参数的类型定义 +type repeatParam struct { + Time int64 `json:"time"` + Params string `json:"params"` +} + +// RepeatSubmit 防止表单重复提交,小于间隔时间视为重复提交 +// +// 间隔时间(单位秒) 默认:5 +// +// 注意之后JSON反序列使用:c.ShouldBindBodyWithJSON(¶ms) +func RepeatSubmit(interval int64) gin.HandlerFunc { + return func(c *gin.Context) { + if interval < 5 { + interval = 5 + } + + // 提交参数 + params := reqctx.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 := constants.CACHE_REPEAT_SUBMIT + ":" + 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, resp.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() + } +} diff --git a/src/framework/middleware/report.go b/src/framework/middleware/report.go index 3a6522ba..dc79caff 100644 --- a/src/framework/middleware/report.go +++ b/src/framework/middleware/report.go @@ -1,11 +1,11 @@ package middleware import ( + "be.ems/src/framework/logger" + "runtime" "time" - "be.ems/src/framework/logger" - "github.com/gin-gonic/gin" ) @@ -19,10 +19,7 @@ func Report() gin.HandlerFunc { // 计算请求处理时间,并打印日志 duration := time.Since(start) - // logger.Infof("%s %s report end=> %v", c.Request.Method, c.Request.RequestURI, duration) - // 获取当前活跃的goroutine数量 - num := runtime.NumGoroutine() - // logger.Infof("当前活跃的goroutine数量 %d\n", num) - logger.Infof("\n访问接口 %s %s\n总耗时 %v\n当前活跃的goroutine数量 %d\n", c.Request.Method, c.Request.RequestURI, duration, num) + numGoroutines := runtime.NumGoroutine() + logger.Infof("\n访问接口: %s %s\n总耗时: %v\n当前活跃的Goroutine数量: %d", c.Request.Method, c.Request.RequestURI, duration, numGoroutines) } } diff --git a/src/framework/middleware/security/referer.go b/src/framework/middleware/security/referer.go index 1fbc99a4..a74a2672 100644 --- a/src/framework/middleware/security/referer.go +++ b/src/framework/middleware/security/referer.go @@ -4,7 +4,7 @@ import ( "net/url" "be.ems/src/framework/config" - "be.ems/src/framework/vo/result" + "be.ems/src/framework/resp" "github.com/gin-gonic/gin" ) @@ -43,7 +43,7 @@ func referer(c *gin.Context) { referer := c.GetHeader("Referer") if referer == "" { // 无效 Referer 未知 - c.AbortWithStatusJSON(200, result.ErrMsg("Invalid referer unknown")) + c.AbortWithStatusJSON(200, resp.ErrMsg("Invalid referer unknown")) return } @@ -51,7 +51,7 @@ func referer(c *gin.Context) { u, err := url.Parse(referer) if err != nil { // 无效 Referer 未知 - c.AbortWithStatusJSON(200, result.ErrMsg("Invalid referer unknown")) + c.AbortWithStatusJSON(200, resp.ErrMsg("Invalid referer unknown")) return } host := u.Host @@ -73,7 +73,7 @@ func referer(c *gin.Context) { } if !ok { // 无效 Referer - c.AbortWithStatusJSON(200, result.ErrMsg("Invalid referer "+host)) + c.AbortWithStatusJSON(200, resp.ErrMsg("Invalid referer "+host)) return } } diff --git a/src/framework/redis/redis.go b/src/framework/redis/redis.go deleted file mode 100644 index 88e5cdd0..00000000 --- a/src/framework/redis/redis.go +++ /dev/null @@ -1,442 +0,0 @@ -package redis - -import ( - "context" - "fmt" - "strings" - "sync" - "time" - - "be.ems/src/framework/config" - "be.ems/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 ConnectPush(source string, rdb *redis.Client) { - if rdb == nil { - delete(rdbMap, source) - return - } - rdbMap[source] = rdb -} - -// 连接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, match string) ([]string, error) { - // 数据源 - rdb := DefaultRDB() - if source != "" { - rdb = RDB(source) - } - - keys := make([]string, 0) - ctx := context.Background() - iter := rdb.Scan(ctx, 0, match, 1000).Iterator() - if err := iter.Err(); err != nil { - logger.Errorf("Failed to scan keys: %v", err) - return keys, err - } - for iter.Next(ctx) { - keys = append(keys, iter.Val()) - } - 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 -} - -// 批量获得缓存数据 [key]result -func GetHashBatch(source string, keys []string) (map[string]map[string]string, error) { - result := make(map[string]map[string]string, 0) - if len(keys) == 0 { - return result, fmt.Errorf("not keys") - } - - // 数据源 - rdb := DefaultRDB() - if source != "" { - rdb = RDB(source) - } - - // 创建一个有限的并发控制信号通道 - sem := make(chan struct{}, 10) - var wg sync.WaitGroup - var mt sync.Mutex - batchSize := 1000 - total := len(keys) - if total < batchSize { - batchSize = total - } - - for i := 0; i < total; i += batchSize { - wg.Add(1) - go func(start int) { - ctx := context.Background() - // 并发控制,限制同时执行的 Goroutine 数量 - sem <- struct{}{} - defer func() { - <-sem - ctx.Done() - wg.Done() - }() - - // 检查索引是否越界 - end := start + batchSize - if end > total { - end = total - } - pipe := rdb.Pipeline() - for _, key := range keys[start:end] { - pipe.HGetAll(ctx, key) - } - - cmds, err := pipe.Exec(ctx) - if err != nil { - logger.Errorf("Failed to get hash batch exec err: %v", err) - return - } - - // 将结果添加到 result map 并发访问 - mt.Lock() - defer mt.Unlock() - - // 处理命令结果 - for _, cmd := range cmds { - if cmd.Err() != nil { - logger.Errorf("Failed to get hash batch cmds err: %v", cmd.Err()) - continue - } - // 将结果转换为 *redis.StringStringMapCmd 类型 - rcmd, ok := cmd.(*redis.MapStringStringCmd) - if !ok { - logger.Errorf("Failed to get hash batch type err: %v", cmd.Err()) - continue - } - - key := "-" - args := rcmd.Args() - if len(args) > 0 { - key = fmt.Sprint(args[1]) - } - - result[key] = rcmd.Val() - } - }(i) - } - - wg.Wait() - return result, 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 -} diff --git a/src/framework/reqctx/auth.go b/src/framework/reqctx/auth.go new file mode 100644 index 00000000..f88149a7 --- /dev/null +++ b/src/framework/reqctx/auth.go @@ -0,0 +1,160 @@ +package reqctx + +import ( + "fmt" + "strings" + + "github.com/gin-gonic/gin" + + "be.ems/src/framework/config" + "be.ems/src/framework/constants" + "be.ems/src/framework/token" +) + +// LoginUser 登录用户信息 +func LoginUser(c *gin.Context) (token.TokenInfo, error) { + value, exists := c.Get(constants.CTX_LOGIN_USER) + if exists && value != nil { + return value.(token.TokenInfo), nil + } + return token.TokenInfo{}, fmt.Errorf("invalid login user information") +} + +// LoginUserToUserID 登录用户信息-用户ID +func LoginUserToUserID(c *gin.Context) int64 { + info, err := LoginUser(c) + if err != nil { + return 0 + } + return info.UserId +} + +// LoginUserToUserName 登录用户信息-用户名称 +func LoginUserToUserName(c *gin.Context) string { + info, err := LoginUser(c) + if err != nil { + return "" + } + return info.User.UserName +} + +// LoginUserByContainRoles 登录用户信息-包含角色KEY +func LoginUserByContainRoles(c *gin.Context, target string) bool { + info, err := LoginUser(c) + if err != nil { + return false + } + if config.IsSystemUser(info.UserId) { + return true + } + roles := info.User.Roles + for _, item := range roles { + if item.RoleKey == target { + return true + } + } + return false +} + +// LoginUserByContainPerms 登录用户信息-包含权限标识 +func LoginUserByContainPerms(c *gin.Context, target string) bool { + loginUser, err := LoginUser(c) + if err != nil { + return false + } + if config.IsSystemUser(loginUser.UserId) { + return true + } + perms := loginUser.Permissions + for _, str := range perms { + if str == target { + return true + } + } + return false +} + +// LoginUserToDataScopeSQL 登录用户信息-角色数据范围过滤SQL字符串 +func LoginUserToDataScopeSQL(c *gin.Context, deptAlias string, userAlias string) string { + dataScopeSQL := "" + // 登录用户信息 + info, err := LoginUser(c) + if err != nil { + return dataScopeSQL + } + userInfo := info.User + + // 如果是系统管理员,则不过滤数据 + if config.IsSystemUser(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 constants.ROLE_SCOPE_ALL == dataScope { + break + } + + if constants.ROLE_SCOPE_CUSTOM != dataScope { + hasKey := false + for _, key := range scopeKeys { + if key == dataScope { + hasKey = true + break + } + } + if hasKey { + continue + } + } + + if constants.ROLE_SCOPE_CUSTOM == dataScope { + sql := fmt.Sprintf(`%s.dept_id IN + ( SELECT dept_id FROM sys_role_dept WHERE role_id = %d ) + AND %s.dept_id NOT IN + ( + SELECT d.parent_id FROM sys_dept d + INNER JOIN sys_role_dept rd ON rd.dept_id = d.dept_id + AND rd.role_id = %d + )`, deptAlias, role.RoleId, deptAlias, role.RoleId) + conditions = append(conditions, sql) + } + + if constants.ROLE_SCOPE_DEPT == dataScope { + sql := fmt.Sprintf("%s.dept_id = %d", deptAlias, userInfo.DeptId) + conditions = append(conditions, sql) + } + + if constants.ROLE_SCOPE_DEPT_CHILD == dataScope { + sql := fmt.Sprintf("%s.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = %d OR find_in_set(%d, ancestors ) )", deptAlias, userInfo.DeptId, userInfo.DeptId) + conditions = append(conditions, sql) + } + + if constants.ROLE_SCOPE_SELF == dataScope { + if userAlias == "" { + sql := fmt.Sprintf("%s.dept_id = %d", deptAlias, userInfo.DeptId) + conditions = append(conditions, sql) + } else { + sql := fmt.Sprintf("%s.user_id = %d", userAlias, userInfo.UserId) + conditions = append(conditions, sql) + } + } + + // 记录角色范围 + scopeKeys = append(scopeKeys, dataScope) + } + + // 构建查询条件语句 + if len(conditions) > 0 { + dataScopeSQL = fmt.Sprintf(" ( %s ) ", strings.Join(conditions, " OR ")) + } + return dataScopeSQL +} diff --git a/src/framework/reqctx/context.go b/src/framework/reqctx/context.go new file mode 100644 index 00000000..dc597025 --- /dev/null +++ b/src/framework/reqctx/context.go @@ -0,0 +1,106 @@ +package reqctx + +import ( + "strings" + + "github.com/gin-gonic/gin" + "golang.org/x/text/language" + + "be.ems/src/framework/constants" +) + +// QueryMap Query参数转换Map +func QueryMap(c *gin.Context) map[string]string { + queryValues := c.Request.URL.Query() + queryParams := make(map[string]string, len(queryValues)) + 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, 0) + c.ShouldBindBodyWithJSON(¶ms) + return params +} + +// RequestParamsMap 请求参数转换Map +func RequestParamsMap(c *gin.Context) map[string]any { + params := make(map[string]any, 0) + // json + if strings.HasPrefix(c.ContentType(), "application/json") { + c.ShouldBindBodyWithJSON(¶ms) + } + + // 表单 + formParams := c.Request.PostForm + for key, value := range formParams { + if _, ok := params[key]; !ok { + params[key] = value[0] + } + } + + // 查询 + queryParams := c.Request.URL.Query() + for key, value := range queryParams { + if _, ok := params[key]; !ok { + params[key] = value[0] + } + } + return params +} + +// Authorization 解析请求头 +func Authorization(c *gin.Context) string { + // Query请求查询 + if authQuery, ok := c.GetQuery(constants.ACCESS_TOKEN); ok && authQuery != "" { + return authQuery + } + // Header请求头 + if authHeader := c.GetHeader(constants.ACCESS_TOKEN); authHeader != "" { + return authHeader + } + + // Query请求查询 + if authQuery, ok := c.GetQuery(constants.ACCESS_TOKEN_QUERY); ok && authQuery != "" { + return authQuery + } + // Header请求头 + authHeader := c.GetHeader(constants.HEADER_KEY) + if authHeader == "" { + return "" + } + // 拆分 Authorization 请求头,提取 JWT 令牌部分 + arr := strings.SplitN(authHeader, constants.HEADER_PREFIX, 2) + if len(arr) < 2 { + return "" + } + return arr[1] +} + +// AcceptLanguage 解析客户端接收语言 zh:中文 en: 英文 +func AcceptLanguage(c *gin.Context) string { + preferredLanguage := language.English + + // Query请求查询 + if v, ok := c.GetQuery("language"); ok && v != "" { + tags, _, _ := language.ParseAcceptLanguage(v) + if len(tags) > 0 { + preferredLanguage = tags[0] + } + } + // Header请求头 + if v := c.GetHeader("Accept-Language"); v != "" { + tags, _, _ := language.ParseAcceptLanguage(v) + if len(tags) > 0 { + preferredLanguage = tags[0] + } + } + + // 只取前缀 + lang := preferredLanguage.String() + arr := strings.Split(lang, "-") + return arr[0] +} diff --git a/src/framework/reqctx/param.go b/src/framework/reqctx/param.go new file mode 100644 index 00000000..a74390c8 --- /dev/null +++ b/src/framework/reqctx/param.go @@ -0,0 +1,35 @@ +package reqctx + +import ( + "github.com/gin-gonic/gin" + + "be.ems/src/framework/ip2region" + "be.ems/src/framework/utils/ua" +) + +// IPAddrLocation 解析ip地址 +func IPAddrLocation(c *gin.Context) (string, string) { + ip := ip2region.ClientIP(c.ClientIP()) + location := "-" //ip2region.RealAddressByIp(ip) + return ip, location +} + +// UaOsBrowser 解析请求用户代理信息 +func UaOsBrowser(c *gin.Context) (string, string) { + userAgent := c.GetHeader("user-agent") + uaInfo := ua.Info(userAgent) + + browser := "-" + if bName, bVersion := uaInfo.Browser(); bName != "" { + browser = bName + if bVersion != "" { + browser = bName + " " + bVersion + } + } + + os := "-" + if bos := uaInfo.OS(); bos != "" { + os = bos + } + return os, browser +} diff --git a/src/framework/vo/result/result.go b/src/framework/resp/api.go similarity index 51% rename from src/framework/vo/result/result.go rename to src/framework/resp/api.go index 192e6461..c5b0815d 100644 --- a/src/framework/vo/result/result.go +++ b/src/framework/resp/api.go @@ -1,7 +1,20 @@ -package result +package resp -import ( - constResult "be.ems/src/framework/constants/result" +const ( + // CODE_ERROR 响应-code错误失败 + CODE_ERROR = 0 + // MSG_ERROR 响应-msg错误失败 + MSG_ERROR = "error" + + // CODE_SUCCESS 响应-msg正常成功 + CODE_SUCCESS = 1 + // MSG_SUCCCESS 响应-code正常成功 + MSG_SUCCCESS = "success" + + // 响应-code加密数据 + CODE_ENCRYPT = 2 + // 响应-msg加密数据 + MSG_ENCRYPT = "encrypt" ) // CodeMsg 响应结果 @@ -12,11 +25,11 @@ func CodeMsg(code int, msg string) map[string]any { return args } -// 响应成功结果 map[string]any{} +// Ok 响应成功结果 func Ok(v map[string]any) map[string]any { args := make(map[string]any) - args["code"] = constResult.CODE_SUCCESS - args["msg"] = constResult.MSG_SUCCESS + args["code"] = CODE_SUCCESS + args["msg"] = MSG_SUCCCESS // v合并到args for key, value := range v { args[key] = value @@ -24,28 +37,28 @@ func Ok(v map[string]any) map[string]any { return args } -// 响应成功结果信息 +// OkMsg 响应成功结果信息 func OkMsg(msg string) map[string]any { args := make(map[string]any) - args["code"] = constResult.CODE_SUCCESS + args["code"] = CODE_SUCCESS args["msg"] = msg return args } -// 响应成功结果数据 +// OkData 响应成功结果数据 func OkData(data any) map[string]any { args := make(map[string]any) - args["code"] = constResult.CODE_SUCCESS - args["msg"] = constResult.MSG_SUCCESS + args["code"] = CODE_SUCCESS + args["msg"] = MSG_SUCCCESS args["data"] = data return args } -// 响应失败结果 map[string]any{} +// Err 响应失败结果 map[string]any{} func Err(v map[string]any) map[string]any { args := make(map[string]any) - args["code"] = constResult.CODE_ERROR - args["msg"] = constResult.MSG_ERROR + args["code"] = CODE_ERROR + args["msg"] = MSG_ERROR // v合并到args for key, value := range v { args[key] = value @@ -53,19 +66,19 @@ func Err(v map[string]any) map[string]any { return args } -// 响应失败结果信息 +// ErrMsg 响应失败结果信息 func ErrMsg(msg string) map[string]any { args := make(map[string]any) - args["code"] = constResult.CODE_ERROR + args["code"] = CODE_ERROR args["msg"] = msg return args } -// 响应失败结果数据 +// ErrData 响应失败结果数据 func ErrData(data any) map[string]any { args := make(map[string]any) - args["code"] = constResult.CODE_ERROR - args["msg"] = constResult.MSG_ERROR + args["code"] = CODE_ERROR + args["msg"] = MSG_ERROR args["data"] = data return args } diff --git a/src/framework/resp/error.go b/src/framework/resp/error.go new file mode 100644 index 00000000..3df849f3 --- /dev/null +++ b/src/framework/resp/error.go @@ -0,0 +1,23 @@ +package resp + +import ( + "fmt" + "strings" + + "github.com/go-playground/validator/v10" +) + +// FormatBindError 格式化Gin ShouldBindWith绑定错误 +// +// binding:"required" 验证失败返回: field=id type=string tag=required value= +func FormatBindError(err error) string { + if errs, ok := err.(validator.ValidationErrors); ok { + var errMsgs []string + for _, e := range errs { + str := fmt.Sprintf("[field=%s, type=%s, tag=%s, param=%s, value=%v]", e.Field(), e.Type().Name(), e.Tag(), e.Param(), e.Value()) + errMsgs = append(errMsgs, str) + } + return strings.Join(errMsgs, ", ") + } + return err.Error() +} diff --git a/src/framework/utils/ssh/files.go b/src/framework/ssh/files.go similarity index 100% rename from src/framework/utils/ssh/files.go rename to src/framework/ssh/files.go diff --git a/src/framework/utils/ssh/sftp.go b/src/framework/ssh/sftp.go similarity index 100% rename from src/framework/utils/ssh/sftp.go rename to src/framework/ssh/sftp.go diff --git a/src/framework/utils/ssh/ssh.go b/src/framework/ssh/ssh.go similarity index 100% rename from src/framework/utils/ssh/ssh.go rename to src/framework/ssh/ssh.go diff --git a/src/framework/utils/ssh/ssh_session.go b/src/framework/ssh/ssh_session.go similarity index 100% rename from src/framework/utils/ssh/ssh_session.go rename to src/framework/ssh/ssh_session.go diff --git a/src/framework/token/token.go b/src/framework/token/token.go new file mode 100644 index 00000000..2b7d5a69 --- /dev/null +++ b/src/framework/token/token.go @@ -0,0 +1,152 @@ +package token + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/golang-jwt/jwt/v5" + + "be.ems/src/framework/config" + "be.ems/src/framework/constants" + "be.ems/src/framework/database/redis" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/generate" +) + +// Remove 清除登录用户信息UUID +func Remove(token string) string { + claims, err := Verify(token) + if err != nil { + logger.Errorf("token verify err %v", err) + return "" + } + // 清除缓存KEY + uuid := claims[constants.JWT_UUID].(string) + tokenKey := constants.CACHE_LOGIN_TOKEN + ":" + uuid + hasKey, err := redis.Has("", tokenKey) + if hasKey > 0 && err == nil { + _ = redis.Del("", tokenKey) + } + return claims[constants.JWT_USER_NAME].(string) +} + +// Create 令牌生成 +func Create(tokenInfo *TokenInfo, ilobArr [4]string) string { + // 生成用户唯一token 32位 + tokenInfo.UUID = generate.Code(32) + tokenInfo.LoginTime = time.Now().UnixMilli() + + // 设置请求用户登录客户端 + tokenInfo.LoginIp = ilobArr[0] + tokenInfo.LoginLocation = ilobArr[1] + tokenInfo.OS = ilobArr[2] + tokenInfo.Browser = ilobArr[3] + + // 设置新登录IP和登录时间 + tokenInfo.User.LoginIp = tokenInfo.LoginIp + tokenInfo.User.LoginTime = tokenInfo.LoginTime + + // 设置用户令牌有效期并存入缓存 + Cache(tokenInfo) + + // 令牌算法 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{ + constants.JWT_UUID: tokenInfo.UUID, + constants.JWT_USER_ID: tokenInfo.UserId, + constants.JWT_USER_NAME: tokenInfo.User.UserName, + "ait": tokenInfo.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(tokenInfo *TokenInfo) { + // 计算配置的有效期 + expTime := config.Get("jwt.expiresIn").(int) + expTimestamp := time.Duration(expTime) * time.Minute + iatTimestamp := time.Now().UnixMilli() + tokenInfo.LoginTime = iatTimestamp + tokenInfo.ExpireTime = iatTimestamp + expTimestamp.Milliseconds() + tokenInfo.User.Password = "" + // 登录信息标识缓存 + tokenKey := constants.CACHE_LOGIN_TOKEN + ":" + tokenInfo.UUID + jsonBytes, err := json.Marshal(tokenInfo) + if err != nil { + return + } + _ = redis.SetByExpire("", tokenKey, string(jsonBytes), expTimestamp) +} + +// RefreshIn 验证令牌有效期,相差不足xx分钟,自动刷新缓存 +func RefreshIn(loginUser *TokenInfo) { + // 相差不足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(token string) (jwt.MapClaims, error) { + jwtToken, err := jwt.Parse(token, func(jToken *jwt.Token) (any, error) { + // 判断加密算法是预期的加密算法 + if _, ok := jToken.Method.(*jwt.SigningMethodHMAC); ok { + secret := config.Get("jwt.secret").(string) + return []byte(secret), nil + } + return nil, jwt.ErrSignatureInvalid + }) + if err != nil { + logger.Errorf("Token Verify Err: %v", err) + return nil, fmt.Errorf("token invalid") + } + // 如果解析负荷成功并通过签名校验 + if claims, ok := jwtToken.Claims.(jwt.MapClaims); ok && jwtToken.Valid { + return claims, nil + } + return nil, fmt.Errorf("token valid error") +} + +// Info 缓存的登录用户信息 +func Info(claims jwt.MapClaims) TokenInfo { + tokenInfo := TokenInfo{} + uuid := claims[constants.JWT_UUID].(string) + tokenKey := constants.CACHE_LOGIN_TOKEN + ":" + uuid + hasKey, err := redis.Has("", tokenKey) + if hasKey > 0 && err == nil { + infoStr, err := redis.Get("", tokenKey) + if infoStr == "" || err != nil { + return tokenInfo + } + if err := json.Unmarshal([]byte(infoStr), &tokenInfo); err != nil { + logger.Errorf("info json err : %v", err) + return tokenInfo + } + } + return tokenInfo +} diff --git a/src/framework/token/token_info.go b/src/framework/token/token_info.go new file mode 100644 index 00000000..77ef68a8 --- /dev/null +++ b/src/framework/token/token_info.go @@ -0,0 +1,18 @@ +package token + +import systemModel "be.ems/src/modules/system/model" + +// TokenInfo 令牌信息对象 +type TokenInfo struct { + UUID string `json:"uuid"` // 用户唯一标识 + UserId int64 `json:"userId"` // 用户ID + DeptId int64 `json:"deptId"` // 部门ID + LoginTime int64 `json:"loginTime"` // 登录时间时间戳 + ExpireTime int64 `json:"expireTime"` // 过期时间时间戳 + LoginIp string `json:"loginIp"` // 登录IP地址 x.x.x.x + LoginLocation string `json:"loginLocation"` // 登录地点 xx xx + Browser string `json:"browser"` // 浏览器类型 + OS string `json:"os"` // 操作系统 + Permissions []string `json:"permissions"` // 权限列表 + User systemModel.SysUser `json:"user"` // 用户信息 +} diff --git a/src/framework/utils/ctx/ctx.go b/src/framework/utils/ctx/ctx.go deleted file mode 100644 index d5c129d1..00000000 --- a/src/framework/utils/ctx/ctx.go +++ /dev/null @@ -1,254 +0,0 @@ -package ctx - -import ( - "fmt" - "strings" - - "be.ems/src/framework/config" - "be.ems/src/framework/constants/common" - "be.ems/src/framework/constants/roledatascope" - "be.ems/src/framework/constants/token" - "be.ems/src/framework/utils/ip2region" - "be.ems/src/framework/utils/ua" - "be.ems/src/framework/vo" - "golang.org/x/text/language" - - "github.com/gin-gonic/gin" -) - -// QueryMapString 查询参数转换Map -func QueryMapString(c *gin.Context) map[string]string { - queryValues := c.Request.URL.Query() - queryParams := make(map[string]string) - for key, values := range queryValues { - queryParams[key] = values[0] - } - return queryParams -} - -// 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.ShouldBindBodyWithJSON(¶ms) - 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.ShouldBindBodyWithJSON(¶ms) - } - - // 表单 - 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 { - // Query请求查询 - if authQuery, ok := c.GetQuery(token.ACCESS_TOKEN); ok && authQuery != "" { - return authQuery - } - // Header请求头 - if authHeader := c.GetHeader(token.ACCESS_TOKEN); authHeader != "" { - return authHeader - } - - // Query请求查询 - if authQuery, ok := c.GetQuery(token.RESPONSE_FIELD); ok && authQuery != "" { - return authQuery - } - // Header请求头 - authHeader := c.GetHeader(token.HEADER_KEY) - if authHeader == "" { - return "" - } - // 拆分 Authorization 请求头,提取 JWT 令牌部分 - arr := strings.SplitN(authHeader, token.HEADER_PREFIX, 2) - if len(arr) < 2 { - return "" - } - return arr[1] -} - -// UaOsBrowser 解析请求用户代理信息 -func UaOsBrowser(c *gin.Context) (string, string) { - userAgent := c.GetHeader("user-agent") - uaInfo := ua.Info(userAgent) - - browser := "app.common.noUaOsBrowser" - bName, bVersion := uaInfo.Browser() - if bName != "" && bVersion != "" { - browser = bName + " " + bVersion - } - - os := "app.common.noUaOsBrowser" - bos := uaInfo.OS() - if bos != "" { - os = bos - } - return os, browser -} - -// AcceptLanguage 解析客户端接收语言 zh:中文 en: 英文 -func AcceptLanguage(c *gin.Context) string { - preferredLanguage := language.English - - // Query请求查询 - if v, ok := c.GetQuery("language"); ok && v != "" { - tags, _, _ := language.ParseAcceptLanguage(v) - if len(tags) > 0 { - preferredLanguage = tags[0] - } - } - // Header请求头 - if v := c.GetHeader("Accept-Language"); v != "" { - tags, _, _ := language.ParseAcceptLanguage(v) - if len(tags) > 0 { - preferredLanguage = tags[0] - } - } - - // 只取前缀 - lang := preferredLanguage.String() - arr := strings.Split(lang, "-") - return arr[0] -} - -// 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("app.common.noLoginUser") -} - -// 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 == dataScope { - sql := fmt.Sprintf(`%s.dept_id = '%s'`, deptAlias, userInfo.DeptID) - 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.parent_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 -} diff --git a/src/framework/utils/date/date.go b/src/framework/utils/date/date.go index 35cf339f..a1006bbe 100644 --- a/src/framework/utils/date/date.go +++ b/src/framework/utils/date/date.go @@ -27,6 +27,12 @@ const ( // // formatStr 时间格式 默认YYYY-MM-DD HH:mm:ss func ParseStrToDate(dateStr, formatStr string) time.Time { + if dateStr == "" || dateStr == "" { + return time.Time{} + } + if formatStr == "" { + formatStr = YYYY_MM_DD_HH_MM_SS + } t, err := time.Parse(formatStr, dateStr) if err != nil { logger.Infof("utils ParseStrToDate err %v", err) diff --git a/src/framework/utils/file/excel.go b/src/framework/utils/file/excel.go index 69b7aa53..2d52683c 100644 --- a/src/framework/utils/file/excel.go +++ b/src/framework/utils/file/excel.go @@ -7,7 +7,7 @@ import ( "path/filepath" "time" - "be.ems/src/framework/constants/uploadsubpath" + "be.ems/src/framework/constants" "be.ems/src/framework/logger" "be.ems/src/framework/utils/date" @@ -26,7 +26,7 @@ func TransferExeclUploadFile(file *multipart.FileHeader) (string, error) { // 上传资源路径 _, dir := resourceUpload() // 新文件名称并组装文件地址 - filePath := filepath.Join(uploadsubpath.IMPORT, date.ParseDatePath(time.Now())) + filePath := filepath.Join(constants.UPLOAD_IMPORT, date.ParseDatePath(time.Now())) fileName := generateFileName(file.Filename) writePathFile := filepath.Join(dir, filePath, fileName) // 存入新文件路径 @@ -138,7 +138,7 @@ func WriteSheet(headerCells map[string]string, dataCells []map[string]any, fileN // 上传资源路径 _, dir := resourceUpload() - filePath := filepath.Join(uploadsubpath.EXPORT, date.ParseDatePath(time.Now())) + filePath := filepath.Join(constants.UPLOAD_EXPORT, date.ParseDatePath(time.Now())) saveFilePath := filepath.Join(dir, filePath, fileName) // 创建文件目录 diff --git a/src/framework/utils/file/file.go b/src/framework/utils/file/file.go index 25ad2585..a03e36e0 100644 --- a/src/framework/utils/file/file.go +++ b/src/framework/utils/file/file.go @@ -12,7 +12,7 @@ import ( "time" "be.ems/src/framework/config" - "be.ems/src/framework/constants/uploadsubpath" + "be.ems/src/framework/constants" "be.ems/src/framework/logger" "be.ems/src/framework/utils/date" "be.ems/src/framework/utils/generate" @@ -237,7 +237,7 @@ func TransferChunkUploadFile(file *multipart.FileHeader, index, identifier strin // 上传资源路径 prefix, dir := resourceUpload() // 新文件名称并组装文件地址 - filePath := filepath.Join(uploadsubpath.CHUNK, date.ParseDatePath(time.Now()), identifier) + filePath := filepath.Join(constants.UPLOAD_CHUNK, date.ParseDatePath(time.Now()), identifier) writePathFile := filepath.Join(dir, filePath, index) // 存入新文件路径 err = transferToNewFile(file, writePathFile) @@ -261,7 +261,7 @@ func ChunkCheckFile(identifier, originalFileName string) ([]string, error) { } // 上传资源路径 _, dir := resourceUpload() - dirPath := path.Join(uploadsubpath.CHUNK, date.ParseDatePath(time.Now()), identifier) + dirPath := path.Join(constants.UPLOAD_CHUNK, date.ParseDatePath(time.Now()), identifier) readPath := path.Join(dir, dirPath) fileList, err := getDirFileNameList(readPath) if err != nil { @@ -286,7 +286,7 @@ func ChunkMergeFile(identifier, originalFileName, subPath string) (string, error // 上传资源路径 prefix, dir := resourceUpload() // 切片存放目录 - dirPath := path.Join(uploadsubpath.CHUNK, date.ParseDatePath(time.Now()), identifier) + dirPath := path.Join(constants.UPLOAD_CHUNK, date.ParseDatePath(time.Now()), identifier) readPath := path.Join(dir, dirPath) // 组合存放文件路径 fileName := generateFileName(originalFileName) @@ -305,7 +305,7 @@ func ChunkMergeFile(identifier, originalFileName, subPath string) (string, error // filePath 上传得到的文件路径 /upload.... // dst 新文件路径 /a/xx.pdf func CopyUploadFile(filePath, dst string) error { - srcPath := ParseUploadFilePath(filePath) + srcPath := ParseUploadFileAbsPath(filePath) src, err := os.Open(srcPath) if err != nil { return err @@ -346,10 +346,10 @@ func ParseUploadFileDir(subPath string) string { return filepath.Join(dir, filePath) } -// ParseUploadFilePath 上传资源本地绝对资源路径 +// ParseUploadFileAbsPath 上传资源本地绝对资源路径 // // filePath 上传文件路径 -func ParseUploadFilePath(filePath string) string { +func ParseUploadFileAbsPath(filePath string) string { prefix, dir := resourceUpload() absPath := strings.Replace(filePath, prefix, dir, 1) return filepath.ToSlash(absPath) diff --git a/src/framework/utils/ip2region/ip2region.xdb b/src/framework/utils/ip2region/ip2region.xdb deleted file mode 100644 index c78b7928..00000000 Binary files a/src/framework/utils/ip2region/ip2region.xdb and /dev/null differ diff --git a/src/framework/utils/machine/launch.go b/src/framework/utils/machine/launch.go index 60d3aa84..d66e344e 100644 --- a/src/framework/utils/machine/launch.go +++ b/src/framework/utils/machine/launch.go @@ -9,7 +9,7 @@ import ( "time" "be.ems/src/framework/config" - "be.ems/src/framework/constants/common" + "be.ems/src/framework/constants" "be.ems/src/framework/logger" "be.ems/src/framework/utils/cmd" "be.ems/src/framework/utils/crypto" @@ -106,8 +106,8 @@ func Launch() { "code": Code, // 机器码 "useTime": time.Now().UnixMilli(), // 首次使用时间 - common.LAUNCH_BOOTLOADER: true, // 启动引导 - common.LAUNCH_BOOTLOADER + "Time": 0, // 引导完成时间 + constants.LAUNCH_BOOTLOADER: true, // 启动引导 + constants.LAUNCH_BOOTLOADER + "Time": 0, // 引导完成时间 } codeFileWrite(LaunchInfo) } else { @@ -151,8 +151,8 @@ func SetLaunchInfo(info map[string]any) error { // Bootloader 启动引导标记 func Bootloader(flag bool) error { return SetLaunchInfo(map[string]any{ - common.LAUNCH_BOOTLOADER: flag, // 启动引导 true开 false关 - common.LAUNCH_BOOTLOADER + "Time": time.Now().UnixMilli(), // 引导完成时间 + constants.LAUNCH_BOOTLOADER: flag, // 启动引导 true开 false关 + constants.LAUNCH_BOOTLOADER + "Time": time.Now().UnixMilli(), // 引导完成时间 }) } diff --git a/src/framework/utils/parse/parse.go b/src/framework/utils/parse/parse.go index a51e313f..d174e741 100644 --- a/src/framework/utils/parse/parse.go +++ b/src/framework/utils/parse/parse.go @@ -91,31 +91,16 @@ func ConvertToCamelCase(str string) string { return strings.Join(words, "") } -// Bit 比特位为单位 +// Bit 比特位为单位 1023.00 B --> 1.00 KB 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) + units := []string{"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"} + for i := 0; i < len(units); i++ { + if bit < 1024 || i == len(units)-1 { + return fmt.Sprintf("%.2f %s", bit, units[i]) + } + bit /= 1024 } + return "" } // CronExpression 解析 Cron 表达式,返回下一次执行的时间戳(毫秒) @@ -146,11 +131,11 @@ func SafeContent(value string) string { } // RemoveDuplicates 数组内字符串去重 -func RemoveDuplicates(ids []string) []string { +func RemoveDuplicates(arr []string) []string { uniqueIDs := make(map[string]bool) uniqueIDSlice := make([]string, 0) - for _, id := range ids { + for _, id := range arr { _, ok := uniqueIDs[id] if !ok && id != "" { uniqueIDs[id] = true @@ -161,6 +146,29 @@ func RemoveDuplicates(ids []string) []string { return uniqueIDSlice } +// RemoveDuplicatesToArray 数组内字符串分隔去重转为字符数组 +func RemoveDuplicatesToArray(keyStr, sep string) []string { + arr := make([]string, 0) + if keyStr == "" { + return arr + } + if strings.Contains(keyStr, sep) { + // 处理字符转数组后去重 + strArr := strings.Split(keyStr, sep) + uniqueKeys := make(map[string]bool) + for _, str := range strArr { + _, ok := uniqueKeys[str] + if !ok && str != "" { + uniqueKeys[str] = true + arr = append(arr, str) + } + } + } else { + arr = append(arr, keyStr) + } + return arr +} + // Color 解析颜色 #fafafa func Color(colorStr string) *color.RGBA { // 去除 # 号 diff --git a/src/framework/utils/repo/repo.go b/src/framework/utils/repo/repo.go deleted file mode 100644 index 6e5e82f3..00000000 --- a/src/framework/utils/repo/repo.go +++ /dev/null @@ -1,139 +0,0 @@ -package repo - -import ( - "fmt" - "reflect" - "strconv" - "strings" - "time" - - "be.ems/src/framework/utils/parse" -) - -// PageNumSize 分页页码记录数 -func PageNumSize(pageNum, pageSize any) (int64, int64) { - // 记录起始索引 - num := parse.Number(pageNum) - if num < 1 { - num = 1 - } - - // 显示记录数 - size := parse.Number(pageSize) - if size < 1 { - 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) - case reflect.Struct: - fmt.Printf("%s time resolution %s %v \n", fieldName, fieldValue.Type(), value) - if fieldValue.Type() == reflect.TypeOf(time.Time{}) && value != nil { - // 解析 value 并转换为 time.Time 类型 - parsedTime, err := time.Parse("2006-01-02 15:04:05 +0800 CST", fmt.Sprintf("%v", value)) - if err != nil { - fmt.Println("Time resolution error:", err) - } else { - // 设置字段的值 - fieldValue.Set(reflect.ValueOf(parsedTime)) - } - } - 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 -} diff --git a/src/framework/utils/token/token.go b/src/framework/utils/token/token.go deleted file mode 100644 index b07879d6..00000000 --- a/src/framework/utils/token/token.go +++ /dev/null @@ -1,157 +0,0 @@ -package token - -import ( - "encoding/json" - "fmt" - "time" - - "be.ems/src/framework/config" - cachekeyConstants "be.ems/src/framework/constants/cachekey" - tokenConstants "be.ems/src/framework/constants/token" - "be.ems/src/framework/logger" - redisCahe "be.ems/src/framework/redis" - "be.ems/src/framework/utils/generate" - "be.ems/src/framework/utils/machine" - "be.ems/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 { - // 生成用户唯一tokne16位 - loginUser.UUID = generate.Code(16) - - // 设置请求用户登录客户端 - loginUser.IPAddr = ilobArgs[0] - loginUser.LoginLocation = ilobArgs[1] - loginUser.OS = ilobArgs[2] - loginUser.Browser = ilobArgs[3] - - // 设置用户令牌有效期并存入缓存 - Cache(loginUser) - - // 设置登录IP和登录时间 - loginUser.User.LoginIP = loginUser.IPAddr - loginUser.User.LoginDate = loginUser.LoginTime - - // 令牌算法 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(machine.Code + "@" + 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(machine.Code + "@" + secret), nil - } - return nil, jwt.ErrSignatureInvalid - }) - if err != nil { - logger.Errorf("token String Verify : %v", err) - // 无效身份授权 - return nil, fmt.Errorf("invalid identity authorization") - } - // 如果解析负荷成功并通过签名校验 - if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { - return claims, nil - } - return nil, fmt.Errorf("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 -} diff --git a/src/framework/vo/loginuser.go b/src/framework/vo/loginuser.go deleted file mode 100644 index f5e538e0..00000000 --- a/src/framework/vo/loginuser.go +++ /dev/null @@ -1,39 +0,0 @@ -package vo - -import systemModel "be.ems/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"` -} diff --git a/src/framework/vo/router.go b/src/framework/vo/router.go deleted file mode 100644 index a3cc4083..00000000 --- a/src/framework/vo/router.go +++ /dev/null @@ -1,17 +0,0 @@ -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"` -} diff --git a/src/framework/vo/router_meta.go b/src/framework/vo/router_meta.go deleted file mode 100644 index b3447e05..00000000 --- a/src/framework/vo/router_meta.go +++ /dev/null @@ -1,17 +0,0 @@ -package vo - -// RouterMeta 路由元信息对象 -type RouterMeta struct { - // 设置该菜单在侧边栏和面包屑中展示的名字 - Title string `json:"title"` - // 设置该菜单的图标 - Icon string `json:"icon"` - // 设置为true,则不会被 缓存 - Cache bool `json:"cache"` - // 内链地址(http(s)://开头), 打开目标位置 '_blank' | '_self' | '' - Target string `json:"target"` - // 在菜单中隐藏子节点 - HideChildInMenu bool `json:"hideChildInMenu"` - // 在菜单中隐藏自己和子节点 - HideInMenu bool `json:"hideInMenu"` -} diff --git a/src/framework/vo/treeselect.go b/src/framework/vo/treeselect.go deleted file mode 100644 index 01fcf895..00000000 --- a/src/framework/vo/treeselect.go +++ /dev/null @@ -1,51 +0,0 @@ -package vo - -import systemModel "be.ems/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 -}