更新前后端代码

This commit is contained in:
2026-08-23 00:48:10 +08:00
parent 1b5468995f
commit 5c1313e4f6
74 changed files with 9995 additions and 306 deletions
+142
View File
@@ -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...))
}
+133
View File
@@ -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)
}
+96
View File
@@ -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
}
+100
View File
@@ -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))
}