[优化] 工具整理

This commit is contained in:
ryan
2026-03-14 15:03:05 +08:00
parent f1476610f6
commit 605a70b428
26 changed files with 316 additions and 240 deletions
-141
View File
@@ -1,141 +0,0 @@
package common
import (
"fmt"
"github.com/google/uuid"
"html/template"
"log/slog"
"net"
"os/exec"
"runtime"
"strconv"
"strings"
)
func OpenBrowser(url string) {
var err error
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
}
if err != nil {
slog.Error("open browser failed", "error", err)
}
}
func GetIp() (ip string) {
ips, err := net.InterfaceAddrs()
if err != nil {
slog.Error("get interface addresses failed", "error", err)
return ip
}
for _, a := range ips {
if ipNet, ok := a.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
if ipNet.IP.To4() != nil {
ip = ipNet.IP.String()
if strings.HasPrefix(ip, "10") {
return
}
if strings.HasPrefix(ip, "172") {
return
}
if strings.HasPrefix(ip, "192.168") {
return
}
ip = ""
}
}
}
return
}
var sizeKB = 1024
var sizeMB = sizeKB * 1024
var sizeGB = sizeMB * 1024
func Bytes2Size(num int64) string {
numStr := ""
unit := "B"
if num/int64(sizeGB) > 1 {
numStr = fmt.Sprintf("%.2f", float64(num)/float64(sizeGB))
unit = "GB"
} else if num/int64(sizeMB) > 1 {
numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeMB)))
unit = "MB"
} else if num/int64(sizeKB) > 1 {
numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeKB)))
unit = "KB"
} else {
numStr = fmt.Sprintf("%d", num)
}
return numStr + " " + unit
}
func Seconds2Time(num int) (time string) {
if num/31104000 > 0 {
time += strconv.Itoa(num/31104000) + " 年 "
num %= 31104000
}
if num/2592000 > 0 {
time += strconv.Itoa(num/2592000) + " 个月 "
num %= 2592000
}
if num/86400 > 0 {
time += strconv.Itoa(num/86400) + " 天 "
num %= 86400
}
if num/3600 > 0 {
time += strconv.Itoa(num/3600) + " 小时 "
num %= 3600
}
if num/60 > 0 {
time += strconv.Itoa(num/60) + " 分钟 "
num %= 60
}
time += strconv.Itoa(num) + " 秒"
return
}
func Interface2String(inter interface{}) string {
switch inter.(type) {
case string:
return inter.(string)
case int:
return fmt.Sprintf("%d", inter.(int))
case float64:
return fmt.Sprintf("%f", inter.(float64))
}
return "Not Implemented"
}
func UnescapeHTML(x string) interface{} {
return template.HTML(x)
}
func IntMax(a int, b int) int {
if a >= b {
return a
} else {
return b
}
}
func GetUUID() string {
code := uuid.New().String()
code = strings.Replace(code, "-", "", -1)
return code
}
func Max(a int, b int) int {
if a >= b {
return a
} else {
return b
}
}
+2 -1
View File
@@ -3,6 +3,7 @@ package controller
import ( import (
"atsflare/common" "atsflare/common"
"atsflare/model" "atsflare/model"
"atsflare/utils"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/http" "net/http"
@@ -75,7 +76,7 @@ func UploadFile(c *gin.Context) {
for _, file := range files { for _, file := range files {
filename := filepath.Base(file.Filename) filename := filepath.Base(file.Filename)
ext := filepath.Ext(filename) ext := filepath.Ext(filename)
link := common.GetUUID() + ext link := utils.GetUUID() + ext
savePath := filepath.Join(uploadPath, link) // both parts are checked, so this path should be safe to use savePath := filepath.Join(uploadPath, link) // both parts are checked, so this path should be safe to use
if err := c.SaveUploadedFile(file, savePath); err != nil { if err := c.SaveUploadedFile(file, savePath); err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
+16 -13
View File
@@ -3,6 +3,9 @@ package controller
import ( import (
"atsflare/common" "atsflare/common"
"atsflare/model" "atsflare/model"
"atsflare/utils/mail"
"atsflare/utils/security"
"atsflare/utils/validation"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -62,7 +65,7 @@ func GetAbout(c *gin.Context) {
func SendEmailVerification(c *gin.Context) { func SendEmailVerification(c *gin.Context) {
email := c.Query("email") email := c.Query("email")
if err := common.Validate.Var(email, "required,email"); err != nil { if err := validation.Validate.Var(email, "required,email"); err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,
"message": "无效的参数", "message": "无效的参数",
@@ -76,13 +79,13 @@ func SendEmailVerification(c *gin.Context) {
}) })
return return
} }
code := common.GenerateVerificationCode(6) code := security.GenerateVerificationCode(6)
common.RegisterVerificationCodeWithKey(email, code, common.EmailVerificationPurpose) security.RegisterVerificationCodeWithKey(email, code, security.EmailVerificationPurpose)
subject := fmt.Sprintf("%s邮箱验证邮件", common.SystemName) subject := fmt.Sprintf("%s邮箱验证邮件", common.SystemName)
content := fmt.Sprintf("<p>您好,你正在进行%s邮箱验证。</p>"+ content := fmt.Sprintf("<p>您好,你正在进行%s邮箱验证。</p>"+
"<p>您的验证码为: <strong>%s</strong></p>"+ "<p>您的验证码为: <strong>%s</strong></p>"+
"<p>验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, code, common.VerificationValidMinutes) "<p>验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, code, security.VerificationValidMinutes)
err := common.SendEmail(subject, email, content) err := mail.SendEmail(subject, email, content)
if err != nil { if err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,
@@ -99,7 +102,7 @@ func SendEmailVerification(c *gin.Context) {
func SendPasswordResetEmail(c *gin.Context) { func SendPasswordResetEmail(c *gin.Context) {
email := c.Query("email") email := c.Query("email")
if err := common.Validate.Var(email, "required,email"); err != nil { if err := validation.Validate.Var(email, "required,email"); err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,
"message": "无效的参数", "message": "无效的参数",
@@ -113,14 +116,14 @@ func SendPasswordResetEmail(c *gin.Context) {
}) })
return return
} }
code := common.GenerateVerificationCode(0) code := security.GenerateVerificationCode(0)
common.RegisterVerificationCodeWithKey(email, code, common.PasswordResetPurpose) security.RegisterVerificationCodeWithKey(email, code, security.PasswordResetPurpose)
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", common.ServerAddress, email, code) link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", common.ServerAddress, email, code)
subject := fmt.Sprintf("%s密码重置", common.SystemName) subject := fmt.Sprintf("%s密码重置", common.SystemName)
content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+ content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+
"<p>点击<a href='%s'>此处</a>进行密码重置。</p>"+ "<p>点击<a href='%s'>此处</a>进行密码重置。</p>"+
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, common.VerificationValidMinutes) "<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, security.VerificationValidMinutes)
err := common.SendEmail(subject, email, content) err := mail.SendEmail(subject, email, content)
if err != nil { if err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,
@@ -150,14 +153,14 @@ func ResetPassword(c *gin.Context) {
}) })
return return
} }
if !common.VerifyCodeWithKey(req.Email, req.Token, common.PasswordResetPurpose) { if !security.VerifyCodeWithKey(req.Email, req.Token, security.PasswordResetPurpose) {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,
"message": "重置链接非法或已过期", "message": "重置链接非法或已过期",
}) })
return return
} }
password := common.GenerateVerificationCode(12) password := security.GenerateVerificationCode(12)
err = model.ResetUserPasswordByEmail(req.Email, password) err = model.ResetUserPasswordByEmail(req.Email, password)
if err != nil { if err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
@@ -166,7 +169,7 @@ func ResetPassword(c *gin.Context) {
}) })
return return
} }
common.DeleteKey(req.Email, common.PasswordResetPurpose) security.DeleteKey(req.Email, security.PasswordResetPurpose)
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "", "message": "",
+2 -1
View File
@@ -4,6 +4,7 @@ import (
"atsflare/common" "atsflare/common"
"atsflare/model" "atsflare/model"
"atsflare/service" "atsflare/service"
"atsflare/utils"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -178,7 +179,7 @@ func GetOptions(c *gin.Context) {
} }
options = append(options, &model.Option{ options = append(options, &model.Option{
Key: k, Key: k,
Value: common.Interface2String(v), Value: utils.Interface2String(v),
}) })
} }
common.OptionMapRWMutex.Unlock() common.OptionMapRWMutex.Unlock()
+7 -5
View File
@@ -3,6 +3,8 @@ package controller
import ( import (
"atsflare/common" "atsflare/common"
"atsflare/model" "atsflare/model"
"atsflare/utils/security"
"atsflare/utils/validation"
"encoding/json" "encoding/json"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -128,7 +130,7 @@ func Register(c *gin.Context) {
}) })
return return
} }
if err := common.Validate.Struct(&user); err != nil { if err := validation.Validate.Struct(&user); err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,
"message": "输入不合法 " + err.Error(), "message": "输入不合法 " + err.Error(),
@@ -143,7 +145,7 @@ func Register(c *gin.Context) {
}) })
return return
} }
if !common.VerifyCodeWithKey(user.Email, user.VerificationCode, common.EmailVerificationPurpose) { if !security.VerifyCodeWithKey(user.Email, user.VerificationCode, security.EmailVerificationPurpose) {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,
"message": "验证码错误或已过期", "message": "验证码错误或已过期",
@@ -313,7 +315,7 @@ func UpdateUser(c *gin.Context) {
if updatedUser.Password == "" { if updatedUser.Password == "" {
updatedUser.Password = "$I_LOVE_U" // make Validator happy :) updatedUser.Password = "$I_LOVE_U" // make Validator happy :)
} }
if err := common.Validate.Struct(&updatedUser); err != nil { if err := validation.Validate.Struct(&updatedUser); err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,
"message": "输入不合法 " + err.Error(), "message": "输入不合法 " + err.Error(),
@@ -374,7 +376,7 @@ func UpdateSelf(c *gin.Context) {
if user.Password == "" { if user.Password == "" {
user.Password = "$I_LOVE_U" // make Validator happy :) user.Password = "$I_LOVE_U" // make Validator happy :)
} }
if err := common.Validate.Struct(&user); err != nil { if err := validation.Validate.Struct(&user); err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,
"message": "输入不合法 " + err.Error(), "message": "输入不合法 " + err.Error(),
@@ -622,7 +624,7 @@ func ManageUser(c *gin.Context) {
func EmailBind(c *gin.Context) { func EmailBind(c *gin.Context) {
email := c.Query("email") email := c.Query("email")
code := c.Query("code") code := c.Query("code")
if !common.VerifyCodeWithKey(email, code, common.EmailVerificationPurpose) { if !security.VerifyCodeWithKey(email, code, security.EmailVerificationPurpose) {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,
"message": "验证码错误或已过期", "message": "验证码错误或已过期",
+2 -1
View File
@@ -2,6 +2,7 @@ package middleware
import ( import (
"atsflare/common" "atsflare/common"
"atsflare/utils/ratelimit"
"context" "context"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"log/slog" "log/slog"
@@ -11,7 +12,7 @@ import (
var timeFormat = "2006-01-02T15:04:05.000Z" var timeFormat = "2006-01-02T15:04:05.000Z"
var inMemoryRateLimiter common.InMemoryRateLimiter var inMemoryRateLimiter ratelimit.InMemoryRateLimiter
func redisRateLimiter(c *gin.Context, maxRequestNum int, duration int64, mark string) { func redisRateLimiter(c *gin.Context, maxRequestNum int, duration int64, mark string) {
ctx := context.Background() ctx := context.Background()
+2 -1
View File
@@ -2,6 +2,7 @@ package model
import ( import (
"atsflare/common" "atsflare/common"
"atsflare/utils/security"
"github.com/glebarez/sqlite" "github.com/glebarez/sqlite"
"gorm.io/driver/mysql" "gorm.io/driver/mysql"
"gorm.io/gorm" "gorm.io/gorm"
@@ -26,7 +27,7 @@ func createRootAccountIfNeed() error {
//if user.Status != common.UserStatusEnabled { //if user.Status != common.UserStatusEnabled {
if err := DB.First(&user).Error; err != nil { if err := DB.First(&user).Error; err != nil {
slog.Info("no user exists, create a root user", "username", "root") slog.Info("no user exists, create a root user", "username", "root")
hashedPassword, err := common.Password2Hash("123456") hashedPassword, err := security.Password2Hash("123456")
if err != nil { if err != nil {
return err return err
} }
+5 -4
View File
@@ -2,6 +2,7 @@ package model
import ( import (
"atsflare/common" "atsflare/common"
"atsflare/utils/security"
"errors" "errors"
"strings" "strings"
) )
@@ -63,7 +64,7 @@ func DeleteUserById(id int) (err error) {
func (user *User) Insert() error { func (user *User) Insert() error {
var err error var err error
if user.Password != "" { if user.Password != "" {
user.Password, err = common.Password2Hash(user.Password) user.Password, err = security.Password2Hash(user.Password)
if err != nil { if err != nil {
return err return err
} }
@@ -75,7 +76,7 @@ func (user *User) Insert() error {
func (user *User) Update(updatePassword bool) error { func (user *User) Update(updatePassword bool) error {
var err error var err error
if updatePassword { if updatePassword {
user.Password, err = common.Password2Hash(user.Password) user.Password, err = security.Password2Hash(user.Password)
if err != nil { if err != nil {
return err return err
} }
@@ -102,7 +103,7 @@ func (user *User) ValidateAndFill() (err error) {
return errors.New("用户名或密码为空") return errors.New("用户名或密码为空")
} }
DB.Where(User{Username: user.Username}).First(user) DB.Where(User{Username: user.Username}).First(user)
okay := common.ValidatePasswordAndHash(password, user.Password) okay := security.ValidatePasswordAndHash(password, user.Password)
if !okay || user.Status != common.UserStatusEnabled { if !okay || user.Status != common.UserStatusEnabled {
return errors.New("用户名或密码错误,或用户已被封禁") return errors.New("用户名或密码错误,或用户已被封禁")
} }
@@ -181,7 +182,7 @@ func ResetUserPasswordByEmail(email string, password string) error {
if email == "" || password == "" { if email == "" || password == "" {
return errors.New("邮箱地址或密码为空!") return errors.New("邮箱地址或密码为空!")
} }
hashedPassword, err := common.Password2Hash(password) hashedPassword, err := security.Password2Hash(password)
if err != nil { if err != nil {
return err return err
} }
+2 -2
View File
@@ -1,9 +1,9 @@
package router package router
import ( import (
"atsflare/common"
"atsflare/controller" "atsflare/controller"
"atsflare/middleware" "atsflare/middleware"
"atsflare/utils/embedfs"
"embed" "embed"
"github.com/gin-contrib/static" "github.com/gin-contrib/static"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -24,7 +24,7 @@ func setWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
fileDownloadRoute.GET("/upload/:file", middleware.DownloadRateLimit(), controller.DownloadFile) fileDownloadRoute.GET("/upload/:file", middleware.DownloadRateLimit(), controller.DownloadFile)
router.Use(normalizeStaticExportDataNavigation()) router.Use(normalizeStaticExportDataNavigation())
router.Use(middleware.Cache()) router.Use(middleware.Cache())
router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/build"))) router.Use(static.Serve("/", embedfs.EmbedFolder(buildFS, "web/build")))
router.NoRoute(func(c *gin.Context) { router.NoRoute(func(c *gin.Context) {
if serveExportedPage(c, exportedBuildFS) { if serveExportedPage(c, exportedBuildFS) {
return return
+23
View File
@@ -0,0 +1,23 @@
package utils
import (
"log/slog"
"os/exec"
"runtime"
)
func OpenBrowser(url string) {
var err error
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
}
if err != nil {
slog.Error("open browser failed", "error", err)
}
}
@@ -1,20 +1,21 @@
package common package embedfs
import ( import (
"embed" "embed"
"github.com/gin-contrib/static"
"io/fs" "io/fs"
"net/http" "net/http"
"strings" "strings"
"github.com/gin-contrib/static"
) )
// Credit: https://github.com/gin-contrib/static/issues/19 // Credit: https://github.com/gin-contrib/static/issues/19
type embedFileSystem struct { type fileSystem struct {
http.FileSystem http.FileSystem
} }
func (e embedFileSystem) Exists(prefix string, path string) bool { func (e fileSystem) Exists(prefix string, path string) bool {
cleanPath := strings.TrimPrefix(path, prefix) cleanPath := strings.TrimPrefix(path, prefix)
cleanPath = strings.TrimPrefix(cleanPath, "/") cleanPath = strings.TrimPrefix(cleanPath, "/")
if cleanPath == "" { if cleanPath == "" {
@@ -33,7 +34,7 @@ func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem {
if err != nil { if err != nil {
panic(err) panic(err)
} }
return embedFileSystem{ return fileSystem{
FileSystem: http.FS(efs), FileSystem: http.FS(efs),
} }
} }
+53
View File
@@ -0,0 +1,53 @@
package utils
import (
"fmt"
"strconv"
)
var sizeKB = 1024
var sizeMB = sizeKB * 1024
var sizeGB = sizeMB * 1024
func Bytes2Size(num int64) string {
numStr := ""
unit := "B"
if num/int64(sizeGB) > 1 {
numStr = fmt.Sprintf("%.2f", float64(num)/float64(sizeGB))
unit = "GB"
} else if num/int64(sizeMB) > 1 {
numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeMB)))
unit = "MB"
} else if num/int64(sizeKB) > 1 {
numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeKB)))
unit = "KB"
} else {
numStr = fmt.Sprintf("%d", num)
}
return numStr + " " + unit
}
func Seconds2Time(num int) (time string) {
if num/31104000 > 0 {
time += strconv.Itoa(num/31104000) + " 年 "
num %= 31104000
}
if num/2592000 > 0 {
time += strconv.Itoa(num/2592000) + " 个月 "
num %= 2592000
}
if num/86400 > 0 {
time += strconv.Itoa(num/86400) + " 天 "
num %= 86400
}
if num/3600 > 0 {
time += strconv.Itoa(num/3600) + " 小时 "
num %= 3600
}
if num/60 > 0 {
time += strconv.Itoa(num/60) + " 分钟 "
num %= 60
}
time += strconv.Itoa(num) + " 秒"
return
}
+7
View File
@@ -0,0 +1,7 @@
package utils
import "html/template"
func UnescapeHTML(x string) interface{} {
return template.HTML(x)
}
@@ -1,6 +1,7 @@
package common package mail
import ( import (
"atsflare/common"
"crypto/tls" "crypto/tls"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
@@ -14,21 +15,21 @@ func SendEmail(subject string, receiver string, content string) error {
"From: %s<%s>\r\n"+ "From: %s<%s>\r\n"+
"Subject: %s\r\n"+ "Subject: %s\r\n"+
"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n", "Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n",
receiver, SystemName, SMTPAccount, encodedSubject, content)) receiver, common.SystemName, common.SMTPAccount, encodedSubject, content))
auth := smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer) auth := smtp.PlainAuth("", common.SMTPAccount, common.SMTPToken, common.SMTPServer)
addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort) addr := fmt.Sprintf("%s:%d", common.SMTPServer, common.SMTPPort)
to := strings.Split(receiver, ";") to := strings.Split(receiver, ";")
var err error var err error
if SMTPPort == 465 { if common.SMTPPort == 465 {
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
InsecureSkipVerify: true, InsecureSkipVerify: true,
ServerName: SMTPServer, ServerName: common.SMTPServer,
} }
conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", SMTPServer, SMTPPort), tlsConfig) conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", common.SMTPServer, common.SMTPPort), tlsConfig)
if err != nil { if err != nil {
return err return err
} }
client, err := smtp.NewClient(conn, SMTPServer) client, err := smtp.NewClient(conn, common.SMTPServer)
if err != nil { if err != nil {
return err return err
} }
@@ -36,7 +37,7 @@ func SendEmail(subject string, receiver string, content string) error {
if err = client.Auth(auth); err != nil { if err = client.Auth(auth); err != nil {
return err return err
} }
if err = client.Mail(SMTPAccount); err != nil { if err = client.Mail(common.SMTPAccount); err != nil {
return err return err
} }
receiverEmails := strings.Split(receiver, ";") receiverEmails := strings.Split(receiver, ";")
@@ -58,7 +59,7 @@ func SendEmail(subject string, receiver string, content string) error {
return err return err
} }
} else { } else {
err = smtp.SendMail(addr, auth, SMTPAccount, to, mail) err = smtp.SendMail(addr, auth, common.SMTPAccount, to, mail)
} }
return err return err
} }
+15
View File
@@ -0,0 +1,15 @@
package utils
func IntMax(a int, b int) int {
if a >= b {
return a
}
return b
}
func Max(a int, b int) int {
if a >= b {
return a
}
return b
}
+34
View File
@@ -0,0 +1,34 @@
package utils
import (
"log/slog"
"net"
"strings"
)
func GetIp() (ip string) {
ips, err := net.InterfaceAddrs()
if err != nil {
slog.Error("get interface addresses failed", "error", err)
return ip
}
for _, a := range ips {
if ipNet, ok := a.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
if ipNet.IP.To4() != nil {
ip = ipNet.IP.String()
if strings.HasPrefix(ip, "10") {
return
}
if strings.HasPrefix(ip, "172") {
return
}
if strings.HasPrefix(ip, "192.168") {
return
}
ip = ""
}
}
}
return
}
@@ -1,4 +1,4 @@
package common package ratelimit
import ( import (
"sync" "sync"
@@ -52,19 +52,16 @@ func (l *InMemoryRateLimiter) Request(key string, maxRequestNum int, duration in
if len(*queue) < maxRequestNum { if len(*queue) < maxRequestNum {
*queue = append(*queue, now) *queue = append(*queue, now)
return true return true
} else {
if now-(*queue)[0] >= duration {
*queue = (*queue)[1:]
*queue = append(*queue, now)
return true
} else {
return false
}
} }
} else { if now-(*queue)[0] >= duration {
s := make([]int64, 0, maxRequestNum) *queue = (*queue)[1:]
l.store[key] = &s *queue = append(*queue, now)
*(l.store[key]) = append(*(l.store[key]), now) return true
}
return false
} }
s := make([]int64, 0, maxRequestNum)
l.store[key] = &s
*(l.store[key]) = append(*(l.store[key]), now)
return true return true
} }
@@ -1,4 +1,4 @@
package common package security
import "golang.org/x/crypto/bcrypt" import "golang.org/x/crypto/bcrypt"
+37
View File
@@ -0,0 +1,37 @@
package security
import "crypto/rand"
func GenerateRandomString(length int) string {
if length <= 0 {
return ""
}
const charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
const n = byte(len(charset))
const threshold = byte(256 - (256 % len(charset)))
out := make([]byte, 0, length)
buf := make([]byte, length)
for len(out) < length {
if _, err := rand.Read(buf); err != nil {
return ""
}
for _, b := range buf {
if b < threshold {
out = append(out, charset[int(b%n)])
if len(out) == length {
break
}
}
}
}
return string(out)
}
func GeneratePassword() string {
return GenerateRandomString(12)
}
func GenerateToken() string {
return GenerateRandomString(22)
}
@@ -1,10 +1,11 @@
package common package security
import ( import (
"github.com/google/uuid"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/google/uuid"
) )
type verificationValue struct { type verificationValue struct {
+13
View File
@@ -0,0 +1,13 @@
package utils
import (
"strings"
"github.com/google/uuid"
)
func GetUUID() string {
code := uuid.New().String()
code = strings.Replace(code, "-", "", -1)
return code
}
@@ -1,4 +1,4 @@
package common package validation
import "github.com/go-playground/validator/v10" import "github.com/go-playground/validator/v10"
+15
View File
@@ -0,0 +1,15 @@
package utils
import "fmt"
func Interface2String(inter interface{}) string {
switch inter.(type) {
case string:
return inter.(string)
case int:
return fmt.Sprintf("%d", inter.(int))
case float64:
return fmt.Sprintf("%f", inter.(float64))
}
return "Not Implemented"
}
@@ -1,5 +1,6 @@
'use client'; 'use client';
import * as echarts from 'echarts';
import type { EChartsOption } from 'echarts'; import type { EChartsOption } from 'echarts';
import ReactECharts from 'echarts-for-react'; import ReactECharts from 'echarts-for-react';
import Link from 'next/link'; import Link from 'next/link';
@@ -272,14 +273,21 @@ export function WorldStage({
useEffect(() => { useEffect(() => {
let disposed = false; let disposed = false;
import('echarts-maps/world.js') import('@/features/dashboard/data/world-geo.json')
.then(() => { .then(({ default: worldGeoJson }) => {
if (!echarts.getMap('world')) {
echarts.registerMap('world', worldGeoJson);
}
if (!echarts.getMap('world')) {
throw new Error('world map registration failed');
}
if (!disposed) { if (!disposed) {
setMapReady(true); setMapReady(true);
setMapFailed(false); setMapFailed(false);
} }
}) })
.catch(() => { .catch((error) => {
console.error('Failed to register ECharts world map', error);
if (!disposed) { if (!disposed) {
setMapReady(false); setMapReady(false);
setMapFailed(true); setMapFailed(true);
@@ -435,6 +443,7 @@ export function WorldStage({
map: 'world', map: 'world',
roam: false, roam: false,
silent: true, silent: true,
regions: [],
top: 18, top: 18,
bottom: 18, bottom: 18,
left: 10, left: 10,
-1
View File
@@ -1 +0,0 @@
declare module 'echarts-maps/world.js';
+3 -1
View File
@@ -71,13 +71,15 @@
* `model/`:模型定义与持久化 * `model/`:模型定义与持久化
* `router/`:路由注册 * `router/`:路由注册
* `middleware/`:认证、鉴权、限流等横切逻辑 * `middleware/`:认证、鉴权、限流等横切逻辑
* `common/`:配置与通用工具 * `common/`:配置、全局运行时状态与初始化入口
* `utils/`:纯工具函数与通用 helper;按功能聚合,多个同类 helper 应拆到对应子目录
禁止: 禁止:
* 在 `controller/` 堆积业务逻辑 * 在 `controller/` 堆积业务逻辑
* 在 `middleware/` 中实现业务流程 * 在 `middleware/` 中实现业务流程
* 为简单需求新增平台层抽象 * 为简单需求新增平台层抽象
* 在 `common/` 混放不依赖全局状态的纯工具实现
### 3.2 Agent ### 3.2 Agent