refactor: 前后端目录结构模块化 + v2 优化移植与缺陷修复

前端:
- src/srv/api → src/api,按域分文件夹(auth/cluster/host/storage/deployment/collector)
- 新增 http.ts 引擎(http.get/post/put/delete),域模块改用语义化调用
- index.html 增加 favicon(匹配应用 logo 的 emerald + Activity 脉冲)

后端分层增强(保留包名):
- 新增 repository/:从 models 拆出 JsonDB 持久化层(InitDB/GetDB/Shutdown/RequestSave)
- 新增 dto/:从 controllers 抽出各域请求契约(带 binding 校验)
- 新增 pkg/crypto、pkg/jwt:从 utils 拆出安全原语(外部 jwt 别名 jwtv5 避免冲突)
- models 只留领域实体 + sanitizer;utils 只留 gin/应用相关(response/errors)

v2 优化移植与缺陷修复:
- 移植密钥分离/SSH TOFU/并发写安全/Loki 生产级配置/Docker 加固
- 修复 deployment.go 并发丢更新、Helm values 重复键、EncryptField 哨兵、loki-gateway 端点等审计缺陷
- 实现真正的优雅关停(main.go http.Server + signal + repository.Shutdown)
- 修复登录契约:后端返回 accessToken,前端 auth.ts/Login.tsx 对齐
- 补 crypto/jwt/repository 单元测试(含并发写回归)

验证: go build/vet/test 全绿;npm run build 通过;登录→鉴权→优雅关停冒烟通过

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Logan.LiandClaude committed 2026-06-30 10:46:26 +08:00
1 parent fb8d10231a
commit 327e282356
93 files changed
+8433 -3089

No files matched your search

