diff --git a/internal/biz/biz.go b/internal/biz/biz.go index 0f36344..2c6e2bb 100644 --- a/internal/biz/biz.go +++ b/internal/biz/biz.go @@ -26,8 +26,10 @@ var ProviderSet = wire.NewSet( system.NewExportTemplateUsecase, system.NewAutoCodeUsecase, system.NewAutoCodeHistoryUsecase, + system.NewAutoCodePackageUsecase, // Example example.NewFileUploadUsecase, example.NewCustomerUsecase, example.NewAttachmentCategoryUsecase, + example.NewBreakpointContinueUsecase, ) diff --git a/internal/biz/example/breakpoint_continue.go b/internal/biz/example/breakpoint_continue.go new file mode 100644 index 0000000..b03e388 --- /dev/null +++ b/internal/biz/example/breakpoint_continue.go @@ -0,0 +1,56 @@ +package example + +import ( + "context" +) + +// ExaFile 文件实体 +type ExaFile struct { + ID uint + FileName string + FileMd5 string + FilePath string + ChunkTotal int + IsFinish bool + ExaFileChunk []ExaFileChunk +} + +// ExaFileChunk 文件切片实体 +type ExaFileChunk struct { + ID uint + ExaFileID uint + FileChunkNumber int + FileChunkPath string +} + +// BreakpointContinueRepo 断点续传仓储接口 +type BreakpointContinueRepo interface { + FindOrCreateFile(ctx context.Context, fileMd5, fileName string, chunkTotal int) (*ExaFile, error) + CreateFileChunk(ctx context.Context, fileID uint, fileChunkPath string, fileChunkNumber int) error + DeleteFileChunk(ctx context.Context, fileMd5, filePath string) error +} + +// BreakpointContinueUsecase 断点续传用例 +type BreakpointContinueUsecase struct { + repo BreakpointContinueRepo +} + +// NewBreakpointContinueUsecase 创建断点续传用例 +func NewBreakpointContinueUsecase(repo BreakpointContinueRepo) *BreakpointContinueUsecase { + return &BreakpointContinueUsecase{repo: repo} +} + +// FindOrCreateFile 查找或创建文件记录 +func (uc *BreakpointContinueUsecase) FindOrCreateFile(ctx context.Context, fileMd5, fileName string, chunkTotal int) (*ExaFile, error) { + return uc.repo.FindOrCreateFile(ctx, fileMd5, fileName, chunkTotal) +} + +// CreateFileChunk 创建文件切片记录 +func (uc *BreakpointContinueUsecase) CreateFileChunk(ctx context.Context, fileID uint, fileChunkPath string, fileChunkNumber int) error { + return uc.repo.CreateFileChunk(ctx, fileID, fileChunkPath, fileChunkNumber) +} + +// DeleteFileChunk 删除文件切片记录 +func (uc *BreakpointContinueUsecase) DeleteFileChunk(ctx context.Context, fileMd5, filePath string) error { + return uc.repo.DeleteFileChunk(ctx, fileMd5, filePath) +} diff --git a/internal/biz/system/auto_code_package.go b/internal/biz/system/auto_code_package.go new file mode 100644 index 0000000..fa0e158 --- /dev/null +++ b/internal/biz/system/auto_code_package.go @@ -0,0 +1,77 @@ +package system + +import ( + "context" +) + +// SysAutoCodePackage 代码包实体 +type SysAutoCodePackage struct { + ID uint + Desc string + Label string + Template string + PackageName string + Module string +} + +// AutoCodePackageRepo 代码包仓储接口 +type AutoCodePackageRepo interface { + Create(ctx context.Context, pkg *SysAutoCodePackage) error + Delete(ctx context.Context, id uint) error + DeleteByNames(ctx context.Context, names []string) error + All(ctx context.Context) ([]SysAutoCodePackage, error) + First(ctx context.Context, packageName, template string) (*SysAutoCodePackage, error) + FindByPackageName(ctx context.Context, packageName string) (*SysAutoCodePackage, error) + BatchCreate(ctx context.Context, pkgs []SysAutoCodePackage) error + DeleteByIDs(ctx context.Context, ids []uint) error +} + +// AutoCodePackageUsecase 代码包用例 +type AutoCodePackageUsecase struct { + repo AutoCodePackageRepo +} + +// NewAutoCodePackageUsecase 创建代码包用例 +func NewAutoCodePackageUsecase(repo AutoCodePackageRepo) *AutoCodePackageUsecase { + return &AutoCodePackageUsecase{repo: repo} +} + +// Create 创建包 +func (uc *AutoCodePackageUsecase) Create(ctx context.Context, pkg *SysAutoCodePackage) error { + return uc.repo.Create(ctx, pkg) +} + +// Delete 删除包 +func (uc *AutoCodePackageUsecase) Delete(ctx context.Context, id uint) error { + return uc.repo.Delete(ctx, id) +} + +// DeleteByNames 根据名称删除包 +func (uc *AutoCodePackageUsecase) DeleteByNames(ctx context.Context, names []string) error { + return uc.repo.DeleteByNames(ctx, names) +} + +// All 获取所有包 +func (uc *AutoCodePackageUsecase) All(ctx context.Context) ([]SysAutoCodePackage, error) { + return uc.repo.All(ctx) +} + +// First 查询包 +func (uc *AutoCodePackageUsecase) First(ctx context.Context, packageName, template string) (*SysAutoCodePackage, error) { + return uc.repo.First(ctx, packageName, template) +} + +// FindByPackageName 根据包名查询 +func (uc *AutoCodePackageUsecase) FindByPackageName(ctx context.Context, packageName string) (*SysAutoCodePackage, error) { + return uc.repo.FindByPackageName(ctx, packageName) +} + +// BatchCreate 批量创建 +func (uc *AutoCodePackageUsecase) BatchCreate(ctx context.Context, pkgs []SysAutoCodePackage) error { + return uc.repo.BatchCreate(ctx, pkgs) +} + +// DeleteByIDs 根据ID批量删除 +func (uc *AutoCodePackageUsecase) DeleteByIDs(ctx context.Context, ids []uint) error { + return uc.repo.DeleteByIDs(ctx, ids) +} diff --git a/internal/data/data.go b/internal/data/data.go index c71bb6c..7b51d5c 100644 --- a/internal/data/data.go +++ b/internal/data/data.go @@ -40,10 +40,12 @@ var ProviderSet = wire.NewSet( datasystem.NewExportTemplateRepo, datasystem.NewAutoCodeRepo, datasystem.NewAutoCodeHistoryRepo, + datasystem.NewAutoCodePackageRepo, // Example dataexample.NewFileUploadRepo, dataexample.NewCustomerRepo, dataexample.NewAttachmentCategoryRepo, + dataexample.NewBreakpointContinueRepo, ) // Data 数据层包装器 diff --git a/internal/data/example/breakpoint_continue.go b/internal/data/example/breakpoint_continue.go new file mode 100644 index 0000000..e792dbe --- /dev/null +++ b/internal/data/example/breakpoint_continue.go @@ -0,0 +1,101 @@ +package example + +import ( + "context" + "errors" + + "kra/internal/biz/example" + "kra/internal/data/model" + + "gorm.io/gorm" +) + +type breakpointContinueRepo struct { + db *gorm.DB +} + +// NewBreakpointContinueRepo 创建断点续传仓储 +func NewBreakpointContinueRepo(db *gorm.DB) example.BreakpointContinueRepo { + return &breakpointContinueRepo{db: db} +} + +// FindOrCreateFile 查找或创建文件记录 +func (r *breakpointContinueRepo) FindOrCreateFile(ctx context.Context, fileMd5, fileName string, chunkTotal int) (*example.ExaFile, error) { + var file model.ExaFile + cfile := model.ExaFile{ + FileMd5: fileMd5, + FileName: fileName, + ChunkTotal: int64(chunkTotal), + } + + // 检查是否已有完成的文件 + if errors.Is(r.db.WithContext(ctx).Where("file_md5 = ? AND is_finish = ?", fileMd5, true).First(&file).Error, gorm.ErrRecordNotFound) { + // 没有完成的文件,查找或创建 + err := r.db.WithContext(ctx).Where("file_md5 = ? AND file_name = ?", fileMd5, fileName). + FirstOrCreate(&file, cfile).Error + if err != nil { + return nil, err + } + // 加载切片 + var chunks []model.ExaFileChunk + r.db.WithContext(ctx).Where("exa_file_id = ?", file.ID).Find(&chunks) + return toBizExaFileWithChunks(&file, chunks), nil + } + + // 已有完成的文件,创建新记录 + cfile.IsFinish = true + cfile.FilePath = file.FilePath + if err := r.db.WithContext(ctx).Create(&cfile).Error; err != nil { + return nil, err + } + return toBizExaFileWithChunks(&cfile, nil), nil +} + +// CreateFileChunk 创建文件切片记录 +func (r *breakpointContinueRepo) CreateFileChunk(ctx context.Context, fileID uint, fileChunkPath string, fileChunkNumber int) error { + chunk := model.ExaFileChunk{ + ExaFileID: int64(fileID), + FileChunkPath: fileChunkPath, + FileChunkNumber: int64(fileChunkNumber), + } + return r.db.WithContext(ctx).Create(&chunk).Error +} + +// DeleteFileChunk 删除文件切片记录 +func (r *breakpointContinueRepo) DeleteFileChunk(ctx context.Context, fileMd5, filePath string) error { + var file model.ExaFile + err := r.db.WithContext(ctx).Where("file_md5 = ?", fileMd5).First(&file). + Updates(map[string]interface{}{ + "is_finish": true, + "file_path": filePath, + }).Error + if err != nil { + return err + } + + return r.db.WithContext(ctx).Where("exa_file_id = ?", file.ID).Delete(&model.ExaFileChunk{}).Unscoped().Error +} + +// 转换函数 +func toBizExaFileWithChunks(m *model.ExaFile, chunks []model.ExaFileChunk) *example.ExaFile { + file := &example.ExaFile{ + ID: uint(m.ID), + FileName: m.FileName, + FileMd5: m.FileMd5, + FilePath: m.FilePath, + ChunkTotal: int(m.ChunkTotal), + IsFinish: m.IsFinish, + } + if len(chunks) > 0 { + file.ExaFileChunk = make([]example.ExaFileChunk, len(chunks)) + for i, chunk := range chunks { + file.ExaFileChunk[i] = example.ExaFileChunk{ + ID: uint(chunk.ID), + ExaFileID: uint(chunk.ExaFileID), + FileChunkNumber: int(chunk.FileChunkNumber), + FileChunkPath: chunk.FileChunkPath, + } + } + } + return file +} diff --git a/internal/data/system/auto_code_package.go b/internal/data/system/auto_code_package.go new file mode 100644 index 0000000..01cb9d9 --- /dev/null +++ b/internal/data/system/auto_code_package.go @@ -0,0 +1,143 @@ +package system + +import ( + "context" + "errors" + + "kra/internal/biz/system" + + "gorm.io/gorm" +) + +// SysAutoCodePackage 代码包数据模型 +type SysAutoCodePackage struct { + gorm.Model + Desc string `json:"desc" gorm:"comment:描述"` + Label string `json:"label" gorm:"comment:展示名"` + Template string `json:"template" gorm:"comment:模版"` + PackageName string `json:"packageName" gorm:"comment:包名"` + Module string `json:"-" gorm:"comment:模块"` +} + +func (SysAutoCodePackage) TableName() string { + return "sys_auto_code_packages" +} + +type autoCodePackageRepo struct { + db *gorm.DB +} + +// NewAutoCodePackageRepo 创建代码包仓储 +func NewAutoCodePackageRepo(db *gorm.DB) system.AutoCodePackageRepo { + return &autoCodePackageRepo{db: db} +} + +// Create 创建包 +func (r *autoCodePackageRepo) Create(ctx context.Context, pkg *system.SysAutoCodePackage) error { + entity := toDataAutoCodePackage(pkg) + if err := r.db.WithContext(ctx).Create(entity).Error; err != nil { + return err + } + pkg.ID = entity.ID + return nil +} + +// Delete 删除包 +func (r *autoCodePackageRepo) Delete(ctx context.Context, id uint) error { + return r.db.WithContext(ctx).Delete(&SysAutoCodePackage{}, id).Error +} + +// DeleteByNames 根据名称删除包 +func (r *autoCodePackageRepo) DeleteByNames(ctx context.Context, names []string) error { + if len(names) == 0 { + return nil + } + return r.db.WithContext(ctx).Where("package_name IN ?", names).Delete(&SysAutoCodePackage{}).Error +} + +// All 获取所有包 +func (r *autoCodePackageRepo) All(ctx context.Context) ([]system.SysAutoCodePackage, error) { + var entities []SysAutoCodePackage + err := r.db.WithContext(ctx).Find(&entities).Error + if err != nil { + return nil, err + } + return toBizAutoCodePackages(entities), nil +} + +// First 查询包 +func (r *autoCodePackageRepo) First(ctx context.Context, packageName, template string) (*system.SysAutoCodePackage, error) { + var entity SysAutoCodePackage + err := r.db.WithContext(ctx).Where("package_name = ? AND template = ?", packageName, template).First(&entity).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + return toBizAutoCodePackage(&entity), nil +} + +// FindByPackageName 根据包名查询 +func (r *autoCodePackageRepo) FindByPackageName(ctx context.Context, packageName string) (*system.SysAutoCodePackage, error) { + var entity SysAutoCodePackage + err := r.db.WithContext(ctx).Where("package_name = ?", packageName).First(&entity).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + return toBizAutoCodePackage(&entity), nil +} + +// BatchCreate 批量创建 +func (r *autoCodePackageRepo) BatchCreate(ctx context.Context, pkgs []system.SysAutoCodePackage) error { + if len(pkgs) == 0 { + return nil + } + entities := make([]SysAutoCodePackage, len(pkgs)) + for i, pkg := range pkgs { + entities[i] = *toDataAutoCodePackage(&pkg) + } + return r.db.WithContext(ctx).Create(&entities).Error +} + +// DeleteByIDs 根据ID批量删除 +func (r *autoCodePackageRepo) DeleteByIDs(ctx context.Context, ids []uint) error { + if len(ids) == 0 { + return nil + } + return r.db.WithContext(ctx).Delete(&SysAutoCodePackage{}, ids).Error +} + +// 转换函数 +func toBizAutoCodePackage(m *SysAutoCodePackage) *system.SysAutoCodePackage { + return &system.SysAutoCodePackage{ + ID: m.ID, + Desc: m.Desc, + Label: m.Label, + Template: m.Template, + PackageName: m.PackageName, + Module: m.Module, + } +} + +func toBizAutoCodePackages(models []SysAutoCodePackage) []system.SysAutoCodePackage { + result := make([]system.SysAutoCodePackage, len(models)) + for i, m := range models { + result[i] = *toBizAutoCodePackage(&m) + } + return result +} + +func toDataAutoCodePackage(pkg *system.SysAutoCodePackage) *SysAutoCodePackage { + return &SysAutoCodePackage{ + Model: gorm.Model{ID: pkg.ID}, + Desc: pkg.Desc, + Label: pkg.Label, + Template: pkg.Template, + PackageName: pkg.PackageName, + Module: pkg.Module, + } +} diff --git a/internal/server/gin.go b/internal/server/gin.go index 57e07e7..0a6c31d 100644 --- a/internal/server/gin.go +++ b/internal/server/gin.go @@ -41,10 +41,12 @@ func NewGinRouter( exportTemplateUsecase *system.ExportTemplateUsecase, autoCodeUsecase *system.AutoCodeUsecase, autoCodeHistoryUsecase *system.AutoCodeHistoryUsecase, + autoCodePackageUsecase *system.AutoCodePackageUsecase, // Example usecases fileUploadUsecase *example.FileUploadUsecase, customerUsecase *example.CustomerUsecase, attachmentCategoryUsecase *example.AttachmentCategoryUsecase, + breakpointContinueUsecase *example.BreakpointContinueUsecase, ) *gin.Engine { gin.SetMode(gin.DebugMode) @@ -91,6 +93,7 @@ func NewGinRouter( exportTemplateUsecase, autoCodeUsecase, autoCodeHistoryUsecase, + autoCodePackageUsecase, ) handler.SetJWTInstance(jwtPkg) @@ -99,6 +102,7 @@ func NewGinRouter( fileUploadUsecase, customerUsecase, attachmentCategoryUsecase, + breakpointContinueUsecase, ) // 创建路由组 diff --git a/internal/server/handler/example/enter.go b/internal/server/handler/example/enter.go index f5e2536..fca61df 100644 --- a/internal/server/handler/example/enter.go +++ b/internal/server/handler/example/enter.go @@ -14,6 +14,7 @@ var ( fileUploadUsecase *example.FileUploadUsecase customerUsecase *example.CustomerUsecase attachmentCategoryUsecase *example.AttachmentCategoryUsecase + breakpointContinueUsecase *example.BreakpointContinueUsecase ) // InitUsecases 初始化业务层依赖 @@ -21,8 +22,10 @@ func InitUsecases( fileUpload *example.FileUploadUsecase, customer *example.CustomerUsecase, attachmentCategory *example.AttachmentCategoryUsecase, + breakpointContinue *example.BreakpointContinueUsecase, ) { fileUploadUsecase = fileUpload customerUsecase = customer attachmentCategoryUsecase = attachmentCategory + breakpointContinueUsecase = breakpointContinue } diff --git a/internal/server/handler/example/exa_breakpoint_continue.go b/internal/server/handler/example/exa_breakpoint_continue.go new file mode 100644 index 0000000..d12c75c --- /dev/null +++ b/internal/server/handler/example/exa_breakpoint_continue.go @@ -0,0 +1,141 @@ +package example + +import ( + "io" + "strconv" + "strings" + + "kra/internal/biz/example" + "kra/pkg/response" + "kra/pkg/utils" + + "github.com/gin-gonic/gin" +) + +// BreakpointContinue 断点续传到服务器 +func (f *FileUploadApi) BreakpointContinue(c *gin.Context) { + fileMd5 := c.Request.FormValue("fileMd5") + fileName := c.Request.FormValue("fileName") + chunkMd5 := c.Request.FormValue("chunkMd5") + chunkNumber, _ := strconv.Atoi(c.Request.FormValue("chunkNumber")) + chunkTotal, _ := strconv.Atoi(c.Request.FormValue("chunkTotal")) + + _, FileHeader, err := c.Request.FormFile("file") + if err != nil { + response.FailWithMessage("接收文件失败", c) + return + } + + file, err := FileHeader.Open() + if err != nil { + response.FailWithMessage("文件读取失败", c) + return + } + defer file.Close() + + content, _ := io.ReadAll(file) + if !utils.CheckMd5(content, chunkMd5) { + response.FailWithMessage("检查md5失败", c) + return + } + + exaFile, err := breakpointContinueUsecase.FindOrCreateFile(c.Request.Context(), fileMd5, fileName, chunkTotal) + if err != nil { + response.FailWithMessage("查找或创建记录失败: "+err.Error(), c) + return + } + + pathC, err := utils.BreakPointContinue(content, fileName, chunkNumber, chunkTotal, fileMd5) + if err != nil { + response.FailWithMessage("断点续传失败: "+err.Error(), c) + return + } + + if err = breakpointContinueUsecase.CreateFileChunk(c.Request.Context(), exaFile.ID, pathC, chunkNumber); err != nil { + response.FailWithMessage("创建文件记录失败: "+err.Error(), c) + return + } + + response.OkWithMessage("切片创建成功", c) +} + +// FindFile 查找文件 +func (f *FileUploadApi) FindFile(c *gin.Context) { + fileMd5 := c.Query("fileMd5") + fileName := c.Query("fileName") + chunkTotal, _ := strconv.Atoi(c.Query("chunkTotal")) + + file, err := breakpointContinueUsecase.FindOrCreateFile(c.Request.Context(), fileMd5, fileName, chunkTotal) + if err != nil { + response.FailWithMessage("查找失败: "+err.Error(), c) + return + } + + response.OkWithDetailed(gin.H{"file": toFileResponse(file)}, "查找成功", c) +} + +// BreakpointContinueFinish 创建文件(合并切片) +func (f *FileUploadApi) BreakpointContinueFinish(c *gin.Context) { + fileMd5 := c.Query("fileMd5") + fileName := c.Query("fileName") + + filePath, err := utils.MakeFile(fileName, fileMd5) + if err != nil { + response.FailWithDetailed(gin.H{"filePath": filePath}, "文件创建失败", c) + return + } + + response.OkWithDetailed(gin.H{"filePath": filePath}, "文件创建成功", c) +} + +// RemoveChunk 删除切片 +func (f *FileUploadApi) RemoveChunk(c *gin.Context) { + var req struct { + FileMd5 string `json:"fileMd5"` + FilePath string `json:"filePath"` + } + if err := c.ShouldBindJSON(&req); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + // 路径穿越拦截 + if strings.Contains(req.FilePath, "..") || strings.Contains(req.FilePath, "../") || + strings.Contains(req.FilePath, "./") || strings.Contains(req.FilePath, ".\\") { + response.FailWithMessage("非法路径,禁止删除", c) + return + } + + if err := utils.RemoveChunk(req.FileMd5); err != nil { + response.FailWithMessage("缓存切片删除失败: "+err.Error(), c) + return + } + + if err := breakpointContinueUsecase.DeleteFileChunk(c.Request.Context(), req.FileMd5, req.FilePath); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + response.OkWithMessage("缓存切片删除成功", c) +} + +func toFileResponse(file *example.ExaFile) map[string]interface{} { + chunks := make([]map[string]interface{}, len(file.ExaFileChunk)) + for i, chunk := range file.ExaFileChunk { + chunks[i] = map[string]interface{}{ + "ID": chunk.ID, + "exaFileID": chunk.ExaFileID, + "fileChunkNumber": chunk.FileChunkNumber, + "fileChunkPath": chunk.FileChunkPath, + } + } + return map[string]interface{}{ + "ID": file.ID, + "fileName": file.FileName, + "fileMd5": file.FileMd5, + "filePath": file.FilePath, + "chunkTotal": file.ChunkTotal, + "isFinish": file.IsFinish, + "exaFileChunk": chunks, + } +} diff --git a/internal/server/handler/system/enter.go b/internal/server/handler/system/enter.go index b46959a..5a11fca 100644 --- a/internal/server/handler/system/enter.go +++ b/internal/server/handler/system/enter.go @@ -22,6 +22,9 @@ type ApiGroup struct { DBApi AutoCodeApi AutoCodeHistoryApi + AutoCodePackageApi + AutoCodeTemplateApi + AutoCodePluginApi } // 业务层依赖 @@ -42,6 +45,7 @@ var ( exportTemplateUsecase *system.ExportTemplateUsecase autoCodeUsecase *system.AutoCodeUsecase autoCodeHistoryUsecase *system.AutoCodeHistoryUsecase + autoCodePackageUsecase *system.AutoCodePackageUsecase ) // InitUsecases 初始化业务层依赖 @@ -62,6 +66,7 @@ func InitUsecases( exportTemplate *system.ExportTemplateUsecase, autoCode *system.AutoCodeUsecase, autoCodeHistory *system.AutoCodeHistoryUsecase, + autoCodePackage *system.AutoCodePackageUsecase, ) { userUsecase = user apiUsecase = api @@ -79,4 +84,10 @@ func InitUsecases( exportTemplateUsecase = exportTemplate autoCodeUsecase = autoCode autoCodeHistoryUsecase = autoCodeHistory + autoCodePackageUsecase = autoCodePackage } + +// Api类型定义 +type AutoCodePackageApi struct{} +type AutoCodeTemplateApi struct{} +type AutoCodePluginApi struct{} diff --git a/internal/server/handler/system/sys_auto_code_package.go b/internal/server/handler/system/sys_auto_code_package.go new file mode 100644 index 0000000..2305fcf --- /dev/null +++ b/internal/server/handler/system/sys_auto_code_package.go @@ -0,0 +1,60 @@ +package system + +import ( + "kra/pkg/response" + + "github.com/gin-gonic/gin" +) + +// GetPackages 获取所有包 +// @Tags AutoCodePackage +// @Summary 获取所有包 +// @Security ApiKeyAuth +// @Produce application/json +// @Success 200 {object} response.Response{data=[]system.SysAutoCodePackage} "获取成功" +// @Router /autoCode/getPackage [post] +func GetPackages(c *gin.Context) { + pkgs, err := autoCodePackageUsecase.All(c.Request.Context()) + if err != nil { + response.FailWithMessage("获取失败: "+err.Error(), c) + return + } + response.OkWithDetailed(pkgs, "获取成功", c) +} + +// DeletePackage 删除包 +// @Tags AutoCodePackage +// @Summary 删除包 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.GetById true "删除包" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /autoCode/delPackage [post] +func DeletePackage(c *gin.Context) { + var req struct { + ID uint `json:"id"` + } + if err := c.ShouldBindJSON(&req); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if err := autoCodePackageUsecase.Delete(c.Request.Context(), req.ID); err != nil { + response.FailWithMessage("删除失败: "+err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +// GetTemplates 获取所有模版 +// @Tags AutoCodePackage +// @Summary 获取所有模版 +// @Security ApiKeyAuth +// @Produce application/json +// @Success 200 {object} response.Response{data=[]string} "获取成功" +// @Router /autoCode/getTemplates [get] +func GetTemplates(c *gin.Context) { + // 返回固定的模版列表 + templates := []string{"package", "plugin"} + response.OkWithDetailed(templates, "获取成功", c) +} diff --git a/internal/server/handler/system/sys_auto_code_plugin.go b/internal/server/handler/system/sys_auto_code_plugin.go new file mode 100644 index 0000000..d03e3fd --- /dev/null +++ b/internal/server/handler/system/sys_auto_code_plugin.go @@ -0,0 +1,63 @@ +package system + +import ( + "kra/pkg/response" + + "github.com/gin-gonic/gin" +) + +// InstallPlugin 安装插件 +// @Tags AutoCodePlugin +// @Summary 安装插件 +// @Security ApiKeyAuth +// @accept multipart/form-data +// @Produce application/json +// @Param plug formData file true "插件文件" +// @Success 200 {object} response.Response{data=[]interface{}} "安装成功" +// @Router /autoCode/installPlugin [post] +func InstallPlugin(c *gin.Context) { + // TODO: 实现插件安装功能 + response.FailWithMessage("功能开发中", c) +} + +// PackagedPlugin 打包插件 +// @Tags AutoCodePlugin +// @Summary 打包插件 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param plugName query string true "插件名称" +// @Success 200 {object} response.Response{msg=string} "打包成功" +// @Router /autoCode/pubPlug [post] +func PackagedPlugin(c *gin.Context) { + // TODO: 实现插件打包功能 + response.FailWithMessage("功能开发中", c) +} + +// InitPluginMenu 初始化插件菜单 +// @Tags AutoCodePlugin +// @Summary 初始化插件菜单 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body map[string]interface{} true "菜单信息" +// @Success 200 {object} response.Response{msg=string} "初始化成功" +// @Router /autoCode/initMenu [post] +func InitPluginMenu(c *gin.Context) { + // TODO: 实现插件菜单初始化功能 + response.FailWithMessage("功能开发中", c) +} + +// InitPluginAPI 初始化插件API +// @Tags AutoCodePlugin +// @Summary 初始化插件API +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body map[string]interface{} true "API信息" +// @Success 200 {object} response.Response{msg=string} "初始化成功" +// @Router /autoCode/initAPI [post] +func InitPluginAPI(c *gin.Context) { + // TODO: 实现插件API初始化功能 + response.FailWithMessage("功能开发中", c) +} diff --git a/internal/server/handler/system/sys_auto_code_template.go b/internal/server/handler/system/sys_auto_code_template.go new file mode 100644 index 0000000..b4401b4 --- /dev/null +++ b/internal/server/handler/system/sys_auto_code_template.go @@ -0,0 +1,49 @@ +package system + +import ( + "kra/pkg/response" + + "github.com/gin-gonic/gin" +) + +// PreviewCode 预览代码 +// @Tags AutoCodeTemplate +// @Summary 预览创建后的代码 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body map[string]interface{} true "预览创建代码" +// @Success 200 {object} response.Response{data=map[string]interface{}} "预览成功" +// @Router /autoCode/preview [post] +func PreviewCode(c *gin.Context) { + // TODO: 实现代码预览功能 + response.FailWithMessage("功能开发中", c) +} + +// CreateCode 创建代码 +// @Tags AutoCodeTemplate +// @Summary 自动代码模板 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body map[string]interface{} true "创建自动代码" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /autoCode/createTemp [post] +func CreateCode(c *gin.Context) { + // TODO: 实现代码创建功能 + response.FailWithMessage("功能开发中", c) +} + +// AddFunc 增加方法 +// @Tags AutoCodeTemplate +// @Summary 增加方法 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body map[string]interface{} true "增加方法" +// @Success 200 {object} response.Response{msg=string} "注入成功" +// @Router /autoCode/addFunc [post] +func AddFunc(c *gin.Context) { + // TODO: 实现方法注入功能 + response.FailWithMessage("功能开发中", c) +} diff --git a/internal/server/middleware/gin_error.go b/internal/server/middleware/gin_error.go new file mode 100644 index 0000000..8499499 --- /dev/null +++ b/internal/server/middleware/gin_error.go @@ -0,0 +1,86 @@ +package middleware + +import ( + "kra/pkg/response" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// 全局错误日志实例 +var errorLogger *zap.Logger + +// ErrorCreator 错误记录创建接口 +type ErrorCreator interface { + CreateError(form, info string) error +} + +// 全局错误创建器 +var errorCreator ErrorCreator + +// SetErrorLogger 设置错误日志 +func SetErrorLogger(logger *zap.Logger) { + errorLogger = logger +} + +// SetErrorCreator 设置错误创建器 +func SetErrorCreator(creator ErrorCreator) { + errorCreator = creator +} + +// ErrorHandler 错误处理中间件 +// 用于统一处理请求过程中产生的错误 +func ErrorHandler() gin.HandlerFunc { + return func(c *gin.Context) { + c.Next() + + // 处理请求过程中产生的错误 + if len(c.Errors) > 0 { + for _, e := range c.Errors { + // 记录错误日志 + if errorLogger != nil { + errorLogger.Error("请求错误", + zap.String("path", c.Request.URL.Path), + zap.String("method", c.Request.Method), + zap.String("error", e.Error()), + ) + } + + // 保存错误到数据库 + if errorCreator != nil { + _ = errorCreator.CreateError(c.Request.URL.Path, e.Error()) + } + } + + // 如果还没有写入响应,返回错误信息 + if !c.Writer.Written() { + lastErr := c.Errors.Last() + response.FailWithMessage(lastErr.Error(), c) + } + } + } +} + +// ErrorResponse 错误响应结构 +type ErrorResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Detail string `json:"detail,omitempty"` +} + +// HandleError 处理错误并返回响应 +func HandleError(c *gin.Context, err error, message string) { + if errorLogger != nil { + errorLogger.Error(message, + zap.String("path", c.Request.URL.Path), + zap.Error(err), + ) + } + + // 保存错误到数据库 + if errorCreator != nil { + _ = errorCreator.CreateError(c.Request.URL.Path, err.Error()) + } + + response.FailWithMessage(message, c) +} diff --git a/internal/server/middleware/gin_limit_ip.go b/internal/server/middleware/gin_limit_ip.go new file mode 100644 index 0000000..1c225d4 --- /dev/null +++ b/internal/server/middleware/gin_limit_ip.go @@ -0,0 +1,99 @@ +package middleware + +import ( + "context" + "errors" + "net/http" + "time" + + "kra/pkg/response" + + "github.com/gin-gonic/gin" + "github.com/redis/go-redis/v9" +) + +// LimitConfig IP限流配置 +type LimitConfig struct { + // GenerationKey 根据业务生成key + GenerationKey func(c *gin.Context) string + // CheckOrMark 检查函数 + CheckOrMark func(key string, expire int, limit int) error + // Expire key 过期时间(秒) + Expire int + // Limit 周期内限制次数 + Limit int +} + +var redisClient redis.UniversalClient + +// SetRedisClient 设置Redis客户端 +func SetRedisClient(client redis.UniversalClient) { + redisClient = client +} + +// LimitWithTime 返回限流中间件 +func (l LimitConfig) LimitWithTime() gin.HandlerFunc { + return func(c *gin.Context) { + if err := l.CheckOrMark(l.GenerationKey(c), l.Expire, l.Limit); err != nil { + c.JSON(http.StatusOK, gin.H{"code": response.ERROR, "msg": err.Error()}) + c.Abort() + return + } + c.Next() + } +} + +// DefaultGenerationKey 默认生成key +func DefaultGenerationKey(c *gin.Context) string { + return "KRA_Limit" + c.ClientIP() +} + +// DefaultCheckOrMark 默认检查函数 +func DefaultCheckOrMark(key string, expire int, limit int) error { + if redisClient == nil { + return nil // Redis未配置,跳过限流 + } + return SetLimitWithTime(key, limit, time.Duration(expire)*time.Second) +} + +// DefaultLimit 默认限流中间件 +func DefaultLimit(expire, limit int) gin.HandlerFunc { + return LimitConfig{ + GenerationKey: DefaultGenerationKey, + CheckOrMark: DefaultCheckOrMark, + Expire: expire, + Limit: limit, + }.LimitWithTime() +} + +// SetLimitWithTime 设置访问次数 +func SetLimitWithTime(key string, limit int, expiration time.Duration) error { + ctx := context.Background() + count, err := redisClient.Exists(ctx, key).Result() + if err != nil { + return err + } + + if count == 0 { + pipe := redisClient.TxPipeline() + pipe.Incr(ctx, key) + pipe.Expire(ctx, key, expiration) + _, err = pipe.Exec(ctx) + return err + } + + times, err := redisClient.Get(ctx, key).Int() + if err != nil { + return err + } + + if times >= limit { + t, err := redisClient.PTTL(ctx, key).Result() + if err != nil { + return errors.New("请求太过频繁,请稍后再试") + } + return errors.New("请求太过频繁, 请 " + t.String() + " 后尝试") + } + + return redisClient.Incr(ctx, key).Err() +} diff --git a/internal/server/middleware/gin_logger.go b/internal/server/middleware/gin_logger.go new file mode 100644 index 0000000..07e9678 --- /dev/null +++ b/internal/server/middleware/gin_logger.go @@ -0,0 +1,95 @@ +package middleware + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "strings" + "time" + + "github.com/gin-gonic/gin" +) + +// LogLayout 日志layout +type LogLayout struct { + Time time.Time `json:"time"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + Path string `json:"path"` + Query string `json:"query,omitempty"` + Body string `json:"body,omitempty"` + IP string `json:"ip"` + UserAgent string `json:"user_agent"` + Error string `json:"error,omitempty"` + Cost time.Duration `json:"cost"` + Source string `json:"source"` +} + +// Logger 日志中间件配置 +type Logger struct { + // Filter 用户自定义过滤 + Filter func(c *gin.Context) bool + // FilterKeyword 关键字过滤 + FilterKeyword func(layout *LogLayout) bool + // AuthProcess 鉴权处理 + AuthProcess func(c *gin.Context, layout *LogLayout) + // Print 日志处理 + Print func(LogLayout) + // Source 服务唯一标识 + Source string +} + +// SetLoggerMiddleware 设置日志中间件 +func (l Logger) SetLoggerMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + path := c.Request.URL.Path + query := c.Request.URL.RawQuery + + var body []byte + if l.Filter != nil && !l.Filter(c) { + body, _ = c.GetRawData() + // 将原body塞回去 + c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + } + + c.Next() + + cost := time.Since(start) + layout := LogLayout{ + Time: time.Now(), + Path: path, + Query: query, + IP: c.ClientIP(), + UserAgent: c.Request.UserAgent(), + Error: strings.TrimRight(c.Errors.ByType(gin.ErrorTypePrivate).String(), "\n"), + Cost: cost, + Source: l.Source, + } + + if l.Filter != nil && !l.Filter(c) { + layout.Body = string(body) + } + + if l.AuthProcess != nil { + l.AuthProcess(c, &layout) + } + + if l.FilterKeyword != nil { + l.FilterKeyword(&layout) + } + + l.Print(layout) + } +} + +// DefaultLogger 默认日志中间件 +func DefaultLogger() gin.HandlerFunc { + return Logger{ + Print: func(layout LogLayout) { + v, _ := json.Marshal(layout) + fmt.Println(string(v)) + }, + Source: "KRA", + }.SetLoggerMiddleware() +} diff --git a/internal/server/middleware/gin_timeout.go b/internal/server/middleware/gin_timeout.go new file mode 100644 index 0000000..47ec883 --- /dev/null +++ b/internal/server/middleware/gin_timeout.go @@ -0,0 +1,55 @@ +package middleware + +import ( + "context" + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +// TimeoutMiddleware 创建超时中间件 +// timeout: 超时时间(例如:time.Second * 30) +// 使用示例: router.GET("path", middleware.TimeoutMiddleware(30*time.Second), HandleFunc) +func TimeoutMiddleware(timeout time.Duration) gin.HandlerFunc { + return func(c *gin.Context) { + ctx, cancel := context.WithTimeout(c.Request.Context(), timeout) + defer cancel() + + c.Request = c.Request.WithContext(ctx) + + // 使用 buffered channel 避免 goroutine 泄漏 + done := make(chan struct{}, 1) + panicChan := make(chan interface{}, 1) + + go func() { + defer func() { + if p := recover(); p != nil { + select { + case panicChan <- p: + default: + } + } + select { + case done <- struct{}{}: + default: + } + }() + c.Next() + }() + + select { + case p := <-panicChan: + panic(p) + case <-done: + return + case <-ctx.Done(): + c.Header("Connection", "close") + c.AbortWithStatusJSON(http.StatusGatewayTimeout, gin.H{ + "code": 504, + "msg": "请求超时", + }) + return + } + } +} diff --git a/internal/server/router/example/exa_file_upload.go b/internal/server/router/example/exa_file_upload.go index 26b9e03..cd233a1 100644 --- a/internal/server/router/example/exa_file_upload.go +++ b/internal/server/router/example/exa_file_upload.go @@ -18,8 +18,13 @@ func (r *FileUploadRouter) InitFileUploadRouter(Router *gin.RouterGroup) { fileUploadRouter.POST("deleteFile", api.DeleteFile) fileUploadRouter.POST("editFileName", api.EditFileName) fileUploadRouter.POST("importURL", api.ImportURL) + // 断点续传 + fileUploadRouter.POST("breakpointContinue", api.BreakpointContinue) + fileUploadRouter.POST("breakpointContinueFinish", api.BreakpointContinueFinish) + fileUploadRouter.POST("removeChunk", api.RemoveChunk) } { fileUploadRouterWithoutRecord.POST("getFileList", api.GetFileList) + fileUploadRouterWithoutRecord.GET("findFile", api.FindFile) } } diff --git a/internal/server/router/system/sys_auto_code.go b/internal/server/router/system/sys_auto_code.go index 51fab08..370a5f7 100644 --- a/internal/server/router/system/sys_auto_code.go +++ b/internal/server/router/system/sys_auto_code.go @@ -1,6 +1,7 @@ package system import ( + handler "kra/internal/server/handler/system" "kra/internal/server/middleware" "github.com/gin-gonic/gin" @@ -12,11 +13,32 @@ type AutoCodeRouter struct{} func (s *AutoCodeRouter) InitAutoCodeRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) { autoCodeRouter := Router.Group("autoCode").Use(middleware.OperationRecordMiddleware()) autoCodeRouterWithoutRecord := Router.Group("autoCode") + publicAutoCodeRouter := PublicRouter.Group("autoCode") { autoCodeRouterWithoutRecord.GET("getDB", autoCodeApi.GetDB) // 获取数据库 autoCodeRouterWithoutRecord.GET("getTables", autoCodeApi.GetTables) // 获取对应数据库的表 autoCodeRouterWithoutRecord.GET("getColumn", autoCodeApi.GetColumn) // 获取指定表所有字段信息 } - // 预留其他路由 - _ = autoCodeRouter + { + // 代码包管理 + autoCodeRouter.POST("getPackage", handler.GetPackages) // 获取所有包 + autoCodeRouter.POST("delPackage", handler.DeletePackage) // 删除包 + autoCodeRouter.GET("getTemplates", handler.GetTemplates) // 获取所有模版 + } + { + // 代码模板 + autoCodeRouter.POST("preview", handler.PreviewCode) // 预览代码 + autoCodeRouter.POST("createTemp", handler.CreateCode) // 创建代码 + autoCodeRouter.POST("addFunc", handler.AddFunc) // 增加方法 + } + { + // 插件管理 + autoCodeRouter.POST("pubPlug", handler.PackagedPlugin) // 打包插件 + autoCodeRouter.POST("installPlugin", handler.InstallPlugin) // 安装插件 + } + { + // 公开路由 + publicAutoCodeRouter.POST("initMenu", handler.InitPluginMenu) // 初始化插件菜单 + publicAutoCodeRouter.POST("initAPI", handler.InitPluginAPI) // 初始化插件API + } } diff --git a/pkg/utils/ast.go b/pkg/utils/ast.go new file mode 100644 index 0000000..51601c3 --- /dev/null +++ b/pkg/utils/ast.go @@ -0,0 +1,143 @@ +package utils + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "log" +) + +// AddImport 增加 import 方法 +func AddImport(astNode ast.Node, imp string) { + impStr := fmt.Sprintf("\"%s\"", imp) + ast.Inspect(astNode, func(node ast.Node) bool { + if genDecl, ok := node.(*ast.GenDecl); ok { + if genDecl.Tok == token.IMPORT { + for i := range genDecl.Specs { + if impNode, ok := genDecl.Specs[i].(*ast.ImportSpec); ok { + if impNode.Path.Value == impStr { + return false + } + } + } + genDecl.Specs = append(genDecl.Specs, &ast.ImportSpec{ + Path: &ast.BasicLit{ + Kind: token.STRING, + Value: impStr, + }, + }) + } + } + return true + }) +} + +// FindFunction 查询特定function方法 +func FindFunction(astNode ast.Node, FunctionName string) *ast.FuncDecl { + var funcDeclP *ast.FuncDecl + ast.Inspect(astNode, func(node ast.Node) bool { + if funcDecl, ok := node.(*ast.FuncDecl); ok { + if funcDecl.Name.String() == FunctionName { + funcDeclP = funcDecl + return false + } + } + return true + }) + return funcDeclP +} + +// FindArray 查询特定数组方法 +func FindArray(astNode ast.Node, identName, selectorExprName string) *ast.CompositeLit { + var assignStmt *ast.CompositeLit + ast.Inspect(astNode, func(n ast.Node) bool { + switch node := n.(type) { + case *ast.AssignStmt: + for _, expr := range node.Rhs { + if exprType, ok := expr.(*ast.CompositeLit); ok { + if arrayType, ok := exprType.Type.(*ast.ArrayType); ok { + sel, ok1 := arrayType.Elt.(*ast.SelectorExpr) + x, ok2 := sel.X.(*ast.Ident) + if ok1 && ok2 && x.Name == identName && sel.Sel.Name == selectorExprName { + assignStmt = exprType + return false + } + } + } + } + } + return true + }) + return assignStmt +} + +// CheckImport 检查是否存在Import +func CheckImport(file *ast.File, importPath string) bool { + for _, imp := range file.Imports { + // Remove quotes around the import path + path := imp.Path.Value[1 : len(imp.Path.Value)-1] + if path == importPath { + return true + } + } + return false +} + +// clearPosition 清除AST节点位置信息 +func clearPosition(astNode ast.Node) { + ast.Inspect(astNode, func(n ast.Node) bool { + switch node := n.(type) { + case *ast.Ident: + node.NamePos = token.NoPos + case *ast.CallExpr: + node.Lparen = token.NoPos + node.Rparen = token.NoPos + case *ast.BasicLit: + node.ValuePos = token.NoPos + case *ast.SelectorExpr: + node.Sel.NamePos = token.NoPos + case *ast.BinaryExpr: + node.OpPos = token.NoPos + case *ast.UnaryExpr: + node.OpPos = token.NoPos + case *ast.StarExpr: + node.Star = token.NoPos + } + return true + }) +} + +// CreateStmt 创建语句 +func CreateStmt(statement string) *ast.ExprStmt { + expr, err := parser.ParseExpr(statement) + if err != nil { + log.Fatal(err) + } + clearPosition(expr) + return &ast.ExprStmt{X: expr} +} + +// IsBlockStmt 判断是否为块语句 +func IsBlockStmt(node ast.Node) bool { + _, ok := node.(*ast.BlockStmt) + return ok +} + +// VariableExistsInBlock 检查变量是否存在于块中 +func VariableExistsInBlock(block *ast.BlockStmt, varName string) bool { + exists := false + ast.Inspect(block, func(n ast.Node) bool { + switch node := n.(type) { + case *ast.AssignStmt: + for _, expr := range node.Lhs { + if ident, ok := expr.(*ast.Ident); ok && ident.Name == varName { + exists = true + return false + } + } + } + return true + }) + return exists +} diff --git a/pkg/utils/directory.go b/pkg/utils/directory.go new file mode 100644 index 0000000..31499d3 --- /dev/null +++ b/pkg/utils/directory.go @@ -0,0 +1,105 @@ +package utils + +import ( + "errors" + "os" + "path/filepath" + "reflect" + "strings" +) + +// PathExists 文件目录是否存在 +func PathExists(path string) (bool, error) { + fi, err := os.Stat(path) + if err == nil { + if fi.IsDir() { + return true, nil + } + return false, errors.New("存在同名文件") + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} + +// CreateDir 批量创建文件夹 +func CreateDir(dirs ...string) error { + for _, v := range dirs { + exist, err := PathExists(v) + if err != nil { + return err + } + if !exist { + if err := os.MkdirAll(v, os.ModePerm); err != nil { + return err + } + } + } + return nil +} + +// FileMove 文件移动 +// src: 源位置,绝对路径或相对路径 +// dst: 目标位置,绝对路径或相对路径,必须为文件夹 +func FileMove(src string, dst string) error { + if dst == "" { + return nil + } + var err error + src, err = filepath.Abs(src) + if err != nil { + return err + } + dst, err = filepath.Abs(dst) + if err != nil { + return err + } + + dir := filepath.Dir(dst) + if _, err = os.Stat(dir); err != nil { + if err = os.MkdirAll(dir, 0o755); err != nil { + return err + } + } + return os.Rename(src, dst) +} + +// DeLFile 删除文件或目录 +func DeLFile(filePath string) error { + return os.RemoveAll(filePath) +} + +// TrimSpace 去除结构体字符串字段的空格 +// target: 目标结构体,传入必须是指针类型 +func TrimSpace(target interface{}) { + t := reflect.TypeOf(target) + if t.Kind() != reflect.Ptr { + return + } + t = t.Elem() + v := reflect.ValueOf(target).Elem() + for i := 0; i < t.NumField(); i++ { + if v.Field(i).Kind() == reflect.String { + v.Field(i).SetString(strings.TrimSpace(v.Field(i).String())) + } + } +} + +// FileExist 判断文件是否存在 +func FileExist(path string) bool { + fi, err := os.Lstat(path) + if err == nil { + return !fi.IsDir() + } + return !os.IsNotExist(err) +} + +// DirExist 判断目录是否存在 +func DirExist(path string) bool { + fi, err := os.Stat(path) + if err == nil { + return fi.IsDir() + } + return false +} diff --git a/pkg/utils/json.go b/pkg/utils/json.go new file mode 100644 index 0000000..486597c --- /dev/null +++ b/pkg/utils/json.go @@ -0,0 +1,48 @@ +package utils + +import ( + "encoding/json" + "strings" +) + +// GetJSONKeys 获取JSON对象的所有键(保持顺序) +func GetJSONKeys(jsonStr string) (keys []string, err error) { + dec := json.NewDecoder(strings.NewReader(jsonStr)) + t, err := dec.Token() + if err != nil { + return nil, err + } + // 确保数据是一个对象 + if t != json.Delim('{') { + return nil, err + } + for dec.More() { + t, err = dec.Token() + if err != nil { + return nil, err + } + keys = append(keys, t.(string)) + + // 解析值 + var value interface{} + err = dec.Decode(&value) + if err != nil { + return nil, err + } + } + return keys, nil +} + +// ToJSON 将对象转换为JSON字符串 +func ToJSON(v interface{}) string { + b, err := json.Marshal(v) + if err != nil { + return "" + } + return string(b) +} + +// FromJSON 将JSON字符串解析为对象 +func FromJSON(jsonStr string, v interface{}) error { + return json.Unmarshal([]byte(jsonStr), v) +} diff --git a/pkg/utils/verify.go b/pkg/utils/verify.go new file mode 100644 index 0000000..fc11e27 --- /dev/null +++ b/pkg/utils/verify.go @@ -0,0 +1,78 @@ +package utils + +// 预定义验证规则 +var ( + IdVerify = Rules{"ID": []string{NotEmpty()}} + ApiVerify = Rules{"Path": {NotEmpty()}, "Description": {NotEmpty()}, "ApiGroup": {NotEmpty()}, "Method": {NotEmpty()}} + MenuVerify = Rules{"Path": {NotEmpty()}, "Name": {NotEmpty()}, "Component": {NotEmpty()}, "Sort": {Ge("0")}} + MenuMetaVerify = Rules{"Title": {NotEmpty()}} + LoginVerify = Rules{"Username": {NotEmpty()}, "Password": {NotEmpty()}} + RegisterVerify = Rules{"Username": {NotEmpty()}, "NickName": {NotEmpty()}, "Password": {NotEmpty()}, "AuthorityId": {NotEmpty()}} + PageInfoVerify = Rules{"Page": {NotEmpty()}, "PageSize": {NotEmpty()}} + CustomerVerify = Rules{"CustomerName": {NotEmpty()}, "CustomerPhoneData": {NotEmpty()}} + AutoCodeVerify = Rules{"Abbreviation": {NotEmpty()}, "StructName": {NotEmpty()}, "PackageName": {NotEmpty()}} + AutoPackageVerify = Rules{"PackageName": {NotEmpty()}} + AuthorityVerify = Rules{"AuthorityId": {NotEmpty()}, "AuthorityName": {NotEmpty()}} + AuthorityIdVerify = Rules{"AuthorityId": {NotEmpty()}} + OldAuthorityVerify = Rules{"OldAuthorityId": {NotEmpty()}} + ChangePasswordVerify = Rules{"Password": {NotEmpty()}, "NewPassword": {NotEmpty()}} + SetUserAuthorityVerify = Rules{"AuthorityId": {NotEmpty()}} +) + +// Rules 验证规则类型 +type Rules map[string][]string + +// NotEmpty 非空验证 +func NotEmpty() string { + return "notEmpty" +} + +// Ge 大于等于验证 +func Ge(value string) string { + return "ge=" + value +} + +// Le 小于等于验证 +func Le(value string) string { + return "le=" + value +} + +// Gt 大于验证 +func Gt(value string) string { + return "gt=" + value +} + +// Lt 小于验证 +func Lt(value string) string { + return "lt=" + value +} + +// Eq 等于验证 +func Eq(value string) string { + return "eq=" + value +} + +// Ne 不等于验证 +func Ne(value string) string { + return "ne=" + value +} + +// Len 长度验证 +func Len(value string) string { + return "len=" + value +} + +// MinLen 最小长度验证 +func MinLen(value string) string { + return "minLen=" + value +} + +// MaxLen 最大长度验证 +func MaxLen(value string) string { + return "maxLen=" + value +} + +// Regexp 正则验证 +func Regexp(value string) string { + return "regexp=" + value +}