后端重构

This commit is contained in:
Yvan 2026-01-08 20:16:27 +08:00
parent 9cb6b965e0
commit f5f77b32fe
23 changed files with 1450 additions and 2 deletions

View File

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

View File

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

View File

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

View File

@ -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 数据层包装器

View File

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

View File

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

View File

@ -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,
)
// 创建路由组

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

143
pkg/utils/ast.go Normal file
View File

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

105
pkg/utils/directory.go Normal file
View File

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

48
pkg/utils/json.go Normal file
View File

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

78
pkg/utils/verify.go Normal file
View File

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