feat(deploy): 新增部署项目管理功能

- 添加部署项目模型和相关服务
- 实现部署项目的增删改查接口
- 新增文件上传和管理功能- 优化数据库迁移和初始化逻辑
This commit is contained in:
zhangtao 2025-08-06 11:38:16 +08:00
parent 288526c5f9
commit d024f0c9e0
13 changed files with 784 additions and 140 deletions

BIN
bin/ego Normal file

Binary file not shown.

View File

@ -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 {

View File

@ -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))
}

View File

@ -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))
}

View File

@ -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" // 使用中
)

View File

@ -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" // 部署失败
)

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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,
})
}

View File

@ -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))
}

View File

@ -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()
);