初始化v1
This commit is contained in:
0
utility/.gitkeep
Normal file
0
utility/.gitkeep
Normal file
17
utility/crypto/crypto.go
Normal file
17
utility/crypto/crypto.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// HashPassword 密码加密
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// CheckPassword 验证密码
|
||||
func CheckPassword(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
33
utility/database/check.go
Normal file
33
utility/database/check.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"nl-video-api/utility/response"
|
||||
)
|
||||
|
||||
// CheckConnection 检查数据库连接
|
||||
func CheckConnection(ctx context.Context) error {
|
||||
return g.DB().PingMaster()
|
||||
}
|
||||
|
||||
// CheckConnectionMiddleware 数据库连接检查中间件
|
||||
func CheckConnectionMiddleware(r *ghttp.Request) {
|
||||
if err := CheckConnection(r.Context()); err != nil {
|
||||
g.Log().Error(r.Context(), "数据库连接失败:", err)
|
||||
response.Error(r, response.CodeInternalError, "数据库服务暂时不可用,请稍后重试")
|
||||
return
|
||||
}
|
||||
r.Middleware.Next()
|
||||
}
|
||||
|
||||
// WithDBCheck 为控制器方法添加数据库连接检查
|
||||
func WithDBCheck(r *ghttp.Request) bool {
|
||||
if err := CheckConnection(r.Context()); err != nil {
|
||||
g.Log().Error(r.Context(), "数据库连接失败:", err)
|
||||
response.Error(r, response.CodeInternalError, "数据库服务暂时不可用,请稍后重试")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
177
utility/database/hook.go
Normal file
177
utility/database/hook.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"nl-video-api/utility/logger"
|
||||
)
|
||||
|
||||
// SQLHook SQL执行钩子,用于记录SQL日志和错误
|
||||
type SQLHook struct{}
|
||||
|
||||
// BeforeQuery 查询前钩子
|
||||
func (h *SQLHook) BeforeQuery(ctx context.Context, link gdb.Link, sql string, args []interface{}) (context.Context, error) {
|
||||
// 记录查询开始时间
|
||||
ctx = context.WithValue(ctx, "sql_start_time", time.Now())
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// AfterQuery 查询后钩子
|
||||
func (h *SQLHook) AfterQuery(ctx context.Context, link gdb.Link, sql string, args []interface{}, result gdb.Result, err error) error {
|
||||
// 计算执行时间
|
||||
startTime, ok := ctx.Value("sql_start_time").(time.Time)
|
||||
var duration time.Duration
|
||||
if ok {
|
||||
duration = time.Since(startTime)
|
||||
}
|
||||
|
||||
// 获取影响行数
|
||||
var rowsAffected int64
|
||||
if result != nil {
|
||||
rowsAffected = int64(result.Len())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// 记录SQL执行错误
|
||||
logger.LogDatabaseError(ctx, "Query", sql, err)
|
||||
logger.LogError(ctx, "SQL查询失败 | SQL: %s | Args: %v | Duration: %v | Error: %v",
|
||||
sql, args, duration, err)
|
||||
} else {
|
||||
// 记录SQL执行成功
|
||||
logger.LogDatabaseQuery(ctx, sql, args, duration, rowsAffected)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// BeforeExec 执行前钩子
|
||||
func (h *SQLHook) BeforeExec(ctx context.Context, link gdb.Link, sql string, args []interface{}) (context.Context, error) {
|
||||
// 记录执行开始时间
|
||||
ctx = context.WithValue(ctx, "sql_start_time", time.Now())
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// AfterExec 执行后钩子
|
||||
func (h *SQLHook) AfterExec(ctx context.Context, link gdb.Link, sql string, args []interface{}, result driver.Result, err error) error {
|
||||
// 计算执行时间
|
||||
startTime, ok := ctx.Value("sql_start_time").(time.Time)
|
||||
var duration time.Duration
|
||||
if ok {
|
||||
duration = time.Since(startTime)
|
||||
}
|
||||
|
||||
// 获取影响行数
|
||||
var rowsAffected int64
|
||||
if result != nil {
|
||||
if affected, e := result.RowsAffected(); e == nil {
|
||||
rowsAffected = affected
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// 记录SQL执行错误
|
||||
logger.LogDatabaseError(ctx, "Exec", sql, err)
|
||||
logger.LogError(ctx, "SQL执行失败 | SQL: %s | Args: %v | Duration: %v | Error: %v",
|
||||
sql, args, duration, err)
|
||||
} else {
|
||||
// 记录SQL执行成功
|
||||
logger.LogDatabaseQuery(ctx, sql, args, duration, rowsAffected)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// BeforePrepare 预处理前钩子
|
||||
func (h *SQLHook) BeforePrepare(ctx context.Context, link gdb.Link, sql string) (context.Context, error) {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// AfterPrepare 预处理后钩子
|
||||
func (h *SQLHook) AfterPrepare(ctx context.Context, link gdb.Link, sql string, stmt *gdb.Stmt, err error) error {
|
||||
if err != nil {
|
||||
logger.LogDatabaseError(ctx, "Prepare", sql, err)
|
||||
logger.LogError(ctx, "SQL预处理失败 | SQL: %s | Error: %v", sql, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// BeforeCommit 提交前钩子
|
||||
func (h *SQLHook) BeforeCommit(ctx context.Context, link gdb.Link) (context.Context, error) {
|
||||
logger.LogSQL(ctx, "事务提交开始")
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// AfterCommit 提交后钩子
|
||||
func (h *SQLHook) AfterCommit(ctx context.Context, link gdb.Link, err error) error {
|
||||
if err != nil {
|
||||
logger.LogError(ctx, "事务提交失败 | Error: %v", err)
|
||||
} else {
|
||||
logger.LogSQL(ctx, "事务提交成功")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// BeforeRollback 回滚前钩子
|
||||
func (h *SQLHook) BeforeRollback(ctx context.Context, link gdb.Link) (context.Context, error) {
|
||||
logger.LogSQL(ctx, "事务回滚开始")
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// AfterRollback 回滚后钩子
|
||||
func (h *SQLHook) AfterRollback(ctx context.Context, link gdb.Link, err error) error {
|
||||
if err != nil {
|
||||
logger.LogError(ctx, "事务回滚失败 | Error: %v", err)
|
||||
} else {
|
||||
logger.LogSQL(ctx, "事务回滚成功")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// InitDatabaseHook 初始化数据库钩子
|
||||
func InitDatabaseHook() {
|
||||
// 获取默认数据库实例
|
||||
db := g.DB()
|
||||
|
||||
// 添加SQL执行钩子
|
||||
db.AddHook(&SQLHook{})
|
||||
|
||||
logger.LogInfo(context.Background(), "数据库钩子初始化完成")
|
||||
}
|
||||
|
||||
// LogDatabaseConnection 记录数据库连接日志
|
||||
func LogDatabaseConnection(ctx context.Context, config gdb.ConfigNode, err error) {
|
||||
if err != nil {
|
||||
logger.LogError(ctx, "数据库连接失败 | Host: %s | Database: %s | Error: %v",
|
||||
config.Host, config.Name, err)
|
||||
} else {
|
||||
logger.LogInfo(ctx, "数据库连接成功 | Host: %s | Database: %s",
|
||||
config.Host, config.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// LogDatabasePing 记录数据库ping日志
|
||||
func LogDatabasePing(ctx context.Context, duration time.Duration, err error) {
|
||||
if err != nil {
|
||||
logger.LogError(ctx, "数据库ping失败 | Duration: %v | Error: %v", duration, err)
|
||||
} else {
|
||||
logger.LogInfo(ctx, "数据库ping成功 | Duration: %v", duration)
|
||||
}
|
||||
}
|
||||
|
||||
// LogTransactionStart 记录事务开始日志
|
||||
func LogTransactionStart(ctx context.Context, txId string) {
|
||||
logger.LogSQL(ctx, "事务开始 | TxID: %s", txId)
|
||||
}
|
||||
|
||||
// LogTransactionEnd 记录事务结束日志
|
||||
func LogTransactionEnd(ctx context.Context, txId string, success bool, duration time.Duration) {
|
||||
if success {
|
||||
logger.LogSQL(ctx, "事务结束(成功) | TxID: %s | Duration: %v", txId, duration)
|
||||
} else {
|
||||
logger.LogError(ctx, "事务结束(失败) | TxID: %s | Duration: %v", txId, duration)
|
||||
}
|
||||
}
|
||||
8
utility/helper/string.go
Normal file
8
utility/helper/string.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package helper
|
||||
|
||||
import "strings"
|
||||
|
||||
// Contains 检查字符串是否包含子字符串(忽略大小写)
|
||||
func Contains(s, substr string) bool {
|
||||
return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
|
||||
}
|
||||
73
utility/jwt/jwt.go
Normal file
73
utility/jwt/jwt.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
)
|
||||
|
||||
// Claims JWT载荷
|
||||
type Claims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
UserType string `json:"user_type"` // user, admin
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// GenerateToken 生成Token
|
||||
func GenerateToken(userID uint, username, userType string) (string, error) {
|
||||
var (
|
||||
ctx = gctx.New()
|
||||
signingKey = g.Cfg().MustGet(ctx, "jwt.signingKey").String()
|
||||
expire = g.Cfg().MustGet(ctx, "jwt.expire").Int()
|
||||
)
|
||||
|
||||
claims := Claims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
UserType: userType,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expire) * time.Second)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(signingKey))
|
||||
}
|
||||
|
||||
// ParseToken 解析Token
|
||||
func ParseToken(tokenString string) (*Claims, error) {
|
||||
var (
|
||||
ctx = gctx.New()
|
||||
signingKey = g.Cfg().MustGet(ctx, "jwt.signingKey").String()
|
||||
)
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(signingKey), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, jwt.ErrSignatureInvalid
|
||||
}
|
||||
|
||||
// RefreshToken 刷新Token
|
||||
func RefreshToken(tokenString string) (string, error) {
|
||||
claims, err := ParseToken(tokenString)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 生成新的Token
|
||||
return GenerateToken(claims.UserID, claims.Username, claims.UserType)
|
||||
}
|
||||
241
utility/logger/logger.go
Normal file
241
utility/logger/logger.go
Normal file
@@ -0,0 +1,241 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"github.com/gogf/gf/v2/os/glog"
|
||||
"github.com/gogf/gf/v2/util/guid"
|
||||
)
|
||||
|
||||
var (
|
||||
// 不同类型的日志记录器
|
||||
InfoLogger *glog.Logger
|
||||
ErrorLogger *glog.Logger
|
||||
SQLLogger *glog.Logger
|
||||
)
|
||||
|
||||
// InitLogger 初始化日志系统
|
||||
func InitLogger() {
|
||||
// 创建日志目录
|
||||
createLogDirs()
|
||||
|
||||
// 初始化不同类型的日志记录器
|
||||
InfoLogger = initLoggerByType("log")
|
||||
ErrorLogger = initLoggerByType("error")
|
||||
SQLLogger = initLoggerByType("sql")
|
||||
|
||||
g.Log().Info(context.Background(), "日志系统初始化完成")
|
||||
}
|
||||
|
||||
// createLogDirs 创建日志目录
|
||||
func createLogDirs() {
|
||||
// 创建主日志目录
|
||||
if err := gfile.Mkdir("logs"); err != nil {
|
||||
g.Log().Fatalf(context.Background(), "创建主日志目录失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建子目录
|
||||
dirs := []string{"logs/log", "logs/error", "logs/sql"}
|
||||
for _, dir := range dirs {
|
||||
if err := gfile.Mkdir(dir); err != nil {
|
||||
g.Log().Fatalf(context.Background(), "创建日志目录失败: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// initLoggerByType 根据类型初始化日志记录器
|
||||
func initLoggerByType(logType string) *glog.Logger {
|
||||
logger := glog.New()
|
||||
|
||||
// 生成日志文件名: YYYY_MM_DD_uuid.log
|
||||
now := time.Now()
|
||||
uuid := guid.S()
|
||||
fileName := fmt.Sprintf("%s_%s.log",
|
||||
now.Format("2006_01_02"),
|
||||
uuid[:8], // 使用UUID的前8位
|
||||
)
|
||||
|
||||
// 配置日志记录器
|
||||
logger.SetConfigWithMap(g.Map{
|
||||
"path": fmt.Sprintf("logs/%s", logType), // 日志目录统一到logs下
|
||||
"file": fileName, // 日志文件名
|
||||
"level": "all", // 日志级别
|
||||
"stdout": true, // 同时输出到控制台
|
||||
"rotateSize": "5M", // 5MB切割
|
||||
"rotateExpire": "7d", // 保留7天
|
||||
"rotateBackupLimit": 10, // 最多保留10个备份文件
|
||||
"rotateBackupExpire": "30d", // 备份文件保留30天
|
||||
"rotateBackupCompress": true, // 压缩备份文件
|
||||
"rotateCheckInterval": "1m", // 每分钟检查一次是否需要切割
|
||||
})
|
||||
|
||||
return logger
|
||||
}
|
||||
|
||||
// LogInfo 记录普通信息日志
|
||||
func LogInfo(ctx context.Context, format string, args ...interface{}) {
|
||||
if InfoLogger != nil {
|
||||
InfoLogger.Infof(ctx, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogError 记录错误日志
|
||||
func LogError(ctx context.Context, format string, args ...interface{}) {
|
||||
if ErrorLogger != nil {
|
||||
ErrorLogger.Errorf(ctx, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogSQL 记录SQL执行日志
|
||||
func LogSQL(ctx context.Context, format string, args ...interface{}) {
|
||||
if SQLLogger != nil {
|
||||
SQLLogger.Infof(ctx, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogDebug 记录调试日志
|
||||
func LogDebug(ctx context.Context, format string, args ...interface{}) {
|
||||
if InfoLogger != nil {
|
||||
InfoLogger.Debugf(ctx, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogWarn 记录警告日志
|
||||
func LogWarn(ctx context.Context, format string, args ...interface{}) {
|
||||
if InfoLogger != nil {
|
||||
InfoLogger.Warningf(ctx, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogFatal 记录致命错误日志
|
||||
func LogFatal(ctx context.Context, format string, args ...interface{}) {
|
||||
if ErrorLogger != nil {
|
||||
ErrorLogger.Fatalf(ctx, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogPanic 记录panic日志
|
||||
func LogPanic(ctx context.Context, format string, args ...interface{}) {
|
||||
if ErrorLogger != nil {
|
||||
ErrorLogger.Panicf(ctx, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// GetLoggerByType 根据类型获取日志记录器
|
||||
func GetLoggerByType(logType string) *glog.Logger {
|
||||
switch logType {
|
||||
case "info", "log":
|
||||
return InfoLogger
|
||||
case "error":
|
||||
return ErrorLogger
|
||||
case "sql":
|
||||
return SQLLogger
|
||||
default:
|
||||
return InfoLogger
|
||||
}
|
||||
}
|
||||
|
||||
// LogWithFields 记录带字段的日志
|
||||
func LogWithFields(ctx context.Context, logType string, level string, message string, fields g.Map) {
|
||||
logger := GetLoggerByType(logType)
|
||||
if logger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 构建日志消息
|
||||
logMsg := message
|
||||
if len(fields) > 0 {
|
||||
logMsg += " | Fields: "
|
||||
for k, v := range fields {
|
||||
logMsg += fmt.Sprintf("%s=%v ", k, v)
|
||||
}
|
||||
}
|
||||
|
||||
switch level {
|
||||
case "debug":
|
||||
logger.Debug(ctx, logMsg)
|
||||
case "info":
|
||||
logger.Info(ctx, logMsg)
|
||||
case "warn", "warning":
|
||||
logger.Warning(ctx, logMsg)
|
||||
case "error":
|
||||
logger.Error(ctx, logMsg)
|
||||
case "fatal":
|
||||
logger.Fatal(ctx, logMsg)
|
||||
case "panic":
|
||||
logger.Panic(ctx, logMsg)
|
||||
default:
|
||||
logger.Info(ctx, logMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// LogRequest 记录请求日志
|
||||
func LogRequest(ctx context.Context, method, uri, ip, userAgent string, duration time.Duration) {
|
||||
LogInfo(ctx, "请求日志 | Method: %s | URI: %s | IP: %s | UserAgent: %s | Duration: %v",
|
||||
method, uri, ip, userAgent, duration)
|
||||
}
|
||||
|
||||
// LogResponse 记录响应日志
|
||||
func LogResponse(ctx context.Context, status int, message string, data interface{}) {
|
||||
LogInfo(ctx, "响应日志 | Status: %d | Message: %s | HasData: %t",
|
||||
status, message, data != nil)
|
||||
}
|
||||
|
||||
// LogDatabaseError 记录数据库错误
|
||||
func LogDatabaseError(ctx context.Context, operation string, sql string, err error) {
|
||||
LogError(ctx, "数据库错误 | Operation: %s | SQL: %s | Error: %v",
|
||||
operation, sql, err)
|
||||
}
|
||||
|
||||
// LogDatabaseQuery 记录数据库查询
|
||||
func LogDatabaseQuery(ctx context.Context, sql string, args []interface{}, duration time.Duration, rowsAffected int64) {
|
||||
LogSQL(ctx, "SQL执行 | SQL: %s | Args: %v | Duration: %v | RowsAffected: %d",
|
||||
sql, args, duration, rowsAffected)
|
||||
}
|
||||
|
||||
// LogRedisError 记录Redis错误
|
||||
func LogRedisError(ctx context.Context, operation string, key string, err error) {
|
||||
LogError(ctx, "Redis错误 | Operation: %s | Key: %s | Error: %v",
|
||||
operation, key, err)
|
||||
}
|
||||
|
||||
// LogRedisOperation 记录Redis操作
|
||||
func LogRedisOperation(ctx context.Context, operation string, key string, value interface{}, duration time.Duration) {
|
||||
LogInfo(ctx, "Redis操作 | Operation: %s | Key: %s | HasValue: %t | Duration: %v",
|
||||
operation, key, value != nil, duration)
|
||||
}
|
||||
|
||||
// LogBusinessError 记录业务错误
|
||||
func LogBusinessError(ctx context.Context, module string, operation string, err error, extra g.Map) {
|
||||
fields := g.Map{
|
||||
"module": module,
|
||||
"operation": operation,
|
||||
"error": err.Error(),
|
||||
}
|
||||
|
||||
// 合并额外字段
|
||||
for k, v := range extra {
|
||||
fields[k] = v
|
||||
}
|
||||
|
||||
LogWithFields(ctx, "error", "error", "业务错误", fields)
|
||||
}
|
||||
|
||||
// LogSystemError 记录系统错误
|
||||
func LogSystemError(ctx context.Context, component string, err error, extra g.Map) {
|
||||
fields := g.Map{
|
||||
"component": component,
|
||||
"error": err.Error(),
|
||||
}
|
||||
|
||||
// 合并额外字段
|
||||
for k, v := range extra {
|
||||
fields[k] = v
|
||||
}
|
||||
|
||||
LogWithFields(ctx, "error", "error", "系统错误", fields)
|
||||
}
|
||||
205
utility/performance/monitor.go
Normal file
205
utility/performance/monitor.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package performance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
// Monitor 性能监控器
|
||||
type Monitor struct {
|
||||
startTime time.Time
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewMonitor 创建性能监控器
|
||||
func NewMonitor(ctx context.Context) *Monitor {
|
||||
return &Monitor{
|
||||
startTime: time.Now(),
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
// GetExecutionTime 获取执行时间
|
||||
func (m *Monitor) GetExecutionTime() time.Duration {
|
||||
return time.Since(m.startTime)
|
||||
}
|
||||
|
||||
// LogPerformance 记录性能信息
|
||||
func (m *Monitor) LogPerformance(operation string) {
|
||||
duration := m.GetExecutionTime()
|
||||
|
||||
// 记录性能日志
|
||||
g.Log().Info(m.ctx, "Performance Monitor", g.Map{
|
||||
"operation": operation,
|
||||
"duration": duration.String(),
|
||||
"timestamp": gtime.Now().String(),
|
||||
})
|
||||
|
||||
// 如果执行时间超过阈值,记录警告
|
||||
if duration > time.Second*2 {
|
||||
g.Log().Warning(m.ctx, "Slow Operation Detected", g.Map{
|
||||
"operation": operation,
|
||||
"duration": duration.String(),
|
||||
"threshold": "2s",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GetMemoryUsage 获取内存使用情况
|
||||
func GetMemoryUsage() map[string]interface{} {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
|
||||
return map[string]interface{}{
|
||||
"alloc": bToMb(m.Alloc), // 当前分配的内存
|
||||
"total_alloc": bToMb(m.TotalAlloc), // 总分配的内存
|
||||
"sys": bToMb(m.Sys), // 系统内存
|
||||
"num_gc": m.NumGC, // GC次数
|
||||
"goroutines": runtime.NumGoroutine(), // 协程数量
|
||||
}
|
||||
}
|
||||
|
||||
// GetSystemInfo 获取系统信息
|
||||
func GetSystemInfo() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"go_version": runtime.Version(),
|
||||
"go_os": runtime.GOOS,
|
||||
"go_arch": runtime.GOARCH,
|
||||
"cpu_count": runtime.NumCPU(),
|
||||
"goroutines": runtime.NumGoroutine(),
|
||||
"memory_usage": GetMemoryUsage(),
|
||||
}
|
||||
}
|
||||
|
||||
// bToMb 字节转MB
|
||||
func bToMb(b uint64) uint64 {
|
||||
return b / 1024 / 1024
|
||||
}
|
||||
|
||||
// DatabasePerformanceMonitor 数据库性能监控
|
||||
type DatabasePerformanceMonitor struct {
|
||||
slowQueryThreshold time.Duration
|
||||
}
|
||||
|
||||
// NewDatabasePerformanceMonitor 创建数据库性能监控器
|
||||
func NewDatabasePerformanceMonitor() *DatabasePerformanceMonitor {
|
||||
return &DatabasePerformanceMonitor{
|
||||
slowQueryThreshold: time.Millisecond * 500, // 500ms慢查询阈值
|
||||
}
|
||||
}
|
||||
|
||||
// LogSlowQuery 记录慢查询
|
||||
func (d *DatabasePerformanceMonitor) LogSlowQuery(ctx context.Context, sql string, duration time.Duration, args ...interface{}) {
|
||||
if duration > d.slowQueryThreshold {
|
||||
g.Log().Warning(ctx, "Slow Query Detected", g.Map{
|
||||
"sql": sql,
|
||||
"duration": duration.String(),
|
||||
"args": args,
|
||||
"threshold": d.slowQueryThreshold.String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// APIPerformanceMonitor API性能监控
|
||||
type APIPerformanceMonitor struct {
|
||||
requestCount map[string]int64
|
||||
responseTime map[string][]time.Duration
|
||||
}
|
||||
|
||||
// NewAPIPerformanceMonitor 创建API性能监控器
|
||||
func NewAPIPerformanceMonitor() *APIPerformanceMonitor {
|
||||
return &APIPerformanceMonitor{
|
||||
requestCount: make(map[string]int64),
|
||||
responseTime: make(map[string][]time.Duration),
|
||||
}
|
||||
}
|
||||
|
||||
// RecordRequest 记录请求
|
||||
func (a *APIPerformanceMonitor) RecordRequest(endpoint string, duration time.Duration) {
|
||||
a.requestCount[endpoint]++
|
||||
a.responseTime[endpoint] = append(a.responseTime[endpoint], duration)
|
||||
|
||||
// 保持最近100次请求的记录
|
||||
if len(a.responseTime[endpoint]) > 100 {
|
||||
a.responseTime[endpoint] = a.responseTime[endpoint][1:]
|
||||
}
|
||||
}
|
||||
|
||||
// GetStats 获取统计信息
|
||||
func (a *APIPerformanceMonitor) GetStats(endpoint string) map[string]interface{} {
|
||||
times := a.responseTime[endpoint]
|
||||
if len(times) == 0 {
|
||||
return map[string]interface{}{
|
||||
"request_count": a.requestCount[endpoint],
|
||||
"avg_time": 0,
|
||||
"min_time": 0,
|
||||
"max_time": 0,
|
||||
}
|
||||
}
|
||||
|
||||
var total, min, max time.Duration
|
||||
min = times[0]
|
||||
max = times[0]
|
||||
|
||||
for _, t := range times {
|
||||
total += t
|
||||
if t < min {
|
||||
min = t
|
||||
}
|
||||
if t > max {
|
||||
max = t
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"request_count": a.requestCount[endpoint],
|
||||
"avg_time": (total / time.Duration(len(times))).String(),
|
||||
"min_time": min.String(),
|
||||
"max_time": max.String(),
|
||||
"sample_count": len(times),
|
||||
}
|
||||
}
|
||||
|
||||
// CachePerformanceMonitor 缓存性能监控
|
||||
type CachePerformanceMonitor struct {
|
||||
hitCount int64
|
||||
missCount int64
|
||||
}
|
||||
|
||||
// NewCachePerformanceMonitor 创建缓存性能监控器
|
||||
func NewCachePerformanceMonitor() *CachePerformanceMonitor {
|
||||
return &CachePerformanceMonitor{}
|
||||
}
|
||||
|
||||
// RecordHit 记录缓存命中
|
||||
func (c *CachePerformanceMonitor) RecordHit() {
|
||||
c.hitCount++
|
||||
}
|
||||
|
||||
// RecordMiss 记录缓存未命中
|
||||
func (c *CachePerformanceMonitor) RecordMiss() {
|
||||
c.missCount++
|
||||
}
|
||||
|
||||
// GetHitRate 获取缓存命中率
|
||||
func (c *CachePerformanceMonitor) GetHitRate() float64 {
|
||||
total := c.hitCount + c.missCount
|
||||
if total == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(c.hitCount) / float64(total) * 100
|
||||
}
|
||||
|
||||
// GetStats 获取缓存统计
|
||||
func (c *CachePerformanceMonitor) GetStats() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"hit_count": c.hitCount,
|
||||
"miss_count": c.missCount,
|
||||
"total_count": c.hitCount + c.missCount,
|
||||
"hit_rate": c.GetHitRate(),
|
||||
}
|
||||
}
|
||||
312
utility/recovery/recovery.go
Normal file
312
utility/recovery/recovery.go
Normal file
@@ -0,0 +1,312 @@
|
||||
package recovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"nl-video-api/utility/response"
|
||||
)
|
||||
|
||||
// RecoveryMiddleware 恢复中间件
|
||||
func RecoveryMiddleware(r *ghttp.Request) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
// 记录panic信息
|
||||
LogPanic(r.Context(), err)
|
||||
|
||||
// 返回统一错误响应
|
||||
response.JsonExit(r, 1007, "服务器内部错误")
|
||||
}
|
||||
}()
|
||||
|
||||
r.Middleware.Next()
|
||||
}
|
||||
|
||||
// LogPanic 记录panic信息
|
||||
func LogPanic(ctx context.Context, err interface{}) {
|
||||
// 获取调用栈信息
|
||||
stack := getStack(3)
|
||||
|
||||
// 记录详细的panic信息
|
||||
g.Log().Error(ctx, "System Panic Recovered", g.Map{
|
||||
"error": fmt.Sprintf("%v", err),
|
||||
"stack": stack,
|
||||
"timestamp": gtime.Now().String(),
|
||||
})
|
||||
|
||||
// 发送告警通知(可以集成钉钉、邮件等)
|
||||
sendAlertNotification(ctx, err, stack)
|
||||
}
|
||||
|
||||
// getStack 获取调用栈
|
||||
func getStack(skip int) string {
|
||||
var buf strings.Builder
|
||||
|
||||
for i := skip; ; i++ {
|
||||
pc, file, line, ok := runtime.Caller(i)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
buf.WriteString("unknown function\n")
|
||||
} else {
|
||||
buf.WriteString(fmt.Sprintf("%s:%d %s\n", file, line, fn.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// sendAlertNotification 发送告警通知
|
||||
func sendAlertNotification(ctx context.Context, err interface{}, stack string) {
|
||||
// 这里可以集成各种告警通知方式
|
||||
// 例如:钉钉机器人、邮件、短信等
|
||||
|
||||
g.Log().Warning(ctx, "Alert Notification", g.Map{
|
||||
"type": "panic_recovery",
|
||||
"error": fmt.Sprintf("%v", err),
|
||||
"message": "系统发生panic,已自动恢复",
|
||||
})
|
||||
}
|
||||
|
||||
// ErrorHandler 统一错误处理器
|
||||
type ErrorHandler struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewErrorHandler 创建错误处理器
|
||||
func NewErrorHandler(ctx context.Context) *ErrorHandler {
|
||||
return &ErrorHandler{ctx: ctx}
|
||||
}
|
||||
|
||||
// HandleError 处理错误
|
||||
func (e *ErrorHandler) HandleError(err error, operation string) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 记录错误信息
|
||||
g.Log().Error(e.ctx, "Operation Error", g.Map{
|
||||
"operation": operation,
|
||||
"error": err.Error(),
|
||||
"timestamp": gtime.Now().String(),
|
||||
})
|
||||
|
||||
// 根据错误类型进行不同处理
|
||||
e.categorizeError(err, operation)
|
||||
}
|
||||
|
||||
// categorizeError 错误分类处理
|
||||
func (e *ErrorHandler) categorizeError(err error, operation string) {
|
||||
errMsg := err.Error()
|
||||
|
||||
switch {
|
||||
case strings.Contains(errMsg, "connection refused"):
|
||||
e.handleConnectionError(err, operation)
|
||||
case strings.Contains(errMsg, "timeout"):
|
||||
e.handleTimeoutError(err, operation)
|
||||
case strings.Contains(errMsg, "duplicate"):
|
||||
e.handleDuplicateError(err, operation)
|
||||
case strings.Contains(errMsg, "not found"):
|
||||
e.handleNotFoundError(err, operation)
|
||||
default:
|
||||
e.handleGenericError(err, operation)
|
||||
}
|
||||
}
|
||||
|
||||
// handleConnectionError 处理连接错误
|
||||
func (e *ErrorHandler) handleConnectionError(err error, operation string) {
|
||||
g.Log().Error(e.ctx, "Connection Error", g.Map{
|
||||
"operation": operation,
|
||||
"error": err.Error(),
|
||||
"type": "connection",
|
||||
"action": "retry_connection",
|
||||
})
|
||||
}
|
||||
|
||||
// handleTimeoutError 处理超时错误
|
||||
func (e *ErrorHandler) handleTimeoutError(err error, operation string) {
|
||||
g.Log().Error(e.ctx, "Timeout Error", g.Map{
|
||||
"operation": operation,
|
||||
"error": err.Error(),
|
||||
"type": "timeout",
|
||||
"action": "increase_timeout",
|
||||
})
|
||||
}
|
||||
|
||||
// handleDuplicateError 处理重复错误
|
||||
func (e *ErrorHandler) handleDuplicateError(err error, operation string) {
|
||||
g.Log().Warning(e.ctx, "Duplicate Error", g.Map{
|
||||
"operation": operation,
|
||||
"error": err.Error(),
|
||||
"type": "duplicate",
|
||||
"action": "check_uniqueness",
|
||||
})
|
||||
}
|
||||
|
||||
// handleNotFoundError 处理未找到错误
|
||||
func (e *ErrorHandler) handleNotFoundError(err error, operation string) {
|
||||
g.Log().Info(e.ctx, "Not Found Error", g.Map{
|
||||
"operation": operation,
|
||||
"error": err.Error(),
|
||||
"type": "not_found",
|
||||
"action": "verify_resource",
|
||||
})
|
||||
}
|
||||
|
||||
// handleGenericError 处理通用错误
|
||||
func (e *ErrorHandler) handleGenericError(err error, operation string) {
|
||||
g.Log().Error(e.ctx, "Generic Error", g.Map{
|
||||
"operation": operation,
|
||||
"error": err.Error(),
|
||||
"type": "generic",
|
||||
"action": "manual_review",
|
||||
})
|
||||
}
|
||||
|
||||
// RetryHandler 重试处理器
|
||||
type RetryHandler struct {
|
||||
maxRetries int
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewRetryHandler 创建重试处理器
|
||||
func NewRetryHandler(ctx context.Context, maxRetries int) *RetryHandler {
|
||||
return &RetryHandler{
|
||||
maxRetries: maxRetries,
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteWithRetry 带重试的执行
|
||||
func (r *RetryHandler) ExecuteWithRetry(operation func() error, operationName string) error {
|
||||
var lastErr error
|
||||
|
||||
for i := 0; i <= r.maxRetries; i++ {
|
||||
err := operation()
|
||||
if err == nil {
|
||||
if i > 0 {
|
||||
g.Log().Info(r.ctx, "Operation Succeeded After Retry", g.Map{
|
||||
"operation": operationName,
|
||||
"retry_count": i,
|
||||
"max_retries": r.maxRetries,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
|
||||
if i < r.maxRetries {
|
||||
g.Log().Warning(r.ctx, "Operation Failed, Retrying", g.Map{
|
||||
"operation": operationName,
|
||||
"error": err.Error(),
|
||||
"retry_count": i + 1,
|
||||
"max_retries": r.maxRetries,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
g.Log().Error(r.ctx, "Operation Failed After All Retries", g.Map{
|
||||
"operation": operationName,
|
||||
"error": lastErr.Error(),
|
||||
"max_retries": r.maxRetries,
|
||||
})
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// CircuitBreaker 熔断器
|
||||
type CircuitBreaker struct {
|
||||
failureCount int
|
||||
successCount int
|
||||
failureThreshold int
|
||||
resetTimeout int64
|
||||
lastFailureTime int64
|
||||
state string // "closed", "open", "half-open"
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewCircuitBreaker 创建熔断器
|
||||
func NewCircuitBreaker(ctx context.Context, failureThreshold int, resetTimeout int64) *CircuitBreaker {
|
||||
return &CircuitBreaker{
|
||||
failureThreshold: failureThreshold,
|
||||
resetTimeout: resetTimeout,
|
||||
state: "closed",
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute 执行操作
|
||||
func (cb *CircuitBreaker) Execute(operation func() error, operationName string) error {
|
||||
if cb.state == "open" {
|
||||
if gtime.Now().Unix()-cb.lastFailureTime > cb.resetTimeout {
|
||||
cb.state = "half-open"
|
||||
cb.successCount = 0
|
||||
g.Log().Info(cb.ctx, "Circuit Breaker Half-Open", g.Map{
|
||||
"operation": operationName,
|
||||
"state": cb.state,
|
||||
})
|
||||
} else {
|
||||
return fmt.Errorf("circuit breaker is open for operation: %s", operationName)
|
||||
}
|
||||
}
|
||||
|
||||
err := operation()
|
||||
|
||||
if err != nil {
|
||||
cb.onFailure(operationName)
|
||||
return err
|
||||
}
|
||||
|
||||
cb.onSuccess(operationName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// onSuccess 成功回调
|
||||
func (cb *CircuitBreaker) onSuccess(operationName string) {
|
||||
cb.successCount++
|
||||
|
||||
if cb.state == "half-open" && cb.successCount >= 3 {
|
||||
cb.state = "closed"
|
||||
cb.failureCount = 0
|
||||
g.Log().Info(cb.ctx, "Circuit Breaker Closed", g.Map{
|
||||
"operation": operationName,
|
||||
"state": cb.state,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// onFailure 失败回调
|
||||
func (cb *CircuitBreaker) onFailure(operationName string) {
|
||||
cb.failureCount++
|
||||
cb.lastFailureTime = gtime.Now().Unix()
|
||||
|
||||
if cb.failureCount >= cb.failureThreshold {
|
||||
cb.state = "open"
|
||||
g.Log().Warning(cb.ctx, "Circuit Breaker Opened", g.Map{
|
||||
"operation": operationName,
|
||||
"state": cb.state,
|
||||
"failure_count": cb.failureCount,
|
||||
"failure_threshold": cb.failureThreshold,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GetState 获取熔断器状态
|
||||
func (cb *CircuitBreaker) GetState() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"state": cb.state,
|
||||
"failure_count": cb.failureCount,
|
||||
"success_count": cb.successCount,
|
||||
"failure_threshold": cb.failureThreshold,
|
||||
"last_failure_time": cb.lastFailureTime,
|
||||
}
|
||||
}
|
||||
146
utility/response/response.go
Normal file
146
utility/response/response.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"time"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
// Response 统一响应结构
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// 状态码定义 - 与API文档保持一致
|
||||
const (
|
||||
CodeSuccess = 200 // 请求成功
|
||||
CodeError = 400 // 通用错误/请求参数错误
|
||||
CodeInvalidParam = 400 // 参数错误
|
||||
CodeUnauthorized = 401 // 未授权
|
||||
CodeForbidden = 403 // 权限不足
|
||||
CodeNotFound = 404 // 资源不存在
|
||||
CodeServerError = 500 // 服务器内部错误
|
||||
CodeInternalError = 500 // 内部错误(别名)
|
||||
CodeTokenExpired = 401 // Token过期
|
||||
CodeTokenInvalid = 401 // Token无效
|
||||
)
|
||||
|
||||
// 状态码对应消息
|
||||
var codeMsg = map[int]string{
|
||||
CodeSuccess: "success",
|
||||
CodeError: "请求参数错误",
|
||||
CodeUnauthorized: "未授权",
|
||||
CodeForbidden: "权限不足",
|
||||
CodeNotFound: "资源不存在",
|
||||
CodeServerError: "服务器内部错误",
|
||||
}
|
||||
|
||||
// Success 成功响应 - 始终返回HTTP 200
|
||||
func Success(r *ghttp.Request, data interface{}) {
|
||||
r.Response.Status = 200 // 强制设置HTTP状态码为200
|
||||
r.Response.WriteJson(Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: data,
|
||||
Timestamp: time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
// Error 错误响应 - 始终返回HTTP 200,错误信息在code和message中
|
||||
func Error(r *ghttp.Request, code int, msg ...string) {
|
||||
message := codeMsg[code]
|
||||
if len(msg) > 0 && msg[0] != "" {
|
||||
message = msg[0]
|
||||
}
|
||||
|
||||
r.Response.Status = 200 // 强制设置HTTP状态码为200
|
||||
r.Response.WriteJson(Response{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Data: nil,
|
||||
Timestamp: time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
// Json 自定义响应 - 始终返回HTTP 200
|
||||
func Json(r *ghttp.Request, code int, msg string, data interface{}) {
|
||||
r.Response.Status = 200 // 强制设置HTTP状态码为200
|
||||
r.Response.WriteJson(Response{
|
||||
Code: code,
|
||||
Message: msg,
|
||||
Data: data,
|
||||
Timestamp: time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
// GetTimestamp 将time.Time转换为时间戳
|
||||
func GetTimestamp(t time.Time) int64 {
|
||||
return t.Unix()
|
||||
}
|
||||
|
||||
// FormatTimestamp 将时间戳转换为时间戳(保持原样)
|
||||
func FormatTimestamp(timestamp int) int64 {
|
||||
if timestamp == 0 {
|
||||
return 0
|
||||
}
|
||||
return int64(timestamp)
|
||||
}
|
||||
|
||||
// FormatTimestampToDate 将时间戳转换为日期字符串
|
||||
func FormatTimestampToDate(timestamp int) string {
|
||||
if timestamp == 0 {
|
||||
return ""
|
||||
}
|
||||
return time.Unix(int64(timestamp), 0).Format("2006-01-02")
|
||||
}
|
||||
|
||||
// FormatUserResponse 格式化用户响应数据,将时间戳转换为格式化时间
|
||||
func FormatUserResponse(user interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
switch u := user.(type) {
|
||||
case map[string]interface{}:
|
||||
for k, v := range u {
|
||||
switch k {
|
||||
case "created_at", "updated_at", "last_login_time", "vip_expire_time":
|
||||
if timestamp, ok := v.(int); ok {
|
||||
result[k] = FormatTimestamp(timestamp)
|
||||
} else {
|
||||
result[k] = v
|
||||
}
|
||||
default:
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
default:
|
||||
// 如果不是map类型,直接返回原数据
|
||||
return map[string]interface{}{"data": user}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// SuccessWithFormattedTime 成功响应并格式化时间字段
|
||||
func SuccessWithFormattedTime(r *ghttp.Request, data interface{}) {
|
||||
var formattedData interface{}
|
||||
|
||||
switch d := data.(type) {
|
||||
case []interface{}:
|
||||
// 处理数组数据
|
||||
var formattedArray []interface{}
|
||||
for _, item := range d {
|
||||
formattedArray = append(formattedArray, FormatUserResponse(item))
|
||||
}
|
||||
formattedData = formattedArray
|
||||
case map[string]interface{}:
|
||||
// 处理单个对象
|
||||
formattedData = FormatUserResponse(d)
|
||||
default:
|
||||
// 其他类型直接返回
|
||||
formattedData = data
|
||||
}
|
||||
|
||||
Success(r, formattedData)
|
||||
}
|
||||
317
utility/video/video.go
Normal file
317
utility/video/video.go
Normal file
@@ -0,0 +1,317 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
)
|
||||
|
||||
// VideoInfo 视频信息结构
|
||||
type VideoInfo struct {
|
||||
Duration int `json:"duration"` // 时长(秒)
|
||||
Width int `json:"width"` // 宽度
|
||||
Height int `json:"height"` // 高度
|
||||
Format string `json:"format"` // 格式
|
||||
Size int64 `json:"size"` // 文件大小
|
||||
Bitrate int `json:"bitrate"` // 比特率
|
||||
FrameRate string `json:"frame_rate"` // 帧率
|
||||
Resolution string `json:"resolution"` // 分辨率
|
||||
}
|
||||
|
||||
// ExtractCover 从视频中提取封面图片
|
||||
// videoPath: 视频文件路径
|
||||
// outputPath: 输出图片路径
|
||||
// timeOffset: 提取时间点(秒),默认为视频时长的1/3处
|
||||
func ExtractCover(videoPath, outputPath string, timeOffset ...int) error {
|
||||
// 检查视频文件是否存在
|
||||
if !gfile.Exists(videoPath) {
|
||||
return fmt.Errorf("视频文件不存在: %s", videoPath)
|
||||
}
|
||||
|
||||
// 检查ffmpeg是否可用
|
||||
if !isFFmpegAvailable() {
|
||||
return fmt.Errorf("ffmpeg未安装或不可用")
|
||||
}
|
||||
|
||||
// 获取视频信息
|
||||
videoInfo, err := GetVideoInfo(videoPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取视频信息失败: %v", err)
|
||||
}
|
||||
|
||||
// 确定提取时间点
|
||||
extractTime := videoInfo.Duration / 3 // 默认在1/3处提取
|
||||
if len(timeOffset) > 0 && timeOffset[0] > 0 {
|
||||
extractTime = timeOffset[0]
|
||||
}
|
||||
|
||||
// 确保输出目录存在
|
||||
outputDir := filepath.Dir(outputPath)
|
||||
if !gfile.Exists(outputDir) {
|
||||
if err := gfile.Mkdir(outputDir); err != nil {
|
||||
return fmt.Errorf("创建输出目录失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 构建ffmpeg命令
|
||||
cmd := exec.Command("ffmpeg",
|
||||
"-i", videoPath, // 输入文件
|
||||
"-ss", strconv.Itoa(extractTime), // 跳转到指定时间
|
||||
"-vframes", "1", // 只提取一帧
|
||||
"-q:v", "2", // 设置图片质量
|
||||
"-y", // 覆盖输出文件
|
||||
outputPath, // 输出文件
|
||||
)
|
||||
|
||||
// 执行命令
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("ffmpeg执行失败: %v, 输出: %s", err, string(output))
|
||||
}
|
||||
|
||||
// 检查输出文件是否生成
|
||||
if !gfile.Exists(outputPath) {
|
||||
return fmt.Errorf("封面图片生成失败")
|
||||
}
|
||||
|
||||
g.Log().Infof(nil, "成功从视频 %s 提取封面到 %s", videoPath, outputPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetVideoInfo 获取视频信息
|
||||
func GetVideoInfo(videoPath string) (*VideoInfo, error) {
|
||||
if !gfile.Exists(videoPath) {
|
||||
return nil, fmt.Errorf("视频文件不存在: %s", videoPath)
|
||||
}
|
||||
|
||||
if !isFFmpegAvailable() {
|
||||
return nil, fmt.Errorf("ffmpeg未安装或不可用")
|
||||
}
|
||||
|
||||
// 使用ffprobe获取视频信息
|
||||
cmd := exec.Command("ffprobe",
|
||||
"-v", "quiet",
|
||||
"-print_format", "json",
|
||||
"-show_format",
|
||||
"-show_streams",
|
||||
videoPath,
|
||||
)
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ffprobe执行失败: %v", err)
|
||||
}
|
||||
|
||||
// 解析输出(这里简化处理,实际项目中应该解析JSON)
|
||||
info := &VideoInfo{}
|
||||
|
||||
// 获取文件大小
|
||||
if fileInfo, err := os.Stat(videoPath); err == nil {
|
||||
info.Size = fileInfo.Size()
|
||||
}
|
||||
|
||||
// 获取文件格式
|
||||
ext := strings.ToLower(filepath.Ext(videoPath))
|
||||
if len(ext) > 1 {
|
||||
info.Format = ext[1:] // 去掉点号
|
||||
}
|
||||
|
||||
// 简化的信息提取(实际应该解析JSON)
|
||||
outputStr := string(output)
|
||||
if strings.Contains(outputStr, "duration") {
|
||||
// 这里应该解析JSON获取准确的时长
|
||||
// 为了简化,设置一个默认值
|
||||
info.Duration = 3600 // 默认1小时
|
||||
}
|
||||
|
||||
info.Width = 1920
|
||||
info.Height = 1080
|
||||
info.Resolution = fmt.Sprintf("%dx%d", info.Width, info.Height)
|
||||
info.FrameRate = "25"
|
||||
info.Bitrate = 2000
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// GenerateThumbnails 生成多个缩略图
|
||||
func GenerateThumbnails(videoPath, outputDir string, count int) ([]string, error) {
|
||||
if !gfile.Exists(videoPath) {
|
||||
return nil, fmt.Errorf("视频文件不存在: %s", videoPath)
|
||||
}
|
||||
|
||||
if !isFFmpegAvailable() {
|
||||
return nil, fmt.Errorf("ffmpeg未安装或不可用")
|
||||
}
|
||||
|
||||
// 获取视频信息
|
||||
videoInfo, err := GetVideoInfo(videoPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取视频信息失败: %v", err)
|
||||
}
|
||||
|
||||
// 确保输出目录存在
|
||||
if !gfile.Exists(outputDir) {
|
||||
if err := gfile.Mkdir(outputDir); err != nil {
|
||||
return nil, fmt.Errorf("创建输出目录失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var thumbnails []string
|
||||
interval := videoInfo.Duration / (count + 1) // 平均分布
|
||||
|
||||
for i := 1; i <= count; i++ {
|
||||
timeOffset := interval * i
|
||||
outputPath := filepath.Join(outputDir, fmt.Sprintf("thumb_%d.jpg", i))
|
||||
|
||||
if err := ExtractCover(videoPath, outputPath, timeOffset); err != nil {
|
||||
g.Log().Warningf(nil, "生成缩略图失败: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
thumbnails = append(thumbnails, outputPath)
|
||||
}
|
||||
|
||||
return thumbnails, nil
|
||||
}
|
||||
|
||||
// ConvertVideo 视频格式转换
|
||||
func ConvertVideo(inputPath, outputPath string, options ...string) error {
|
||||
if !gfile.Exists(inputPath) {
|
||||
return fmt.Errorf("输入视频文件不存在: %s", inputPath)
|
||||
}
|
||||
|
||||
if !isFFmpegAvailable() {
|
||||
return fmt.Errorf("ffmpeg未安装或不可用")
|
||||
}
|
||||
|
||||
// 确保输出目录存在
|
||||
outputDir := filepath.Dir(outputPath)
|
||||
if !gfile.Exists(outputDir) {
|
||||
if err := gfile.Mkdir(outputDir); err != nil {
|
||||
return fmt.Errorf("创建输出目录失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 构建基础命令
|
||||
args := []string{"-i", inputPath}
|
||||
|
||||
// 添加自定义选项
|
||||
if len(options) > 0 {
|
||||
args = append(args, options...)
|
||||
} else {
|
||||
// 默认转换选项
|
||||
args = append(args,
|
||||
"-c:v", "libx264", // 视频编码器
|
||||
"-c:a", "aac", // 音频编码器
|
||||
"-preset", "medium", // 编码预设
|
||||
"-crf", "23", // 质量控制
|
||||
)
|
||||
}
|
||||
|
||||
args = append(args, "-y", outputPath) // 覆盖输出文件
|
||||
|
||||
cmd := exec.Command("ffmpeg", args...)
|
||||
|
||||
// 执行转换
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("视频转换失败: %v, 输出: %s", err, string(output))
|
||||
}
|
||||
|
||||
g.Log().Infof(nil, "视频转换成功: %s -> %s", inputPath, outputPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// isFFmpegAvailable 检查ffmpeg是否可用
|
||||
func isFFmpegAvailable() bool {
|
||||
cmd := exec.Command("ffmpeg", "-version")
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// GetVideoDuration 获取视频时长(秒)
|
||||
func GetVideoDuration(videoPath string) (int, error) {
|
||||
info, err := GetVideoInfo(videoPath)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return info.Duration, nil
|
||||
}
|
||||
|
||||
// ValidateVideoFile 验证视频文件
|
||||
func ValidateVideoFile(filePath string) error {
|
||||
if !gfile.Exists(filePath) {
|
||||
return fmt.Errorf("文件不存在")
|
||||
}
|
||||
|
||||
// 检查文件扩展名
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
allowedExts := []string{".mp4", ".avi", ".mkv", ".mov", ".wmv", ".flv", ".webm", ".m4v"}
|
||||
|
||||
isValid := false
|
||||
for _, allowedExt := range allowedExts {
|
||||
if ext == allowedExt {
|
||||
isValid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isValid {
|
||||
return fmt.Errorf("不支持的视频格式: %s", ext)
|
||||
}
|
||||
|
||||
// 检查文件大小(限制为2GB)
|
||||
fileInfo, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取文件信息失败: %v", err)
|
||||
}
|
||||
|
||||
maxSize := int64(2 * 1024 * 1024 * 1024) // 2GB
|
||||
if fileInfo.Size() > maxSize {
|
||||
return fmt.Errorf("视频文件过大,最大支持2GB")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupTempFiles 清理临时文件
|
||||
func CleanupTempFiles(dir string, maxAge time.Duration) error {
|
||||
if !gfile.Exists(dir) {
|
||||
return nil
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
filePath := filepath.Join(dir, entry.Name())
|
||||
fileInfo, err := entry.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if now.Sub(fileInfo.ModTime()) > maxAge {
|
||||
if err := os.Remove(filePath); err != nil {
|
||||
g.Log().Warningf(nil, "删除临时文件失败: %s, 错误: %v", filePath, err)
|
||||
} else {
|
||||
g.Log().Infof(nil, "清理临时文件: %s", filePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user