feat: 合并Gin_Vue
This commit is contained in:
20
src/framework/utils/crypto/crypto.go
Normal file
20
src/framework/utils/crypto/crypto.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// BcryptHash Bcrypt密码加密
|
||||
func BcryptHash(originStr string) string {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(originStr), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(hash)
|
||||
}
|
||||
|
||||
// BcryptCompare Bcrypt密码匹配检查
|
||||
func BcryptCompare(originStr, hashStr string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashStr), []byte(originStr))
|
||||
return err == nil
|
||||
}
|
||||
199
src/framework/utils/ctx/ctx.go
Normal file
199
src/framework/utils/ctx/ctx.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package ctx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"ems.agt/src/framework/config"
|
||||
"ems.agt/src/framework/constants/common"
|
||||
"ems.agt/src/framework/constants/roledatascope"
|
||||
"ems.agt/src/framework/constants/token"
|
||||
"ems.agt/src/framework/utils/ip2region"
|
||||
"ems.agt/src/framework/utils/ua"
|
||||
"ems.agt/src/framework/vo"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
)
|
||||
|
||||
// QueryMap 查询参数转换Map
|
||||
func QueryMap(c *gin.Context) map[string]any {
|
||||
queryValues := c.Request.URL.Query()
|
||||
queryParams := make(map[string]any)
|
||||
for key, values := range queryValues {
|
||||
queryParams[key] = values[0]
|
||||
}
|
||||
return queryParams
|
||||
}
|
||||
|
||||
// BodyJSONMap JSON参数转换Map
|
||||
func BodyJSONMap(c *gin.Context) map[string]any {
|
||||
params := make(map[string]any)
|
||||
c.ShouldBindBodyWith(¶ms, binding.JSON)
|
||||
return params
|
||||
}
|
||||
|
||||
// RequestParamsMap 请求参数转换Map
|
||||
func RequestParamsMap(c *gin.Context) map[string]any {
|
||||
params := make(map[string]any)
|
||||
// json
|
||||
if strings.HasPrefix(c.ContentType(), "application/json") {
|
||||
c.ShouldBindBodyWith(¶ms, binding.JSON)
|
||||
}
|
||||
|
||||
// 表单
|
||||
bodyParams := c.Request.PostForm
|
||||
for key, value := range bodyParams {
|
||||
params[key] = value[0]
|
||||
}
|
||||
|
||||
// 查询
|
||||
queryParams := c.Request.URL.Query()
|
||||
for key, value := range queryParams {
|
||||
params[key] = value[0]
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
// IPAddrLocation 解析ip地址
|
||||
func IPAddrLocation(c *gin.Context) (string, string) {
|
||||
ip := ip2region.ClientIP(c.ClientIP())
|
||||
location := ip2region.RealAddressByIp(ip)
|
||||
return ip, location
|
||||
}
|
||||
|
||||
// Authorization 解析请求头
|
||||
func Authorization(c *gin.Context) string {
|
||||
authHeader := c.GetHeader(token.HEADER_KEY)
|
||||
if authHeader == "" {
|
||||
return ""
|
||||
}
|
||||
// 拆分 Authorization 请求头,提取 JWT 令牌部分
|
||||
arr := strings.Split(authHeader, token.HEADER_PREFIX)
|
||||
if len(arr) == 2 && arr[1] == "" {
|
||||
return ""
|
||||
}
|
||||
return arr[1]
|
||||
}
|
||||
|
||||
// UaOsBrowser 解析请求用户代理信息
|
||||
func UaOsBrowser(c *gin.Context) (string, string) {
|
||||
userAgent := c.GetHeader("user-agent")
|
||||
uaInfo := ua.Info(userAgent)
|
||||
|
||||
browser := "未知 未知"
|
||||
bName, bVersion := uaInfo.Browser()
|
||||
if bName != "" && bVersion != "" {
|
||||
browser = bName + " " + bVersion
|
||||
}
|
||||
|
||||
os := "未知 未知"
|
||||
bos := uaInfo.OS()
|
||||
if bos != "" {
|
||||
os = bos
|
||||
}
|
||||
return os, browser
|
||||
}
|
||||
|
||||
// LoginUser 登录用户信息
|
||||
func LoginUser(c *gin.Context) (vo.LoginUser, error) {
|
||||
value, exists := c.Get(common.CTX_LOGIN_USER)
|
||||
if exists {
|
||||
return value.(vo.LoginUser), nil
|
||||
}
|
||||
return vo.LoginUser{}, fmt.Errorf("无效登录用户信息")
|
||||
}
|
||||
|
||||
// LoginUserToUserID 登录用户信息-用户ID
|
||||
func LoginUserToUserID(c *gin.Context) string {
|
||||
value, exists := c.Get(common.CTX_LOGIN_USER)
|
||||
if exists {
|
||||
loginUser := value.(vo.LoginUser)
|
||||
return loginUser.UserID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// LoginUserToUserName 登录用户信息-用户名称
|
||||
func LoginUserToUserName(c *gin.Context) string {
|
||||
value, exists := c.Get(common.CTX_LOGIN_USER)
|
||||
if exists {
|
||||
loginUser := value.(vo.LoginUser)
|
||||
return loginUser.User.UserName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// LoginUserToDataScopeSQL 登录用户信息-角色数据范围过滤SQL字符串
|
||||
func LoginUserToDataScopeSQL(c *gin.Context, deptAlias string, userAlias string) string {
|
||||
dataScopeSQL := ""
|
||||
// 登录用户信息
|
||||
loginUser, err := LoginUser(c)
|
||||
if err != nil {
|
||||
return dataScopeSQL
|
||||
}
|
||||
userInfo := loginUser.User
|
||||
|
||||
// 如果是管理员,则不过滤数据
|
||||
if config.IsAdmin(userInfo.UserID) {
|
||||
return dataScopeSQL
|
||||
}
|
||||
// 无用户角色
|
||||
if len(userInfo.Roles) <= 0 {
|
||||
return dataScopeSQL
|
||||
}
|
||||
|
||||
// 记录角色权限范围定义添加过, 非自定数据权限不需要重复拼接SQL
|
||||
var scopeKeys []string
|
||||
var conditions []string
|
||||
for _, role := range userInfo.Roles {
|
||||
dataScope := role.DataScope
|
||||
|
||||
if roledatascope.ALL == dataScope {
|
||||
break
|
||||
}
|
||||
|
||||
if roledatascope.CUSTOM != dataScope {
|
||||
hasKey := false
|
||||
for _, key := range scopeKeys {
|
||||
if key == dataScope {
|
||||
hasKey = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasKey {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if roledatascope.CUSTOM == dataScope {
|
||||
sql := fmt.Sprintf(`%s.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = '%s' )`, deptAlias, role.RoleID)
|
||||
conditions = append(conditions, sql)
|
||||
}
|
||||
|
||||
if roledatascope.DEPT_AND_CHILD == dataScope {
|
||||
sql := fmt.Sprintf(`%s.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = '%s' or find_in_set('%s' , ancestors ) )`, deptAlias, userInfo.DeptID, userInfo.DeptID)
|
||||
conditions = append(conditions, sql)
|
||||
}
|
||||
|
||||
if roledatascope.SELF == dataScope {
|
||||
// 数据权限为仅本人且没有userAlias别名不查询任何数据
|
||||
if userAlias == "" {
|
||||
sql := fmt.Sprintf(`%s.dept_id = '0'`, deptAlias)
|
||||
conditions = append(conditions, sql)
|
||||
} else {
|
||||
sql := fmt.Sprintf(`%s.user_id = '%s'`, userAlias, userInfo.UserID)
|
||||
conditions = append(conditions, sql)
|
||||
}
|
||||
}
|
||||
|
||||
// 记录角色范围
|
||||
scopeKeys = append(scopeKeys, dataScope)
|
||||
}
|
||||
|
||||
// 构建查询条件语句
|
||||
if len(conditions) > 0 {
|
||||
dataScopeSQL = fmt.Sprintf(" AND ( %s ) ", strings.Join(conditions, " OR "))
|
||||
}
|
||||
return dataScopeSQL
|
||||
}
|
||||
69
src/framework/utils/date/date.go
Normal file
69
src/framework/utils/date/date.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package date
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
// 年 列如:2022
|
||||
YYYY = "2006"
|
||||
// 年-月 列如:2022-12
|
||||
YYYY_MM = "2006-01"
|
||||
// 年-月-日 列如:2022-12-30
|
||||
YYYY_MM_DD = "2006-01-02"
|
||||
// 年月日时分秒 列如:20221230010159
|
||||
YYYYMMDDHHMMSS = "20060102150405"
|
||||
// 年-月-日 时:分:秒 列如:2022-12-30 01:01:59
|
||||
YYYY_MM_DD_HH_MM_SS = "2006-01-02 15:04:05"
|
||||
)
|
||||
|
||||
// 格式时间字符串
|
||||
//
|
||||
// dateStr 时间字符串
|
||||
//
|
||||
// formatStr 时间格式 默认YYYY-MM-DD HH:mm:ss
|
||||
func ParseStrToDate(dateStr, formatStr string) time.Time {
|
||||
t, err := time.Parse(formatStr, dateStr)
|
||||
if err != nil {
|
||||
logger.Infof("utils ParseStrToDate err %v", err)
|
||||
return time.Time{}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// 格式时间
|
||||
//
|
||||
// date 可转的Date对象
|
||||
//
|
||||
// formatStr 时间格式 默认YYYY-MM-DD HH:mm:ss
|
||||
func ParseDateToStr(date any, formatStr string) string {
|
||||
t, ok := date.(time.Time)
|
||||
if !ok {
|
||||
switch v := date.(type) {
|
||||
case int64:
|
||||
if v == 0 {
|
||||
return ""
|
||||
}
|
||||
t = time.UnixMilli(v)
|
||||
case string:
|
||||
parsedTime, err := time.Parse(formatStr, v)
|
||||
if err != nil {
|
||||
logger.Infof("utils ParseDateToStr err %v", err)
|
||||
return ""
|
||||
}
|
||||
t = parsedTime
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return t.Format(formatStr)
|
||||
}
|
||||
|
||||
// 格式时间成日期路径
|
||||
//
|
||||
// 年/月 列如:2022/12
|
||||
func ParseDatePath(date time.Time) string {
|
||||
return date.Format("2006/01")
|
||||
}
|
||||
154
src/framework/utils/file/excel.go
Normal file
154
src/framework/utils/file/excel.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/constants/uploadsubpath"
|
||||
"ems.agt/src/framework/logger"
|
||||
"ems.agt/src/framework/utils/date"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// TransferExeclUploadFile 表格文件上传保存
|
||||
//
|
||||
// file 上传文件对象
|
||||
func TransferExeclUploadFile(file *multipart.FileHeader) (string, error) {
|
||||
// 上传文件检查
|
||||
err := isAllowWrite(file.Filename, []string{".xls", ".xlsx"}, file.Size)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// 上传资源路径
|
||||
_, dir := resourceUpload()
|
||||
// 新文件名称并组装文件地址
|
||||
filePath := filepath.Join(uploadsubpath.IMPORT, date.ParseDatePath(time.Now()))
|
||||
fileName := generateFileName(file.Filename)
|
||||
writePathFile := filepath.Join(dir, filePath, fileName)
|
||||
// 存入新文件路径
|
||||
err = transferToNewFile(file, writePathFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.ToSlash(writePathFile), nil
|
||||
}
|
||||
|
||||
// 表格读取数据
|
||||
//
|
||||
// filePath 文件路径地址
|
||||
//
|
||||
// sheetName 工作簿名称, 空字符默认Sheet1
|
||||
func ReadSheet(filePath, sheetName string) ([]map[string]string, error) {
|
||||
data := make([]map[string]string, 0)
|
||||
// 打开 Excel 文件
|
||||
f, err := excelize.OpenFile(filePath)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
logger.Errorf("工作表文件关闭失败 : %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 检查工作簿是否存在
|
||||
if sheetName == "" {
|
||||
sheetName = "Sheet1"
|
||||
}
|
||||
if visible, _ := f.GetSheetVisible(sheetName); !visible {
|
||||
return data, fmt.Errorf("读取工作簿 %s 失败", sheetName)
|
||||
}
|
||||
|
||||
// 获取工作簿记录
|
||||
rows, err := f.GetRows(sheetName)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
|
||||
for i, row := range rows {
|
||||
// 跳过第一行
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
// 遍历每个单元格
|
||||
rowData := map[string]string{}
|
||||
for columnIndex, cellValue := range row {
|
||||
columnName, _ := excelize.ColumnNumberToName(columnIndex + 1)
|
||||
rowData[columnName] = cellValue
|
||||
}
|
||||
|
||||
data = append(data, rowData)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// 表格写入数据
|
||||
//
|
||||
// headerCells 第一行表头标题 "A1":"?"
|
||||
//
|
||||
// dataCells 从第二行开始的数据 "A2":"?"
|
||||
//
|
||||
// fileName 文件名称
|
||||
//
|
||||
// sheetName 工作簿名称, 空字符默认Sheet1
|
||||
func WriteSheet(headerCells map[string]string, dataCells []map[string]any, fileName, sheetName string) (string, error) {
|
||||
f := excelize.NewFile()
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
logger.Errorf("工作表文件关闭失败 : %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 创建一个工作表
|
||||
if sheetName == "" {
|
||||
sheetName = "Sheet1"
|
||||
}
|
||||
index, err := f.NewSheet(sheetName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建工作表失败 %v", err)
|
||||
}
|
||||
// 设置工作簿的默认工作表
|
||||
f.SetActiveSheet(index)
|
||||
|
||||
// 首个和最后key名称
|
||||
firstKey := "A"
|
||||
lastKey := "B"
|
||||
|
||||
// 第一行表头标题
|
||||
for key, title := range headerCells {
|
||||
f.SetCellValue(sheetName, key, title)
|
||||
if key[:1] > lastKey {
|
||||
lastKey = key[:1]
|
||||
}
|
||||
}
|
||||
|
||||
// 设置工作表上宽度为 20
|
||||
f.SetColWidth(sheetName, firstKey, lastKey, 20)
|
||||
|
||||
// 从第二行开始的数据
|
||||
for _, cell := range dataCells {
|
||||
for key, value := range cell {
|
||||
f.SetCellValue(sheetName, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// 上传资源路径
|
||||
_, dir := resourceUpload()
|
||||
filePath := filepath.Join(uploadsubpath.EXPORT, date.ParseDatePath(time.Now()))
|
||||
saveFilePath := filepath.Join(dir, filePath, fileName)
|
||||
|
||||
// 创建文件目录
|
||||
if err := os.MkdirAll(filepath.Dir(saveFilePath), 0750); err != nil {
|
||||
return "", fmt.Errorf("创建保存文件失败 %v", err)
|
||||
}
|
||||
|
||||
// 根据指定路径保存文件
|
||||
if err := f.SaveAs(saveFilePath); err != nil {
|
||||
return "", fmt.Errorf("保存工作表失败 %v", err)
|
||||
}
|
||||
return saveFilePath, nil
|
||||
}
|
||||
297
src/framework/utils/file/file.go
Normal file
297
src/framework/utils/file/file.go
Normal file
@@ -0,0 +1,297 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/config"
|
||||
"ems.agt/src/framework/constants/uploadsubpath"
|
||||
"ems.agt/src/framework/logger"
|
||||
"ems.agt/src/framework/utils/date"
|
||||
"ems.agt/src/framework/utils/generate"
|
||||
"ems.agt/src/framework/utils/parse"
|
||||
"ems.agt/src/framework/utils/regular"
|
||||
)
|
||||
|
||||
/**最大文件名长度 */
|
||||
const DEFAULT_FILE_NAME_LENGTH = 100
|
||||
|
||||
// 文件上传路径 prefix, dir
|
||||
func resourceUpload() (string, string) {
|
||||
upload := config.Get("staticFile.upload").(map[string]any)
|
||||
dir, err := filepath.Abs(upload["dir"].(string))
|
||||
if err != nil {
|
||||
logger.Errorf("file resourceUpload err %v", err)
|
||||
}
|
||||
return upload["prefix"].(string), dir
|
||||
}
|
||||
|
||||
// 最大上传文件大小
|
||||
func uploadFileSize() int64 {
|
||||
fileSize := 1 * 1024 * 1024
|
||||
size := config.Get("upload.fileSize").(int)
|
||||
if size > 1 {
|
||||
fileSize = size * 1024 * 1024
|
||||
}
|
||||
return int64(fileSize)
|
||||
}
|
||||
|
||||
// 文件上传扩展名白名单
|
||||
func uploadWhiteList() []string {
|
||||
arr := config.Get("upload.whitelist").([]any)
|
||||
strings := make([]string, len(arr))
|
||||
for i, val := range arr {
|
||||
if str, ok := val.(string); ok {
|
||||
strings[i] = str
|
||||
}
|
||||
}
|
||||
return strings
|
||||
}
|
||||
|
||||
// 生成文件名称 fileName_随机值.extName
|
||||
//
|
||||
// fileName 原始文件名称含后缀,如:logo.png
|
||||
func generateFileName(fileName string) string {
|
||||
fileExt := filepath.Ext(fileName)
|
||||
// 替换掉后缀和特殊字符保留文件名
|
||||
newFileName := regular.Replace(fileName, fileExt, "")
|
||||
newFileName = regular.Replace(newFileName, `[<>:"\\|?*]+`, "")
|
||||
newFileName = strings.TrimSpace(newFileName)
|
||||
return fmt.Sprintf("%s_%s%s", newFileName, generate.Code(6), fileExt)
|
||||
}
|
||||
|
||||
// 检查文件允许写入本地
|
||||
//
|
||||
// fileName 原始文件名称含后缀,如:midway1_logo_iipc68.png
|
||||
//
|
||||
// allowExts 允许上传拓展类型,['.png']
|
||||
func isAllowWrite(fileName string, allowExts []string, fileSize int64) error {
|
||||
// 判断上传文件名称长度
|
||||
if len(fileName) > DEFAULT_FILE_NAME_LENGTH {
|
||||
return fmt.Errorf("上传文件名称长度限制最长为 %d", DEFAULT_FILE_NAME_LENGTH)
|
||||
}
|
||||
|
||||
// 最大上传文件大小
|
||||
maxFileSize := uploadFileSize()
|
||||
if fileSize > maxFileSize {
|
||||
return fmt.Errorf("最大上传文件大小 %s", parse.Bit(float64(maxFileSize)))
|
||||
}
|
||||
|
||||
// 判断文件拓展是否为允许的拓展类型
|
||||
fileExt := filepath.Ext(fileName)
|
||||
hasExt := false
|
||||
if len(allowExts) == 0 {
|
||||
allowExts = uploadWhiteList()
|
||||
}
|
||||
for _, ext := range allowExts {
|
||||
if ext == fileExt {
|
||||
hasExt = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasExt {
|
||||
return fmt.Errorf("上传文件类型不支持,仅支持以下类型:%s", strings.Join(allowExts, "、"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查文件允许本地读取
|
||||
//
|
||||
// filePath 文件存放资源路径,URL相对地址
|
||||
func isAllowRead(filePath string) error {
|
||||
// 禁止目录上跳级别
|
||||
if strings.Contains(filePath, "..") {
|
||||
return fmt.Errorf("禁止目录上跳级别")
|
||||
}
|
||||
|
||||
// 检查允许下载的文件规则
|
||||
fileExt := filepath.Ext(filePath)
|
||||
hasExt := false
|
||||
for _, ext := range uploadWhiteList() {
|
||||
if ext == fileExt {
|
||||
hasExt = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasExt {
|
||||
return fmt.Errorf("非法下载的文件规则:%s", fileExt)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TransferUploadFile 上传资源文件转存
|
||||
//
|
||||
// subPath 子路径,默认 UploadSubPath.DEFAULT
|
||||
//
|
||||
// allowExts 允许上传拓展类型(含“.”),如 ['.png','.jpg']
|
||||
func TransferUploadFile(file *multipart.FileHeader, subPath string, allowExts []string) (string, error) {
|
||||
// 上传文件检查
|
||||
err := isAllowWrite(file.Filename, allowExts, file.Size)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// 上传资源路径
|
||||
prefix, dir := resourceUpload()
|
||||
// 新文件名称并组装文件地址
|
||||
fileName := generateFileName(file.Filename)
|
||||
filePath := filepath.Join(subPath, date.ParseDatePath(time.Now()))
|
||||
writePathFile := filepath.Join(dir, filePath, fileName)
|
||||
// 存入新文件路径
|
||||
err = transferToNewFile(file, writePathFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
urlPath := filepath.Join(prefix, filePath, fileName)
|
||||
return filepath.ToSlash(urlPath), nil
|
||||
}
|
||||
|
||||
// ReadUploadFileStream 上传资源文件读取
|
||||
//
|
||||
// filePath 文件存放资源路径,URL相对地址 如:/upload/common/2023/06/xxx.png
|
||||
//
|
||||
// headerRange 断点续传范围区间,bytes=0-12131
|
||||
func ReadUploadFileStream(filePath, headerRange string) (map[string]any, error) {
|
||||
// 读取文件检查
|
||||
err := isAllowRead(filePath)
|
||||
if err != nil {
|
||||
return map[string]any{}, err
|
||||
}
|
||||
// 上传资源路径
|
||||
prefix, dir := resourceUpload()
|
||||
fileAsbPath := strings.Replace(filePath, prefix, dir, 1)
|
||||
|
||||
// 响应结果
|
||||
result := map[string]any{
|
||||
"range": "",
|
||||
"chunkSize": 0,
|
||||
"fileSize": 0,
|
||||
"data": nil,
|
||||
}
|
||||
|
||||
// 文件大小
|
||||
fileSize := getFileSize(fileAsbPath)
|
||||
if fileSize <= 0 {
|
||||
return result, nil
|
||||
}
|
||||
result["fileSize"] = fileSize
|
||||
|
||||
if headerRange != "" {
|
||||
partsStr := strings.Replace(headerRange, "bytes=", "", 1)
|
||||
parts := strings.Split(partsStr, "-")
|
||||
start, err := strconv.ParseInt(parts[0], 10, 64)
|
||||
if err != nil || start > fileSize {
|
||||
start = 0
|
||||
}
|
||||
end, err := strconv.ParseInt(parts[1], 10, 64)
|
||||
if err != nil || end > fileSize {
|
||||
end = fileSize - 1
|
||||
}
|
||||
if start > end {
|
||||
start = end
|
||||
}
|
||||
|
||||
// 分片结果
|
||||
result["range"] = fmt.Sprintf("bytes %d-%d/%d", start, end, fileSize)
|
||||
result["chunkSize"] = end - start + 1
|
||||
byteArr, err := getFileStream(fileAsbPath, start, end)
|
||||
if err != nil {
|
||||
return map[string]any{}, errors.New("读取文件失败")
|
||||
}
|
||||
result["data"] = byteArr
|
||||
return result, nil
|
||||
}
|
||||
|
||||
byteArr, err := getFileStream(fileAsbPath, 0, fileSize)
|
||||
if err != nil {
|
||||
return map[string]any{}, errors.New("读取文件失败")
|
||||
}
|
||||
result["data"] = byteArr
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// TransferChunkUploadFile 上传资源切片文件转存
|
||||
//
|
||||
// file 上传文件对象
|
||||
//
|
||||
// index 切片文件序号
|
||||
//
|
||||
// identifier 切片文件目录标识符
|
||||
func TransferChunkUploadFile(file *multipart.FileHeader, index, identifier string) (string, error) {
|
||||
// 上传文件检查
|
||||
err := isAllowWrite(file.Filename, []string{}, file.Size)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// 上传资源路径
|
||||
prefix, dir := resourceUpload()
|
||||
// 新文件名称并组装文件地址
|
||||
filePath := filepath.Join(uploadsubpath.CHUNK, date.ParseDatePath(time.Now()), identifier)
|
||||
writePathFile := filepath.Join(dir, filePath, index)
|
||||
// 存入新文件路径
|
||||
err = transferToNewFile(file, writePathFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
urlPath := filepath.Join(prefix, filePath, index)
|
||||
return filepath.ToSlash(urlPath), nil
|
||||
}
|
||||
|
||||
// 上传资源切片文件检查
|
||||
//
|
||||
// identifier 切片文件目录标识符
|
||||
//
|
||||
// originalFileName 原始文件名称,如logo.png
|
||||
func ChunkCheckFile(identifier, originalFileName string) ([]string, error) {
|
||||
// 读取文件检查
|
||||
err := isAllowWrite(originalFileName, []string{}, 0)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
// 上传资源路径
|
||||
_, dir := resourceUpload()
|
||||
dirPath := path.Join(uploadsubpath.CHUNK, date.ParseDatePath(time.Now()), identifier)
|
||||
readPath := path.Join(dir, dirPath)
|
||||
fileList, err := getDirFileNameList(readPath)
|
||||
if err != nil {
|
||||
return []string{}, errors.New("读取文件失败")
|
||||
}
|
||||
return fileList, nil
|
||||
}
|
||||
|
||||
// 上传资源切片文件检查
|
||||
//
|
||||
// identifier 切片文件目录标识符
|
||||
//
|
||||
// originalFileName 原始文件名称,如logo.png
|
||||
//
|
||||
// subPath 子路径,默认 DEFAULT
|
||||
func ChunkMergeFile(identifier, originalFileName, subPath string) (string, error) {
|
||||
// 读取文件检查
|
||||
err := isAllowWrite(originalFileName, []string{}, 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// 上传资源路径
|
||||
prefix, dir := resourceUpload()
|
||||
// 切片存放目录
|
||||
dirPath := path.Join(uploadsubpath.CHUNK, date.ParseDatePath(time.Now()), identifier)
|
||||
readPath := path.Join(dir, dirPath)
|
||||
// 组合存放文件路径
|
||||
fileName := generateFileName(originalFileName)
|
||||
filePath := path.Join(subPath, date.ParseDatePath(time.Now()))
|
||||
writePath := path.Join(dir, filePath)
|
||||
err = mergeToNewFile(readPath, writePath, fileName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
urlPath := filepath.Join(prefix, filePath, fileName)
|
||||
return filepath.ToSlash(urlPath), nil
|
||||
}
|
||||
185
src/framework/utils/file/utils.go
Normal file
185
src/framework/utils/file/utils.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"ems.agt/src/framework/logger"
|
||||
)
|
||||
|
||||
// transferToNewFile 读取目标文件转移到新路径下
|
||||
//
|
||||
// readFilePath 读取目标文件
|
||||
//
|
||||
// writePath 写入路径
|
||||
//
|
||||
// fileName 文件名称
|
||||
func transferToNewFile(file *multipart.FileHeader, dst string) error {
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
if err = os.MkdirAll(filepath.Dir(dst), 0750); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, src)
|
||||
return err
|
||||
}
|
||||
|
||||
// mergeToNewFile 将多个文件合并成一个文件并删除合并前的切片目录文件
|
||||
//
|
||||
// dirPath 读取要合并文件的目录
|
||||
//
|
||||
// writePath 写入路径
|
||||
//
|
||||
// fileName 文件名称
|
||||
func mergeToNewFile(dirPath string, writePath string, fileName string) error {
|
||||
// 读取目录下所有文件并排序,注意文件名称是否数值
|
||||
fileNameList, err := getDirFileNameList(dirPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取合并目标文件失败: %v", err)
|
||||
}
|
||||
if len(fileNameList) <= 0 {
|
||||
return fmt.Errorf("读取合并目标文件失败")
|
||||
}
|
||||
|
||||
// 排序
|
||||
sort.Slice(fileNameList, func(i, j int) bool {
|
||||
numI, _ := strconv.Atoi(fileNameList[i])
|
||||
numJ, _ := strconv.Atoi(fileNameList[j])
|
||||
return numI < numJ
|
||||
})
|
||||
|
||||
// 写入到新路径文件
|
||||
newFilePath := filepath.Join(writePath, fileName)
|
||||
if err = os.MkdirAll(filepath.Dir(newFilePath), 0750); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 转移完成后删除切片目录
|
||||
defer os.Remove(dirPath)
|
||||
|
||||
// 打开新路径文件
|
||||
outputFile, err := os.Create(newFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file: %v", err)
|
||||
}
|
||||
defer outputFile.Close()
|
||||
|
||||
// 逐个读取文件后进行流拷贝数据块
|
||||
for _, fileName := range fileNameList {
|
||||
chunkPath := filepath.Join(dirPath, fileName)
|
||||
// 拷贝结束后删除切片
|
||||
defer os.Remove(chunkPath)
|
||||
// 打开切片文件
|
||||
inputFile, err := os.Open(chunkPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file: %v", err)
|
||||
}
|
||||
defer inputFile.Close()
|
||||
// 拷贝文件流
|
||||
_, err = io.Copy(outputFile, inputFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy file data: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getFileSize 读取文件大小
|
||||
func getFileSize(filePath string) int64 {
|
||||
// 获取文件信息
|
||||
fileInfo, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed stat %s: %v", filePath, err)
|
||||
return 0
|
||||
}
|
||||
// 获取文件大小(字节数)
|
||||
return fileInfo.Size()
|
||||
}
|
||||
|
||||
// 读取文件流用于返回下载
|
||||
//
|
||||
// filePath 文件路径
|
||||
// startOffset, endOffset 分片块读取区间,根据文件切片的块范围
|
||||
func getFileStream(filePath string, startOffset, endOffset int64) ([]byte, error) {
|
||||
// 打开文件
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 获取文件的大小
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fileSize := fileInfo.Size()
|
||||
|
||||
// 确保起始和结束偏移量在文件范围内
|
||||
if startOffset > fileSize {
|
||||
startOffset = 0
|
||||
}
|
||||
if endOffset >= fileSize {
|
||||
endOffset = fileSize - 1
|
||||
}
|
||||
|
||||
// 计算切片的大小
|
||||
chunkSize := endOffset - startOffset + 1
|
||||
|
||||
// 创建 SectionReader
|
||||
reader := io.NewSectionReader(file, startOffset, chunkSize)
|
||||
|
||||
// 创建一个缓冲区来存储读取的数据
|
||||
buffer := make([]byte, chunkSize)
|
||||
|
||||
// 读取数据到缓冲区
|
||||
_, err = reader.Read(buffer)
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buffer, nil
|
||||
}
|
||||
|
||||
// 获取文件目录下所有文件名称,不含目录名称
|
||||
//
|
||||
// filePath 文件路径
|
||||
func getDirFileNameList(dirPath string) ([]string, error) {
|
||||
fileNames := []string{}
|
||||
|
||||
dir, err := os.Open(dirPath)
|
||||
if err != nil {
|
||||
return fileNames, nil
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
fileInfos, err := dir.Readdir(-1)
|
||||
if err != nil {
|
||||
return fileNames, err
|
||||
}
|
||||
|
||||
for _, fileInfo := range fileInfos {
|
||||
if fileInfo.Mode().IsRegular() {
|
||||
fileNames = append(fileNames, fileInfo.Name())
|
||||
}
|
||||
}
|
||||
|
||||
return fileNames, nil
|
||||
}
|
||||
43
src/framework/utils/generate/generate.go
Normal file
43
src/framework/utils/generate/generate.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/logger"
|
||||
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
)
|
||||
|
||||
// 生成随机Code
|
||||
// 包含数字、小写字母
|
||||
// 不保证长度满足
|
||||
func Code(size int) string {
|
||||
str, err := gonanoid.Generate("0123456789abcdefghijklmnopqrstuvwxyz", size)
|
||||
if err != nil {
|
||||
logger.Infof("%d : %v", size, err)
|
||||
return ""
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// 生成随机字符串
|
||||
// 包含数字、大小写字母、下划线、横杠
|
||||
// 不保证长度满足
|
||||
func String(size int) string {
|
||||
str, err := gonanoid.New(size)
|
||||
if err != nil {
|
||||
logger.Infof("%d : %v", size, err)
|
||||
return ""
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// 随机数 纯数字0-9
|
||||
func Number(size int) int {
|
||||
source := rand.NewSource(time.Now().UnixNano())
|
||||
random := rand.New(source)
|
||||
min := int64(0)
|
||||
max := int64(9 * int(size))
|
||||
return int(random.Int63n(max-min+1) + min)
|
||||
}
|
||||
238
src/framework/utils/ip2region/binding.go
Normal file
238
src/framework/utils/ip2region/binding.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package ip2region
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderInfoLength = 256
|
||||
VectorIndexRows = 256
|
||||
VectorIndexCols = 256
|
||||
VectorIndexSize = 8
|
||||
SegmentIndexBlockSize = 14
|
||||
)
|
||||
|
||||
// --- Index policy define
|
||||
|
||||
type IndexPolicy int
|
||||
|
||||
const (
|
||||
VectorIndexPolicy IndexPolicy = 1
|
||||
BTreeIndexPolicy IndexPolicy = 2
|
||||
)
|
||||
|
||||
func (i IndexPolicy) String() string {
|
||||
switch i {
|
||||
case VectorIndexPolicy:
|
||||
return "VectorIndex"
|
||||
case BTreeIndexPolicy:
|
||||
return "BtreeIndex"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// --- Header define
|
||||
|
||||
type Header struct {
|
||||
// data []byte
|
||||
Version uint16
|
||||
IndexPolicy IndexPolicy
|
||||
CreatedAt uint32
|
||||
StartIndexPtr uint32
|
||||
EndIndexPtr uint32
|
||||
}
|
||||
|
||||
func NewHeader(input []byte) (*Header, error) {
|
||||
if len(input) < 16 {
|
||||
return nil, fmt.Errorf("invalid input buffer")
|
||||
}
|
||||
|
||||
return &Header{
|
||||
Version: binary.LittleEndian.Uint16(input),
|
||||
IndexPolicy: IndexPolicy(binary.LittleEndian.Uint16(input[2:])),
|
||||
CreatedAt: binary.LittleEndian.Uint32(input[4:]),
|
||||
StartIndexPtr: binary.LittleEndian.Uint32(input[8:]),
|
||||
EndIndexPtr: binary.LittleEndian.Uint32(input[12:]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// --- searcher implementation
|
||||
|
||||
type Searcher struct {
|
||||
handle *os.File
|
||||
|
||||
ioCount int
|
||||
|
||||
// use it only when this feature enabled.
|
||||
// Preload the vector index will reduce the number of IO operations
|
||||
// thus speedup the search process
|
||||
vectorIndex []byte
|
||||
|
||||
// content buffer.
|
||||
// running with the whole xdb file cached
|
||||
contentBuff []byte
|
||||
}
|
||||
|
||||
func baseNew(dbFile string, vIndex []byte, cBuff []byte) (*Searcher, error) {
|
||||
var err error
|
||||
|
||||
// content buff first
|
||||
if cBuff != nil {
|
||||
return &Searcher{
|
||||
vectorIndex: nil,
|
||||
contentBuff: cBuff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// open the xdb binary file
|
||||
handle, err := os.OpenFile(dbFile, os.O_RDONLY, 0600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Searcher{
|
||||
handle: handle,
|
||||
vectorIndex: vIndex,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewWithFileOnly(dbFile string) (*Searcher, error) {
|
||||
return baseNew(dbFile, nil, nil)
|
||||
}
|
||||
|
||||
func NewWithVectorIndex(dbFile string, vIndex []byte) (*Searcher, error) {
|
||||
return baseNew(dbFile, vIndex, nil)
|
||||
}
|
||||
|
||||
func NewWithBuffer(cBuff []byte) (*Searcher, error) {
|
||||
return baseNew("", nil, cBuff)
|
||||
}
|
||||
|
||||
func (s *Searcher) Close() {
|
||||
if s.handle != nil {
|
||||
err := s.handle.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetIOCount return the global io count for the last search
|
||||
func (s *Searcher) GetIOCount() int {
|
||||
return s.ioCount
|
||||
}
|
||||
|
||||
// SearchByStr find the region for the specified ip string
|
||||
func (s *Searcher) SearchByStr(str string) (string, error) {
|
||||
ip, err := CheckIP(str)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return s.Search(ip)
|
||||
}
|
||||
|
||||
// Search find the region for the specified long ip
|
||||
func (s *Searcher) Search(ip uint32) (string, error) {
|
||||
// reset the global ioCount
|
||||
s.ioCount = 0
|
||||
|
||||
// locate the segment index block based on the vector index
|
||||
var il0 = (ip >> 24) & 0xFF
|
||||
var il1 = (ip >> 16) & 0xFF
|
||||
var idx = il0*VectorIndexCols*VectorIndexSize + il1*VectorIndexSize
|
||||
var sPtr, ePtr = uint32(0), uint32(0)
|
||||
if s.vectorIndex != nil {
|
||||
sPtr = binary.LittleEndian.Uint32(s.vectorIndex[idx:])
|
||||
ePtr = binary.LittleEndian.Uint32(s.vectorIndex[idx+4:])
|
||||
} else if s.contentBuff != nil {
|
||||
sPtr = binary.LittleEndian.Uint32(s.contentBuff[HeaderInfoLength+idx:])
|
||||
ePtr = binary.LittleEndian.Uint32(s.contentBuff[HeaderInfoLength+idx+4:])
|
||||
} else {
|
||||
// read the vector index block
|
||||
var buff = make([]byte, VectorIndexSize)
|
||||
err := s.read(int64(HeaderInfoLength+idx), buff)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read vector index block at %d: %w", HeaderInfoLength+idx, err)
|
||||
}
|
||||
|
||||
sPtr = binary.LittleEndian.Uint32(buff)
|
||||
ePtr = binary.LittleEndian.Uint32(buff[4:])
|
||||
}
|
||||
|
||||
// fmt.Printf("sPtr=%d, ePtr=%d", sPtr, ePtr)
|
||||
|
||||
// binary search the segment index to get the region
|
||||
var dataLen, dataPtr = 0, uint32(0)
|
||||
var buff = make([]byte, SegmentIndexBlockSize)
|
||||
var l, h = 0, int((ePtr - sPtr) / SegmentIndexBlockSize)
|
||||
for l <= h {
|
||||
m := (l + h) >> 1
|
||||
p := sPtr + uint32(m*SegmentIndexBlockSize)
|
||||
err := s.read(int64(p), buff)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read segment index at %d: %w", p, err)
|
||||
}
|
||||
|
||||
// decode the data step by step to reduce the unnecessary operations
|
||||
sip := binary.LittleEndian.Uint32(buff)
|
||||
if ip < sip {
|
||||
h = m - 1
|
||||
} else {
|
||||
eip := binary.LittleEndian.Uint32(buff[4:])
|
||||
if ip > eip {
|
||||
l = m + 1
|
||||
} else {
|
||||
dataLen = int(binary.LittleEndian.Uint16(buff[8:]))
|
||||
dataPtr = binary.LittleEndian.Uint32(buff[10:])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//fmt.Printf("dataLen: %d, dataPtr: %d", dataLen, dataPtr)
|
||||
if dataLen == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// load and return the region data
|
||||
var regionBuff = make([]byte, dataLen)
|
||||
err := s.read(int64(dataPtr), regionBuff)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read region at %d: %w", dataPtr, err)
|
||||
}
|
||||
|
||||
return string(regionBuff), nil
|
||||
}
|
||||
|
||||
// do the data read operation based on the setting.
|
||||
// content buffer first or will read from the file.
|
||||
// this operation will invoke the Seek for file based read.
|
||||
func (s *Searcher) read(offset int64, buff []byte) error {
|
||||
if s.contentBuff != nil {
|
||||
cLen := copy(buff, s.contentBuff[offset:])
|
||||
if cLen != len(buff) {
|
||||
return fmt.Errorf("incomplete read: readed bytes should be %d", len(buff))
|
||||
}
|
||||
} else {
|
||||
_, err := s.handle.Seek(offset, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("seek to %d: %w", offset, err)
|
||||
}
|
||||
|
||||
s.ioCount++
|
||||
rLen, err := s.handle.Read(buff)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handle read: %w", err)
|
||||
}
|
||||
|
||||
if rLen != len(buff) {
|
||||
return fmt.Errorf("incomplete read: readed bytes should be %d", len(buff))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
89
src/framework/utils/ip2region/ip2region.go
Normal file
89
src/framework/utils/ip2region/ip2region.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package ip2region
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/logger"
|
||||
)
|
||||
|
||||
// 网络地址(内网)
|
||||
const LOCAT_HOST = "127.0.0.1"
|
||||
|
||||
// 全局查询对象
|
||||
var searcher *Searcher
|
||||
|
||||
//go:embed ip2region.xdb
|
||||
var ip2regionDB embed.FS
|
||||
|
||||
func init() {
|
||||
// 从 dbPath 加载整个 xdb 到内存
|
||||
buf, err := ip2regionDB.ReadFile("ip2region.xdb")
|
||||
if err != nil {
|
||||
logger.Fatalf("failed error load xdb from : %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 用全局的 cBuff 创建完全基于内存的查询对象。
|
||||
base, err := NewWithBuffer(buf)
|
||||
if err != nil {
|
||||
logger.Errorf("failed error create searcher with content: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 赋值到全局查询对象
|
||||
searcher = base
|
||||
}
|
||||
|
||||
// RegionSearchByIp 查询IP所在地
|
||||
//
|
||||
// 国家|区域|省份|城市|ISP
|
||||
func RegionSearchByIp(ip string) (string, int, int64) {
|
||||
ip = ClientIP(ip)
|
||||
if ip == LOCAT_HOST {
|
||||
return "0|0|0|内网IP|内网IP", 0, 0
|
||||
}
|
||||
tStart := time.Now()
|
||||
region, err := searcher.SearchByStr(ip)
|
||||
if err != nil {
|
||||
logger.Errorf("failed to SearchIP(%s): %s\n", ip, err)
|
||||
return "0|0|0|0|0", 0, 0
|
||||
}
|
||||
return region, 0, time.Since(tStart).Milliseconds()
|
||||
}
|
||||
|
||||
// RealAddressByIp 地址IP所在地
|
||||
//
|
||||
// 218.4.167.70 江苏省 苏州市
|
||||
func RealAddressByIp(ip string) string {
|
||||
ip = ClientIP(ip)
|
||||
if ip == LOCAT_HOST {
|
||||
return "内网IP"
|
||||
}
|
||||
region, err := searcher.SearchByStr(ip)
|
||||
if err != nil {
|
||||
logger.Errorf("failed to SearchIP(%s): %s\n", ip, err)
|
||||
return "未知"
|
||||
}
|
||||
parts := strings.Split(region, "|")
|
||||
province := parts[2]
|
||||
city := parts[3]
|
||||
if province == "0" && city != "0" {
|
||||
return city
|
||||
}
|
||||
return province + " " + city
|
||||
}
|
||||
|
||||
// ClientIP 处理客户端IP地址显示iPv4
|
||||
//
|
||||
// 转换 ip2region.ClientIP(c.ClientIP())
|
||||
func ClientIP(ip string) string {
|
||||
if strings.HasPrefix(ip, "::ffff:") {
|
||||
ip = strings.Replace(ip, "::ffff:", "", 1)
|
||||
}
|
||||
if ip == LOCAT_HOST || ip == "::1" {
|
||||
return LOCAT_HOST
|
||||
}
|
||||
return ip
|
||||
}
|
||||
BIN
src/framework/utils/ip2region/ip2region.xdb
Normal file
BIN
src/framework/utils/ip2region/ip2region.xdb
Normal file
Binary file not shown.
175
src/framework/utils/ip2region/util.go
Normal file
175
src/framework/utils/ip2region/util.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package ip2region
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var shiftIndex = []int{24, 16, 8, 0}
|
||||
|
||||
func CheckIP(ip string) (uint32, error) {
|
||||
var ps = strings.Split(strings.TrimSpace(ip), ".")
|
||||
if len(ps) != 4 {
|
||||
return 0, fmt.Errorf("invalid ip address `%s`", ip)
|
||||
}
|
||||
|
||||
var val = uint32(0)
|
||||
for i, s := range ps {
|
||||
d, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("the %dth part `%s` is not an integer", i, s)
|
||||
}
|
||||
|
||||
if d < 0 || d > 255 {
|
||||
return 0, fmt.Errorf("the %dth part `%s` should be an integer bettween 0 and 255", i, s)
|
||||
}
|
||||
|
||||
val |= uint32(d) << shiftIndex[i]
|
||||
}
|
||||
|
||||
// convert the ip to integer
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func Long2IP(ip uint32) string {
|
||||
return fmt.Sprintf("%d.%d.%d.%d", (ip>>24)&0xFF, (ip>>16)&0xFF, (ip>>8)&0xFF, ip&0xFF)
|
||||
}
|
||||
|
||||
func MidIP(sip uint32, eip uint32) uint32 {
|
||||
return uint32((uint64(sip) + uint64(eip)) >> 1)
|
||||
}
|
||||
|
||||
// LoadHeader load the header info from the specified handle
|
||||
func LoadHeader(handle *os.File) (*Header, error) {
|
||||
_, err := handle.Seek(0, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("seek to the header: %w", err)
|
||||
}
|
||||
|
||||
var buff = make([]byte, HeaderInfoLength)
|
||||
rLen, err := handle.Read(buff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rLen != len(buff) {
|
||||
return nil, fmt.Errorf("incomplete read: readed bytes should be %d", len(buff))
|
||||
}
|
||||
|
||||
return NewHeader(buff)
|
||||
}
|
||||
|
||||
// LoadHeaderFromFile load header info from the specified db file path
|
||||
func LoadHeaderFromFile(dbFile string) (*Header, error) {
|
||||
handle, err := os.OpenFile(dbFile, os.O_RDONLY, 0600)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open xdb file `%s`: %w", dbFile, err)
|
||||
}
|
||||
|
||||
defer func(handle *os.File) {
|
||||
_ = handle.Close()
|
||||
}(handle)
|
||||
|
||||
header, err := LoadHeader(handle)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return header, nil
|
||||
}
|
||||
|
||||
// LoadHeaderFromBuff wrap the header info from the content buffer
|
||||
func LoadHeaderFromBuff(cBuff []byte) (*Header, error) {
|
||||
return NewHeader(cBuff[0:256])
|
||||
}
|
||||
|
||||
// LoadVectorIndex util function to load the vector index from the specified file handle
|
||||
func LoadVectorIndex(handle *os.File) ([]byte, error) {
|
||||
// load all the vector index block
|
||||
_, err := handle.Seek(HeaderInfoLength, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("seek to vector index: %w", err)
|
||||
}
|
||||
|
||||
var buff = make([]byte, VectorIndexRows*VectorIndexCols*VectorIndexSize)
|
||||
rLen, err := handle.Read(buff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rLen != len(buff) {
|
||||
return nil, fmt.Errorf("incomplete read: readed bytes should be %d", len(buff))
|
||||
}
|
||||
|
||||
return buff, nil
|
||||
}
|
||||
|
||||
// LoadVectorIndexFromFile load vector index from a specified file path
|
||||
func LoadVectorIndexFromFile(dbFile string) ([]byte, error) {
|
||||
handle, err := os.OpenFile(dbFile, os.O_RDONLY, 0600)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open xdb file `%s`: %w", dbFile, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = handle.Close()
|
||||
}()
|
||||
|
||||
vIndex, err := LoadVectorIndex(handle)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vIndex, nil
|
||||
}
|
||||
|
||||
// LoadContent load the whole xdb content from the specified file handle
|
||||
func LoadContent(handle *os.File) ([]byte, error) {
|
||||
// get file size
|
||||
fi, err := handle.Stat()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stat: %w", err)
|
||||
}
|
||||
|
||||
size := fi.Size()
|
||||
|
||||
// seek to the head of the file
|
||||
_, err = handle.Seek(0, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("seek to get xdb file length: %w", err)
|
||||
}
|
||||
|
||||
var buff = make([]byte, size)
|
||||
rLen, err := handle.Read(buff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rLen != len(buff) {
|
||||
return nil, fmt.Errorf("incomplete read: readed bytes should be %d", len(buff))
|
||||
}
|
||||
|
||||
return buff, nil
|
||||
}
|
||||
|
||||
// LoadContentFromFile load the whole xdb content from the specified db file path
|
||||
func LoadContentFromFile(dbFile string) ([]byte, error) {
|
||||
handle, err := os.OpenFile(dbFile, os.O_RDONLY, 0600)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open xdb file `%s`: %w", dbFile, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = handle.Close()
|
||||
}()
|
||||
|
||||
cBuff, err := LoadContent(handle)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cBuff, nil
|
||||
}
|
||||
|
||||
167
src/framework/utils/parse/parse.go
Normal file
167
src/framework/utils/parse/parse.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
// Number 解析数值型
|
||||
func Number(str any) int64 {
|
||||
switch str := str.(type) {
|
||||
case string:
|
||||
if str == "" {
|
||||
return 0
|
||||
}
|
||||
num, err := strconv.ParseInt(str, 10, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return num
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
||||
return reflect.ValueOf(str).Int()
|
||||
case float32, float64:
|
||||
return int64(reflect.ValueOf(str).Float())
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Boolean 解析布尔型
|
||||
func Boolean(str any) bool {
|
||||
switch str := str.(type) {
|
||||
case string:
|
||||
if str == "" || str == "false" || str == "0" {
|
||||
return false
|
||||
}
|
||||
// 尝试将字符串解析为数字
|
||||
if num, err := strconv.ParseFloat(str, 64); err == nil {
|
||||
return num != 0
|
||||
}
|
||||
return true
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
||||
num := reflect.ValueOf(str).Int()
|
||||
return num != 0
|
||||
case float32, float64:
|
||||
num := reflect.ValueOf(str).Float()
|
||||
return num != 0
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertToCamelCase 字符串转换驼峰形式
|
||||
//
|
||||
// 字符串 dict/inline/data/:dictId 结果 DictInlineDataDictId
|
||||
func ConvertToCamelCase(str string) string {
|
||||
if len(str) == 0 {
|
||||
return str
|
||||
}
|
||||
reg := regexp.MustCompile(`[-_:/]\w`)
|
||||
result := reg.ReplaceAllStringFunc(str, func(match string) string {
|
||||
return strings.ToUpper(string(match[1]))
|
||||
})
|
||||
|
||||
words := strings.Fields(result)
|
||||
for i, word := range words {
|
||||
str := word[1:]
|
||||
str = strings.ReplaceAll(str, "/", "")
|
||||
words[i] = strings.ToUpper(word[:1]) + str
|
||||
}
|
||||
|
||||
return strings.Join(words, "")
|
||||
}
|
||||
|
||||
// Bit 比特位为单位
|
||||
func Bit(bit float64) string {
|
||||
var GB, MB, KB string
|
||||
|
||||
if bit > float64(1<<30) {
|
||||
GB = fmt.Sprintf("%0.2f", bit/(1<<30))
|
||||
}
|
||||
|
||||
if bit > float64(1<<20) && bit < (1<<30) {
|
||||
MB = fmt.Sprintf("%.2f", bit/(1<<20))
|
||||
}
|
||||
|
||||
if bit > float64(1<<10) && bit < (1<<20) {
|
||||
KB = fmt.Sprintf("%.2f", bit/(1<<10))
|
||||
}
|
||||
|
||||
if GB != "" {
|
||||
return GB + "GB"
|
||||
} else if MB != "" {
|
||||
return MB + "MB"
|
||||
} else if KB != "" {
|
||||
return KB + "KB"
|
||||
} else {
|
||||
return fmt.Sprintf("%vB", bit)
|
||||
}
|
||||
}
|
||||
|
||||
// CronExpression 解析 Cron 表达式,返回下一次执行的时间戳(毫秒)
|
||||
//
|
||||
// 【*/5 * * * * ?】 6个参数
|
||||
func CronExpression(expression string) int64 {
|
||||
specParser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
|
||||
schedule, err := specParser.Parse(expression)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return schedule.Next(time.Now()).UnixMilli()
|
||||
}
|
||||
|
||||
// SafeContent 内容值进行安全掩码
|
||||
func SafeContent(value string) string {
|
||||
if len(value) < 3 {
|
||||
return strings.Repeat("*", len(value))
|
||||
} else if len(value) < 6 {
|
||||
return string(value[0]) + strings.Repeat("*", len(value)-1)
|
||||
} else if len(value) < 10 {
|
||||
return string(value[0]) + strings.Repeat("*", len(value)-2) + string(value[len(value)-1])
|
||||
} else if len(value) < 15 {
|
||||
return value[:2] + strings.Repeat("*", len(value)-4) + value[len(value)-2:]
|
||||
} else {
|
||||
return value[:3] + strings.Repeat("*", len(value)-6) + value[len(value)-3:]
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveDuplicates 数组内字符串去重
|
||||
func RemoveDuplicates(ids []string) []string {
|
||||
uniqueIDs := make(map[string]bool)
|
||||
uniqueIDSlice := make([]string, 0)
|
||||
|
||||
for _, id := range ids {
|
||||
_, ok := uniqueIDs[id]
|
||||
if !ok && id != "" {
|
||||
uniqueIDs[id] = true
|
||||
uniqueIDSlice = append(uniqueIDSlice, id)
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueIDSlice
|
||||
}
|
||||
|
||||
// Color 解析颜色 #fafafa
|
||||
func Color(colorStr string) *color.RGBA {
|
||||
// 去除 # 号
|
||||
colorStr = colorStr[1:]
|
||||
|
||||
// 将颜色字符串拆分为 R、G、B 分量
|
||||
r, _ := strconv.ParseInt(colorStr[0:2], 16, 0)
|
||||
g, _ := strconv.ParseInt(colorStr[2:4], 16, 0)
|
||||
b, _ := strconv.ParseInt(colorStr[4:6], 16, 0)
|
||||
|
||||
return &color.RGBA{
|
||||
R: uint8(r),
|
||||
G: uint8(g),
|
||||
B: uint8(b),
|
||||
A: 255, // 不透明
|
||||
}
|
||||
}
|
||||
86
src/framework/utils/regular/regular.go
Normal file
86
src/framework/utils/regular/regular.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package regular
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/dlclark/regexp2"
|
||||
)
|
||||
|
||||
// Replace 正则替换
|
||||
func Replace(originStr, pattern, repStr string) string {
|
||||
regex := regexp.MustCompile(pattern)
|
||||
return regex.ReplaceAllString(originStr, repStr)
|
||||
}
|
||||
|
||||
// 判断是否为有效用户名格式
|
||||
//
|
||||
// 用户名不能以数字开头,可包含大写小写字母,数字,且不少于5位
|
||||
func ValidUsername(username string) bool {
|
||||
if username == "" {
|
||||
return false
|
||||
}
|
||||
pattern := `^[a-zA-Z][a-z0-9A-Z]{5,}`
|
||||
match, err := regexp.MatchString(pattern, username)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
// 判断是否为有效密码格式
|
||||
//
|
||||
// 密码至少包含大小写字母、数字、特殊符号,且不少于6位
|
||||
func ValidPassword(password string) bool {
|
||||
if password == "" {
|
||||
return false
|
||||
}
|
||||
pattern := `^(?![A-Za-z0-9]+$)(?![a-z0-9\W]+$)(?![A-Za-z\W]+$)(?![A-Z0-9\W]+$)[a-zA-Z0-9\W]{6,}$`
|
||||
re := regexp2.MustCompile(pattern, 0)
|
||||
match, err := re.MatchString(password)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
// 判断是否为有效手机号格式,1开头的11位手机号
|
||||
func ValidMobile(mobile string) bool {
|
||||
if mobile == "" {
|
||||
return false
|
||||
}
|
||||
pattern := `^1[3|4|5|6|7|8|9][0-9]\d{8}$`
|
||||
match, err := regexp.MatchString(pattern, mobile)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
// 判断是否为有效邮箱格式
|
||||
func ValidEmail(email string) bool {
|
||||
if email == "" {
|
||||
return false
|
||||
}
|
||||
pattern := `^(([^<>()\\.,;:\s@"]+(\.[^<>()\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+\.)+[a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}))$`
|
||||
re := regexp2.MustCompile(pattern, 0)
|
||||
match, err := re.MatchString(email)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
// 判断是否为http(s)://开头
|
||||
//
|
||||
// link 网络链接
|
||||
func ValidHttp(link string) bool {
|
||||
if link == "" {
|
||||
return false
|
||||
}
|
||||
pattern := `^http(s)?:\/\/+`
|
||||
match, err := regexp.MatchString(pattern, link)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return match
|
||||
}
|
||||
132
src/framework/utils/repo/repo.go
Normal file
132
src/framework/utils/repo/repo.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"ems.agt/src/framework/utils/parse"
|
||||
)
|
||||
|
||||
// PageNumSize 分页页码记录数
|
||||
func PageNumSize(pageNum, pageSize any) (int64, int64) {
|
||||
// 记录起始索引
|
||||
num := parse.Number(pageNum)
|
||||
if num > 5000 {
|
||||
num = 5000
|
||||
}
|
||||
if num < 1 {
|
||||
num = 1
|
||||
}
|
||||
|
||||
// 显示记录数
|
||||
size := parse.Number(pageSize)
|
||||
if size > 50000 {
|
||||
size = 50000
|
||||
}
|
||||
if size < 0 {
|
||||
size = 10
|
||||
}
|
||||
return num - 1, size
|
||||
}
|
||||
|
||||
// SetFieldValue 判断结构体内是否存在指定字段并设置值
|
||||
func SetFieldValue(obj any, fieldName string, value any) {
|
||||
// 获取结构体的反射值
|
||||
userValue := reflect.ValueOf(obj)
|
||||
|
||||
// 获取字段的反射值
|
||||
fieldValue := userValue.Elem().FieldByName(fieldName)
|
||||
|
||||
// 检查字段是否存在
|
||||
if fieldValue.IsValid() && fieldValue.CanSet() {
|
||||
// 获取字段的类型
|
||||
fieldType := fieldValue.Type()
|
||||
|
||||
// 转换传入的值类型为字段类型
|
||||
switch fieldType.Kind() {
|
||||
case reflect.String:
|
||||
if value == nil {
|
||||
fieldValue.SetString("")
|
||||
} else {
|
||||
fieldValue.SetString(fmt.Sprintf("%v", value))
|
||||
}
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
intValue, err := strconv.ParseInt(fmt.Sprintf("%v", value), 10, 64)
|
||||
if err != nil {
|
||||
intValue = 0
|
||||
}
|
||||
fieldValue.SetInt(intValue)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
uintValue, err := strconv.ParseUint(fmt.Sprintf("%v", value), 10, 64)
|
||||
if err != nil {
|
||||
uintValue = 0
|
||||
}
|
||||
fieldValue.SetUint(uintValue)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
floatValue, err := strconv.ParseFloat(fmt.Sprintf("%v", value), 64)
|
||||
if err != nil {
|
||||
floatValue = 0
|
||||
}
|
||||
fieldValue.SetFloat(floatValue)
|
||||
default:
|
||||
// 设置字段的值
|
||||
fieldValue.Set(reflect.ValueOf(value).Convert(fieldValue.Type()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertIdsSlice 将 []string 转换为 []any
|
||||
func ConvertIdsSlice(ids []string) []any {
|
||||
// 将 []string 转换为 []any
|
||||
arr := make([]any, len(ids))
|
||||
for i, v := range ids {
|
||||
arr[i] = v
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// 查询-参数值的占位符
|
||||
func KeyPlaceholderByQuery(sum int) string {
|
||||
placeholders := make([]string, sum)
|
||||
for i := 0; i < sum; i++ {
|
||||
placeholders[i] = "?"
|
||||
}
|
||||
return strings.Join(placeholders, ",")
|
||||
}
|
||||
|
||||
// 插入-参数映射键值占位符 keys, placeholder, values
|
||||
func KeyPlaceholderValueByInsert(params map[string]any) ([]string, string, []any) {
|
||||
// 参数映射的键
|
||||
keys := make([]string, len(params))
|
||||
// 参数映射的值
|
||||
values := make([]any, len(params))
|
||||
sum := 0
|
||||
for k, v := range params {
|
||||
keys[sum] = k
|
||||
values[sum] = v
|
||||
sum++
|
||||
}
|
||||
// 参数值的占位符
|
||||
placeholders := make([]string, sum)
|
||||
for i := 0; i < sum; i++ {
|
||||
placeholders[i] = "?"
|
||||
}
|
||||
return keys, strings.Join(placeholders, ","), values
|
||||
}
|
||||
|
||||
// 更新-参数映射键值占位符 keys, values
|
||||
func KeyValueByUpdate(params map[string]any) ([]string, []any) {
|
||||
// 参数映射的键
|
||||
keys := make([]string, len(params))
|
||||
// 参数映射的值
|
||||
values := make([]any, len(params))
|
||||
sum := 0
|
||||
for k, v := range params {
|
||||
keys[sum] = k + "=?"
|
||||
values[sum] = v
|
||||
sum++
|
||||
}
|
||||
return keys, values
|
||||
}
|
||||
151
src/framework/utils/token/token.go
Normal file
151
src/framework/utils/token/token.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/config"
|
||||
cachekeyConstants "ems.agt/src/framework/constants/cachekey"
|
||||
tokenConstants "ems.agt/src/framework/constants/token"
|
||||
"ems.agt/src/framework/logger"
|
||||
redisCahe "ems.agt/src/framework/redis"
|
||||
"ems.agt/src/framework/utils/generate"
|
||||
"ems.agt/src/framework/vo"
|
||||
|
||||
jwt "github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// Remove 清除登录用户信息UUID
|
||||
func Remove(tokenStr string) string {
|
||||
claims, err := Verify(tokenStr)
|
||||
if err != nil {
|
||||
logger.Errorf("token verify err %v", err)
|
||||
return ""
|
||||
}
|
||||
// 清除缓存KEY
|
||||
uuid := claims[tokenConstants.JWT_UUID].(string)
|
||||
tokenKey := cachekeyConstants.LOGIN_TOKEN_KEY + uuid
|
||||
hasKey, _ := redisCahe.Has("", tokenKey)
|
||||
if hasKey {
|
||||
redisCahe.Del("", tokenKey)
|
||||
}
|
||||
return claims[tokenConstants.JWT_NAME].(string)
|
||||
}
|
||||
|
||||
// Create 令牌生成
|
||||
func Create(loginUser *vo.LoginUser, ilobArgs ...string) string {
|
||||
// 生成用户唯一tokne32位
|
||||
loginUser.UUID = generate.Code(32)
|
||||
|
||||
// 设置请求用户登录客户端
|
||||
loginUser.IPAddr = ilobArgs[0]
|
||||
loginUser.LoginLocation = ilobArgs[1]
|
||||
loginUser.OS = ilobArgs[2]
|
||||
loginUser.Browser = ilobArgs[3]
|
||||
|
||||
// 设置用户令牌有效期并存入缓存
|
||||
Cache(loginUser)
|
||||
|
||||
// 令牌算法 HS256 HS384 HS512
|
||||
algorithm := config.Get("jwt.algorithm").(string)
|
||||
var method *jwt.SigningMethodHMAC
|
||||
switch algorithm {
|
||||
case "HS512":
|
||||
method = jwt.SigningMethodHS512
|
||||
case "HS384":
|
||||
method = jwt.SigningMethodHS384
|
||||
case "HS256":
|
||||
default:
|
||||
method = jwt.SigningMethodHS256
|
||||
}
|
||||
// 生成令牌负荷绑定uuid标识
|
||||
jwtToken := jwt.NewWithClaims(method, jwt.MapClaims{
|
||||
tokenConstants.JWT_UUID: loginUser.UUID,
|
||||
tokenConstants.JWT_KEY: loginUser.UserID,
|
||||
tokenConstants.JWT_NAME: loginUser.User.UserName,
|
||||
"exp": loginUser.ExpireTime,
|
||||
"ait": loginUser.LoginTime,
|
||||
})
|
||||
|
||||
// 生成令牌设置密钥
|
||||
secret := config.Get("jwt.secret").(string)
|
||||
tokenStr, err := jwtToken.SignedString([]byte(secret))
|
||||
if err != nil {
|
||||
logger.Infof("jwt sign err : %v", err)
|
||||
return ""
|
||||
}
|
||||
return tokenStr
|
||||
}
|
||||
|
||||
// Cache 缓存登录用户信息
|
||||
func Cache(loginUser *vo.LoginUser) {
|
||||
// 计算配置的有效期
|
||||
expTime := config.Get("jwt.expiresIn").(int)
|
||||
expTimestamp := time.Duration(expTime) * time.Minute
|
||||
iatTimestamp := time.Now().UnixMilli()
|
||||
loginUser.LoginTime = iatTimestamp
|
||||
loginUser.ExpireTime = iatTimestamp + expTimestamp.Milliseconds()
|
||||
// 根据登录标识将loginUser缓存
|
||||
tokenKey := cachekeyConstants.LOGIN_TOKEN_KEY + loginUser.UUID
|
||||
jsonBytes, err := json.Marshal(loginUser)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
redisCahe.SetByExpire("", tokenKey, string(jsonBytes), expTimestamp)
|
||||
}
|
||||
|
||||
// RefreshIn 验证令牌有效期,相差不足xx分钟,自动刷新缓存
|
||||
func RefreshIn(loginUser *vo.LoginUser) {
|
||||
// 相差不足xx分钟,自动刷新缓存
|
||||
refreshTime := config.Get("jwt.refreshIn").(int)
|
||||
refreshTimestamp := time.Duration(refreshTime) * time.Minute
|
||||
// 过期时间
|
||||
expireTimestamp := loginUser.ExpireTime
|
||||
currentTimestamp := time.Now().UnixMilli()
|
||||
if expireTimestamp-currentTimestamp <= refreshTimestamp.Milliseconds() {
|
||||
Cache(loginUser)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify 校验令牌是否有效
|
||||
func Verify(tokenString string) (jwt.MapClaims, error) {
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
|
||||
// 判断加密算法是预期的加密算法
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); ok {
|
||||
secret := config.Get("jwt.secret").(string)
|
||||
return []byte(secret), nil
|
||||
}
|
||||
return nil, jwt.ErrSignatureInvalid
|
||||
})
|
||||
if err != nil {
|
||||
logger.Errorf("token String Verify : %v", err)
|
||||
return nil, errors.New("无效身份授权")
|
||||
}
|
||||
// 如果解析负荷成功并通过签名校验
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
return nil, errors.New("token valid error")
|
||||
}
|
||||
|
||||
// LoginUser 缓存的登录用户信息
|
||||
func LoginUser(claims jwt.MapClaims) vo.LoginUser {
|
||||
uuid := claims[tokenConstants.JWT_UUID].(string)
|
||||
tokenKey := cachekeyConstants.LOGIN_TOKEN_KEY + uuid
|
||||
hasKey, _ := redisCahe.Has("", tokenKey)
|
||||
var loginUser vo.LoginUser
|
||||
if hasKey {
|
||||
loginUserStr, _ := redisCahe.Get("", tokenKey)
|
||||
if loginUserStr == "" {
|
||||
return loginUser
|
||||
}
|
||||
err := json.Unmarshal([]byte(loginUserStr), &loginUser)
|
||||
if err != nil {
|
||||
logger.Errorf("loginuser info json err : %v", err)
|
||||
return loginUser
|
||||
}
|
||||
return loginUser
|
||||
}
|
||||
return loginUser
|
||||
}
|
||||
8
src/framework/utils/ua/ua.go
Normal file
8
src/framework/utils/ua/ua.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package ua
|
||||
|
||||
import "github.com/mssola/user_agent"
|
||||
|
||||
// 获取user-agent信息
|
||||
func Info(userAgent string) *user_agent.UserAgent {
|
||||
return user_agent.New(userAgent)
|
||||
}
|
||||
Reference in New Issue
Block a user