From 89d6ed41e99f734d8fe5e3f1bfd08fb4f31bc05b Mon Sep 17 00:00:00 2001 From: Havoc412 <2993167370@qq.com> Date: Wed, 16 Oct 2024 11:33:32 +0800 Subject: [PATCH] change animal Model, wait next change --- AutoMigrateMySQL/config/config.go | 49 ++++ AutoMigrateMySQL/history/main-v1.go | 186 ++++++++++++++ AutoMigrateMySQL/main-v2.go | 248 +++++++++++++++++++ AutoMigrateMySQL/test/test.go | 19 ++ app/http/controller/web/animal_controller.go | 4 +- app/model/animal_com.go | 4 +- app/model/animal_face_breed.go | 6 +- app/model/animal_notice.go | 3 + app/model/migrate/AutoMigrate.go | 14 +- app/utils/gorm_v2/utils.go | 3 + test/animal_face_breed_test.go | 47 ++++ test/insertAnimal_test.go | 1 - 12 files changed, 570 insertions(+), 14 deletions(-) create mode 100644 AutoMigrateMySQL/config/config.go create mode 100644 AutoMigrateMySQL/history/main-v1.go create mode 100644 AutoMigrateMySQL/main-v2.go create mode 100644 AutoMigrateMySQL/test/test.go create mode 100644 app/model/animal_notice.go create mode 100644 test/animal_face_breed_test.go diff --git a/AutoMigrateMySQL/config/config.go b/AutoMigrateMySQL/config/config.go new file mode 100644 index 0000000..4f19b39 --- /dev/null +++ b/AutoMigrateMySQL/config/config.go @@ -0,0 +1,49 @@ +package config + +import ( + "encoding/json" + "fmt" + "log" + "os" +) + +// Config 包含所有配置部分 +type Config struct { + MySQL MySQLConfig `json:"mysql"` +} + +// MySQLConfig 用于存储 MySQL 数据库的配置 +type MySQLConfig struct { + Username string `json:"username"` + Password string `json:"password"` + Host string `json:"host"` + Database string `json:"database"` +} + +// LoadConfig 从文件中加载所有配置信息 +func LoadConfig(filename string) (*Config, error) { + file, err := os.Open(filename) + if err != nil { + return nil, fmt.Errorf("could not open config file: %v", err) + } + defer file.Close() + + var config Config + decoder := json.NewDecoder(file) + if err := decoder.Decode(&config); err != nil { + return nil, fmt.Errorf("could not decode config file: %v", err) + } + + return &config, nil +} + +func main() { + config, err := LoadConfig("config.json") + if err != nil { + log.Fatalf("Error loading config: %v", err) + } + + // 使用 MySQL 配置信息 + fmt.Printf("Connecting to MySQL database at %s\n", config.MySQL.Host) + // 使用 config.MySQL.Username, config.MySQL.Password, config.MySQL.Database 来连接数据库 +} diff --git a/AutoMigrateMySQL/history/main-v1.go b/AutoMigrateMySQL/history/main-v1.go new file mode 100644 index 0000000..5fc8d83 --- /dev/null +++ b/AutoMigrateMySQL/history/main-v1.go @@ -0,0 +1,186 @@ +package main + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "log" + "reflect" + "regexp" + "strings" + "time" + + "gorm.io/datatypes" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +// 从 AST 中提取结构体字段类型 +func getFieldType(expr ast.Expr) reflect.Type { + switch t := expr.(type) { + case *ast.Ident: + // fmt.Println("t.Name:", t.Name) + switch t.Name { + case "string": + return reflect.TypeOf("") + case "int": + return reflect.TypeOf(0) + case "bool": + return reflect.TypeOf(true) + case "uint8": + return reflect.TypeOf(uint8(0)) + case "uint16": + return reflect.TypeOf(uint16(0)) + case "uint32": + return reflect.TypeOf(uint32(0)) + case "uint64": + return reflect.TypeOf(uint64(0)) + case "float64": + return reflect.TypeOf(float64(0)) + } + case *ast.ArrayType: + elemType := getFieldType(t.Elt) + if elemType != nil { + return reflect.SliceOf(elemType) + } + case *ast.SelectorExpr: // info time.Time 的特化识别 + if pkgIdent, ok := t.X.(*ast.Ident); ok { + if pkgIdent.Name == "time" && t.Sel.Name == "Time" { + return reflect.TypeOf(time.Time{}) + } + if pkgIdent.Name == "datatypes" && t.Sel.Name == "JSON" { + return reflect.TypeOf(datatypes.JSON{}) + } + } + case *ast.StarExpr: + // Handle pointer to a type + return reflect.PtrTo(getFieldType(t.X)) + } + return nil +} + +// convertToSnakeCase 将大写字符转换为小写并用下划线隔开 +func convertToSnakeCase(name string) string { + // 使用正则表达式找到大写字符并在前面加上下划线,然后转换为小写 + re := regexp.MustCompile("([a-z0-9])([A-Z])") + snake := re.ReplaceAllString(name, "${1}_${2}") + return strings.ToLower(snake) +} + +func main() { + filePath := "./table_defs/table_defs.go" // 指定Go源文件 + fset := token.NewFileSet() // 创建文件集,用于记录位置 + + // 解析文件,得到*ast.File结构 + f, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments) + if err != nil { + log.Fatal(err) + } + + // 用于保存结构体信息的map + structs := make(map[string]reflect.Type) + + // 遍历文件中的所有声明 + for _, decl := range f.Decls { + // 检查声明是否为类型声明(type T struct {...}) + genDecl, ok := decl.(*ast.GenDecl) + if !ok || genDecl.Tok != token.TYPE { + continue + } + + // 遍历类型声明中的所有规格(可能有多个类型在一个声明中,例如:type (A struct{}; B struct{})) + for _, spec := range genDecl.Specs { + typeSpec, ok := spec.(*ast.TypeSpec) + if !ok { + continue + } + + // 检查类型是否为结构体 + structType, ok := typeSpec.Type.(*ast.StructType) + if !ok { + continue + } + + // info 过滤空表 + if len(structType.Fields.List) == 0 { + continue + } + + // 构建反射类型 + fields := make([]reflect.StructField, 0) + // fmt.Println(typeSpec.Name.Name, len(structType.Fields.List)) + + for _, field := range structType.Fields.List { + if len(field.Names) == 0 { + // 处理嵌入结构体 + ident, ok := field.Type.(*ast.Ident) + if !ok { + log.Printf("Unsupported embedded type for field %v\n", field.Type) + continue + } + embedType, ok := structs[ident.Name] + if !ok { + log.Printf("Embedded type %s not found\n", ident.Name) + continue + } + // 获取嵌入结构体的所有字段 + for i := 0; i < embedType.NumField(); i++ { + fields = append(fields, embedType.Field(i)) + } + } else { + for _, fieldName := range field.Names { + fieldType := getFieldType(field.Type) + if fieldType == nil { + continue + } + + // 处理标签 + tag := "" + if field.Tag != nil { + tag = field.Tag.Value + } + + fields = append(fields, reflect.StructField{ + Name: fieldName.Name, + Type: fieldType, + Tag: reflect.StructTag(tag), + }) + // fmt.Println(fieldName.Name, field.Type, fieldType, tag) + } + } + } + + // 创建结构体类型 + structName := typeSpec.Name.Name + structReflectType := reflect.StructOf(fields) + structs[structName] = structReflectType + fmt.Println(fmt.Sprintf("get struct: %s\n", structName)) + } + } + + // 初始化数据库 + dsn := "root:havocantelope412@tcp(127.0.0.1:3306)/pawwander_dev?charset=utf8mb4&parseTime=True&loc=Local" + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) // 打开 DB 连接 + + if err != nil { + log.Fatal(err) + } + + // 通过反射创建结构体实例并迁移数据库 + for name, typ := range structs { + if name == "General" { + continue + } + instance := reflect.New(typ).Interface() + + // 手动设定 表名 + tableName := convertToSnakeCase(name) // 你可以根据实际情况生成表名 + db = db.Table(tableName) + + fmt.Printf("Created instance of %s: %+v\n", name, instance) + if err := db.AutoMigrate(instance); err != nil { + log.Fatalf("Failed to migrate %s: %v", name, err) + } + } +} diff --git a/AutoMigrateMySQL/main-v2.go b/AutoMigrateMySQL/main-v2.go new file mode 100644 index 0000000..47844ec --- /dev/null +++ b/AutoMigrateMySQL/main-v2.go @@ -0,0 +1,248 @@ +package main + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "log" + "os" + "path/filepath" + "reflect" + "regexp" + "strings" + "time" + + . "catface/AutoMigrateMySQL/config" + + "gorm.io/datatypes" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +// 仿照 AutoMigrate 原本的效果 +func convertToSnakeCase(name string) string { + // 使用正则表达式找到大写字符并在前面加上下划线,然后转换为小写 + re := regexp.MustCompile("([a-z0-9])([A-Z])") + snake := re.ReplaceAllString(name, "${1}_${2}") + return strings.ToLower(snake) +} + +// 从 AST 中提取结构体字段类型 +func getFieldType(expr ast.Expr) reflect.Type { + switch t := expr.(type) { + case *ast.Ident: + // fmt.Println("t.Name:", t.Name) + switch t.Name { + case "string": + return reflect.TypeOf("") + case "int": + return reflect.TypeOf(0) + case "bool": + return reflect.TypeOf(true) + case "uint8": + return reflect.TypeOf(uint8(0)) + case "uint16": + return reflect.TypeOf(uint16(0)) + case "uint32": + return reflect.TypeOf(uint32(0)) + case "uint64": + return reflect.TypeOf(uint64(0)) + case "float64": + return reflect.TypeOf(float64(0)) + } + case *ast.ArrayType: + elemType := getFieldType(t.Elt) + if elemType != nil { + return reflect.SliceOf(elemType) + } + case *ast.SelectorExpr: // info time.Time 的特化识别 + if pkgIdent, ok := t.X.(*ast.Ident); ok { + if pkgIdent.Name == "time" && t.Sel.Name == "Time" { + return reflect.TypeOf(time.Time{}) + } + if pkgIdent.Name == "datatypes" && t.Sel.Name == "JSON" { + return reflect.TypeOf(datatypes.JSON{}) + } + } + case *ast.StarExpr: // question 暂时好像不影响。 + // Handle pointer to a type + return reflect.PtrTo(getFieldType(t.X)) + } + return nil +} + +// 用于保存结构体信息的map +var structs = make(map[string]reflect.Type) + +// 遍历文件中的所有声明 +func getStruct(fDecls []ast.Decl) { + for _, decl := range fDecls { + // 检查声明是否为类型声明(type T struct {...}) + genDecl, ok := decl.(*ast.GenDecl) + if !ok || genDecl.Tok != token.TYPE { + continue + } + + // 遍历类型声明中的所有规格(可能有多个类型在一个声明中,例如:type (A struct{}; B struct{})) + for _, spec := range genDecl.Specs { + typeSpec, ok := spec.(*ast.TypeSpec) + if !ok { + continue + } + + // 检查类型是否为结构体 + structType, ok := typeSpec.Type.(*ast.StructType) + if !ok { + continue + } + + // 过滤空表 + if len(structType.Fields.List) == 0 { + continue + } + + // 构建反射类型 + fields := make([]reflect.StructField, 0) + // fmt.Println(typeSpec.Name.Name, len(structType.Fields.List)) + + for _, field := range structType.Fields.List { + if len(field.Names) == 0 { + // 处理嵌入结构体 + ident, ok := field.Type.(*ast.Ident) + if !ok { + log.Printf("Unsupported embedded type for field %v\n", field.Type) + continue + } + embedType, ok := structs[ident.Name] + if !ok { + log.Printf("Embedded type %s not found\n", ident.Name) + continue + } + // 获取嵌入结构体的所有字段 + for i := 0; i < embedType.NumField(); i++ { + fields = append(fields, embedType.Field(i)) + } + } else { + for _, fieldName := range field.Names { + fieldType := getFieldType(field.Type) + if fieldType == nil { + continue + } + + // 处理标签 + tag := "" + if field.Tag != nil { + tag = field.Tag.Value + } + + fields = append(fields, reflect.StructField{ + Name: fieldName.Name, + Type: fieldType, + Tag: reflect.StructTag(tag), + }) + // fmt.Println(fieldName.Name, field.Type, fieldType, tag) + } + } + } + + // 创建结构体类型 + structName := typeSpec.Name.Name + structReflectType := reflect.StructOf(fields) + structs[structName] = structReflectType + fmt.Println("get struct: ", structName) + } + } +} + +func autoMigrate() { + config, err := LoadConfig("config.json") + if err != nil { + log.Fatalln("Error loading config: %v", err) + } + // info 初始化数据库 + dsn := fmt.Sprintf( + "%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", + config.MySQL.Username, + config.MySQL.Password, + config.MySQL.Host, + config.MySQL.Database, + ) + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) // 打开 DB 连接 + + if err != nil { + log.Fatal(err) + } + + // 通过反射创建结构体实例并迁移数据库 + for name, typ := range structs { + if name == "General" { + continue + } + instance := reflect.New(typ).Interface() + + // 手动设定表名 + tableName := convertToSnakeCase(name) + db = db.Table(tableName) + + fmt.Printf("Created instance of %s: %+v\n", name, instance) + if err := db.AutoMigrate(instance); err != nil { + log.Fatalf("Failed to migrate %s: %v", name, err) + } + } +} + +func main() { + const dirPath = "./table_defs" // 指定目录路径 + const rootFileName = "table_defs.go" // info 根结构体所在的文件 + fset := token.NewFileSet() // 创建文件集,用于记录位置 + + // mark stage-1 + // 列出指定目录下的所有文件和子目录 + entries, err := os.ReadDir(dirPath) + if err != nil { + log.Fatal(err) + } + + // 前置根文件 + var targetIndex int + var found bool + for i, entry := range entries { + if entry.Name() == rootFileName { + targetIndex = i + found = true + break + } + } + + if found { + targetEntry := entries[targetIndex] + entries = append(entries[:targetIndex], entries[targetIndex+1:]...) + entries = append([]os.DirEntry{targetEntry}, entries...) + } else { + log.Fatalf("File %s not found in directory %s", rootFileName, dirPath) + } + + // 正常遍历 + for _, entry := range entries { + // 构建完整路径 + path := filepath.Join(dirPath, entry.Name()) + + // 检查文件后缀是否为 .go + if !entry.IsDir() && filepath.Ext(path) == ".go" { + // 解析文件,得到 *ast.File 结构 + f, err := parser.ParseFile(fset, path, nil, parser.ParseComments) + if err != nil { + log.Printf("Error parsing file %s: %s", path, err) + continue + } + + // mark stage-2 + getStruct(f.Decls) + log.Printf("Parsed file: %s", path) + } + } + + // mark stage-3 + autoMigrate() +} diff --git a/AutoMigrateMySQL/test/test.go b/AutoMigrateMySQL/test/test.go new file mode 100644 index 0000000..0ae2d40 --- /dev/null +++ b/AutoMigrateMySQL/test/test.go @@ -0,0 +1,19 @@ +package main + +import ( + "fmt" + // . "pawwander/table_defs" + "regexp" + "strings" +) + +func convertToSnakeCase(name string) string { + // 使用正则表达式找到大写字符并在前面加上下划线,然后转换为小写 + re := regexp.MustCompile("([a-z0-9])([A-Z])") + snake := re.ReplaceAllString(name, "${1}_${2}") + return strings.ToLower(snake) +} + +func main() { + fmt.Println(convertToSnakeCase("UserActivity")) +} diff --git a/app/http/controller/web/animal_controller.go b/app/http/controller/web/animal_controller.go index 5f7a729..16bf0ee 100644 --- a/app/http/controller/web/animal_controller.go +++ b/app/http/controller/web/animal_controller.go @@ -7,6 +7,7 @@ import ( "catface/app/utils/response" "github.com/gin-gonic/gin" + ) type Animals struct { // INFO 起到一个标记的作用,这样 web.xxx 的时候不同模块就不会命名冲突了。 @@ -30,11 +31,10 @@ func (a *Animals) List(context *gin.Context) { } } -// v1 +// v0.1 // func (a *Animals) Detail(context *gin.Context) { // // 1. Get Params // anmId, err := strconv.Atoi(context.Param("anm_id")) - // // 2. Select & Filter // var animal model.Animal // err = variable.GormDbMysql.Table("animals").Model(&animal).Where("id = ?", anmId).Scan(&animal).Error // TIP GORM.First 采取默认的 diff --git a/app/model/animal_com.go b/app/model/animal_com.go index bc07fa1..9852adf 100644 --- a/app/model/animal_com.go +++ b/app/model/animal_com.go @@ -2,11 +2,11 @@ package model // INFO 一些基础表单的整合 -type Breed struct { +type AnmBreed struct { BriefModel } -type Sterilzation struct { // TEST How to use BriefModel, the dif between Common +type AnmSterilzation struct { // TEST How to use BriefModel, the dif between Common Id int64 `json:"id"` NameZh string `json:"name_zh"` NameEn string `json:"name_en"` diff --git a/app/model/animal_face_breed.go b/app/model/animal_face_breed.go index a50e54c..1598b17 100644 --- a/app/model/animal_face_breed.go +++ b/app/model/animal_face_breed.go @@ -1,11 +1,13 @@ package model +// INFO 写在前面:实际上这个模块是 CatFace 子模块在维护,所以对应的 curd 都交给 python 了。 + /** * @description: 保留 Top 3, 辅助 catface - breed 子模型判断; 单独建表,因为只会被 CatFace 模块使用。 * @return {*} */ type AnmFaceBreed struct { // TODO 迁移 python 的时候再考虑一下细节 - BriefModel + BaseModel Top1 uint8 Prob1 float64 Top2 uint8 @@ -13,6 +15,6 @@ type AnmFaceBreed struct { // TODO 迁移 python 的时候再考虑一下细节 Top3 uint8 Prob3 float64 - AnimalId int64 // INFO 外键设定? + AnimalId int64 `gorm:"index;column:animal_id"` // INFO 外键设定? Animal Animal } diff --git a/app/model/animal_notice.go b/app/model/animal_notice.go new file mode 100644 index 0000000..2449e1d --- /dev/null +++ b/app/model/animal_notice.go @@ -0,0 +1,3 @@ +package model + +// TODO Notice 模块,从 Python 迁移 diff --git a/app/model/migrate/AutoMigrate.go b/app/model/migrate/AutoMigrate.go index caab6e4..ae7ab00 100644 --- a/app/model/migrate/AutoMigrate.go +++ b/app/model/migrate/AutoMigrate.go @@ -14,7 +14,7 @@ var DB *gorm.DB // 这种写法是方柏包外使用 // 自动迁移表 func autoMigrateTable() { - err := DB.AutoMigrate(&model.Animal{}, &model.Breed{}, &model.Sterilzation{}, &model.AnmStatus{}, &model.AnmGender{}) + err := DB.AutoMigrate(&model.Animal{}, &model.AnmBreed{}, &model.AnmSterilzation{}, &model.AnmStatus{}, &model.AnmGender{}) if err != nil { fmt.Println("autoMigrateTable error:", err) } @@ -26,7 +26,7 @@ func testInsertSterilzation() { statusesEN := []string{"unknown", "unsterilized", "sterilized"} for i := 0; i < len(statusesZH); i++ { - sterilzation := model.Sterilzation{ + sterilzation := model.AnmSterilzation{ NameZh: statusesZH[i], NameEn: statusesEN[i], } @@ -43,7 +43,7 @@ func testInsertBreed() { colorsZH := []string{"不明", "橘白", "奶牛", "白猫", "黑猫", "橘猫", "狸花", "狸白", "简州", "三花", "彩狸"} colorsEN := []string{"unknown", "orange", "cow", "white", "black", "orangeCat", "tabby", "tabbyWhite", "jianzhong", "threeColor", "colorCat"} for i := 0; i < len(colorsZH); i++ { - breed := model.Breed{ + breed := model.AnmBreed{ BriefModel: model.BriefModel{ NameZh: colorsZH[i], NameEn: colorsEN[i], @@ -102,11 +102,11 @@ func insertData() { testInsertBreed() fmt.Println("testInsertBreed success.") - testInsertStatus() - fmt.Println("testInsertStatus success.") + // testInsertStatus() + // fmt.Println("testInsertStatus success.") - testInsertAnmGender() - fmt.Println("testInsertAnmGender success.") + // testInsertAnmGender() + // fmt.Println("testInsertAnmGender success.") } func main() { diff --git a/app/utils/gorm_v2/utils.go b/app/utils/gorm_v2/utils.go index eb8ad5c..8e28886 100644 --- a/app/utils/gorm_v2/utils.go +++ b/app/utils/gorm_v2/utils.go @@ -8,6 +8,9 @@ import "gorm.io/gorm" * @param {map[string][]uint8} conditions * @return {*} */ +// INFO 特性,源于 MySQL 键值 index from 1, +// 同时 go 在解析参数之时,对于 Query 为空的情况会得到 [0] 的结果, +// 所以就可以用这种方式简单的过滤掉。 func BuildWhere(db *gorm.DB, conditions map[string][]uint8) *gorm.DB { for field, values := range conditions { if len(values) == 0 || len(values) == 1 && values[0] == 0 { diff --git a/test/animal_face_breed_test.go b/test/animal_face_breed_test.go new file mode 100644 index 0000000..4b1b148 --- /dev/null +++ b/test/animal_face_breed_test.go @@ -0,0 +1,47 @@ +package test + +import ( + "catface/app/model" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAnmFaceBreed(t *testing.T) { + Init() + + err := DB.AutoMigrate(&model.AnmFaceBreed{}) + if err != nil { + t.Error(err) + } + + // // INFO 查询表上的所有索引 + // var indexes []struct { + // IndexName string + // ColumnName string + // } + // DB.Raw(`SELECT INDEX_NAME, COLUMN_NAME FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?`, "Hav'sCats", "anm_face_breeds").Scan(&indexes) + // fmt.Println("All Indexes:", len(indexes)) // QUESTION 输出 0 ? + // for _, index := range indexes { + // fmt.Printf("Index Name: %s, Column Name: %s\n", index.IndexName, index.ColumnName) + // } + + animalFaceBreed := model.AnmFaceBreed{ + AnimalId: 1, + Top1: 3, + Prob1: 0.9, + Top2: 4, + Prob2: 0.05, + Top3: 5, + Prob3: 0.05, + } + + // res := DB.Create(&animalFaceBreed) + // assert.Nil(t, res.Error) + + // 可以进一步检查数据是否正确插入,例如通过查询数据库来验证 + var temp model.AnmFaceBreed + result := DB.First(&temp, 1) //animalFaceBreed.BaseModel.Id) // ATT 这里用 Id 直接去拿到默认值 0 + assert.Nil(t, result.Error) + assert.Equal(t, animalFaceBreed.Top1, temp.Top1) +} diff --git a/test/insertAnimal_test.go b/test/insertAnimal_test.go index 36ed1b8..723b250 100644 --- a/test/insertAnimal_test.go +++ b/test/insertAnimal_test.go @@ -127,7 +127,6 @@ func TestCreateAnimal(t *testing.T) { // 插入数据到数据库 result := DB.Create(&animal) - // 检查插入是否成功 assert.Nil(t, result.Error)