更新前后端代码
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"filestoragesystem/internal/model"
|
||||
)
|
||||
|
||||
var logger *zap.Logger
|
||||
|
||||
// Logger 获取全局日志对象
|
||||
func Logger() *zap.Logger {
|
||||
if logger == nil {
|
||||
logger, _ = zap.NewProduction()
|
||||
}
|
||||
return logger
|
||||
}
|
||||
|
||||
// InitLogger 初始化zap日志(控制台 + 可选文件)
|
||||
func InitLogger(level, dir string) (*zap.Logger, error) {
|
||||
lvl := zapcore.InfoLevel
|
||||
_ = lvl.UnmarshalText([]byte(level))
|
||||
|
||||
encoderCfg := zap.NewProductionEncoderConfig()
|
||||
encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder
|
||||
encoderCfg.EncodeLevel = zapcore.CapitalLevelEncoder
|
||||
|
||||
cores := make([]zapcore.Core, 0, 3)
|
||||
|
||||
// 控制台
|
||||
consoleSyncer := zapcore.AddSync(os.Stdout)
|
||||
consoleEncoder := zapcore.NewConsoleEncoder(encoderCfg)
|
||||
cores = append(cores, zapcore.NewCore(consoleEncoder, consoleSyncer, lvl))
|
||||
|
||||
// 文件
|
||||
if dir != "" {
|
||||
if err := os.MkdirAll(dir, 0o755); err == nil {
|
||||
fileSyncer := zapcore.AddSync(&dateRollFile{dir: dir})
|
||||
fileEncoder := zapcore.NewJSONEncoder(encoderCfg)
|
||||
cores = append(cores, zapcore.NewCore(fileEncoder, fileSyncer, lvl))
|
||||
}
|
||||
}
|
||||
|
||||
logger = zap.New(zapcore.NewTee(cores...), zap.AddCaller(), zap.AddCallerSkip(1))
|
||||
return logger, nil
|
||||
}
|
||||
|
||||
// dateRollFile 按天滚动的日志文件
|
||||
type dateRollFile struct {
|
||||
dir string
|
||||
f *os.File
|
||||
day string
|
||||
}
|
||||
|
||||
func (d *dateRollFile) Write(p []byte) (int, error) {
|
||||
today := time.Now().Format("2006-01-02")
|
||||
if d.f == nil || d.day != today {
|
||||
if d.f != nil {
|
||||
_ = d.f.Close()
|
||||
}
|
||||
path := filepath.Join(d.dir, "app-"+today+".log")
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
d.f, d.day = f, today
|
||||
}
|
||||
return d.f.Write(p)
|
||||
}
|
||||
|
||||
func (d *dateRollFile) Sync() error {
|
||||
if d.f != nil {
|
||||
return d.f.Sync()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- 系统日志异步落库 ----
|
||||
|
||||
var sysLogDB *gorm.DB
|
||||
var sysLogCh chan model.SystemLog
|
||||
|
||||
// InitSystemLog 初始化系统日志异步写入器
|
||||
func InitSystemLog(db *gorm.DB) {
|
||||
sysLogDB = db
|
||||
sysLogCh = make(chan model.SystemLog, 256)
|
||||
go func() {
|
||||
for entry := range sysLogCh {
|
||||
if err := sysLogDB.Create(&entry).Error; err != nil {
|
||||
Logger().Error("写入系统日志失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// SystemLog 异步记录一条系统日志
|
||||
func SystemLog(level, module, message string) {
|
||||
if sysLogCh == nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case sysLogCh <- model.SystemLog{Level: level, Module: module, Message: message, CreatedAt: time.Now()}:
|
||||
default:
|
||||
// 队列满则丢弃,避免阻塞业务
|
||||
}
|
||||
}
|
||||
|
||||
// SystemLogDetail 异步记录带详情的系统日志
|
||||
func SystemLogDetail(level, module, message, requestID, ip, extra string) {
|
||||
if sysLogCh == nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case sysLogCh <- model.SystemLog{
|
||||
Level: level, Module: module, Message: message,
|
||||
RequestID: requestID, IP: ip, ExtraData: extra, CreatedAt: time.Now(),
|
||||
}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Info 记录info级别系统日志
|
||||
func Info(module, format string, args ...interface{}) {
|
||||
SystemLog("info", module, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Warn 记录warn级别系统日志
|
||||
func Warn(module, format string, args ...interface{}) {
|
||||
SystemLog("warn", module, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Error 记录error级别系统日志
|
||||
func Error(module, format string, args ...interface{}) {
|
||||
SystemLog("error", module, fmt.Sprintf(format, args...))
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ClientIP 获取客户端真实IP
|
||||
func ClientIP(c *gin.Context) string {
|
||||
// 优先取反向代理头
|
||||
for _, header := range []string{"X-Real-IP", "X-Forwarded-For"} {
|
||||
if v := c.GetHeader(header); v != "" {
|
||||
ips := strings.Split(v, ",")
|
||||
ip := strings.TrimSpace(ips[0])
|
||||
if ip != "" {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
}
|
||||
return c.ClientIP()
|
||||
}
|
||||
|
||||
// SanitizePath 清理虚拟路径,防止路径穿越
|
||||
func SanitizePath(p string) string {
|
||||
p = strings.ReplaceAll(p, "\\", "/")
|
||||
parts := make([]string, 0, 8)
|
||||
for _, seg := range strings.Split(p, "/") {
|
||||
seg = strings.TrimSpace(seg)
|
||||
if seg == "" || seg == "." {
|
||||
continue
|
||||
}
|
||||
if seg == ".." {
|
||||
if len(parts) > 0 {
|
||||
parts = parts[:len(parts)-1]
|
||||
}
|
||||
continue
|
||||
}
|
||||
parts = append(parts, seg)
|
||||
}
|
||||
return strings.Join(parts, "/")
|
||||
}
|
||||
|
||||
// IsValidPassword 检查密码强度:至少8位,包含字母和数字
|
||||
func IsValidPassword(pwd string) bool {
|
||||
if len(pwd) < 8 {
|
||||
return false
|
||||
}
|
||||
hasLetter, hasDigit := false, false
|
||||
for _, ch := range pwd {
|
||||
switch {
|
||||
case ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z':
|
||||
hasLetter = true
|
||||
case ch >= '0' && ch <= '9':
|
||||
hasDigit = true
|
||||
}
|
||||
}
|
||||
return hasLetter && hasDigit
|
||||
}
|
||||
|
||||
// IsEmail 简单校验邮箱格式
|
||||
func IsEmail(s string) bool {
|
||||
at := strings.IndexByte(s, '@')
|
||||
if at <= 0 || at == len(s)-1 {
|
||||
return false
|
||||
}
|
||||
domain := s[at+1:]
|
||||
return strings.Contains(domain, ".") && strings.IndexByte(domain, '.') != 0 && strings.IndexByte(domain, '.') != len(domain)-1
|
||||
}
|
||||
|
||||
// HumanSize 将字节数转为可读大小
|
||||
func HumanSize(n int64) string {
|
||||
const unit = 1024
|
||||
if n < unit {
|
||||
return itoa(n) + " B"
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for m := n / unit; m >= unit && exp < 4; m /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
val := float64(n) / float64(div)
|
||||
s := trimFloat(val)
|
||||
units := []string{"KB", "MB", "GB", "TB", "PB"}
|
||||
return s + " " + units[exp]
|
||||
}
|
||||
|
||||
func itoa(n int64) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
neg := n < 0
|
||||
if neg {
|
||||
n = -n
|
||||
}
|
||||
var buf [20]byte
|
||||
i := len(buf)
|
||||
for n > 0 {
|
||||
i--
|
||||
buf[i] = byte('0' + n%10)
|
||||
n /= 10
|
||||
}
|
||||
if neg {
|
||||
i--
|
||||
buf[i] = '-'
|
||||
}
|
||||
return string(buf[i:])
|
||||
}
|
||||
|
||||
func trimFloat(f float64) string {
|
||||
// 保留两位小数并去掉末尾多余的0
|
||||
s := itoa(int64(f))
|
||||
frac := f - float64(int64(f))
|
||||
if frac < 0.005 {
|
||||
return s
|
||||
}
|
||||
d1 := int64(frac * 10)
|
||||
if d1 > 9 {
|
||||
d1 = 9
|
||||
}
|
||||
frac = frac*10 - float64(d1)
|
||||
d2 := int64(frac * 10)
|
||||
if d2 > 9 {
|
||||
d2 = 9
|
||||
}
|
||||
if d2 < 5 {
|
||||
if d1 == 0 {
|
||||
return s
|
||||
}
|
||||
return s + "." + itoa(d1)
|
||||
}
|
||||
return s + "." + itoa(d1) + itoa(d2)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 统一响应码
|
||||
const (
|
||||
CodeSuccess = 0
|
||||
CodeError = 1
|
||||
CodeParamError = 400
|
||||
CodeUnauthorized = 401
|
||||
CodeForbidden = 403
|
||||
CodeNotFound = 404
|
||||
CodeServerErr = 500
|
||||
)
|
||||
|
||||
// Response 统一响应结构
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// PageData 分页响应结构
|
||||
type PageData struct {
|
||||
List interface{} `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// OK 成功响应
|
||||
func OK(c *gin.Context, data interface{}) {
|
||||
c.JSON(http.StatusOK, Response{Code: CodeSuccess, Message: "success", Data: data})
|
||||
}
|
||||
|
||||
// OKMsg 成功响应(仅消息)
|
||||
func OKMsg(c *gin.Context, msg string) {
|
||||
c.JSON(http.StatusOK, Response{Code: CodeSuccess, Message: msg})
|
||||
}
|
||||
|
||||
// OKPage 分页成功响应
|
||||
func OKPage(c *gin.Context, list interface{}, total int64, page, pageSize int) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Code: CodeSuccess,
|
||||
Message: "success",
|
||||
Data: PageData{List: list, Total: total, Page: page, PageSize: pageSize},
|
||||
})
|
||||
}
|
||||
|
||||
// Fail 失败响应
|
||||
func Fail(c *gin.Context, httpCode, code int, msg string) {
|
||||
c.JSON(httpCode, Response{Code: code, Message: msg})
|
||||
}
|
||||
|
||||
// FailMsg 默认400失败响应
|
||||
func FailMsg(c *gin.Context, msg string) {
|
||||
Fail(c, http.StatusBadRequest, CodeError, msg)
|
||||
}
|
||||
|
||||
// Unauthorized 401
|
||||
func Unauthorized(c *gin.Context, msg string) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, Response{Code: CodeUnauthorized, Message: msg})
|
||||
}
|
||||
|
||||
// Forbidden 403
|
||||
func Forbidden(c *gin.Context, msg string) {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, Response{Code: CodeForbidden, Message: msg})
|
||||
}
|
||||
|
||||
// NotFound 404
|
||||
func NotFound(c *gin.Context, msg string) {
|
||||
c.JSON(http.StatusNotFound, Response{Code: CodeNotFound, Message: msg})
|
||||
}
|
||||
|
||||
// ServerError 500
|
||||
func ServerError(c *gin.Context, msg string) {
|
||||
c.JSON(http.StatusInternalServerError, Response{Code: CodeServerErr, Message: msg})
|
||||
}
|
||||
|
||||
// ParsePage 解析分页参数
|
||||
func ParsePage(c *gin.Context) (page, pageSize int) {
|
||||
page = 1
|
||||
pageSize = 20
|
||||
if v, err := strconv.Atoi(c.Query("page")); err == nil && v > 0 {
|
||||
page = v
|
||||
}
|
||||
if v, err := strconv.Atoi(c.Query("page_size")); err == nil && v > 0 && v <= 100 {
|
||||
pageSize = v
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var jwtSecret = "please-change-this-secret-key"
|
||||
var jwtExpire = 7 * time.Hour
|
||||
|
||||
// InitJWT 初始化JWT参数
|
||||
func InitJWT(secret, expire string) {
|
||||
if secret != "" {
|
||||
jwtSecret = secret
|
||||
}
|
||||
if d, err := time.ParseDuration(expire); err == nil && d > 0 {
|
||||
jwtExpire = d
|
||||
}
|
||||
}
|
||||
|
||||
// JWTClaims 自定义Claims
|
||||
type JWTClaims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
RoleCode string `json:"role_code"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// GenerateToken 生成JWT Token
|
||||
func GenerateToken(userID uint, username, roleCode string) (string, error) {
|
||||
claims := JWTClaims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
RoleCode: roleCode,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(jwtExpire)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: "filestoragesystem",
|
||||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(jwtSecret))
|
||||
}
|
||||
|
||||
// ParseToken 解析并校验JWT Token
|
||||
func ParseToken(tokenStr string) (*JWTClaims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenStr, &JWTClaims{}, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("非法的签名算法")
|
||||
}
|
||||
return []byte(jwtSecret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
return nil, errors.New("无效的Token")
|
||||
}
|
||||
|
||||
// HashPassword bcrypt加密密码
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// CheckPassword 校验密码
|
||||
func CheckPassword(hash, password string) bool {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
|
||||
}
|
||||
|
||||
// MD5 计算字节串的MD5
|
||||
func MD5(data []byte) string {
|
||||
h := md5.New()
|
||||
h.Write(data)
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// RandomKey 生成指定长度的随机hex字符串
|
||||
func RandomKey(n int) string {
|
||||
b := make([]byte, (n+1)/2)
|
||||
_, _ = rand.Read(b)
|
||||
return hex.EncodeToString(b)[:n]
|
||||
}
|
||||
|
||||
// HMACSHA256 计算HMAC-SHA256签名(hex编码)
|
||||
func HMACSHA256(secret, message string) string {
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write([]byte(message))
|
||||
return hex.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
Reference in New Issue
Block a user