diff --git a/bin/ego b/bin/ego new file mode 100644 index 0000000..0f8c48e Binary files /dev/null and b/bin/ego differ diff --git a/internal/conf/db.go b/internal/conf/db.go index 601cd65..55b8cc1 100644 --- a/internal/conf/db.go +++ b/internal/conf/db.go @@ -94,6 +94,8 @@ func initDatabase(connString string) error { zap.Int("maxOpenConns", 20), zap.Int("maxIdleConns", 10)) + // 迁移数据库结构 + //Migration() return nil } @@ -146,6 +148,7 @@ func Migration() error { // 自动迁移模式 models := []any{ &model.SysDeployFile{}, + &model.SysDeployProject{}, } for _, model := range models { diff --git a/internal/handler/sys_deploy_file_handler.go b/internal/handler/sys_deploy_file_handler.go index 653c651..025a409 100644 --- a/internal/handler/sys_deploy_file_handler.go +++ b/internal/handler/sys_deploy_file_handler.go @@ -40,7 +40,7 @@ func (h *SysDeployFileHandler) Create(c *gin.Context) { // @Tags 部署文件管理 // @Accept json // @Produce json -// @Param id path string true "部署ID" +// @Param id path string true "文件ID" // @Success 200 {object} serializer.Response // @Router /deploy-files/{id} [get] func (h *SysDeployFileHandler) GetByID(c *gin.Context) { @@ -54,7 +54,7 @@ func (h *SysDeployFileHandler) GetByID(c *gin.Context) { // @Tags 部署文件管理 // @Accept json // @Produce json -// @Param id path string true "部署ID" +// @Param id path string true "文件ID" // @Param deployFile body model.SysDeployFile true "部署文件信息" // @Success 200 {object} serializer.Response // @Router /deploy-files/{id} [put] @@ -69,7 +69,7 @@ func (h *SysDeployFileHandler) UpdateByID(c *gin.Context) { // @Tags 部署文件管理 // @Accept json // @Produce json -// @Param id path string true "部署ID" +// @Param id path string true "文件ID" // @Success 200 {object} serializer.Response // @Router /deploy-files/{id} [delete] func (h *SysDeployFileHandler) DeleteByID(c *gin.Context) { @@ -90,3 +90,31 @@ func (h *SysDeployFileHandler) GetByCondition(c *gin.Context) { logger.Info(c, "条件查询部署文件记录") c.JSON(200, h.deployFileService.GetByCondition(c)) } + +// GetByParentID 根据项目ID获取文件列表 +// @Summary 获取项目文件列表 +// @Description 根据项目ID获取关联的文件列表 +// @Tags 部署文件管理 +// @Accept json +// @Produce json +// @Param parentId path string true "项目ID" +// @Success 200 {object} serializer.Response +// @Router /deploy-files/parent/{parentId} [get] +func (h *SysDeployFileHandler) GetByParentID(c *gin.Context) { + logger.Info(c, "根据项目ID获取文件列表", zap.String("parentId", c.Param("parentId"))) + c.JSON(200, h.deployFileService.GetByParentID(c)) +} + +// SetActiveFile 设置活跃文件 +// @Summary 设置活跃文件 +// @Description 将指定文件设置为使用中状态,其他文件设为未使用状态 +// @Tags 部署文件管理 +// @Accept json +// @Produce json +// @Param id path string true "文件ID" +// @Success 200 {object} serializer.Response +// @Router /deploy-files/{id}/active [put] +func (h *SysDeployFileHandler) SetActiveFile(c *gin.Context) { + logger.Info(c, "设置活跃文件", zap.String("id", c.Param("id"))) + c.JSON(200, h.deployFileService.SetActiveFile(c)) +} diff --git a/internal/handler/sys_deploy_project_handler.go b/internal/handler/sys_deploy_project_handler.go new file mode 100644 index 0000000..12992e5 --- /dev/null +++ b/internal/handler/sys_deploy_project_handler.go @@ -0,0 +1,92 @@ +package handler + +import ( + "ego/internal/service" + "ego/pkg/logger" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// SysDeployProjectHandler 部署项目处理器 +type SysDeployProjectHandler struct { + deployProjectService *service.SysDeployProjectService +} + +// NewSysDeployProjectHandler 构建部署项目处理器 +func NewSysDeployProjectHandler(deployProjectService *service.SysDeployProjectService) *SysDeployProjectHandler { + return &SysDeployProjectHandler{ + deployProjectService: deployProjectService, + } +} + +// Create 创建部署项目记录 +// @Summary 创建部署项目记录 +// @Description 创建新的部署项目记录 +// @Tags 部署项目管理 +// @Accept json +// @Produce json +// @Param deployProject body model.SysDeployProject true "部署项目信息" +// @Success 200 {object} serializer.Response +// @Router /deploy-projects [post] +func (h *SysDeployProjectHandler) Create(c *gin.Context) { + logger.Info(c, "创建部署项目记录") + c.JSON(200, h.deployProjectService.Create(c)) +} + +// GetByID 根据ID获取部署项目记录 +// @Summary 获取部署项目记录 +// @Description 根据ID获取部署项目记录详情 +// @Tags 部署项目管理 +// @Accept json +// @Produce json +// @Param id path string true "部署ID" +// @Success 200 {object} serializer.Response +// @Router /deploy-projects/{id} [get] +func (h *SysDeployProjectHandler) GetByID(c *gin.Context) { + logger.Info(c, "获取部署项目记录", zap.String("id", c.Param("id"))) + c.JSON(200, h.deployProjectService.GetByID(c)) +} + +// UpdateByID 根据ID更新部署项目记录 +// @Summary 更新部署项目记录 +// @Description 根据ID更新部署项目记录 +// @Tags 部署项目管理 +// @Accept json +// @Produce json +// @Param id path string true "部署ID" +// @Param deployProject body model.SysDeployProject true "部署项目信息" +// @Success 200 {object} serializer.Response +// @Router /deploy-projects/{id} [put] +func (h *SysDeployProjectHandler) UpdateByID(c *gin.Context) { + logger.Info(c, "更新部署项目记录", zap.String("id", c.Param("id"))) + c.JSON(200, h.deployProjectService.UpdateByID(c)) +} + +// DeleteByID 根据ID删除部署项目记录 +// @Summary 删除部署项目记录 +// @Description 根据ID删除部署项目记录 +// @Tags 部署项目管理 +// @Accept json +// @Produce json +// @Param id path string true "部署ID" +// @Success 200 {object} serializer.Response +// @Router /deploy-projects/{id} [delete] +func (h *SysDeployProjectHandler) DeleteByID(c *gin.Context) { + logger.Info(c, "删除部署项目记录", zap.String("id", c.Param("id"))) + c.JSON(200, h.deployProjectService.DeleteByID(c)) +} + +// GetByCondition 条件查询部署项目记录 +// @Summary 条件查询部署项目记录 +// @Description 根据条件分页查询部署项目记录 +// @Tags 部署项目管理 +// @Accept json +// @Produce json +// @Param params query types.Params true "查询参数" +// @Success 200 {object} serializer.Response +// @Router /deploy-projects [get] +func (h *SysDeployProjectHandler) GetByCondition(c *gin.Context) { + logger.Info(c, "条件查询部署项目记录") + c.JSON(200, h.deployProjectService.GetByCondition(c)) +} diff --git a/internal/model/sys_deploy_file.go b/internal/model/sys_deploy_file.go index 58440a0..aa3d844 100644 --- a/internal/model/sys_deploy_file.go +++ b/internal/model/sys_deploy_file.go @@ -1,33 +1,23 @@ package model import ( - "mime/multipart" "time" ) // SysDeployFile 部署文件记录表 type SysDeployFile struct { - DeployId string `gorm:"column:deploy_id;type:varchar(64);primary_key;comment:部署ID" json:"deployId"` - FileName string `gorm:"column:file_name;type:varchar(255);not null;comment:原始文件名" json:"fileName"` - ProjectName string `gorm:"column:project_name;type:varchar(100);not null;comment:项目名称" json:"projectName" binding:"required"` - Domain string `gorm:"column:domain;type:varchar(255);not null;comment:访问域名" json:"domain" binding:"required"` - File *multipart.FileHeader `gorm:"-" json:"file" binding:"required"` - DeployPath string `gorm:"column:deploy_path;type:varchar(500);not null;comment:部署路径" json:"deployPath"` - FileSize int64 `gorm:"column:file_size;type:bigint;comment:文件大小(字节)" json:"fileSize"` - FileHash string `gorm:"column:file_hash;type:varchar(64);comment:文件哈希值" json:"fileHash"` - Status string `gorm:"column:status;type:char(1);default:1;comment:状态(0停用 1正常 2部署中 3部署失败)" json:"status"` - DeployStatus string `gorm:"column:deploy_status;type:char(1);default:0;comment:部署状态(0未部署 1部署成功 2部署失败)" json:"deployStatus"` - ErrorMsg string `gorm:"column:error_msg;type:text;comment:错误信息" json:"errorMsg"` - Version string `gorm:"column:version;type:varchar(50);comment:版本号" json:"version"` - Description string `gorm:"column:description;type:varchar(500);comment:描述" json:"description"` - DelFlag string `gorm:"column:del_flag;type:char(1);default:0;comment:删除标志(0代表存在 1代表删除)" json:"delFlag"` - CreateBy string `gorm:"column:create_by;type:varchar(64);comment:创建者" json:"createBy"` - CreateTime *time.Time `gorm:"column:create_time;type:datetime;comment:创建时间" json:"createTime"` - UpdateBy string `gorm:"column:update_by;type:varchar(64);comment:更新者" json:"updateBy"` - UpdateTime *time.Time `gorm:"column:update_time;type:datetime;comment:更新时间" json:"updateTime"` - DeployTime *time.Time `gorm:"column:deploy_time;type:datetime;comment:部署时间" json:"deployTime"` - LastAccessTime *time.Time `gorm:"column:last_access_time;type:datetime;comment:最后访问时间" json:"lastAccessTime"` - AccessCount int64 `gorm:"column:access_count;type:bigint;default:0;comment:访问次数" json:"accessCount"` + FileId string `gorm:"column:file_id;type:varchar(64);primary_key;comment:文件ID" json:"fileId"` + ParentId string `gorm:"column:parent_id;type:varchar(64);not null;comment:项目ID" json:"parentId" binding:"required"` + FileName string `gorm:"column:file_name;type:varchar(255);not null;comment:原始文件名" json:"fileName" binding:"required"` + FileSize int64 `gorm:"column:file_size;type:bigint;comment:文件大小(字节)" json:"fileSize"` + FileHash string `gorm:"column:file_hash;type:varchar(64);comment:文件哈希值" json:"fileHash"` + FilePath string `gorm:"column:file_path;type:varchar(255);comment:文件路径" json:"filePath"` + Status string `gorm:"column:status;type:char(1);default:0;comment:文件状态(0未使用 1使用中)" json:"status"` + DelFlag string `gorm:"column:del_flag;type:char(1);default:0;comment:删除标志(0代表存在 1代表删除)" json:"delFlag"` + CreateBy string `gorm:"column:create_by;type:varchar(64);comment:创建者" json:"createBy"` + CreateTime *time.Time `gorm:"column:create_time;type:datetime;comment:创建时间" json:"createTime"` + UpdateBy string `gorm:"column:update_by;type:varchar(64);comment:更新者" json:"updateBy"` + UpdateTime *time.Time `gorm:"column:update_time;type:datetime;comment:更新时间" json:"updateTime"` } // TableName 表名 @@ -35,17 +25,8 @@ func (m *SysDeployFile) TableName() string { return "sys_deploy_file" } -// DeployFileStatus 部署文件状态常量 +// FileStatus 文件状态常量 const ( - DeployFileStatusDisabled = "0" // 停用 - DeployFileStatusNormal = "1" // 正常 - DeployFileStatusDeploying = "2" // 部署中 - DeployFileStatusFailed = "3" // 部署失败 -) - -// DeployStatus 部署状态常量 -const ( - DeployStatusNotDeployed = "0" // 未部署 - DeployStatusSuccess = "1" // 部署成功 - DeployStatusFailed = "2" // 部署失败 + FileStatusNotUsed = "0" // 未使用 + FileStatusInUse = "1" // 使用中 ) diff --git a/internal/model/sys_deploy_project.go b/internal/model/sys_deploy_project.go new file mode 100644 index 0000000..3a74978 --- /dev/null +++ b/internal/model/sys_deploy_project.go @@ -0,0 +1,44 @@ +package model + +import "time" + +// SysDeployProject 部署项目记录表 +type SysDeployProject struct { + DeployId string `gorm:"column:deploy_id;type:varchar(64);primary_key;comment:部署ID" json:"deployId"` + ProjectName string `gorm:"column:project_name;type:varchar(100);not null;comment:项目名称" json:"projectName" binding:"required"` + Domain string `gorm:"column:domain;type:varchar(255);not null;comment:访问域名" json:"domain" binding:"required"` + DeployPath string `gorm:"column:deploy_path;type:varchar(500);not null;comment:部署路径" json:"deployPath"` + Status string `gorm:"column:status;type:char(1);default:1;comment:状态(0停用 1正常 2部署中 3部署失败)" json:"status"` + DeployStatus string `gorm:"column:deploy_status;type:char(1);default:0;comment:部署状态(0未部署 1部署成功 2部署失败)" json:"deployStatus"` + ErrorMsg string `gorm:"column:error_msg;type:text;comment:错误信息" json:"errorMsg"` + Version string `gorm:"column:version;type:varchar(50);comment:版本号" json:"version"` + Description string `gorm:"column:description;type:varchar(500);comment:描述" json:"description"` + DelFlag string `gorm:"column:del_flag;type:char(1);default:0;comment:删除标志(0代表存在 1代表删除)" json:"delFlag"` + CreateBy string `gorm:"column:create_by;type:varchar(64);comment:创建者" json:"createBy"` + CreateTime *time.Time `gorm:"column:create_time;type:datetime;comment:创建时间" json:"createTime"` + UpdateBy string `gorm:"column:update_by;type:varchar(64);comment:更新者" json:"updateBy"` + UpdateTime *time.Time `gorm:"column:update_time;type:datetime;comment:更新时间" json:"updateTime"` + DeployTime *time.Time `gorm:"column:deploy_time;type:datetime;comment:部署时间" json:"deployTime"` + LastAccessTime *time.Time `gorm:"column:last_access_time;type:datetime;comment:最后访问时间" json:"lastAccessTime"` + AccessCount int64 `gorm:"column:access_count;type:bigint;default:0;comment:访问次数" json:"accessCount"` +} + +// TableName 表名 +func (m *SysDeployProject) TableName() string { + return "sys_deploy_project" +} + +// DeployProjectStatus 部署项目状态常量 +const ( + DeployProjectStatusDisabled = "0" // 停用 + DeployProjectStatusNormal = "1" // 正常 + DeployProjectStatusDeploying = "2" // 部署中 + DeployProjectStatusFailed = "3" // 部署失败 +) + +// DeployStatus 部署状态常量 +const ( + DeployStatusNotDeployed = "0" // 未部署 + DeployStatusSuccess = "1" // 部署成功 + DeployStatusFailed = "2" // 部署失败 +) diff --git a/internal/router/sys_deploy_file_router.go b/internal/router/sys_deploy_file_router.go index 76d2b35..610474f 100644 --- a/internal/router/sys_deploy_file_router.go +++ b/internal/router/sys_deploy_file_router.go @@ -31,4 +31,6 @@ func SysDeployFileHandlerRouter(group *gin.RouterGroup, h *handler.SysDeployFile g.PUT("/", h.UpdateByID) g.DELETE("/:id", h.DeleteByID) g.GET("", h.GetByCondition) + g.GET("/parent/:parentId", h.GetByParentID) + g.PUT("/:id/active", h.SetActiveFile) } diff --git a/internal/router/sys_deploy_project_router.go b/internal/router/sys_deploy_project_router.go new file mode 100644 index 0000000..49d450f --- /dev/null +++ b/internal/router/sys_deploy_project_router.go @@ -0,0 +1,34 @@ +package router + +import ( + "ego/internal/handler" + "ego/internal/middleware" + "ego/internal/wire" + + "github.com/gin-gonic/gin" +) + +func init() { + apiRouterFns = append(apiRouterFns, func(group *gin.RouterGroup) { + SysDeployProjectHandlerRouter(group, wire.InjectSysDeployProjectHandler()) + }) +} + +// SysDeployProjectHandlerRouter 部署项目相关路由 +// @Summary 部署项目管理路由 +// @Description 包含部署项目的增删改查等接口 +// @Tags 部署项目管理 +// @Accept json +// @Produce json +func SysDeployProjectHandlerRouter(group *gin.RouterGroup, h *handler.SysDeployProjectHandler) { + // 部署项目管理路由组 + g := group.Group("/DeployProjects") + // 鉴权 + g.Use(middleware.AuthRequired()) + + g.POST("/", h.Create) + g.GET("/:id", h.GetByID) + g.PUT("/", h.UpdateByID) + g.DELETE("/:id", h.DeleteByID) + g.GET("", h.GetByCondition) +} diff --git a/internal/service/sys_deploy_file_service.go b/internal/service/sys_deploy_file_service.go index b1c1ef7..2f01719 100644 --- a/internal/service/sys_deploy_file_service.go +++ b/internal/service/sys_deploy_file_service.go @@ -10,6 +10,7 @@ import ( "io" "os" "path/filepath" + "strings" "time" "github.com/gin-gonic/gin" @@ -30,37 +31,69 @@ func NewSysDeployFileService(db *gorm.DB) *SysDeployFileService { // Create 创建部署文件记录 func (s *SysDeployFileService) Create(c *gin.Context) serializer.Response { + // 获取上传的文件 + file, err := c.FormFile("file") + if err != nil { + logger.Error(c, "获取上传文件失败!") + return serializer.ParamErr("获取上传文件失败!", err) + } + + // 校验文件类型 + if !strings.HasSuffix(strings.ToLower(file.Filename), ".zip") { + logger.Error(c, "只支持zip格式文件!") + return serializer.ParamErr("只支持zip格式文件!", nil) + } + + // 获取文件名(不包含扩展名) + filename := strings.TrimSuffix(file.Filename, filepath.Ext(file.Filename)) var deployFile model.SysDeployFile - if err := c.ShouldBind(&deployFile); err != nil { - logger.Error(c, "参数绑定失败!") - return serializer.ParamErr("参数绑定失败!", err) - } + deployFile.FileName = filename - // 验证必填字段 - if deployFile.Domain == "" { - logger.Error(c, "域名不能为空!") - return serializer.ParamErr("域名不能为空!", fmt.Errorf("域名不能为空")) - } - - // 检查域名是否已存在 - var existingDeployFile model.SysDeployFile - if err := s.Db.Where("domain = ? AND del_flag = ?", deployFile.Domain, "0").First(&existingDeployFile).Error; err == nil { - logger.Error(c, "域名已存在!") - return serializer.ParamErr("域名已存在,请使用其他域名!", fmt.Errorf("域名已存在")) - } - - // 生成部署ID + // 生成文件ID if id, err := SysSequenceServiceBuilder(deployFile.TableName()).GenerateId(); err == nil { - deployFile.DeployId = id + deployFile.FileId = id } else { return serializer.DBErr("序列生成失败!", err) } + // 生成新的文件名:使用文件ID + .zip 后缀 + newFileName := deployFile.FileId + ".zip" + // 设置文件保存路径 + deployFile.FilePath = filepath.Join("/data", newFileName) + + // 保存文件到指定路径 + if err := c.SaveUploadedFile(file, deployFile.FilePath); err != nil { + logger.Error(c, "保存文件失败!") + return serializer.ParamErr("保存文件失败!", err) + } + + // 打开保存的文件以计算哈希值 + savedFile, err := os.Open(deployFile.FilePath) + if err != nil { + logger.Error(c, "打开保存的文件失败!") + return serializer.ParamErr("打开保存的文件失败!", err) + } + defer savedFile.Close() + + // 计算文件哈希值 + deployFile.FileHash, err = s.CalculateFileHash(savedFile) + if err != nil { + logger.Error(c, "计算文件哈希值失败!") + return serializer.ParamErr("计算文件哈希值失败!", err) + } + + // 获取文件大小(从保存的文件获取更准确) + fileInfo, err := savedFile.Stat() + if err != nil { + logger.Error(c, "获取文件信息失败!") + return serializer.ParamErr("获取文件信息失败!", err) + } + deployFile.FileSize = fileInfo.Size() + // 设置默认值 now := time.Now() deployFile.CreateTime = &now - deployFile.Status = model.DeployFileStatusNormal - deployFile.DeployStatus = model.DeployStatusNotDeployed + deployFile.Status = model.FileStatusNotUsed deployFile.DelFlag = "0" // 获取当前用户 @@ -85,7 +118,7 @@ func (s *SysDeployFileService) GetByID(c *gin.Context) serializer.Response { } var deployFile model.SysDeployFile - if err := s.Db.Where("deploy_id = ? AND del_flag = ? AND create_by = ?", c.Param("id"), "0", currentUserId).First(&deployFile).Error; err != nil { + if err := s.Db.Where("file_id = ? AND del_flag = ? AND create_by = ?", c.Param("id"), "0", currentUserId).First(&deployFile).Error; err != nil { logger.Error(c, "获取部署文件记录失败!") return serializer.DBErr("获取部署文件记录失败!", err) } @@ -100,12 +133,17 @@ func (s *SysDeployFileService) UpdateByID(c *gin.Context) serializer.Response { return serializer.ParamErr("参数绑定失败!", err) } - id := deployFile.DeployId + id := deployFile.FileId if id == "" { logger.Error(c, "id 不可为空!") return serializer.ParamErr("id不可为空!", fmt.Errorf("id不可为空")) } + if deployFile.ParentId == "" { + logger.Error(c, "项目ID不能为空!") + return serializer.ParamErr("项目ID不能为空!", fmt.Errorf("项目ID不能为空")) + } + // 获取当前用户ID currentUserId := c.GetString("id") if currentUserId == "" { @@ -114,7 +152,7 @@ func (s *SysDeployFileService) UpdateByID(c *gin.Context) serializer.Response { // 检查权限:只能更新自己创建的数据 var existingDeployFile model.SysDeployFile - if err := s.Db.Where("deploy_id = ? AND del_flag = ? AND create_by = ?", id, "0", currentUserId).First(&existingDeployFile).Error; err != nil { + if err := s.Db.Where("file_id = ? AND del_flag = ? AND create_by = ?", id, "0", currentUserId).First(&existingDeployFile).Error; err != nil { logger.Error(c, "部署文件记录不存在或无权限访问!") return serializer.ParamErr("部署文件记录不存在或无权限访问!", err) } @@ -126,7 +164,7 @@ func (s *SysDeployFileService) UpdateByID(c *gin.Context) serializer.Response { // 获取当前用户 deployFile.UpdateBy = currentUserId - if err := s.Db.Model(&deployFile).Where("deploy_id = ? AND del_flag = ? AND create_by = ?", id, "0", currentUserId).Updates(&deployFile).Error; err != nil { + if err := s.Db.Model(&deployFile).Where("file_id = ? AND del_flag = ? AND create_by = ?", id, "0", currentUserId).Updates(&deployFile).Error; err != nil { logger.Error(c, "更新部署文件记录失败!") return serializer.DBErr("更新部署文件记录失败!", err) } @@ -154,22 +192,8 @@ func (s *SysDeployFileService) DeleteByID(c *gin.Context) serializer.Response { "update_by": currentUserId, } - // 删除已经部署的文件夹 - deployFile := model.SysDeployFile{} - if err := s.Db.Where("deploy_id = ? AND create_by = ?", id, currentUserId).First(&deployFile).Error; err != nil { - logger.Error(c, "获取部署文件记录失败或无权限访问!") - return serializer.DBErr("获取部署文件记录失败或无权限访问!", err) - } - - // 删除 /home/:projectName - err := os.RemoveAll(filepath.Join("/home", deployFile.Domain)) - if err != nil { - logger.Error(c, "删除部署文件夹失败!") - return serializer.DBErr("删除部署文件夹失败!", err) - } - // 删除数据库记录 - if err := s.Db.Model(&model.SysDeployFile{}).Where("deploy_id = ? AND create_by = ?", id, currentUserId).Updates(data).Error; err != nil { + if err := s.Db.Model(&model.SysDeployFile{}).Where("file_id = ? AND create_by = ?", id, currentUserId).Updates(data).Error; err != nil { logger.Error(c, "删除部署文件记录失败!") return serializer.DBErr("删除部署文件记录失败!", err) } @@ -238,41 +262,89 @@ func (s *SysDeployFileService) GetByCondition(c *gin.Context) serializer.Respons }) } -// UpdateDeployStatus 更新部署状态 -func (s *SysDeployFileService) UpdateDeployStatus(deployId, status, errorMsg string) error { - data := map[string]any{ - "deploy_status": status, - "update_time": time.Now(), +// GetByParentID 根据项目ID获取文件列表 +func (s *SysDeployFileService) GetByParentID(c *gin.Context) serializer.Response { + parentId := c.Param("parentId") + if parentId == "" { + logger.Error(c, "项目ID不可为空!") + return serializer.ParamErr("项目ID不可为空!", fmt.Errorf("项目ID不可为空")) } - if status == model.DeployStatusSuccess { - now := time.Now() - data["deploy_time"] = &now + // 获取当前用户ID + currentUserId := c.GetString("id") + if currentUserId == "" { + return serializer.ParamErr("用户信息获取失败!", nil) } - if errorMsg != "" { - data["error_msg"] = errorMsg + var deployFiles []model.SysDeployFile + if err := s.Db.Where("parent_id = ? AND del_flag = ? AND create_by = ?", parentId, "0", currentUserId). + Order("create_time DESC").Find(&deployFiles).Error; err != nil { + logger.Error(c, "获取文件列表失败!") + return serializer.DBErr("获取文件列表失败!", err) } - return s.Db.Model(&model.SysDeployFile{}).Where("deploy_id = ?", deployId).Updates(data).Error + return serializer.Succ("查询成功!", deployFiles) } -// UpdateAccessInfo 更新访问信息 -func (s *SysDeployFileService) UpdateAccessInfo(deployId string) error { - now := time.Now() - return s.Db.Model(&model.SysDeployFile{}). - Where("deploy_id = ?", deployId). - Updates(map[string]any{ - "last_access_time": &now, - "access_count": gorm.Expr("access_count + 1"), - }).Error -} +// SetActiveFile 设置活跃文件 +func (s *SysDeployFileService) SetActiveFile(c *gin.Context) serializer.Response { + fileId := c.Param("id") + if fileId == "" { + logger.Error(c, "文件ID不可为空!") + return serializer.ParamErr("文件ID不可为空!", fmt.Errorf("文件ID不可为空")) + } -// GetByDomain 根据域名获取部署文件记录 -func (s *SysDeployFileService) GetByDomain(domain string) (*model.SysDeployFile, error) { + // 获取当前用户ID + currentUserId := c.GetString("id") + if currentUserId == "" { + return serializer.ParamErr("用户信息获取失败!", nil) + } + + // 获取文件信息 var deployFile model.SysDeployFile - err := s.Db.Where("domain = ? AND status = ? AND del_flag = ?", - domain, model.DeployFileStatusNormal, "0").First(&deployFile).Error + if err := s.Db.Where("file_id = ? AND del_flag = ? AND create_by = ?", fileId, "0", currentUserId).First(&deployFile).Error; err != nil { + logger.Error(c, "文件不存在或无权限访问!") + return serializer.ParamErr("文件不存在或无权限访问!", err) + } + + // 开始事务 + tx := s.Db.Begin() + + // 将同项目下的其他文件设为未使用状态 + if err := tx.Model(&model.SysDeployFile{}). + Where("parent_id = ? AND del_flag = ? AND create_by = ?", deployFile.ParentId, "0", currentUserId). + Updates(map[string]any{ + "status": model.FileStatusNotUsed, + "update_time": time.Now(), + "update_by": currentUserId, + }).Error; err != nil { + tx.Rollback() + logger.Error(c, "更新文件状态失败!") + return serializer.DBErr("更新文件状态失败!", err) + } + + // 将当前文件设为使用中状态 + if err := tx.Model(&model.SysDeployFile{}). + Where("file_id = ?", fileId). + Updates(map[string]any{ + "status": model.FileStatusInUse, + "update_time": time.Now(), + "update_by": currentUserId, + }).Error; err != nil { + tx.Rollback() + logger.Error(c, "设置活跃文件失败!") + return serializer.DBErr("设置活跃文件失败!", err) + } + + tx.Commit() + return serializer.Succ("设置活跃文件成功!", nil) +} + +// GetActiveFileByParentID 根据项目ID获取当前使用中的文件 +func (s *SysDeployFileService) GetActiveFileByParentID(parentId string) (*model.SysDeployFile, error) { + var deployFile model.SysDeployFile + err := s.Db.Where("parent_id = ? AND status = ? AND del_flag = ?", + parentId, model.FileStatusInUse, "0").First(&deployFile).Error if err != nil { return nil, err } diff --git a/internal/service/sys_deploy_project_service.go b/internal/service/sys_deploy_project_service.go new file mode 100644 index 0000000..e2fe121 --- /dev/null +++ b/internal/service/sys_deploy_project_service.go @@ -0,0 +1,289 @@ +package service + +import ( + "ego/internal/model" + "ego/internal/serializer" + "ego/internal/types" + "ego/pkg/logger" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// SysDeployProjectService 部署项目服务 +type SysDeployProjectService struct { + Db *gorm.DB +} + +// NewSysDeployProjectService 构建部署项目服务 +func NewSysDeployProjectService(db *gorm.DB) *SysDeployProjectService { + return &SysDeployProjectService{ + Db: db, + } +} + +// Create 创建部署项目记录 +func (s *SysDeployProjectService) Create(c *gin.Context) serializer.Response { + var deployProject model.SysDeployProject + if err := c.ShouldBind(&deployProject); err != nil { + logger.Error(c, "参数绑定失败!") + return serializer.ParamErr("参数绑定失败!", err) + } + + // 验证必填字段 + if deployProject.Domain == "" { + logger.Error(c, "域名不能为空!") + return serializer.ParamErr("域名不能为空!", fmt.Errorf("域名不能为空")) + } + + // 检查域名是否已存在 + var existingDeployProject model.SysDeployProject + if err := s.Db.Where("domain = ? AND del_flag = ?", deployProject.Domain, "0").First(&existingDeployProject).Error; err == nil { + logger.Error(c, "域名已存在!") + return serializer.ParamErr("域名已存在,请使用其他域名!", fmt.Errorf("域名已存在")) + } + + // 生成部署ID + if id, err := SysSequenceServiceBuilder(deployProject.TableName()).GenerateId(); err == nil { + deployProject.DeployId = id + } else { + return serializer.DBErr("序列生成失败!", err) + } + + // 设置默认值 + now := time.Now() + deployProject.CreateTime = &now + deployProject.Status = model.DeployProjectStatusNormal + deployProject.DeployStatus = model.DeployStatusNotDeployed + deployProject.DelFlag = "0" + + // 获取当前用户 + if createBy := c.GetString("id"); createBy != "" { + deployProject.CreateBy = createBy + } + + if err := s.Db.Create(&deployProject).Error; err != nil { + logger.Error(c, "创建部署项目记录失败!") + return serializer.DBErr("创建部署项目记录失败!", err) + } + + return serializer.Succ("创建部署项目记录成功!", deployProject) +} + +// GetByID 根据ID获取部署项目记录 +func (s *SysDeployProjectService) GetByID(c *gin.Context) serializer.Response { + // 获取当前用户ID + currentUserId := c.GetString("id") + if currentUserId == "" { + return serializer.ParamErr("用户信息获取失败!", nil) + } + + var deployProject model.SysDeployProject + if err := s.Db.Where("deploy_id = ? AND del_flag = ? AND create_by = ?", c.Param("id"), "0", currentUserId).First(&deployProject).Error; err != nil { + logger.Error(c, "获取部署项目记录失败!") + return serializer.DBErr("获取部署项目记录失败!", err) + } + return serializer.Succ("查询成功!", deployProject) +} + +// UpdateByID 根据ID更新部署项目记录 +func (s *SysDeployProjectService) UpdateByID(c *gin.Context) serializer.Response { + var deployProject model.SysDeployProject + if err := c.ShouldBind(&deployProject); err != nil { + logger.Error(c, "参数绑定失败!") + return serializer.ParamErr("参数绑定失败!", err) + } + + id := deployProject.DeployId + if id == "" { + logger.Error(c, "id 不可为空!") + return serializer.ParamErr("id不可为空!", fmt.Errorf("id不可为空")) + } + + // 获取当前用户ID + currentUserId := c.GetString("id") + if currentUserId == "" { + return serializer.ParamErr("用户信息获取失败!", nil) + } + + // 检查权限:只能更新自己创建的数据 + var existingDeployProject model.SysDeployProject + if err := s.Db.Where("deploy_id = ? AND del_flag = ? AND create_by = ?", id, "0", currentUserId).First(&existingDeployProject).Error; err != nil { + logger.Error(c, "部署项目记录不存在或无权限访问!") + return serializer.ParamErr("部署项目记录不存在或无权限访问!", err) + } + + // 设置更新时间 + now := time.Now() + deployProject.UpdateTime = &now + + // 获取当前用户 + deployProject.UpdateBy = currentUserId + + if err := s.Db.Model(&deployProject).Where("deploy_id = ? AND del_flag = ? AND create_by = ?", id, "0", currentUserId).Updates(&deployProject).Error; err != nil { + logger.Error(c, "更新部署项目记录失败!") + return serializer.DBErr("更新部署项目记录失败!", err) + } + return serializer.Succ("更新部署项目记录成功!", deployProject) +} + +// DeleteByID 根据ID删除部署项目记录 +func (s *SysDeployProjectService) DeleteByID(c *gin.Context) serializer.Response { + id := c.Param("id") + if id == "" { + logger.Error(c, "id 不可为空!") + return serializer.ParamErr("id不可为空!", fmt.Errorf("id不可为空")) + } + + // 获取当前用户ID + currentUserId := c.GetString("id") + if currentUserId == "" { + return serializer.ParamErr("用户信息获取失败!", nil) + } + + // 软删除 + data := map[string]any{ + "del_flag": "1", + "update_time": time.Now(), + "update_by": currentUserId, + } + + // 删除已经部署的文件夹 + deployProject := model.SysDeployProject{} + if err := s.Db.Where("deploy_id = ? AND create_by = ?", id, currentUserId).First(&deployProject).Error; err != nil { + logger.Error(c, "获取部署项目记录失败或无权限访问!") + return serializer.DBErr("获取部署项目记录失败或无权限访问!", err) + } + + // 删除 /home/:projectName + err := os.RemoveAll(filepath.Join("/home", deployProject.Domain)) + if err != nil { + logger.Error(c, "删除部署文件夹失败!") + return serializer.DBErr("删除部署文件夹失败!", err) + } + + // 同时软删除关联的文件记录 + fileData := map[string]any{ + "del_flag": "1", + "update_time": time.Now(), + "update_by": currentUserId, + } + if err := s.Db.Model(&model.SysDeployFile{}).Where("parent_id = ?", id).Updates(fileData).Error; err != nil { + logger.Error(c, "删除关联文件记录失败!") + return serializer.DBErr("删除关联文件记录失败!", err) + } + + // 删除数据库记录 + if err := s.Db.Model(&model.SysDeployProject{}).Where("deploy_id = ? AND create_by = ?", id, currentUserId).Updates(data).Error; err != nil { + logger.Error(c, "删除部署项目记录失败!") + return serializer.DBErr("删除部署项目记录失败!", err) + } + + return serializer.Succ("删除部署项目记录成功!", nil) +} + +// GetByCondition 条件查询部署项目记录 +func (s *SysDeployProjectService) GetByCondition(c *gin.Context) serializer.Response { + var p types.Params + if err := c.ShouldBind(&p); err != nil { + return serializer.ParamErr("参数绑定失败!", err) + } + + queryStr, args, err := p.ConvertToGormConditions() + if err != nil { + logger.Error(c, "参数绑定失败!") + return serializer.ParamErr("参数绑定失败!", err) + } + + var total int64 + var deployProjects []model.SysDeployProject + offset := (p.Page - 1) * p.Limit + + // 获取当前用户ID + currentUserId := c.GetString("id") + if currentUserId == "" { + return serializer.ParamErr("用户信息获取失败!", nil) + } + + // 构建基础查询 + db := s.Db.Model(&model.SysDeployProject{}) + + // 如果有查询条件,添加条件 + if queryStr != "" { + db = db.Where(queryStr, args...) + } + + // 添加用户权限过滤:只能查询自己创建的数据 + db = db.Where("create_by = ?", currentUserId) + + // 排序 + if p.Sort != "" { + db = db.Order(p.Sort) + } else { + db = db.Order("create_time DESC") + } + + // 执行分页查询 + if err := db.Where("del_flag = ?", "0").Offset(offset).Limit(p.Limit).Find(&deployProjects).Error; err != nil { + logger.Error(c, "获取部署项目记录失败!") + return serializer.DBErr("获取部署项目记录失败!", err) + } + + // 执行总数查询 + if err := db.Where("del_flag = ?", "0").Count(&total).Error; err != nil { + logger.Error(c, "获取部署项目记录总数失败!") + return serializer.DBErr("获取部署项目记录总数失败!", err) + } + + return serializer.Succ("查询成功!", gin.H{ + "total": total, + "items": deployProjects, + "page": p.Page, + "limit": p.Limit, + }) +} + +// UpdateDeployStatus 更新部署状态 +func (s *SysDeployProjectService) UpdateDeployStatus(deployId, status, errorMsg string) error { + data := map[string]any{ + "deploy_status": status, + "update_time": time.Now(), + } + + if status == model.DeployStatusSuccess { + now := time.Now() + data["deploy_time"] = &now + } + + if errorMsg != "" { + data["error_msg"] = errorMsg + } + + return s.Db.Model(&model.SysDeployProject{}).Where("deploy_id = ?", deployId).Updates(data).Error +} + +// UpdateAccessInfo 更新访问信息 +func (s *SysDeployProjectService) UpdateAccessInfo(deployId string) error { + now := time.Now() + return s.Db.Model(&model.SysDeployProject{}). + Where("deploy_id = ?", deployId). + Updates(map[string]any{ + "last_access_time": &now, + "access_count": gorm.Expr("access_count + 1"), + }).Error +} + +// GetByDomain 根据域名获取部署项目记录 +func (s *SysDeployProjectService) GetByDomain(domain string) (*model.SysDeployProject, error) { + var deployProject model.SysDeployProject + err := s.Db.Where("domain = ? AND status = ? AND del_flag = ?", + domain, model.DeployProjectStatusNormal, "0").First(&deployProject).Error + if err != nil { + return nil, err + } + return &deployProject, nil +} diff --git a/internal/service/sys_upload_service.go b/internal/service/sys_upload_service.go index 5c79845..50bcaac 100644 --- a/internal/service/sys_upload_service.go +++ b/internal/service/sys_upload_service.go @@ -107,24 +107,66 @@ func (s *SysUploadService) UploadZip(c *gin.Context) serializer.Response { // 返回域名格式的结果 domain := fmt.Sprintf("%s.unbug.cn", filename) - // 创建部署文件记录 - deployFile := model.SysDeployFile{ - FileName: file.Filename, - ProjectName: filename, - Domain: domain, - DeployPath: targetDir, - FileSize: file.Size, - Status: model.DeployFileStatusNormal, - DeployStatus: model.DeployStatusSuccess, - Description: fmt.Sprintf("自动部署项目: %s", filename), + // 先创建或获取项目记录 + var deployProject model.SysDeployProject + currentUserId := c.GetString("id") + + // 检查是否已存在相同域名的项目 + err = s.Db.Where("domain = ? AND del_flag = ?", domain, "0").First(&deployProject).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + // 创建新项目 + deployProject = model.SysDeployProject{ + ProjectName: filename, + Domain: domain, + DeployPath: targetDir, + Status: model.DeployProjectStatusNormal, + DeployStatus: model.DeployStatusSuccess, + Description: fmt.Sprintf("自动部署项目: %s", filename), + DelFlag: "0", + CreateBy: currentUserId, + } + + // 生成项目ID + if id, err := SysSequenceServiceBuilder(deployProject.TableName()).GenerateId(); err == nil { + deployProject.DeployId = id + } else { + logger.Error(c, "生成项目ID失败!", zap.Error(err)) + return serializer.DBErr("生成项目ID失败!", err) + } + + // 设置时间 + now := time.Now() + deployProject.CreateTime = &now + deployProject.DeployTime = &now + + // 保存项目到数据库 + if err := s.Db.Create(&deployProject).Error; err != nil { + logger.Error(c, "保存项目记录失败!", zap.Error(err)) + return serializer.DBErr("保存项目记录失败!", err) + } + } else { + logger.Error(c, "查询项目记录失败!", zap.Error(err)) + return serializer.DBErr("查询项目记录失败!", err) + } } - // 生成部署ID + // 创建文件记录 + deployFile := model.SysDeployFile{ + ParentId: deployProject.DeployId, + FileName: file.Filename, + FileSize: file.Size, + Status: model.FileStatusInUse, // 新上传的文件设为使用中 + DelFlag: "0", + CreateBy: currentUserId, + } + + // 生成文件ID if id, err := SysSequenceServiceBuilder(deployFile.TableName()).GenerateId(); err == nil { - deployFile.DeployId = id + deployFile.FileId = id } else { - logger.Error(c, "生成部署ID失败!", zap.Error(err)) - return serializer.DBErr("生成部署ID失败!", err) + logger.Error(c, "生成文件ID失败!", zap.Error(err)) + return serializer.DBErr("生成文件ID失败!", err) } // 计算文件哈希值 @@ -135,24 +177,23 @@ func (s *SysUploadService) UploadZip(c *gin.Context) serializer.Response { // 设置时间 now := time.Now() deployFile.CreateTime = &now - deployFile.DeployTime = &now - deployFile.DelFlag = "0" - // 获取当前用户 - if createBy := c.GetString("id"); createBy != "" { - deployFile.CreateBy = createBy - } + // 将同项目下的其他文件设为未使用状态 + s.Db.Model(&model.SysDeployFile{}). + Where("parent_id = ? AND del_flag = ?", deployProject.DeployId, "0"). + Update("status", model.FileStatusNotUsed) - // 保存到数据库 + // 保存文件记录到数据库 if err := s.Db.Create(&deployFile).Error; err != nil { - logger.Error(c, "保存部署文件记录失败!", zap.Error(err)) + logger.Error(c, "保存文件记录失败!", zap.Error(err)) // 即使保存记录失败,也不影响文件部署 } return serializer.Succ("上传成功!", map[string]interface{}{ - "domain": domain, - "path": targetDir, - "deployId": deployFile.DeployId, + "domain": domain, + "path": targetDir, + "projectId": deployProject.DeployId, + "fileId": deployFile.FileId, }) } diff --git a/internal/wire/wire.go b/internal/wire/wire.go index baeff72..02224b4 100644 --- a/internal/wire/wire.go +++ b/internal/wire/wire.go @@ -23,6 +23,7 @@ var HandlerSet = wire.NewSet( handler.NewSysLoginLogHandler, handler.NewSysUploadHandler, handler.NewSysDeployFileHandler, + handler.NewSysDeployProjectHandler, ) // UserServiceSet 定义 service 层依赖 @@ -45,6 +46,11 @@ var DeployFileServiceSet = wire.NewSet( service.NewSysDeployFileService, ) +// DeployProjectServiceSet 部署项目服务层依赖 +var DeployProjectServiceSet = wire.NewSet( + service.NewSysDeployProjectService, +) + // InjectSysUserHandler 注入 handler func InjectSysUserHandler() *handler.SysUserHandler { panic(wire.Build( @@ -68,3 +74,8 @@ func InjectSysUploadHandler() *handler.SysUploadHandler { func InjectSysDeployFileHandler() *handler.SysDeployFileHandler { panic(wire.Build(HandlerSet, DeployFileServiceSet, DBSet)) } + +// InjectSysDeployProjectHandler 注入部署项目处理器 +func InjectSysDeployProjectHandler() *handler.SysDeployProjectHandler { + panic(wire.Build(HandlerSet, DeployProjectServiceSet, DBSet)) +} diff --git a/sql/deploy_file.sql b/sql/deploy_file.sql index d7160c3..70c2b90 100644 --- a/sql/deploy_file.sql +++ b/sql/deploy_file.sql @@ -1,12 +1,9 @@ --- 部署文件记录表 -CREATE TABLE `sys_deploy_file` ( +-- 部署项目记录表 +CREATE TABLE `sys_deploy_project` ( `deploy_id` varchar(64) NOT NULL COMMENT '部署ID', - `file_name` varchar(255) NOT NULL COMMENT '原始文件名', `project_name` varchar(100) NOT NULL COMMENT '项目名称', `domain` varchar(255) NOT NULL COMMENT '访问域名', `deploy_path` varchar(500) NOT NULL COMMENT '部署路径', - `file_size` bigint DEFAULT NULL COMMENT '文件大小(字节)', - `file_hash` varchar(64) DEFAULT NULL COMMENT '文件哈希值', `status` char(1) DEFAULT '1' COMMENT '状态(0停用 1正常 2部署中 3部署失败)', `deploy_status` char(1) DEFAULT '0' COMMENT '部署状态(0未部署 1部署成功 2部署失败)', `error_msg` text COMMENT '错误信息', @@ -27,31 +24,81 @@ CREATE TABLE `sys_deploy_file` ( KEY `idx_deploy_status` (`deploy_status`), KEY `idx_create_time` (`create_time`), KEY `idx_del_flag` (`del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='部署项目记录表'; + +-- 部署文件记录表 +CREATE TABLE `sys_deploy_file` ( + `file_id` varchar(64) NOT NULL COMMENT '文件ID', + `parent_id` varchar(64) NOT NULL COMMENT '项目ID', + `file_name` varchar(255) NOT NULL COMMENT '原始文件名', + `file_size` bigint DEFAULT NULL COMMENT '文件大小(字节)', + `file_hash` varchar(64) DEFAULT NULL COMMENT '文件哈希值', + `status` char(1) DEFAULT '0' COMMENT '文件状态(0未使用 1使用中)', + `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)', + `create_by` varchar(64) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`file_id`), + KEY `idx_parent_id` (`parent_id`), + KEY `idx_file_name` (`file_name`), + KEY `idx_status` (`status`), + KEY `idx_create_time` (`create_time`), + KEY `idx_del_flag` (`del_flag`), + CONSTRAINT `fk_deploy_file_parent` FOREIGN KEY (`parent_id`) REFERENCES `sys_deploy_project` (`deploy_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='部署文件记录表'; -- 插入示例数据 -INSERT INTO `sys_deploy_file` ( +INSERT INTO `sys_deploy_project` ( `deploy_id`, - `file_name`, `project_name`, `domain`, `deploy_path`, - `file_size`, `status`, `deploy_status`, `description`, `create_by`, `create_time` ) VALUES ( - 'DEPLOY001', - 'my-project-v1.0.0.zip', + 'PROJECT001', 'my-project', 'my-project.unbug.cn', '/home/my-project', + '1', + '1', + '示例项目', + 'admin', + NOW() +); + +-- 插入示例文件数据 +INSERT INTO `sys_deploy_file` ( + `file_id`, + `parent_id`, + `file_name`, + `file_size`, + `file_hash`, + `status`, + `create_by`, + `create_time` +) VALUES +( + 'FILE001', + 'PROJECT001', + 'my-project-v1.0.0.zip', 1048576, - '1', - '1', - '示例项目部署文件', + 'abc123def456', + '1', + 'admin', + NOW() +), +( + 'FILE002', + 'PROJECT001', + 'my-project-v1.0.1.zip', + 1024000, + 'def456ghi789', + '0', 'admin', NOW() ); \ No newline at end of file