init
This commit is contained in:
82
server/pkg/jwt/jwt.go
Normal file
82
server/pkg/jwt/jwt.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"dianshang/internal/config"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// Claims JWT声明结构
|
||||
type Claims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
UserType string `json:"user_type"` // user, admin
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
var jwtSecret []byte
|
||||
|
||||
// Init 初始化JWT
|
||||
func Init(cfg config.JWTConfig) {
|
||||
jwtSecret = []byte(cfg.Secret)
|
||||
}
|
||||
|
||||
// GenerateToken 生成JWT token
|
||||
func GenerateToken(userID uint, userType string, expire int) (string, error) {
|
||||
nowTime := time.Now()
|
||||
expireTime := nowTime.Add(time.Duration(expire) * time.Second)
|
||||
|
||||
claims := Claims{
|
||||
UserID: userID,
|
||||
UserType: userType,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expireTime),
|
||||
IssuedAt: jwt.NewNumericDate(nowTime),
|
||||
NotBefore: jwt.NewNumericDate(nowTime),
|
||||
Issuer: "dianshang",
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(jwtSecret)
|
||||
}
|
||||
|
||||
// ParseToken 解析JWT token
|
||||
func ParseToken(tokenString string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return jwtSecret, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
// RefreshToken 刷新token
|
||||
func RefreshToken(tokenString string, expire int) (string, error) {
|
||||
claims, err := ParseToken(tokenString)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 检查token是否即将过期(剩余时间少于30分钟)
|
||||
if time.Until(claims.ExpiresAt.Time) > 30*time.Minute {
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// 生成新token
|
||||
return GenerateToken(claims.UserID, claims.UserType, expire)
|
||||
}
|
||||
|
||||
// ValidateToken 验证token有效性
|
||||
func ValidateToken(tokenString string) bool {
|
||||
_, err := ParseToken(tokenString)
|
||||
return err == nil
|
||||
}
|
||||
532
server/pkg/logger/logger.go
Normal file
532
server/pkg/logger/logger.go
Normal file
@@ -0,0 +1,532 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
// LogConfig 日志配置
|
||||
type LogConfig struct {
|
||||
Level string `mapstructure:"level"`
|
||||
Filename string `mapstructure:"filename"`
|
||||
MaxSize int `mapstructure:"maxSize"`
|
||||
MaxAge int `mapstructure:"maxAge"`
|
||||
MaxBackups int `mapstructure:"maxBackups"`
|
||||
EnableConsole bool `mapstructure:"enableConsole"`
|
||||
EnableFile bool `mapstructure:"enableFile"`
|
||||
Format string `mapstructure:"format"`
|
||||
EnableCaller bool `mapstructure:"enableCaller"`
|
||||
EnableOperation bool `mapstructure:"enableOperation"`
|
||||
EnablePerf bool `mapstructure:"enablePerf"`
|
||||
PerfThreshold int64 `mapstructure:"perfThreshold"`
|
||||
}
|
||||
|
||||
var (
|
||||
logConfig LogConfig
|
||||
)
|
||||
|
||||
var (
|
||||
log *logrus.Logger
|
||||
isDevelopment bool
|
||||
)
|
||||
|
||||
// ContextKey 上下文键类型
|
||||
type ContextKey string
|
||||
|
||||
const (
|
||||
// RequestIDKey 请求ID键
|
||||
RequestIDKey ContextKey = "request_id"
|
||||
// UserIDKey 用户ID键
|
||||
UserIDKey ContextKey = "user_id"
|
||||
// OperationKey 操作类型键
|
||||
OperationKey ContextKey = "operation"
|
||||
// ModuleKey 模块名称键
|
||||
ModuleKey ContextKey = "module"
|
||||
)
|
||||
|
||||
// CustomFormatter 自定义日志格式器
|
||||
type CustomFormatter struct {
|
||||
TimestampFormat string
|
||||
Environment string
|
||||
}
|
||||
|
||||
// Format 格式化日志
|
||||
func (f *CustomFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
timestamp := entry.Time.Format(f.TimestampFormat)
|
||||
|
||||
// 获取调用者信息
|
||||
caller := ""
|
||||
if entry.HasCaller() {
|
||||
caller = fmt.Sprintf("%s:%d", filepath.Base(entry.Caller.File), entry.Caller.Line)
|
||||
}
|
||||
|
||||
// 开发环境:可读性更好的文本格式
|
||||
var msg strings.Builder
|
||||
msg.WriteString(fmt.Sprintf("[%s] %s %s",
|
||||
timestamp,
|
||||
strings.ToUpper(entry.Level.String()),
|
||||
entry.Message))
|
||||
|
||||
if caller != "" {
|
||||
msg.WriteString(fmt.Sprintf(" (%s)", caller))
|
||||
}
|
||||
|
||||
// 添加字段信息
|
||||
if len(entry.Data) > 0 {
|
||||
msg.WriteString(" |")
|
||||
for k, v := range entry.Data {
|
||||
msg.WriteString(fmt.Sprintf(" %s=%v", k, v))
|
||||
}
|
||||
}
|
||||
msg.WriteString("\n")
|
||||
|
||||
return []byte(msg.String()), nil
|
||||
}
|
||||
|
||||
// Init 初始化日志
|
||||
func Init(cfg LogConfig) {
|
||||
log = logrus.New()
|
||||
logConfig = cfg // 存储配置
|
||||
|
||||
// 判断是否为开发环境
|
||||
isDevelopment = os.Getenv("GIN_MODE") != "release"
|
||||
|
||||
// 设置日志级别
|
||||
level, err := logrus.ParseLevel(cfg.Level)
|
||||
if err != nil {
|
||||
level = logrus.InfoLevel
|
||||
}
|
||||
log.SetLevel(level)
|
||||
|
||||
// 设置调用者信息
|
||||
log.SetReportCaller(cfg.EnableCaller)
|
||||
|
||||
// 设置日志格式化器
|
||||
var formatter logrus.Formatter
|
||||
if cfg.Format == "json" {
|
||||
// 生产环境使用JSON格式
|
||||
formatter = &logrus.JSONFormatter{
|
||||
TimestampFormat: "2006-01-02 15:04:05",
|
||||
DisableHTMLEscape: true, // 禁用HTML转义,避免特殊字符问题
|
||||
FieldMap: logrus.FieldMap{
|
||||
logrus.FieldKeyTime: "timestamp",
|
||||
logrus.FieldKeyLevel: "level",
|
||||
logrus.FieldKeyMsg: "message",
|
||||
logrus.FieldKeyFunc: "caller",
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// 开发环境使用文本格式
|
||||
formatter = &CustomFormatter{
|
||||
TimestampFormat: "2006-01-02 15:04:05",
|
||||
Environment: "development",
|
||||
}
|
||||
}
|
||||
log.SetFormatter(formatter)
|
||||
|
||||
// 配置输出
|
||||
var outputs []io.Writer
|
||||
|
||||
// 控制台输出
|
||||
if cfg.EnableConsole {
|
||||
outputs = append(outputs, os.Stdout)
|
||||
}
|
||||
|
||||
// 文件输出
|
||||
if cfg.EnableFile && cfg.Filename != "" {
|
||||
// 确保日志目录存在
|
||||
logDir := filepath.Dir(cfg.Filename)
|
||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||
log.Fatalf("创建日志目录失败: %v", err)
|
||||
}
|
||||
|
||||
// 配置lumberjack进行日志轮转
|
||||
lumberjackLogger := &lumberjack.Logger{
|
||||
Filename: cfg.Filename,
|
||||
MaxSize: cfg.MaxSize, // MB
|
||||
MaxAge: cfg.MaxAge, // days
|
||||
MaxBackups: cfg.MaxBackups, // files
|
||||
LocalTime: true,
|
||||
Compress: true,
|
||||
}
|
||||
outputs = append(outputs, lumberjackLogger)
|
||||
}
|
||||
|
||||
// 设置输出
|
||||
if len(outputs) > 0 {
|
||||
if len(outputs) == 1 {
|
||||
log.SetOutput(outputs[0])
|
||||
} else {
|
||||
log.SetOutput(io.MultiWriter(outputs...))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetLogger 获取日志实例
|
||||
func GetLogger() *logrus.Logger {
|
||||
if log == nil {
|
||||
log = logrus.New()
|
||||
}
|
||||
return log
|
||||
}
|
||||
|
||||
// GenerateRequestID 生成请求ID
|
||||
func GenerateRequestID() string {
|
||||
return uuid.New().String()
|
||||
}
|
||||
|
||||
// WithContext 从上下文创建带有上下文信息的日志条目
|
||||
func WithContext(ctx context.Context) *logrus.Entry {
|
||||
entry := GetLogger().WithFields(logrus.Fields{})
|
||||
|
||||
if requestID := ctx.Value(RequestIDKey); requestID != nil {
|
||||
entry = entry.WithField("request_id", requestID)
|
||||
}
|
||||
|
||||
if userID := ctx.Value(UserIDKey); userID != nil {
|
||||
entry = entry.WithField("user_id", userID)
|
||||
}
|
||||
|
||||
if operation := ctx.Value(OperationKey); operation != nil {
|
||||
entry = entry.WithField("operation", operation)
|
||||
}
|
||||
|
||||
if module := ctx.Value(ModuleKey); module != nil {
|
||||
entry = entry.WithField("module", module)
|
||||
}
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
// Debug 调试日志
|
||||
func Debug(args ...interface{}) {
|
||||
GetLogger().Debug(args...)
|
||||
}
|
||||
|
||||
// Debugf 格式化调试日志
|
||||
func Debugf(format string, args ...interface{}) {
|
||||
GetLogger().Debugf(format, args...)
|
||||
}
|
||||
|
||||
// DebugWithContext 带上下文的调试日志
|
||||
func DebugWithContext(ctx context.Context, args ...interface{}) {
|
||||
WithContext(ctx).Debug(args...)
|
||||
}
|
||||
|
||||
// DebugfWithContext 带上下文的格式化调试日志
|
||||
func DebugfWithContext(ctx context.Context, format string, args ...interface{}) {
|
||||
WithContext(ctx).Debugf(format, args...)
|
||||
}
|
||||
|
||||
// Info 信息日志
|
||||
func Info(args ...interface{}) {
|
||||
GetLogger().Info(args...)
|
||||
}
|
||||
|
||||
// Infof 格式化信息日志
|
||||
func Infof(format string, args ...interface{}) {
|
||||
GetLogger().Infof(format, args...)
|
||||
}
|
||||
|
||||
// InfoWithContext 带上下文的信息日志
|
||||
func InfoWithContext(ctx context.Context, args ...interface{}) {
|
||||
WithContext(ctx).Info(args...)
|
||||
}
|
||||
|
||||
// InfofWithContext 带上下文的格式化信息日志
|
||||
func InfofWithContext(ctx context.Context, format string, args ...interface{}) {
|
||||
WithContext(ctx).Infof(format, args...)
|
||||
}
|
||||
|
||||
// Warn 警告日志
|
||||
func Warn(args ...interface{}) {
|
||||
GetLogger().Warn(args...)
|
||||
}
|
||||
|
||||
// Warnf 格式化警告日志
|
||||
func Warnf(format string, args ...interface{}) {
|
||||
GetLogger().Warnf(format, args...)
|
||||
}
|
||||
|
||||
// WarnWithContext 带上下文的警告日志
|
||||
func WarnWithContext(ctx context.Context, args ...interface{}) {
|
||||
WithContext(ctx).Warn(args...)
|
||||
}
|
||||
|
||||
// WarnfWithContext 带上下文的格式化警告日志
|
||||
func WarnfWithContext(ctx context.Context, format string, args ...interface{}) {
|
||||
WithContext(ctx).Warnf(format, args...)
|
||||
}
|
||||
|
||||
// Error 错误日志
|
||||
func Error(args ...interface{}) {
|
||||
GetLogger().Error(args...)
|
||||
}
|
||||
|
||||
// Errorf 格式化错误日志
|
||||
func Errorf(format string, args ...interface{}) {
|
||||
GetLogger().Errorf(format, args...)
|
||||
}
|
||||
|
||||
// ErrorWithContext 带上下文的错误日志
|
||||
func ErrorWithContext(ctx context.Context, args ...interface{}) {
|
||||
WithContext(ctx).Error(args...)
|
||||
}
|
||||
|
||||
// ErrorfWithContext 带上下文的格式化错误日志
|
||||
func ErrorfWithContext(ctx context.Context, format string, args ...interface{}) {
|
||||
WithContext(ctx).Errorf(format, args...)
|
||||
}
|
||||
|
||||
// Fatal 致命错误日志
|
||||
func Fatal(args ...interface{}) {
|
||||
GetLogger().Fatal(args...)
|
||||
}
|
||||
|
||||
// Fatalf 格式化致命错误日志
|
||||
func Fatalf(format string, args ...interface{}) {
|
||||
GetLogger().Fatalf(format, args...)
|
||||
}
|
||||
|
||||
// WithField 添加字段
|
||||
func WithField(key string, value interface{}) *logrus.Entry {
|
||||
return GetLogger().WithField(key, value)
|
||||
}
|
||||
|
||||
// WithFields 添加多个字段
|
||||
func WithFields(fields logrus.Fields) *logrus.Entry {
|
||||
return GetLogger().WithFields(fields)
|
||||
}
|
||||
|
||||
// WithError 添加错误字段
|
||||
func WithError(err error) *logrus.Entry {
|
||||
return GetLogger().WithError(err)
|
||||
}
|
||||
|
||||
// LogRequest 记录请求日志
|
||||
func LogRequest(method, path, ip, userAgent string, statusCode int, duration int64) {
|
||||
GetLogger().WithFields(logrus.Fields{
|
||||
"type": "http_request",
|
||||
"method": method,
|
||||
"path": path,
|
||||
"ip": ip,
|
||||
"user_agent": userAgent,
|
||||
"status": statusCode,
|
||||
"duration": duration,
|
||||
"timestamp": time.Now().Unix(),
|
||||
}).Info("HTTP Request")
|
||||
}
|
||||
|
||||
// LogRequestWithContext 带上下文的请求日志
|
||||
func LogRequestWithContext(ctx context.Context, method, path, ip, userAgent string, statusCode int, duration int64) {
|
||||
WithContext(ctx).WithFields(logrus.Fields{
|
||||
"type": "http_request",
|
||||
"method": method,
|
||||
"path": path,
|
||||
"ip": ip,
|
||||
"user_agent": userAgent,
|
||||
"status": statusCode,
|
||||
"duration": duration,
|
||||
"timestamp": time.Now().Unix(),
|
||||
}).Info("HTTP Request")
|
||||
}
|
||||
|
||||
// LogError 记录错误日志
|
||||
func LogError(err error, context map[string]interface{}) {
|
||||
entry := GetLogger().WithError(err).WithField("type", "application_error")
|
||||
|
||||
// 添加错误堆栈信息
|
||||
if pc, file, line, ok := runtime.Caller(1); ok {
|
||||
entry = entry.WithFields(logrus.Fields{
|
||||
"error_file": filepath.Base(file),
|
||||
"error_line": line,
|
||||
"error_func": runtime.FuncForPC(pc).Name(),
|
||||
})
|
||||
}
|
||||
|
||||
if context != nil {
|
||||
entry = entry.WithFields(context)
|
||||
}
|
||||
|
||||
entry.Error("Application Error")
|
||||
}
|
||||
|
||||
// LogErrorWithContext 带上下文的错误日志
|
||||
func LogErrorWithContext(ctx context.Context, err error, context map[string]interface{}) {
|
||||
entry := WithContext(ctx).WithError(err).WithField("type", "application_error")
|
||||
|
||||
// 添加错误堆栈信息
|
||||
if pc, file, line, ok := runtime.Caller(1); ok {
|
||||
entry = entry.WithFields(logrus.Fields{
|
||||
"error_file": filepath.Base(file),
|
||||
"error_line": line,
|
||||
"error_func": runtime.FuncForPC(pc).Name(),
|
||||
})
|
||||
}
|
||||
|
||||
if context != nil {
|
||||
entry = entry.WithFields(context)
|
||||
}
|
||||
|
||||
entry.Error("Application Error")
|
||||
}
|
||||
|
||||
// LogOperation 记录操作日志
|
||||
func LogOperation(userID uint, userType, operation, resource, method, path, ip string, requestData, responseData interface{}, statusCode int, duration time.Duration) {
|
||||
if !logConfig.EnableOperation {
|
||||
return
|
||||
}
|
||||
|
||||
GetLogger().WithFields(logrus.Fields{
|
||||
"type": "operation",
|
||||
"user_id": userID,
|
||||
"user_type": userType,
|
||||
"operation": operation,
|
||||
"resource": resource,
|
||||
"method": method,
|
||||
"path": path,
|
||||
"ip": ip,
|
||||
"request_data": requestData,
|
||||
"response_data": responseData,
|
||||
"status_code": statusCode,
|
||||
"duration": duration.Milliseconds(),
|
||||
"timestamp": time.Now().Unix(),
|
||||
}).Info("User Operation")
|
||||
}
|
||||
|
||||
// LogOperationWithContext 带上下文的操作日志
|
||||
func LogOperationWithContext(ctx context.Context, userID uint, userType, operation, resource, method, path, ip string, requestData, responseData interface{}, statusCode int, duration time.Duration) {
|
||||
if !logConfig.EnableOperation {
|
||||
return
|
||||
}
|
||||
|
||||
WithContext(ctx).WithFields(logrus.Fields{
|
||||
"type": "operation",
|
||||
"user_id": userID,
|
||||
"user_type": userType,
|
||||
"operation": operation,
|
||||
"resource": resource,
|
||||
"method": method,
|
||||
"path": path,
|
||||
"ip": ip,
|
||||
"request_data": requestData,
|
||||
"response_data": responseData,
|
||||
"status_code": statusCode,
|
||||
"duration": duration.Milliseconds(),
|
||||
"timestamp": time.Now().Unix(),
|
||||
}).Info("User Operation")
|
||||
}
|
||||
|
||||
// LogPerformance 记录性能日志
|
||||
func LogPerformance(operation string, duration time.Duration, details map[string]interface{}) {
|
||||
if !logConfig.EnablePerf {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否超过阈值
|
||||
if duration.Milliseconds() < logConfig.PerfThreshold {
|
||||
return
|
||||
}
|
||||
|
||||
entry := GetLogger().WithFields(logrus.Fields{
|
||||
"type": "performance",
|
||||
"operation": operation,
|
||||
"duration": duration.Milliseconds(),
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
|
||||
if details != nil {
|
||||
entry = entry.WithFields(details)
|
||||
}
|
||||
|
||||
// 根据耗时判断日志级别
|
||||
if duration > 5*time.Second {
|
||||
entry.Error("Slow Operation Detected")
|
||||
} else if duration > 1*time.Second {
|
||||
entry.Warn("Performance Warning")
|
||||
} else {
|
||||
entry.Info("Performance Log")
|
||||
}
|
||||
}
|
||||
|
||||
// LogPerformanceWithContext 带上下文的性能日志
|
||||
func LogPerformanceWithContext(ctx context.Context, operation string, duration time.Duration, details map[string]interface{}) {
|
||||
if !logConfig.EnablePerf {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否超过阈值
|
||||
if duration.Milliseconds() < logConfig.PerfThreshold {
|
||||
return
|
||||
}
|
||||
|
||||
entry := WithContext(ctx).WithFields(logrus.Fields{
|
||||
"type": "performance",
|
||||
"operation": operation,
|
||||
"duration": duration.Milliseconds(),
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
|
||||
if details != nil {
|
||||
entry = entry.WithFields(details)
|
||||
}
|
||||
|
||||
// 根据耗时判断日志级别
|
||||
if duration > 5*time.Second {
|
||||
entry.Error("Slow Operation Detected")
|
||||
} else if duration > 1*time.Second {
|
||||
entry.Warn("Performance Warning")
|
||||
} else {
|
||||
entry.Info("Performance Log")
|
||||
}
|
||||
}
|
||||
|
||||
// LogDatabase 记录数据库操作日志
|
||||
func LogDatabase(operation, table string, duration time.Duration, rowsAffected int64, query string) {
|
||||
entry := GetLogger().WithFields(logrus.Fields{
|
||||
"type": "database",
|
||||
"operation": operation,
|
||||
"table": table,
|
||||
"duration": duration.Milliseconds(),
|
||||
"rows_affected": rowsAffected,
|
||||
"query": query,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
|
||||
// 根据耗时判断日志级别
|
||||
if duration > 2*time.Second {
|
||||
entry.Warn("Slow Database Query")
|
||||
} else {
|
||||
entry.Debug("Database Operation")
|
||||
}
|
||||
}
|
||||
|
||||
// LogDatabaseWithContext 带上下文的数据库操作日志
|
||||
func LogDatabaseWithContext(ctx context.Context, operation, table string, duration time.Duration, rowsAffected int64, query string) {
|
||||
entry := WithContext(ctx).WithFields(logrus.Fields{
|
||||
"type": "database",
|
||||
"operation": operation,
|
||||
"table": table,
|
||||
"duration": duration.Milliseconds(),
|
||||
"rows_affected": rowsAffected,
|
||||
"query": query,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
|
||||
// 根据耗时判断日志级别
|
||||
if duration > 2*time.Second {
|
||||
entry.Warn("Slow Database Query")
|
||||
} else {
|
||||
entry.Debug("Database Operation")
|
||||
}
|
||||
}
|
||||
204
server/pkg/response/response.go
Normal file
204
server/pkg/response/response.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
Pages int `json:"pages"`
|
||||
}
|
||||
|
||||
// 响应码定义
|
||||
const (
|
||||
SUCCESS = 200
|
||||
ERROR = 500
|
||||
|
||||
// 用户相关错误码
|
||||
ERROR_USER_NOT_FOUND = 10001
|
||||
ERROR_USER_ALREADY_EXIST = 10002
|
||||
ERROR_USER_DISABLED = 10003
|
||||
ERROR_INVALID_TOKEN = 10004
|
||||
ERROR_TOKEN_EXPIRED = 10005
|
||||
|
||||
// 商品相关错误码
|
||||
ERROR_PRODUCT_NOT_FOUND = 20001
|
||||
ERROR_PRODUCT_DISABLED = 20002
|
||||
ERROR_INSUFFICIENT_STOCK = 20003
|
||||
|
||||
// 订单相关错误码
|
||||
ERROR_ORDER_NOT_FOUND = 30001
|
||||
ERROR_ORDER_STATUS_ERROR = 30002
|
||||
ERROR_ORDER_CANNOT_PAY = 30003
|
||||
|
||||
// 参数相关错误码
|
||||
ERROR_INVALID_PARAMS = 40001
|
||||
ERROR_MISSING_PARAMS = 40002
|
||||
|
||||
// 权限相关错误码
|
||||
ERROR_UNAUTHORIZED = 40101
|
||||
ERROR_FORBIDDEN = 40103
|
||||
)
|
||||
|
||||
// 错误信息映射
|
||||
var codeMsg = map[int]string{
|
||||
SUCCESS: "操作成功",
|
||||
ERROR: "操作失败",
|
||||
|
||||
ERROR_USER_NOT_FOUND: "用户不存在",
|
||||
ERROR_USER_ALREADY_EXIST: "用户已存在",
|
||||
ERROR_USER_DISABLED: "用户已被禁用",
|
||||
ERROR_INVALID_TOKEN: "无效的token",
|
||||
ERROR_TOKEN_EXPIRED: "token已过期",
|
||||
|
||||
ERROR_PRODUCT_NOT_FOUND: "商品不存在",
|
||||
ERROR_PRODUCT_DISABLED: "商品已下架",
|
||||
ERROR_INSUFFICIENT_STOCK: "库存不足",
|
||||
|
||||
ERROR_ORDER_NOT_FOUND: "订单不存在",
|
||||
ERROR_ORDER_STATUS_ERROR: "订单状态错误",
|
||||
ERROR_ORDER_CANNOT_PAY: "订单无法支付",
|
||||
|
||||
ERROR_INVALID_PARAMS: "参数错误",
|
||||
ERROR_MISSING_PARAMS: "缺少必要参数",
|
||||
|
||||
ERROR_UNAUTHORIZED: "未授权",
|
||||
ERROR_FORBIDDEN: "权限不足",
|
||||
}
|
||||
|
||||
// GetMsg 获取错误信息
|
||||
func GetMsg(code int) string {
|
||||
msg, ok := codeMsg[code]
|
||||
if ok {
|
||||
return msg
|
||||
}
|
||||
return codeMsg[ERROR]
|
||||
}
|
||||
|
||||
// Success 成功响应
|
||||
func Success(c *gin.Context, data interface{}) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Code: SUCCESS,
|
||||
Message: GetMsg(SUCCESS),
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// SuccessWithMessage 成功响应(自定义消息)
|
||||
func SuccessWithMessage(c *gin.Context, message string, data interface{}) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Code: SUCCESS,
|
||||
Message: message,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// Error 错误响应
|
||||
func Error(c *gin.Context, code int) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Code: code,
|
||||
Message: GetMsg(code),
|
||||
})
|
||||
}
|
||||
|
||||
// ErrorWithMessage 错误响应(自定义消息)
|
||||
func ErrorWithMessage(c *gin.Context, code int, message string) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Code: code,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// ErrorWithData 错误响应(带数据)
|
||||
func ErrorWithData(c *gin.Context, code int, data interface{}) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Code: code,
|
||||
Message: GetMsg(code),
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// Page 分页响应
|
||||
func Page(c *gin.Context, list interface{}, total int64, page, pageSize int) {
|
||||
pages := int(total) / pageSize
|
||||
if int(total)%pageSize > 0 {
|
||||
pages++
|
||||
}
|
||||
|
||||
Success(c, PageData{
|
||||
List: list,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Pages: pages,
|
||||
})
|
||||
}
|
||||
|
||||
// PageQuery 查询分页响应(不显示成功消息)
|
||||
func PageQuery(c *gin.Context, list interface{}, total int64, page, pageSize int) {
|
||||
pages := int(total) / pageSize
|
||||
if int(total)%pageSize > 0 {
|
||||
pages++
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Code: SUCCESS,
|
||||
Message: "",
|
||||
Data: PageData{
|
||||
List: list,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Pages: pages,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Unauthorized 未授权响应
|
||||
func Unauthorized(c *gin.Context) {
|
||||
c.JSON(http.StatusUnauthorized, Response{
|
||||
Code: ERROR_UNAUTHORIZED,
|
||||
Message: GetMsg(ERROR_UNAUTHORIZED),
|
||||
})
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
// Forbidden 权限不足响应
|
||||
func Forbidden(c *gin.Context) {
|
||||
c.JSON(http.StatusForbidden, Response{
|
||||
Code: ERROR_FORBIDDEN,
|
||||
Message: GetMsg(ERROR_FORBIDDEN),
|
||||
})
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
// BadRequest 参数错误响应
|
||||
func BadRequest(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusBadRequest, Response{
|
||||
Code: ERROR_INVALID_PARAMS,
|
||||
Message: message,
|
||||
})
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
// InternalServerError 服务器内部错误响应
|
||||
func InternalServerError(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusInternalServerError, Response{
|
||||
Code: ERROR,
|
||||
Message: message,
|
||||
})
|
||||
c.Abort()
|
||||
}
|
||||
75
server/pkg/storage/local.go
Normal file
75
server/pkg/storage/local.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"dianshang/internal/config"
|
||||
)
|
||||
|
||||
// LocalStorage 本地存储服务
|
||||
type LocalStorage struct {
|
||||
config *config.UploadConfig
|
||||
}
|
||||
|
||||
// NewLocalStorage 创建本地存储服务实例
|
||||
func NewLocalStorage(cfg *config.UploadConfig) *LocalStorage {
|
||||
return &LocalStorage{
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// UploadFile 上传文件到本地
|
||||
func (s *LocalStorage) UploadFile(file multipart.File, filename string, objectPath string) (string, error) {
|
||||
// 创建保存路径
|
||||
savePath := filepath.Join(s.config.StaticPath, objectPath, filename)
|
||||
|
||||
// 确保目录存在
|
||||
if err := os.MkdirAll(filepath.Dir(savePath), 0755); err != nil {
|
||||
return "", fmt.Errorf("创建目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存文件
|
||||
if err := saveFile(file, savePath); err != nil {
|
||||
return "", fmt.Errorf("保存文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 返回文件URL
|
||||
fileURL := fmt.Sprintf("%s/%s/%s", s.config.BaseURL, objectPath, filename)
|
||||
return fileURL, nil
|
||||
}
|
||||
|
||||
// DeleteFile 删除本地文件
|
||||
func (s *LocalStorage) DeleteFile(filePath string) error {
|
||||
err := os.Remove(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除本地文件失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsFileExist 检查文件是否存在
|
||||
func (s *LocalStorage) IsFileExist(filePath string) bool {
|
||||
_, err := os.Stat(filePath)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// GetFileURL 获取文件访问URL
|
||||
func (s *LocalStorage) GetFileURL(objectPath string, filename string) string {
|
||||
return fmt.Sprintf("%s/%s/%s", s.config.BaseURL, objectPath, filename)
|
||||
}
|
||||
|
||||
// saveFile 保存上传的文件
|
||||
func saveFile(file multipart.File, dst string) error {
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, file)
|
||||
return err
|
||||
}
|
||||
273
server/pkg/storage/oss.go
Normal file
273
server/pkg/storage/oss.go
Normal file
@@ -0,0 +1,273 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"dianshang/internal/config"
|
||||
"dianshang/pkg/logger"
|
||||
|
||||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// OSSStorage 阿里云OSS存储服务
|
||||
type OSSStorage struct {
|
||||
client *oss.Client
|
||||
bucket *oss.Bucket
|
||||
config *config.OSSConfig
|
||||
}
|
||||
|
||||
// NewOSSStorage 创建OSS存储服务实例
|
||||
func NewOSSStorage(cfg *config.OSSConfig) (*OSSStorage, error) {
|
||||
// 创建OSSClient实例
|
||||
client, err := oss.New(cfg.Endpoint, cfg.AccessKeyID, cfg.AccessKeySecret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建OSS客户端失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取存储空间
|
||||
bucket, err := client.Bucket(cfg.BucketName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取OSS Bucket失败: %w", err)
|
||||
}
|
||||
|
||||
return &OSSStorage{
|
||||
client: client,
|
||||
bucket: bucket,
|
||||
config: cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UploadFile 上传文件到OSS
|
||||
func (s *OSSStorage) UploadFile(file multipart.File, filename string, objectPath string) (string, error) {
|
||||
// 生成OSS对象路径
|
||||
objectKey := s.generateObjectKey(objectPath, filename)
|
||||
logger.Infof("[OSS] 开始上传文件,对象路径: %s", objectKey)
|
||||
|
||||
// 获取文件MIME类型
|
||||
contentType := getContentType(filename)
|
||||
logger.Debugf("[OSS] 文件MIME类型: %s", contentType)
|
||||
|
||||
// 上传文件到OSS,设置Content-Type和其他元数据
|
||||
// 使用 ObjectACL 设置为公共读,确保文件可以直接访问
|
||||
err := s.bucket.PutObject(objectKey, file,
|
||||
oss.ContentType(contentType),
|
||||
oss.ObjectACL(oss.ACLPublicRead),
|
||||
oss.ContentDisposition("inline"), // 关键:设置为inline而不是attachment
|
||||
)
|
||||
if err != nil {
|
||||
logger.Errorf("[OSS] 上传文件失败,对象路径: %s, 错误: %v", objectKey, err)
|
||||
return "", fmt.Errorf("上传文件到OSS失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取OSS返回的文件URL
|
||||
fileURL, err := s.GetFileURL(objectKey)
|
||||
if err != nil {
|
||||
logger.Errorf("[OSS] 获取文件URL失败,对象路径: %s, 错误: %v", objectKey, err)
|
||||
return "", fmt.Errorf("获取OSS文件URL失败: %w", err)
|
||||
}
|
||||
logger.Infof("[OSS] 上传成功,对象路径: %s, OSS返回URL: %s", objectKey, fileURL)
|
||||
return fileURL, nil
|
||||
}
|
||||
|
||||
// UploadFromBytes 从字节数组上传文件到OSS
|
||||
func (s *OSSStorage) UploadFromBytes(data []byte, filename string, objectPath string) (string, error) {
|
||||
// 生成OSS对象路径
|
||||
objectKey := s.generateObjectKey(objectPath, filename)
|
||||
logger.Infof("[OSS] 开始上传字节数组,对象路径: %s, 大小: %d bytes", objectKey, len(data))
|
||||
|
||||
// 获取文件MIME类型
|
||||
contentType := getContentType(filename)
|
||||
logger.Debugf("[OSS] 文件MIME类型: %s", contentType)
|
||||
|
||||
// 上传文件到OSS,设置Content-Type和其他元数据
|
||||
err := s.bucket.PutObject(objectKey, strings.NewReader(string(data)),
|
||||
oss.ContentType(contentType),
|
||||
oss.ObjectACL(oss.ACLPublicRead),
|
||||
oss.ContentDisposition("inline"),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Errorf("[OSS] 上传字节数组失败,对象路径: %s, 错误: %v", objectKey, err)
|
||||
return "", fmt.Errorf("上传文件到OSS失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取OSS返回的文件URL
|
||||
fileURL, err := s.GetFileURL(objectKey)
|
||||
if err != nil {
|
||||
logger.Errorf("[OSS] 获取文件URL失败,对象路径: %s, 错误: %v", objectKey, err)
|
||||
return "", fmt.Errorf("获取OSS文件URL失败: %w", err)
|
||||
}
|
||||
logger.Infof("[OSS] 上传成功,对象路径: %s, OSS返回URL: %s", objectKey, fileURL)
|
||||
return fileURL, nil
|
||||
}
|
||||
|
||||
// UploadFromReader 从Reader上传文件到OSS
|
||||
func (s *OSSStorage) UploadFromReader(reader io.Reader, filename string, objectPath string) (string, error) {
|
||||
// 生成OSS对象路径
|
||||
objectKey := s.generateObjectKey(objectPath, filename)
|
||||
logger.Infof("[OSS] 开始上传Reader数据,对象路径: %s", objectKey)
|
||||
|
||||
// 获取文件MIME类型
|
||||
contentType := getContentType(filename)
|
||||
logger.Debugf("[OSS] 文件MIME类型: %s", contentType)
|
||||
|
||||
// 上传文件到OSS,设置Content-Type和其他元数据
|
||||
err := s.bucket.PutObject(objectKey, reader,
|
||||
oss.ContentType(contentType),
|
||||
oss.ObjectACL(oss.ACLPublicRead),
|
||||
oss.ContentDisposition("inline"),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Errorf("[OSS] 上传Reader数据失败,对象路径: %s, 错误: %v", objectKey, err)
|
||||
return "", fmt.Errorf("上传文件到OSS失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取OSS返回的文件URL
|
||||
fileURL, err := s.GetFileURL(objectKey)
|
||||
if err != nil {
|
||||
logger.Errorf("[OSS] 获取文件URL失败,对象路径: %s, 错误: %v", objectKey, err)
|
||||
return "", fmt.Errorf("获取OSS文件URL失败: %w", err)
|
||||
}
|
||||
logger.Infof("[OSS] 上传成功,对象路径: %s, OSS返回URL: %s", objectKey, fileURL)
|
||||
return fileURL, nil
|
||||
}
|
||||
|
||||
// DeleteFile 删除OSS中的文件
|
||||
func (s *OSSStorage) DeleteFile(objectKey string) error {
|
||||
err := s.bucket.DeleteObject(objectKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除OSS文件失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsObjectExist 检查对象是否存在
|
||||
func (s *OSSStorage) IsObjectExist(objectKey string) (bool, error) {
|
||||
exist, err := s.bucket.IsObjectExist(objectKey)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("检查OSS对象是否存在失败: %w", err)
|
||||
}
|
||||
return exist, nil
|
||||
}
|
||||
|
||||
// GetFileURL 获取文件访问URL(使用OSS SDK标准方法)
|
||||
func (s *OSSStorage) GetFileURL(objectKey string) (string, error) {
|
||||
// 如果配置了自定义域名,使用自定义域名
|
||||
if s.config.Domain != "" {
|
||||
url := fmt.Sprintf("%s/%s", strings.TrimRight(s.config.Domain, "/"), objectKey)
|
||||
logger.Debugf("[OSS] 使用自定义域名生成URL: %s", url)
|
||||
return url, nil
|
||||
}
|
||||
|
||||
// 使用OSS SDK获取对象的公共URL
|
||||
// 对于公共读的Bucket,直接拼接URL
|
||||
url := fmt.Sprintf("https://%s.%s/%s", s.config.BucketName, s.config.Endpoint, objectKey)
|
||||
logger.Debugf("[OSS] 使用OSS标准域名生成URL: %s", url)
|
||||
return url, nil
|
||||
}
|
||||
|
||||
// GeneratePresignedURL 生成临时访问URL(带签名)
|
||||
func (s *OSSStorage) GeneratePresignedURL(objectKey string, expireSeconds int64) (string, error) {
|
||||
signedURL, err := s.bucket.SignURL(objectKey, oss.HTTPGet, expireSeconds)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("生成预签名URL失败: %w", err)
|
||||
}
|
||||
return signedURL, nil
|
||||
}
|
||||
|
||||
// generateObjectKey 生成OSS对象键名
|
||||
func (s *OSSStorage) generateObjectKey(objectPath string, filename string) string {
|
||||
// 组合基础路径和对象路径
|
||||
basePath := strings.TrimRight(s.config.BasePath, "/")
|
||||
objectPath = strings.TrimLeft(objectPath, "/")
|
||||
|
||||
// OSS 路径统一使用斜杠,避免 Windows 下使用反斜杠
|
||||
if basePath == "" {
|
||||
return strings.ReplaceAll(filepath.ToSlash(filepath.Join(objectPath, filename)), "\\", "/")
|
||||
}
|
||||
|
||||
return strings.ReplaceAll(filepath.ToSlash(filepath.Join(basePath, objectPath, filename)), "\\", "/")
|
||||
}
|
||||
|
||||
// GenerateFilename 生成唯一文件名
|
||||
func GenerateFilename(originalFilename string) string {
|
||||
ext := filepath.Ext(originalFilename)
|
||||
name := strings.TrimSuffix(originalFilename, ext)
|
||||
|
||||
// 生成时间戳和UUID
|
||||
timestamp := time.Now().Format("20060102150405")
|
||||
uuidStr := uuid.New().String()[:8]
|
||||
|
||||
// 清理文件名,移除特殊字符
|
||||
name = strings.ReplaceAll(name, " ", "_")
|
||||
name = strings.ReplaceAll(name, "(", "")
|
||||
name = strings.ReplaceAll(name, ")", "")
|
||||
|
||||
return fmt.Sprintf("%s_%s_%s%s", name, timestamp, uuidStr, ext)
|
||||
}
|
||||
|
||||
// IsValidImageType 验证图片文件类型
|
||||
func IsValidImageType(filename string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
validExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"}
|
||||
|
||||
for _, validExt := range validExts {
|
||||
if ext == validExt {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getContentType 根据文件名获取MIME类型
|
||||
func getContentType(filename string) string {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
|
||||
// 常见图片类型
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg":
|
||||
return "image/jpeg"
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".gif":
|
||||
return "image/gif"
|
||||
case ".webp":
|
||||
return "image/webp"
|
||||
case ".svg":
|
||||
return "image/svg+xml"
|
||||
case ".bmp":
|
||||
return "image/bmp"
|
||||
case ".ico":
|
||||
return "image/x-icon"
|
||||
// 常见文档类型
|
||||
case ".pdf":
|
||||
return "application/pdf"
|
||||
case ".doc":
|
||||
return "application/msword"
|
||||
case ".docx":
|
||||
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
case ".xls":
|
||||
return "application/vnd.ms-excel"
|
||||
case ".xlsx":
|
||||
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
case ".txt":
|
||||
return "text/plain; charset=utf-8"
|
||||
case ".json":
|
||||
return "application/json"
|
||||
case ".xml":
|
||||
return "application/xml"
|
||||
default:
|
||||
// 使用Go标准库自动检测
|
||||
if mimeType := mime.TypeByExtension(ext); mimeType != "" {
|
||||
return mimeType
|
||||
}
|
||||
// 默认类型
|
||||
return "application/octet-stream"
|
||||
}
|
||||
}
|
||||
315
server/pkg/utils/utils.go
Normal file
315
server/pkg/utils/utils.go
Normal file
@@ -0,0 +1,315 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
// MD5 MD5加密
|
||||
func MD5(str string) string {
|
||||
h := md5.New()
|
||||
h.Write([]byte(str))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// GenerateRandomString 生成随机字符串
|
||||
func GenerateRandomString(length int) string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
randomByte := make([]byte, 1)
|
||||
rand.Read(randomByte)
|
||||
b[i] = charset[randomByte[0]%byte(len(charset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// GenerateOrderNo 生成订单号
|
||||
func GenerateOrderNo() string {
|
||||
now := time.Now()
|
||||
timestamp := now.Format("20060102150405")
|
||||
random := GenerateRandomString(6)
|
||||
return timestamp + strings.ToUpper(random)
|
||||
}
|
||||
|
||||
// GenerateWechatOutTradeNo 生成微信支付商户订单号
|
||||
// 格式:WX + 时间戳(精确到毫秒) + 8位随机字符串
|
||||
// 确保每次支付请求都有唯一的订单号,避免重复支付问题
|
||||
func GenerateWechatOutTradeNo() string {
|
||||
now := time.Now()
|
||||
// 使用纳秒时间戳确保唯一性
|
||||
timestamp := now.Format("20060102150405") + fmt.Sprintf("%03d", now.Nanosecond()/1000000)
|
||||
random := GenerateRandomString(8)
|
||||
return "WX" + timestamp + strings.ToUpper(random)
|
||||
}
|
||||
|
||||
// GenerateRefundNo 生成退款单号
|
||||
func GenerateRefundNo() string {
|
||||
now := time.Now()
|
||||
timestamp := now.Format("20060102150405")
|
||||
random := GenerateRandomString(6)
|
||||
return "RF" + timestamp + strings.ToUpper(random)
|
||||
}
|
||||
|
||||
// GenerateWechatOutRefundNo 生成微信退款单号
|
||||
// 格式:WXR + 时间戳(精确到毫秒) + 8位随机字符串
|
||||
func GenerateWechatOutRefundNo() string {
|
||||
now := time.Now()
|
||||
// 使用纳秒时间戳确保唯一性
|
||||
timestamp := now.Format("20060102150405") + fmt.Sprintf("%03d", now.Nanosecond()/1000000)
|
||||
random := GenerateRandomString(8)
|
||||
return "WXR" + timestamp + strings.ToUpper(random)
|
||||
}
|
||||
|
||||
// StringToUint 字符串转uint
|
||||
func StringToUint(str string) uint {
|
||||
if str == "" {
|
||||
return 0
|
||||
}
|
||||
num, err := strconv.ParseUint(str, 10, 32)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return uint(num)
|
||||
}
|
||||
|
||||
// StringToInt 字符串转int
|
||||
func StringToInt(str string) int {
|
||||
if str == "" {
|
||||
return 0
|
||||
}
|
||||
num, err := strconv.Atoi(str)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return num
|
||||
}
|
||||
|
||||
// StringToFloat64 字符串转float64
|
||||
func StringToFloat64(str string) float64 {
|
||||
if str == "" {
|
||||
return 0
|
||||
}
|
||||
num, err := strconv.ParseFloat(str, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return num
|
||||
}
|
||||
|
||||
// UintToString uint转字符串
|
||||
func UintToString(num uint) string {
|
||||
return strconv.FormatUint(uint64(num), 10)
|
||||
}
|
||||
|
||||
// IntToString 整数转字符串
|
||||
func IntToString(num int) string {
|
||||
return strconv.Itoa(num)
|
||||
}
|
||||
|
||||
// FloatToString 浮点数转字符串
|
||||
func FloatToString(num float64) string {
|
||||
return fmt.Sprintf("%.0f", num)
|
||||
}
|
||||
|
||||
// BoolToInt bool转int
|
||||
func BoolToInt(b bool) int {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// FormatPrice 格式化价格(分转元)
|
||||
func FormatPrice(price uint) string {
|
||||
yuan := float64(price) / 100
|
||||
return fmt.Sprintf("%.2f", yuan)
|
||||
}
|
||||
|
||||
// ParsePrice 解析价格(元转分)
|
||||
func ParsePrice(priceStr string) uint {
|
||||
price, err := strconv.ParseFloat(priceStr, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return uint(math.Round(price * 100))
|
||||
}
|
||||
|
||||
// InArray 检查元素是否在数组中
|
||||
func InArray(needle interface{}, haystack []interface{}) bool {
|
||||
for _, item := range haystack {
|
||||
if item == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// InStringArray 检查字符串是否在字符串数组中
|
||||
func InStringArray(needle string, haystack []string) bool {
|
||||
for _, item := range haystack {
|
||||
if item == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// InUintArray 检查uint是否在uint数组中
|
||||
func InUintArray(needle uint, haystack []uint) bool {
|
||||
for _, item := range haystack {
|
||||
if item == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RemoveDuplicateStrings 去除字符串数组重复元素
|
||||
func RemoveDuplicateStrings(slice []string) []string {
|
||||
keys := make(map[string]bool)
|
||||
result := []string{}
|
||||
for _, item := range slice {
|
||||
if !keys[item] {
|
||||
keys[item] = true
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// RemoveDuplicateUints 去除uint数组重复元素
|
||||
func RemoveDuplicateUints(slice []uint) []uint {
|
||||
keys := make(map[uint]bool)
|
||||
result := []uint{}
|
||||
for _, item := range slice {
|
||||
if !keys[item] {
|
||||
keys[item] = true
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetClientIP 获取客户端IP
|
||||
func GetClientIP(remoteAddr, xForwardedFor, xRealIP string) string {
|
||||
if xRealIP != "" {
|
||||
return xRealIP
|
||||
}
|
||||
if xForwardedFor != "" {
|
||||
ips := strings.Split(xForwardedFor, ",")
|
||||
if len(ips) > 0 {
|
||||
return strings.TrimSpace(ips[0])
|
||||
}
|
||||
}
|
||||
if remoteAddr != "" {
|
||||
ip := strings.Split(remoteAddr, ":")[0]
|
||||
return ip
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// Pagination 分页计算
|
||||
type Pagination struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Total int64 `json:"total"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// NewPagination 创建分页对象
|
||||
func NewPagination(page, pageSize int) *Pagination {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
return &Pagination{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Offset: offset,
|
||||
Limit: pageSize,
|
||||
}
|
||||
}
|
||||
|
||||
// IsValidEmail 验证邮箱格式
|
||||
func IsValidEmail(email string) bool {
|
||||
// 简单的邮箱格式验证
|
||||
return strings.Contains(email, "@") && strings.Contains(email, ".")
|
||||
}
|
||||
|
||||
// IsValidPhone 验证手机号格式
|
||||
func IsValidPhone(phone string) bool {
|
||||
// 简单的手机号格式验证(中国大陆)
|
||||
if len(phone) != 11 {
|
||||
return false
|
||||
}
|
||||
return strings.HasPrefix(phone, "1")
|
||||
}
|
||||
|
||||
// TruncateString 截断字符串
|
||||
func TruncateString(str string, length int) string {
|
||||
if len(str) <= length {
|
||||
return str
|
||||
}
|
||||
return str[:length] + "..."
|
||||
}
|
||||
|
||||
// GetTimeRange 获取时间范围
|
||||
func GetTimeRange(rangeType string) (time.Time, time.Time) {
|
||||
now := time.Now()
|
||||
var start, end time.Time
|
||||
|
||||
switch rangeType {
|
||||
case "today":
|
||||
start = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
end = start.Add(24 * time.Hour).Add(-time.Nanosecond)
|
||||
case "yesterday":
|
||||
yesterday := now.AddDate(0, 0, -1)
|
||||
start = time.Date(yesterday.Year(), yesterday.Month(), yesterday.Day(), 0, 0, 0, 0, yesterday.Location())
|
||||
end = start.Add(24 * time.Hour).Add(-time.Nanosecond)
|
||||
case "week":
|
||||
weekday := int(now.Weekday())
|
||||
if weekday == 0 {
|
||||
weekday = 7
|
||||
}
|
||||
start = now.AddDate(0, 0, -weekday+1)
|
||||
start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, start.Location())
|
||||
end = start.AddDate(0, 0, 7).Add(-time.Nanosecond)
|
||||
case "month":
|
||||
start = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||
end = start.AddDate(0, 1, 0).Add(-time.Nanosecond)
|
||||
default:
|
||||
start = now
|
||||
end = now
|
||||
}
|
||||
|
||||
return start, end
|
||||
}
|
||||
Reference in New Issue
Block a user