first commit
This commit is contained in:
31
internal/utils/captcha.go
Normal file
31
internal/utils/captcha.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// GenerateCaptcha 生成6位数字验证码
|
||||
func GenerateCaptcha() (string, error) {
|
||||
// 生成6位随机数字
|
||||
b := make([]byte, 3)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 将随机字节转换为6位数字
|
||||
num := int(b[0])<<16 | int(b[1])<<8 | int(b[2])
|
||||
return fmt.Sprintf("%06d", num%1000000), nil
|
||||
}
|
||||
|
||||
// GenerateEmailCaptchaContent 生成验证码邮件内容
|
||||
func GenerateEmailCaptchaContent(code, username, action string) string {
|
||||
return fmt.Sprintf(`
|
||||
<h3>验证码</h3>
|
||||
<p>您好,%s</p>
|
||||
<p>您正在进行%s操作,验证码为:</p>
|
||||
<h2 style="color: #1890ff;">%s</h2>
|
||||
<p>验证码有效期为5分钟,请勿泄露给他人。</p>
|
||||
<p>如果这不是您的操作,请忽略此邮件。</p>
|
||||
`, username, action, code)
|
||||
}
|
77
internal/utils/config.go
Normal file
77
internal/utils/config.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig
|
||||
Database DatabaseConfig
|
||||
JWT JWTConfig
|
||||
Email EmailConfig
|
||||
Upload UploadConfig
|
||||
Site SiteConfig
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Port string
|
||||
Mode string
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Type string
|
||||
Path string
|
||||
}
|
||||
|
||||
type JWTConfig struct {
|
||||
Secret string
|
||||
Expire string
|
||||
}
|
||||
|
||||
type EmailConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
type UploadConfig struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
type SiteConfig struct {
|
||||
Title string `mapstructure:"title"`
|
||||
Description string `mapstructure:"description"`
|
||||
BaseURL string `mapstructure:"base_url"`
|
||||
ICP string `mapstructure:"icp"`
|
||||
Copyright string `mapstructure:"copyright"`
|
||||
Logo string `mapstructure:"logo"`
|
||||
Favicon string `mapstructure:"favicon"`
|
||||
}
|
||||
|
||||
func LoadConfig() (*Config, error) {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath("./config")
|
||||
|
||||
// 读取环境变量
|
||||
viper.AutomaticEnv()
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := &Config{}
|
||||
if err := viper.Unmarshal(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 环境变量优先
|
||||
if port := os.Getenv("SERVER_PORT"); port != "" {
|
||||
config.Server.Port = port
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
232
internal/utils/config_persist.go
Normal file
232
internal/utils/config_persist.go
Normal file
@@ -0,0 +1,232 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"fmt"
|
||||
|
||||
"os"
|
||||
|
||||
"path/filepath"
|
||||
|
||||
"sync"
|
||||
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConfigVersion struct {
|
||||
Version int `json:"version"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
UpdatedBy string `json:"updated_by"`
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
type ConfigWithVersion struct {
|
||||
Config *Config `json:"config"`
|
||||
Version ConfigVersion `json:"version"`
|
||||
}
|
||||
|
||||
var (
|
||||
configMutex sync.RWMutex
|
||||
|
||||
configFile = "config/config.json" // JSON格式更适合动态更新
|
||||
|
||||
)
|
||||
|
||||
// SaveConfig 保存配置到文件
|
||||
|
||||
func SaveConfig(config *Config, updatedBy, comment string) error {
|
||||
|
||||
configMutex.Lock()
|
||||
|
||||
defer configMutex.Unlock()
|
||||
|
||||
// 读取当前版本
|
||||
|
||||
currentVersion := 0
|
||||
|
||||
if existing, err := LoadPersistedConfig(); err == nil {
|
||||
|
||||
currentVersion = existing.Version.Version
|
||||
|
||||
}
|
||||
|
||||
// 创建新的配置版本
|
||||
|
||||
configWithVersion := ConfigWithVersion{
|
||||
|
||||
Config: config,
|
||||
|
||||
Version: ConfigVersion{
|
||||
|
||||
Version: currentVersion + 1,
|
||||
|
||||
UpdatedAt: time.Now(),
|
||||
|
||||
UpdatedBy: updatedBy,
|
||||
|
||||
Comment: comment,
|
||||
},
|
||||
}
|
||||
|
||||
// 确保配置目录存在
|
||||
|
||||
configDir := filepath.Dir(configFile)
|
||||
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// 备份旧配置
|
||||
|
||||
if err := backupConfig(); err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// 将配置转换为JSON
|
||||
|
||||
data, err := json.MarshalIndent(configWithVersion, "", " ")
|
||||
|
||||
if err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
|
||||
return os.WriteFile(configFile, data, 0644)
|
||||
|
||||
}
|
||||
|
||||
// LoadPersistedConfig 加载持久化的配置
|
||||
|
||||
func LoadPersistedConfig() (*ConfigWithVersion, error) {
|
||||
|
||||
configMutex.RLock()
|
||||
|
||||
defer configMutex.RUnlock()
|
||||
|
||||
// 检查配置文件是否存在
|
||||
|
||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||
|
||||
// 如果不存在,创建默认配置
|
||||
|
||||
config, err := LoadConfig()
|
||||
|
||||
if err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
return &ConfigWithVersion{
|
||||
|
||||
Config: config,
|
||||
|
||||
Version: ConfigVersion{
|
||||
|
||||
Version: 1,
|
||||
|
||||
UpdatedAt: time.Now(),
|
||||
|
||||
UpdatedBy: "system",
|
||||
|
||||
Comment: "初始配置",
|
||||
},
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
// 读取配置文件
|
||||
|
||||
data, err := os.ReadFile(configFile)
|
||||
|
||||
if err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
// 解析JSON配置
|
||||
|
||||
var configWithVersion ConfigWithVersion
|
||||
|
||||
if err := json.Unmarshal(data, &configWithVersion); err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
return &configWithVersion, nil
|
||||
|
||||
}
|
||||
|
||||
// MergeConfig 合并配置(环境变量优先)
|
||||
|
||||
func MergeConfig(persisted, env *Config) *Config {
|
||||
|
||||
if env.Server.Port != "" {
|
||||
|
||||
persisted.Server.Port = env.Server.Port
|
||||
|
||||
}
|
||||
|
||||
if env.Server.Mode != "" {
|
||||
|
||||
persisted.Server.Mode = env.Server.Mode
|
||||
|
||||
}
|
||||
|
||||
// ... 其他配置项的合并 ...
|
||||
|
||||
return persisted
|
||||
|
||||
}
|
||||
|
||||
// backupConfig 备份配置文件
|
||||
|
||||
func backupConfig() error {
|
||||
|
||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// 确保备份目录存在
|
||||
|
||||
backupDir := "config/backups"
|
||||
|
||||
if err := os.MkdirAll(backupDir, 0755); err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// 读取当前配置
|
||||
|
||||
data, err := os.ReadFile(configFile)
|
||||
|
||||
if err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// 创建备份文件名
|
||||
|
||||
backupFile := filepath.Join(backupDir,
|
||||
|
||||
fmt.Sprintf("config_%s.json", time.Now().Format("20060102150405")))
|
||||
|
||||
// 写入备份文件
|
||||
|
||||
return os.WriteFile(backupFile, data, 0644)
|
||||
|
||||
}
|
56
internal/utils/database.go
Normal file
56
internal/utils/database.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"licserver/internal/model"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func InitDB(config *DatabaseConfig) (*gorm.DB, error) {
|
||||
|
||||
db, err := gorm.Open(sqlite.Open(config.Path), &gorm.Config{})
|
||||
|
||||
if err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
// 自动迁移数据库结构
|
||||
|
||||
err = db.AutoMigrate(
|
||||
|
||||
&model.User{},
|
||||
|
||||
&model.Device{},
|
||||
|
||||
&model.DeviceModel{},
|
||||
|
||||
&model.PasswordResetToken{},
|
||||
|
||||
&model.Captcha{},
|
||||
|
||||
&model.FileUpload{},
|
||||
|
||||
&model.UploadChunk{},
|
||||
|
||||
&model.LicenseCode{},
|
||||
|
||||
&model.LicenseLog{},
|
||||
|
||||
&model.AccessToken{},
|
||||
|
||||
&model.TokenLog{},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
return db, nil
|
||||
|
||||
}
|
40
internal/utils/email.go
Normal file
40
internal/utils/email.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
)
|
||||
|
||||
type EmailService struct {
|
||||
config *EmailConfig
|
||||
auth smtp.Auth
|
||||
}
|
||||
|
||||
func NewEmailService(config *EmailConfig) *EmailService {
|
||||
auth := smtp.PlainAuth("", config.Username, config.Password, config.Host)
|
||||
return &EmailService{
|
||||
config: config,
|
||||
auth: auth,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *EmailService) SendEmail(to, subject, body string) error {
|
||||
addr := fmt.Sprintf("%s:%d", s.config.Host, s.config.Port)
|
||||
msg := []byte(fmt.Sprintf("To: %s\r\n"+
|
||||
"Subject: %s\r\n"+
|
||||
"Content-Type: text/html; charset=UTF-8\r\n"+
|
||||
"\r\n"+
|
||||
"%s\r\n", to, subject, body))
|
||||
|
||||
return smtp.SendMail(addr, s.auth, s.config.Username, []string{to}, msg)
|
||||
}
|
||||
|
||||
func GenerateResetToken() (string, error) {
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
29
internal/utils/errors.go
Normal file
29
internal/utils/errors.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package utils
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// 通用错误
|
||||
ErrInvalidInput = errors.New("无效的输入")
|
||||
ErrNotFound = errors.New("资源不存在")
|
||||
ErrUnauthorized = errors.New("未授权的访问")
|
||||
ErrForbidden = errors.New("禁止访问")
|
||||
|
||||
// 授权相关错误
|
||||
ErrInvalidToken = errors.New("无效的令牌")
|
||||
ErrTokenExpired = errors.New("令牌已过期")
|
||||
ErrInvalidCaptcha = errors.New("无效的验证码")
|
||||
ErrCaptchaExpired = errors.New("验证码已过期")
|
||||
ErrInvalidLicense = errors.New("无效的授权码")
|
||||
ErrLicenseExpired = errors.New("授权码已过期")
|
||||
ErrLicenseUsed = errors.New("授权码已被使用")
|
||||
ErrDeviceNotFound = errors.New("设备不存在")
|
||||
ErrDeviceRegistered = errors.New("设备已注册")
|
||||
)
|
||||
|
||||
// ErrorResponse 统一错误响应结构
|
||||
type ErrorResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
51
internal/utils/jwt.go
Normal file
51
internal/utils/jwt.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
UserID uint
|
||||
Username string
|
||||
Role string
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func GenerateToken(userID uint, username, role string, config *JWTConfig) (string, error) {
|
||||
expDuration, err := time.ParseDuration(config.Expire)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
claims := Claims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
Role: role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expDuration)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(config.Secret))
|
||||
}
|
||||
|
||||
func ParseToken(tokenString string, config *JWTConfig) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(config.Secret), 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")
|
||||
}
|
50
internal/utils/logger.go
Normal file
50
internal/utils/logger.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
logFile *os.File
|
||||
}
|
||||
|
||||
func NewLogger(logPath string) (*Logger, error) {
|
||||
// 确保日志目录存在
|
||||
if err := os.MkdirAll(filepath.Dir(logPath), 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 打开日志文件
|
||||
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Logger{logFile: file}, nil
|
||||
}
|
||||
|
||||
func (l *Logger) Info(format string, args ...interface{}) {
|
||||
l.log("INFO", format, args...)
|
||||
}
|
||||
|
||||
func (l *Logger) Error(format string, args ...interface{}) {
|
||||
l.log("ERROR", format, args...)
|
||||
}
|
||||
|
||||
func (l *Logger) Debug(format string, args ...interface{}) {
|
||||
l.log("DEBUG", format, args...)
|
||||
}
|
||||
|
||||
func (l *Logger) log(level, format string, args ...interface{}) {
|
||||
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||||
message := fmt.Sprintf(format, args...)
|
||||
logLine := fmt.Sprintf("[%s] [%s] %s\n", timestamp, level, message)
|
||||
l.logFile.WriteString(logLine)
|
||||
}
|
||||
|
||||
func (l *Logger) Close() error {
|
||||
return l.logFile.Close()
|
||||
}
|
68
internal/utils/test_helper.go
Normal file
68
internal/utils/test_helper.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"licserver/internal/model"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TestDB 创建测试数据库连接
|
||||
func TestDB(t *testing.T) *gorm.DB {
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 迁移测试表
|
||||
err = db.AutoMigrate(
|
||||
&model.User{},
|
||||
&model.Device{},
|
||||
&model.DeviceModel{},
|
||||
&model.LicenseCode{},
|
||||
&model.LicenseLog{},
|
||||
&model.AccessToken{},
|
||||
&model.TokenLog{},
|
||||
&model.Captcha{},
|
||||
&model.PasswordResetToken{},
|
||||
&model.FileUpload{},
|
||||
&model.UploadChunk{},
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
// TestConfig 创建测试配置
|
||||
func TestConfig() *Config {
|
||||
return &Config{
|
||||
Server: ServerConfig{
|
||||
Port: "8080",
|
||||
Mode: "test",
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
Type: "sqlite3",
|
||||
Path: ":memory:",
|
||||
},
|
||||
JWT: JWTConfig{
|
||||
Secret: "test-secret",
|
||||
Expire: "24h",
|
||||
},
|
||||
Email: EmailConfig{
|
||||
Host: "smtp.example.com",
|
||||
Port: 587,
|
||||
Username: "test@example.com",
|
||||
Password: "test-password",
|
||||
},
|
||||
Upload: UploadConfig{
|
||||
Path: "./test-uploads",
|
||||
},
|
||||
Site: SiteConfig{
|
||||
Title: "Test Site",
|
||||
Description: "Test Description",
|
||||
BaseURL: "http://localhost:8080",
|
||||
ICP: "Test ICP",
|
||||
Copyright: "Test Copyright",
|
||||
},
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user