refactor: 删除冗余常量文件并整合常量定义

This commit is contained in:
TsMask
2025-02-20 09:50:29 +08:00
parent 1b435074cb
commit a1296b6fe6
63 changed files with 1823 additions and 1748 deletions

View File

@@ -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
file: /usr/local/omc/etc/testconfig.yaml

View File

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

View File

@@ -1,12 +0,0 @@
package admin
// 系统管理员常量信息
// 系统管理员-系统指定角色ID
const ROLE_ID = "1"
// 系统管理员-系统指定角色KEY
const ROLE_KEY = "system"
// 系统管理员-系统指定权限
const PERMISSION = "*:*:*"

View File

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

View File

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

View File

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

View File

@@ -1,12 +0,0 @@
package captcha
// 验证码常量信息
// 验证码有效期,单位秒
const EXPIRATION = 2 * 60
// 验证码类型-数值计算
const TYPE_CHAR = "char"
// 验证码类型-字符验证
const TYPE_MATH = "math"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: "仅本人数据权限",
}

View File

@@ -1,20 +0,0 @@
package roledatascope
// 系统角色数据范围常量
const (
// 全部数据权限
ALL = "1"
// 自定数据权限
CUSTOM = "2"
// 部门数据权限
DEPT = "3"
// 部门及以下数据权限
DEPT_AND_CHILD = "4"
// 仅本人数据权限
SELF = "5"
)

View File

@@ -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 = "*:*:*"

View File

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

View File

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

View File

@@ -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: "切片",
}

View File

@@ -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: "授权文件",
}

View File

@@ -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 调度任务日志收集结构体,执行任务时传入的接收参数

View File

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

View File

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

View File

@@ -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);`)

View File

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

View File

@@ -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 翻译模板字符串

View File

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

View File

@@ -172,4 +172,3 @@ func LoadContentFromFile(dbFile string) ([]byte, error) {
return cBuff, nil
}

View File

@@ -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 处理敏感属性字段

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(&params)
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(&params)
}
// 表单
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]
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"` // 用户信息
}

View File

@@ -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(&params)
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(&params)
}
// 表单
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
}

View File

@@ -27,6 +27,12 @@ const (
//
// formatStr 时间格式 默认YYYY-MM-DD HH:mm:ss
func ParseStrToDate(dateStr, formatStr string) time.Time {
if dateStr == "" || dateStr == "<nil>" {
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)

View File

@@ -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)
// 创建文件目录

View File

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

View File

@@ -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(), // 引导完成时间
})
}

View File

@@ -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 {
// 去除 # 号

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +0,0 @@
package vo
// RouterMeta 路由元信息对象
type RouterMeta struct {
// 设置该菜单在侧边栏和面包屑中展示的名字
Title string `json:"title"`
// 设置该菜单的图标
Icon string `json:"icon"`
// 设置为true则不会被 <keep-alive>缓存
Cache bool `json:"cache"`
// 内链地址http(s)://开头), 打开目标位置 '_blank' | '_self' | ''
Target string `json:"target"`
// 在菜单中隐藏子节点
HideChildInMenu bool `json:"hideChildInMenu"`
// 在菜单中隐藏自己和子节点
HideInMenu bool `json:"hideInMenu"`
}

View File

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