[优化] 工具整理

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 (
"atsflare/common"
"atsflare/model"
"atsflare/utils"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
@@ -75,7 +76,7 @@ func UploadFile(c *gin.Context) {
for _, file := range files {
filename := filepath.Base(file.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
if err := c.SaveUploadedFile(file, savePath); err != nil {
c.JSON(http.StatusOK, gin.H{
+16 -13
View File
@@ -3,6 +3,9 @@ package controller
import (
"atsflare/common"
"atsflare/model"
"atsflare/utils/mail"
"atsflare/utils/security"
"atsflare/utils/validation"
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
@@ -62,7 +65,7 @@ func GetAbout(c *gin.Context) {
func SendEmailVerification(c *gin.Context) {
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{
"success": false,
"message": "无效的参数",
@@ -76,13 +79,13 @@ func SendEmailVerification(c *gin.Context) {
})
return
}
code := common.GenerateVerificationCode(6)
common.RegisterVerificationCodeWithKey(email, code, common.EmailVerificationPurpose)
code := security.GenerateVerificationCode(6)
security.RegisterVerificationCodeWithKey(email, code, security.EmailVerificationPurpose)
subject := fmt.Sprintf("%s邮箱验证邮件", common.SystemName)
content := fmt.Sprintf("<p>您好,你正在进行%s邮箱验证。</p>"+
"<p>您的验证码为: <strong>%s</strong></p>"+
"<p>验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, code, common.VerificationValidMinutes)
err := common.SendEmail(subject, email, content)
"<p>验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, code, security.VerificationValidMinutes)
err := mail.SendEmail(subject, email, content)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
@@ -99,7 +102,7 @@ func SendEmailVerification(c *gin.Context) {
func SendPasswordResetEmail(c *gin.Context) {
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{
"success": false,
"message": "无效的参数",
@@ -113,14 +116,14 @@ func SendPasswordResetEmail(c *gin.Context) {
})
return
}
code := common.GenerateVerificationCode(0)
common.RegisterVerificationCodeWithKey(email, code, common.PasswordResetPurpose)
code := security.GenerateVerificationCode(0)
security.RegisterVerificationCodeWithKey(email, code, security.PasswordResetPurpose)
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", common.ServerAddress, email, code)
subject := fmt.Sprintf("%s密码重置", common.SystemName)
content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+
"<p>点击<a href='%s'>此处</a>进行密码重置。</p>"+
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, common.VerificationValidMinutes)
err := common.SendEmail(subject, email, content)
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, security.VerificationValidMinutes)
err := mail.SendEmail(subject, email, content)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
@@ -150,14 +153,14 @@ func ResetPassword(c *gin.Context) {
})
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{
"success": false,
"message": "重置链接非法或已过期",
})
return
}
password := common.GenerateVerificationCode(12)
password := security.GenerateVerificationCode(12)
err = model.ResetUserPasswordByEmail(req.Email, password)
if err != nil {
c.JSON(http.StatusOK, gin.H{
@@ -166,7 +169,7 @@ func ResetPassword(c *gin.Context) {
})
return
}
common.DeleteKey(req.Email, common.PasswordResetPurpose)
security.DeleteKey(req.Email, security.PasswordResetPurpose)
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
+2 -1
View File
@@ -4,6 +4,7 @@ import (
"atsflare/common"
"atsflare/model"
"atsflare/service"
"atsflare/utils"
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
@@ -178,7 +179,7 @@ func GetOptions(c *gin.Context) {
}
options = append(options, &model.Option{
Key: k,
Value: common.Interface2String(v),
Value: utils.Interface2String(v),
})
}
common.OptionMapRWMutex.Unlock()
+7 -5
View File
@@ -3,6 +3,8 @@ package controller
import (
"atsflare/common"
"atsflare/model"
"atsflare/utils/security"
"atsflare/utils/validation"
"encoding/json"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
@@ -128,7 +130,7 @@ func Register(c *gin.Context) {
})
return
}
if err := common.Validate.Struct(&user); err != nil {
if err := validation.Validate.Struct(&user); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "输入不合法 " + err.Error(),
@@ -143,7 +145,7 @@ func Register(c *gin.Context) {
})
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{
"success": false,
"message": "验证码错误或已过期",
@@ -313,7 +315,7 @@ func UpdateUser(c *gin.Context) {
if updatedUser.Password == "" {
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{
"success": false,
"message": "输入不合法 " + err.Error(),
@@ -374,7 +376,7 @@ func UpdateSelf(c *gin.Context) {
if user.Password == "" {
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{
"success": false,
"message": "输入不合法 " + err.Error(),
@@ -622,7 +624,7 @@ func ManageUser(c *gin.Context) {
func EmailBind(c *gin.Context) {
email := c.Query("email")
code := c.Query("code")
if !common.VerifyCodeWithKey(email, code, common.EmailVerificationPurpose) {
if !security.VerifyCodeWithKey(email, code, security.EmailVerificationPurpose) {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "验证码错误或已过期",
+2 -1
View File
@@ -2,6 +2,7 @@ package middleware
import (
"atsflare/common"
"atsflare/utils/ratelimit"
"context"
"github.com/gin-gonic/gin"
"log/slog"
@@ -11,7 +12,7 @@ import (
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) {
ctx := context.Background()
+2 -1
View File
@@ -2,6 +2,7 @@ package model
import (
"atsflare/common"
"atsflare/utils/security"
"github.com/glebarez/sqlite"
"gorm.io/driver/mysql"
"gorm.io/gorm"
@@ -26,7 +27,7 @@ func createRootAccountIfNeed() error {
//if user.Status != common.UserStatusEnabled {
if err := DB.First(&user).Error; err != nil {
slog.Info("no user exists, create a root user", "username", "root")
hashedPassword, err := common.Password2Hash("123456")
hashedPassword, err := security.Password2Hash("123456")
if err != nil {
return err
}
+5 -4
View File
@@ -2,6 +2,7 @@ package model
import (
"atsflare/common"
"atsflare/utils/security"
"errors"
"strings"
)
@@ -63,7 +64,7 @@ func DeleteUserById(id int) (err error) {
func (user *User) Insert() error {
var err error
if user.Password != "" {
user.Password, err = common.Password2Hash(user.Password)
user.Password, err = security.Password2Hash(user.Password)
if err != nil {
return err
}
@@ -75,7 +76,7 @@ func (user *User) Insert() error {
func (user *User) Update(updatePassword bool) error {
var err error
if updatePassword {
user.Password, err = common.Password2Hash(user.Password)
user.Password, err = security.Password2Hash(user.Password)
if err != nil {
return err
}
@@ -102,7 +103,7 @@ func (user *User) ValidateAndFill() (err error) {
return errors.New("用户名或密码为空")
}
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 {
return errors.New("用户名或密码错误,或用户已被封禁")
}
@@ -181,7 +182,7 @@ func ResetUserPasswordByEmail(email string, password string) error {
if email == "" || password == "" {
return errors.New("邮箱地址或密码为空!")
}
hashedPassword, err := common.Password2Hash(password)
hashedPassword, err := security.Password2Hash(password)
if err != nil {
return err
}
+2 -2
View File
@@ -1,9 +1,9 @@
package router
import (
"atsflare/common"
"atsflare/controller"
"atsflare/middleware"
"atsflare/utils/embedfs"
"embed"
"github.com/gin-contrib/static"
"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)
router.Use(normalizeStaticExportDataNavigation())
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) {
if serveExportedPage(c, exportedBuildFS) {
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,39 +1,40 @@
package common
import (
"embed"
"github.com/gin-contrib/static"
"io/fs"
"net/http"
"strings"
)
// Credit: https://github.com/gin-contrib/static/issues/19
type embedFileSystem struct {
http.FileSystem
}
func (e embedFileSystem) Exists(prefix string, path string) bool {
cleanPath := strings.TrimPrefix(path, prefix)
cleanPath = strings.TrimPrefix(cleanPath, "/")
if cleanPath == "" {
return false
}
_, err := e.Open(cleanPath)
if err != nil {
return false
}
return true
}
func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem {
efs, err := fs.Sub(fsEmbed, targetPath)
if err != nil {
panic(err)
}
return embedFileSystem{
FileSystem: http.FS(efs),
}
}
package embedfs
import (
"embed"
"io/fs"
"net/http"
"strings"
"github.com/gin-contrib/static"
)
// Credit: https://github.com/gin-contrib/static/issues/19
type fileSystem struct {
http.FileSystem
}
func (e fileSystem) Exists(prefix string, path string) bool {
cleanPath := strings.TrimPrefix(path, prefix)
cleanPath = strings.TrimPrefix(cleanPath, "/")
if cleanPath == "" {
return false
}
_, err := e.Open(cleanPath)
if err != nil {
return false
}
return true
}
func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem {
efs, err := fs.Sub(fsEmbed, targetPath)
if err != nil {
panic(err)
}
return fileSystem{
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 (
"atsflare/common"
"crypto/tls"
"encoding/base64"
"fmt"
@@ -14,21 +15,21 @@ func SendEmail(subject string, receiver string, content string) error {
"From: %s<%s>\r\n"+
"Subject: %s\r\n"+
"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n",
receiver, SystemName, SMTPAccount, encodedSubject, content))
auth := smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer)
addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort)
receiver, common.SystemName, common.SMTPAccount, encodedSubject, content))
auth := smtp.PlainAuth("", common.SMTPAccount, common.SMTPToken, common.SMTPServer)
addr := fmt.Sprintf("%s:%d", common.SMTPServer, common.SMTPPort)
to := strings.Split(receiver, ";")
var err error
if SMTPPort == 465 {
if common.SMTPPort == 465 {
tlsConfig := &tls.Config{
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 {
return err
}
client, err := smtp.NewClient(conn, SMTPServer)
client, err := smtp.NewClient(conn, common.SMTPServer)
if err != nil {
return err
}
@@ -36,7 +37,7 @@ func SendEmail(subject string, receiver string, content string) error {
if err = client.Auth(auth); err != nil {
return err
}
if err = client.Mail(SMTPAccount); err != nil {
if err = client.Mail(common.SMTPAccount); err != nil {
return err
}
receiverEmails := strings.Split(receiver, ";")
@@ -58,7 +59,7 @@ func SendEmail(subject string, receiver string, content string) error {
return err
}
} else {
err = smtp.SendMail(addr, auth, SMTPAccount, to, mail)
err = smtp.SendMail(addr, auth, common.SMTPAccount, to, mail)
}
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 (
"sync"
@@ -52,19 +52,16 @@ func (l *InMemoryRateLimiter) Request(key string, maxRequestNum int, duration in
if len(*queue) < maxRequestNum {
*queue = append(*queue, now)
return true
} else {
if now-(*queue)[0] >= duration {
*queue = (*queue)[1:]
*queue = append(*queue, now)
return true
} else {
return false
}
}
} else {
s := make([]int64, 0, maxRequestNum)
l.store[key] = &s
*(l.store[key]) = append(*(l.store[key]), now)
if now-(*queue)[0] >= duration {
*queue = (*queue)[1:]
*queue = append(*queue, now)
return true
}
return false
}
s := make([]int64, 0, maxRequestNum)
l.store[key] = &s
*(l.store[key]) = append(*(l.store[key]), now)
return true
}
@@ -1,4 +1,4 @@
package common
package security
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 (
"github.com/google/uuid"
"strings"
"sync"
"time"
"github.com/google/uuid"
)
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"
+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';
import * as echarts from 'echarts';
import type { EChartsOption } from 'echarts';
import ReactECharts from 'echarts-for-react';
import Link from 'next/link';
@@ -272,14 +273,21 @@ export function WorldStage({
useEffect(() => {
let disposed = false;
import('echarts-maps/world.js')
.then(() => {
import('@/features/dashboard/data/world-geo.json')
.then(({ default: worldGeoJson }) => {
if (!echarts.getMap('world')) {
echarts.registerMap('world', worldGeoJson);
}
if (!echarts.getMap('world')) {
throw new Error('world map registration failed');
}
if (!disposed) {
setMapReady(true);
setMapFailed(false);
}
})
.catch(() => {
.catch((error) => {
console.error('Failed to register ECharts world map', error);
if (!disposed) {
setMapReady(false);
setMapFailed(true);
@@ -435,6 +443,7 @@ export function WorldStage({
map: 'world',
roam: false,
silent: true,
regions: [],
top: 18,
bottom: 18,
left: 10,
-1
View File
@@ -1 +0,0 @@
declare module 'echarts-maps/world.js';
+3 -1
View File
@@ -71,13 +71,15 @@
* `model/`:模型定义与持久化
* `router/`:路由注册
* `middleware/`:认证、鉴权、限流等横切逻辑
* `common/`:配置与通用工具
* `common/`:配置、全局运行时状态与初始化入口
* `utils/`:纯工具函数与通用 helper;按功能聚合,多个同类 helper 应拆到对应子目录
禁止:
* 在 `controller/` 堆积业务逻辑
* 在 `middleware/` 中实现业务流程
* 为简单需求新增平台层抽象
* 在 `common/` 混放不依赖全局状态的纯工具实现
### 3.2 Agent