+154 -27
View File
@@ -3,12 +3,32 @@ package services
import (
"context"
"fmt"
"strconv"
"strings"
"sync"
"time"
"lokideploy/models"
"lokideploy/repository"
"lokideploy/utils"
)
// LokiVersionList 支持的 Loki 版本列表(按推荐程度排序)
var LokiVersionList = []string{
"3.4.0",
"3.3.0",
"3.2.0",
"3.1.0",
"3.0.0",
"2.9.8",
"2.9.0",
"2.8.9",
}
// minSupportedMajorVersion 最低支持的主版本号
const minSupportedMajorVersion = 2
// DeploymentService 部署业务逻辑层
type DeploymentService struct{}
func NewDeploymentService() *DeploymentService {
@@ -33,7 +53,14 @@ type CreateDeploymentRequest struct {
}
func (s *DeploymentService) Create(req *CreateDeploymentRequest) (*models.LokiDeployment, error) {
db := models.GetDB()
// 版本兼容性预检查
if req.Version != "" {
if err := ValidateLokiVersion(req.Version); err != nil {
return nil, err
}
}
db := repository.GetDB()
d := models.LokiDeployment{
ID: db.NextID("deployments"),
Name: req.Name,
@@ -88,13 +115,13 @@ func (s *DeploymentService) Create(req *CreateDeploymentRequest) (*models.LokiDe
db.Lock()
db.Deployments = append(db.Deployments, d)
db.Unlock()
go db.Save()
db.RequestSave()
return &d, nil
}
func (s *DeploymentService) Get(id uint) (*models.LokiDeployment, error) {
db := models.GetDB()
db := repository.GetDB()
db.RLock()
defer db.RUnlock()
for i := range db.Deployments {
@@ -108,7 +135,7 @@ func (s *DeploymentService) Get(id uint) (*models.LokiDeployment, error) {
}
func (s *DeploymentService) List(page, pageSize int) ([]models.LokiDeployment, int64, error) {
db := models.GetDB()
db := repository.GetDB()
db.RLock()
defer db.RUnlock()
@@ -130,7 +157,7 @@ func (s *DeploymentService) List(page, pageSize int) ([]models.LokiDeployment, i
}
func (s *DeploymentService) Update(id uint, updates map[string]interface{}) error {
db := models.GetDB()
db := repository.GetDB()
db.Lock()
defer db.Unlock()
for i := range db.Deployments {
@@ -145,7 +172,7 @@ func (s *DeploymentService) Update(id uint, updates map[string]interface{}) erro
db.Deployments[i].Endpoint = v
}
db.Deployments[i].UpdatedAt = time.Now()
go db.Save()
db.RequestSave()
return nil
}
}
@@ -153,19 +180,32 @@ func (s *DeploymentService) Update(id uint, updates map[string]interface{}) erro
}
func (s *DeploymentService) Delete(id uint) error {
db := models.GetDB()
db := repository.GetDB()
db.Lock()
defer db.Unlock()
for i, d := range db.Deployments {
if d.ID == id {
db.Deployments = append(db.Deployments[:i], db.Deployments[i+1:]...)
go db.Save()
db.RequestSave()
return nil
}
}
return utils.ErrNotFound
}
// ==================== Goroutine 管理改进 ====================
// deployCtxEntry 保存正在进行的部署上下文
type deployCtxEntry struct {
cancel context.CancelFunc
done chan struct{}
}
var (
deployCtxMap = make(map[uint]*deployCtxEntry)
deployCtxMu sync.Mutex
)
func (s *DeploymentService) StartDeploy(id uint) error {
deployment, err := s.Get(id)
if err != nil {
@@ -174,19 +214,41 @@ func (s *DeploymentService) StartDeploy(id uint) error {
if deployment.Status == "deploying" {
return fmt.Errorf("部署正在进行中,请勿重复操作")
}
s.Update(id, map[string]interface{}{"status": "deploying"})
// 检查已有 goroutine 状态,防止重复部署
deployCtxMu.Lock()
if entry, exists := deployCtxMap[id]; exists {
select {
case <-entry.done:
// 已完成,可以重新部署
delete(deployCtxMap, id)
default:
deployCtxMu.Unlock()
return fmt.Errorf("部署正在进行中,请勿重复操作")
}
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
defer cancel()
done := make(chan struct{})
deployCtxMap[id] = &deployCtxEntry{cancel: cancel, done: done}
deployCtxMu.Unlock()
s.Update(id, map[string]interface{}{"status": "deploying"})
logFunc := func(level, message, detail string) {
s.AddLog(id, level, message, detail)
}
deployer := NewLokiDeployer(deployment, logFunc)
go func() {
err := deployer.Deploy(ctx)
if err != nil {
defer close(done)
defer func() {
deployCtxMu.Lock()
delete(deployCtxMap, id)
deployCtxMu.Unlock()
}()
deployer := NewLokiDeployer(deployment, logFunc)
if err := deployer.Deploy(ctx); err != nil {
s.Update(id, map[string]interface{}{
"status": "failed",
"statusMessage": fmt.Sprintf("Deployment failed: %v", err),
@@ -201,6 +263,7 @@ func (s *DeploymentService) StartDeploy(id uint) error {
s.AddLog(id, "success", "Deployment completed successfully", "")
}
}()
return nil
}
@@ -212,6 +275,14 @@ func (s *DeploymentService) StopDeployment(id uint) error {
if deployment.Status != "running" && deployment.Status != "deploying" {
return fmt.Errorf("部署未在运行,无法停止")
}
// 取消正在进行的部署 goroutine
deployCtxMu.Lock()
if entry, exists := deployCtxMap[id]; exists {
entry.cancel()
}
deployCtxMu.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
CleanupLoki(ctx, deployment)
@@ -223,14 +294,17 @@ func (s *DeploymentService) StopDeployment(id uint) error {
func (s *DeploymentService) RestartDeployment(id uint) error {
if err := s.StopDeployment(id); err != nil {
return err
// 如果停止失败但不是因为部署未运行,返回错误
if !strings.Contains(err.Error(), "部署未在运行") {
return err
}
}
time.Sleep(2 * time.Second)
return s.StartDeploy(id)
}
func (s *DeploymentService) AddLog(deploymentID uint, level, message, detail string) {
db := models.GetDB()
db := repository.GetDB()
log := models.DeploymentLog{
ID: db.NextID("logs"),
DeploymentID: deploymentID,
@@ -242,11 +316,11 @@ func (s *DeploymentService) AddLog(deploymentID uint, level, message, detail str
db.Lock()
db.Logs = append(db.Logs, log)
db.Unlock()
go db.Save()
db.RequestSave()
}
func (s *DeploymentService) GetLogs(deploymentID uint, page, pageSize int) ([]models.DeploymentLog, int64, error) {
db := models.GetDB()
db := repository.GetDB()
db.RLock()
defer db.RUnlock()
@@ -270,7 +344,7 @@ func (s *DeploymentService) GetLogs(deploymentID uint, page, pageSize int) ([]mo
}
func (s *DeploymentService) GetStats() (map[string]interface{}, error) {
db := models.GetDB()
db := repository.GetDB()
db.RLock()
defer db.RUnlock()
@@ -289,18 +363,19 @@ func (s *DeploymentService) GetStats() (map[string]interface{}, error) {
}
return map[string]interface{}{
"totalDeployments": int64(len(db.Deployments)),
"running": running,
"failed": failed,
"pending": pending,
"stopped": stopped,
"clusterCount": int64(len(db.Clusters)),
"hostCount": int64(len(db.Hosts)),
"totalDeployments": int64(len(db.Deployments)),
"running": running,
"failed": failed,
"pending": pending,
"stopped": stopped,
"clusterCount": int64(len(db.Clusters)),
"hostCount": int64(len(db.Hosts)),
"supportedVersions": LokiVersionList,
}, nil
}
func (s *DeploymentService) loadRelations(d *models.LokiDeployment) {
db := models.GetDB()
db := repository.GetDB()
if d.ClusterID != nil && *d.ClusterID > 0 {
for i := range db.Clusters {
if db.Clusters[i].ID == *d.ClusterID {
@@ -337,7 +412,14 @@ func (s *DeploymentService) getEndpoint(d *models.LokiDeployment) string {
if ns == "" {
ns = "loki"
}
return fmt.Sprintf("http://loki.%s.svc.cluster.local:3100", ns)
// k8s-yaml manifest 固定创建名为 loki 的 Service;
// k8s-helm (grafana/loki) 以 release 名(=部署名,默认 loki)暴露 Service。
// 注意:不使用 loki-gateway,gateway 仅在 Distributed+gateway 启用时存在。
svc := "loki"
if d.Mode == "k8s-helm" && d.Name != "" {
svc = d.Name
}
return fmt.Sprintf("http://%s.%s.svc.cluster.local:3100", svc, ns)
case "host-binary":
if d.Host != nil {
return fmt.Sprintf("http://%s:3100", d.Host.HostIP)
@@ -345,3 +427,48 @@ func (s *DeploymentService) getEndpoint(d *models.LokiDeployment) string {
}
return ""
}
// ==================== 版本兼容性检查 ====================
// ValidateLokiVersion 检查 Loki 版本是否在支持列表中且满足最低版本要求
func ValidateLokiVersion(version string) error {
if version == "" {
return nil // 使用默认值,不报错
}
// 基本格式验证
parts := strings.Split(version, ".")
if len(parts) < 2 {
return fmt.Errorf("版本号格式无效: %s,期望格式如 3.0.0", version)
}
major, err := strconv.Atoi(parts[0])
if err != nil {
return fmt.Errorf("无法解析主版本号: %s", version)
}
minor, err := strconv.Atoi(parts[1])
if err != nil {
return fmt.Errorf("无法解析次版本号: %s", version)
}
// 最低版本检查
if major < minSupportedMajorVersion {
return fmt.Errorf("Loki %s 版本过低,最低支持 %d.x 版本", version, minSupportedMajorVersion)
}
if major == 2 && minor < 8 {
return fmt.Errorf("Loki %s 版本过低,2.x 最低支持 2.8.x 版本", version)
}
if major >= 4 {
return fmt.Errorf("Loki %s 是未发布的版本,请使用已知稳定版本", version)
}
return nil
}
// GetRecommendedVersions 返回推荐的 Loki 版本列表
func GetRecommendedVersions() []string {
result := make([]string, len(LokiVersionList))
copy(result, LokiVersionList)
return result
}