├── .gitmodules ├── cli ├── main.go └── cmd │ ├── serve.go │ ├── import.go │ ├── root.go │ └── exam.go ├── config.example.yaml ├── .gitignore ├── http ├── conf.go ├── school.go ├── http.go ├── utils.go ├── analyze.go └── query.go ├── .vscode └── settings.json ├── lib ├── db.go ├── exd │ ├── exam_creater.go │ ├── exd.go │ └── excel_importer.go └── utils │ └── utils.go ├── model ├── exam.go └── score.go ├── go.mod ├── config └── config.go ├── README.md └── go.sum /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "frontend"] 2 | path = frontend 3 | url = https://github.com/qwqcode/qwquiver-frontend.git 4 | -------------------------------------------------------------------------------- /cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/qwqcode/qwquiver/cli/cmd" 5 | ) 6 | 7 | func init() { 8 | 9 | } 10 | 11 | func main() { 12 | cmd.Execute() 13 | } 14 | -------------------------------------------------------------------------------- /config.example.yaml: -------------------------------------------------------------------------------- 1 | name: "成绩统计站" # 网站标题 2 | copyright: "(c) 2020 qwqaq.com" # 网站版权 3 | address: "qwqaq.com" # 域名 4 | port: 8080 # 端口 5 | dbFile: "./data/qwquiver.db" # 数据库文件路径 6 | logFile: "./data/qwquiver.log" # 日志文件路径 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | config.yaml 18 | data 19 | .history 20 | 21 | # rela 22 | cli/cmd/rela.go 23 | 24 | rela 25 | -------------------------------------------------------------------------------- /http/conf.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | "github.com/qwqcode/qwquiver/lib/exd" 6 | "github.com/qwqcode/qwquiver/model" 7 | ) 8 | 9 | // Get Api: /api/conf [已废弃] 10 | func confHandler(c echo.Context) error { 11 | examList := exd.GetAllExams() 12 | examGrpList := exd.GetAllExamGrps() 13 | fieldTransDict := model.ScoreFieldTransMap 14 | 15 | return RespData(c, Map{ 16 | "examList": examList, 17 | "examGrpList": examGrpList, 18 | "fieldTransDict": fieldTransDict, 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Nuxt", 4 | "Parms", 5 | "argparse", 6 | "autoload", 7 | "bbolt", 8 | "cond", 9 | "consola", 10 | "fastify", 11 | "gorm", 12 | "jquery", 13 | "mixins", 14 | "nuxtjs", 15 | "pflag", 16 | "qwqaq", 17 | "qwqcode", 18 | "qwquiver", 19 | "relas", 20 | "sqlite", 21 | "subcommands", 22 | "subparsers", 23 | "typeof", 24 | "unshift" 25 | ] 26 | } -------------------------------------------------------------------------------- /cli/cmd/serve.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/qwqcode/qwquiver/http" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var serveCmd = &cobra.Command{ 11 | Use: "serve", 12 | Version: rootCmd.Version, 13 | Aliases: []string{"server"}, 14 | Short: "启动服务器", 15 | Long: rootCmd.Long, 16 | Run: func(cmd *cobra.Command, args []string) { 17 | fmt.Println(Banner) 18 | fmt.Println("-------------------") 19 | http.Run() 20 | }, 21 | Args: cobra.NoArgs, 22 | } 23 | 24 | func init() { 25 | rootCmd.AddCommand(serveCmd) 26 | 27 | // Global configs 28 | flagPV(serveCmd, "name", "n", "qwquiver", "网站标题") 29 | flagPV(serveCmd, "address", "a", "", "网站地址 (例如: qwqaq.com)") 30 | flagPV(serveCmd, "port", "p", 8087, "网站端口") 31 | } 32 | -------------------------------------------------------------------------------- /http/school.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | "github.com/qwqcode/qwquiver/lib/exd" 6 | ) 7 | 8 | // GetAll Api: /api/school/all 9 | func schoolAllHandler(c echo.Context) error { 10 | examName := c.QueryParam("exam") 11 | exam := exd.GetExam(examName) 12 | 13 | if exam == nil { 14 | return RespError(c, "Exam 不存在") 15 | } 16 | // examConf := lib.GetExamConf(examName) 17 | 18 | result := map[string][]string{} 19 | 20 | schoolList := []string{} 21 | exam.NewQuery().Select("school").Group("school").Find(&schoolList) 22 | 23 | for _, school := range schoolList { 24 | if school != "" { 25 | if result[school] == nil { 26 | result[school] = []string{} 27 | } 28 | 29 | classList := []string{} 30 | exam.NewQuery().Select("class").Where("school = ?", school).Group("class").Find(&classList) 31 | result[school] = append(result[school], classList...) 32 | } 33 | } 34 | 35 | return RespData(c, Map{ 36 | "school": result, 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /lib/db.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "github.com/qwqcode/qwquiver/config" 5 | "gorm.io/driver/sqlite" 6 | "gorm.io/gorm" 7 | ) 8 | 9 | const ExamTbPf string = "EXAM_SCORES_" // 用于存放 Exam 数据的数据表名前缀 10 | const ExamConfTb string = "EXAM_CONF" // ExamConf 数据表名 11 | 12 | // DB is database 13 | var DB *gorm.DB 14 | 15 | // OpenDb 打开数据库 16 | func OpenDb(dbFile string) (err error) { 17 | DB, err = gorm.Open(sqlite.Open(config.Instance.DbFile), &gorm.Config{}) 18 | return 19 | } 20 | 21 | // GetTables 获取所有数据表名称 22 | func GetTables() (tables []string) { 23 | rows, err := DB.Raw("SELECT `name` FROM sqlite_master WHERE type='table';").Rows() 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | for rows.Next() { 29 | var table string 30 | if err := rows.Scan(&table); err != nil { 31 | panic(err) 32 | } 33 | 34 | tables = append(tables, table) 35 | // if table != "schema_migrations" { 36 | // tables = append(tables, table) 37 | // } 38 | } 39 | return 40 | } 41 | 42 | // HasTable 判断数据表是否存在 43 | func HasTable(name string) bool { 44 | return DB.Migrator().HasTable(name) 45 | } 46 | 47 | // DropTable 删除数据表 48 | func DropTable(name string) error { 49 | return DB.Migrator().DropTable(name) 50 | } 51 | -------------------------------------------------------------------------------- /http/http.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | "github.com/labstack/echo/v4/middleware" 9 | echolog "github.com/onrik/logrus/echo" 10 | "github.com/qwqcode/qwquiver/bindata" 11 | "github.com/qwqcode/qwquiver/config" 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | var api *echo.Group 16 | var Injections = [](func(api *echo.Group)){} 17 | 18 | // Run 运行 http server 19 | func Run() { 20 | e := echo.New() 21 | e.HideBanner = true 22 | 23 | e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ 24 | AllowOrigins: []string{"http://localhost:3000"}, // For dev 25 | })) 26 | e.Logger = echolog.NewLogger(logrus.StandardLogger(), "") 27 | e.Use(echolog.Middleware(echolog.DefaultConfig)) 28 | 29 | fileServer := http.FileServer(bindata.AssetFile()) 30 | e.GET("/*", echo.WrapHandler(fileServer)) 31 | 32 | api := e.Group("/api") 33 | api.GET("/query", queryHandler) 34 | api.GET("/query/avg", queryAvgHandler) 35 | api.GET("/conf", confHandler) 36 | api.GET("/analyze", analyzeHandler) 37 | api.GET("/school/all", schoolAllHandler) 38 | 39 | // 功能注入 40 | for _, inject := range config.HTTPInjections { 41 | inject(e, api) 42 | } 43 | 44 | e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", config.Instance.Port))) 45 | } 46 | -------------------------------------------------------------------------------- /http/utils.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/labstack/echo/v4" 7 | ) 8 | 9 | type ( 10 | // Map is a map 11 | Map = map[string]interface{} 12 | ) 13 | 14 | // JSONResult JSON 响应数据结构 15 | type JSONResult struct { 16 | Success bool `json:"success"` // 是否成功 17 | Msg string `json:"msg"` // 消息 18 | Data interface{} `json:"data"` // 数据 19 | Extra interface{} `json:"extra"` // 数据 20 | } 21 | 22 | // RespJSON is normal json result 23 | func RespJSON(c echo.Context, msg string, data interface{}, success bool) error { 24 | return c.JSON(http.StatusOK, &JSONResult{ 25 | Success: success, 26 | Msg: msg, 27 | Data: data, 28 | }) 29 | } 30 | 31 | // RespData is just response data 32 | func RespData(c echo.Context, data interface{}) error { 33 | return c.JSON(http.StatusOK, &JSONResult{ 34 | Success: true, 35 | Data: data, 36 | }) 37 | } 38 | 39 | // RespSuccess is just response success 40 | func RespSuccess(c echo.Context) error { 41 | return c.JSON(http.StatusOK, &JSONResult{ 42 | Success: true, 43 | }) 44 | } 45 | 46 | // RespError is just response error 47 | func RespError(c echo.Context, msg string, details ...string) error { 48 | return c.JSON(http.StatusInternalServerError, &JSONResult{ 49 | Success: false, 50 | Msg: msg, 51 | Extra: Map{ 52 | "errDetails": details, 53 | }, 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /model/exam.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/qwqcode/qwquiver/lib" 5 | "github.com/qwqcode/qwquiver/lib/utils" 6 | "gorm.io/gorm" 7 | ) 8 | 9 | const _examTbPf string = "EXAM_SCORES_" // 用于存放 Exam 数据的数据表名前缀 10 | 11 | // Exam 考试配置数据 模型 12 | type Exam struct { 13 | Name string `gorm:"primaryKey"` // Same as a Bucket's Name 14 | Grp string // 分类 15 | Label string // 实际显示的名称 16 | Subj string // 考试包含科目 17 | SubjFullScore string // 每科的满分分数 18 | Date string // 考试日期 eg. "2020-08-07" 19 | Note string // 备注 20 | } 21 | 22 | // GetName 获取 Exam 名 23 | func (exam Exam) GetName() string { 24 | return exam.Name 25 | } 26 | 27 | // GetTableName 获取 Exam 数据表名 28 | func (exam Exam) GetTableName() string { 29 | return lib.ExamTbPf + exam.Name 30 | } 31 | 32 | // NewQuery 新建 Exam 查询 33 | func (exam Exam) NewQuery() *gorm.DB { 34 | return lib.DB.Table(lib.ExamTbPf + exam.Name) 35 | } 36 | 37 | // GetTable 获取存放成绩的数据表 38 | func (exam Exam) GetTable() *gorm.DB { 39 | return exam.NewQuery() 40 | } 41 | 42 | // CountScores 获取成绩数据条数(考生数量) 43 | func (exam Exam) CountScores() (num int64) { 44 | lib.DB.Table(lib.ExamTbPf + exam.Name).Count(&num) 45 | return 46 | } 47 | 48 | // GetSubjects 获取所有考试科目 49 | func (exam Exam) GetSubjects() (subjects []string, err error) { 50 | if exam.Subj != "" { 51 | if err := utils.JSONDecode(exam.Subj, &subjects); err != nil { 52 | return []string{}, err 53 | } 54 | } 55 | return 56 | } 57 | -------------------------------------------------------------------------------- /cli/cmd/import.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/qwqcode/qwquiver/lib/exd" 8 | "github.com/qwqcode/qwquiver/lib/utils" 9 | "github.com/qwqcode/qwquiver/model" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | // importCmd represents the import command 14 | var importCmd = &cobra.Command{ 15 | Use: "import [Excel 文件路径]", 16 | Version: rootCmd.Version, 17 | Aliases: []string{"excel"}, 18 | Short: "数据导入", 19 | Long: `qwquiver 数据导入工具 - 快速导入 excel 成绩数据 20 | 21 | 表头可选字段名:` + getOptionalFieldNames(), 22 | Run: func(cmd *cobra.Command, args []string) { 23 | // 导入 Excel 24 | examName := "" // default is "", will use filename as the examName 25 | if len(args) <= 1 { // be effective on importing single file 26 | flagExamName, _ := cmd.Flags().GetString("exam-name") // read the flag “name” 27 | if strings.TrimSpace(flagExamName) != "" { 28 | examName = flagExamName 29 | } 30 | } 31 | 32 | examConfJSON, _ := cmd.Flags().GetString("exam-conf") 33 | 34 | // 导入多个文件 35 | for _, filename := range args { 36 | exd.ImportExcel(examName, filename, examConfJSON) 37 | } 38 | }, 39 | Args: cobra.MinimumNArgs(1), 40 | } 41 | 42 | func init() { 43 | rootCmd.AddCommand(importCmd) 44 | 45 | flagP(importCmd, "exam-name", "n", "", "Exam 名称") 46 | flagP(importCmd, "exam-conf", "c", "", "Exam 配置 (JSON格式)") 47 | } 48 | 49 | func getOptionalFieldNames() (s string) { 50 | for _, fn := range utils.GetStructFields(&model.Score{}) { 51 | s += fmt.Sprintf("%s (%s), ", model.ScoreFieldTransMap[fn], fn) 52 | } 53 | return 54 | } 55 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/qwqcode/qwquiver 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/360EntSecGroup-Skylar/excelize v1.4.1 7 | github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1 8 | github.com/cheggaaa/pb v1.0.29 9 | github.com/kr/pretty v0.1.0 // indirect 10 | github.com/labstack/echo/v4 v4.1.16 11 | github.com/mattn/go-colorable v0.1.7 // indirect 12 | github.com/mattn/go-runewidth v0.0.7 // indirect 13 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 14 | github.com/mitchellh/go-homedir v1.1.0 15 | github.com/mozillazg/go-pinyin v0.18.0 16 | github.com/oleiade/reflections v1.0.0 17 | github.com/onrik/logrus v0.7.0 18 | github.com/onsi/ginkgo v1.14.0 // indirect 19 | github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 20 | github.com/sirupsen/logrus v1.6.0 21 | github.com/spf13/cobra v0.0.5 22 | github.com/spf13/viper v1.3.2 23 | github.com/stretchr/testify v1.6.1 // indirect 24 | github.com/thoas/go-funk v0.7.0 25 | github.com/valyala/fasttemplate v1.2.0 // indirect 26 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 27 | golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de // indirect 28 | golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect 29 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 // indirect 30 | golang.org/x/text v0.3.3 // indirect 31 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 32 | google.golang.org/protobuf v1.25.0 // indirect 33 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 34 | gopkg.in/oleiade/reflections.v1 v1.0.0 35 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect 36 | gorm.io/driver/sqlite v1.1.0 37 | gorm.io/gorm v1.9.19 38 | ) 39 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/labstack/echo/v4" 8 | "github.com/mitchellh/go-homedir" 9 | "github.com/sirupsen/logrus" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | // Instance 配置实例 15 | var Instance *Config 16 | 17 | // Config 配置 18 | // @link https://godoc.org/github.com/mitchellh/mapstructure 19 | type Config struct { 20 | Name string `mapstructure:"name"` // 网站标题 21 | Copyright string `mapstructure:"copyright"` // 网站版权 22 | Address string `mapstructure:"address"` // 地址 23 | Port int `mapstructure:"port"` // 端口 24 | 25 | DbFile string `mapstructure:"dbFile"` // 数据库文件路径 26 | LogFile string `mapstructure:"logFile"` // 日志文件路径 27 | } 28 | 29 | // Init 初始化配置 30 | func Init(cfgFile string) { 31 | viper.SetConfigType("yaml") 32 | 33 | if cfgFile != "" { 34 | // Use config file from the flag. 35 | viper.SetConfigFile(cfgFile) 36 | } else { 37 | // Find home directory. 38 | home, err := homedir.Dir() 39 | if err != nil { 40 | logrus.Error(err) 41 | } 42 | 43 | viper.AddConfigPath(".") 44 | viper.AddConfigPath(home) 45 | viper.AddConfigPath("/etc/qwquiver/") 46 | viper.SetConfigName(".qwquiver") 47 | } 48 | 49 | viper.SetEnvPrefix("QWQU") 50 | viper.AutomaticEnv() 51 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 52 | 53 | if err := viper.ReadInConfig(); err == nil { 54 | fmt.Println("Using config file:", viper.ConfigFileUsed()) 55 | } 56 | 57 | Instance = &Config{} 58 | err := viper.Unmarshal(&Instance) 59 | if err != nil { 60 | logrus.Errorf("unable to decode into struct, %v", err) 61 | } 62 | } 63 | 64 | var ( 65 | // CMDInjections 将在 Cmd 初始化时被执行(用于功能注入) 66 | CMDInjections = [](func(rootCmd *cobra.Command)){} 67 | // HTTPInjections 将在 Http 初始化时被执行(用于功能注入) 68 | HTTPInjections = [](func(e *echo.Echo, api *echo.Group)){} 69 | ) 70 | -------------------------------------------------------------------------------- /lib/exd/exam_creater.go: -------------------------------------------------------------------------------- 1 | package exd 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/qwqcode/qwquiver/lib" 7 | "github.com/qwqcode/qwquiver/model" 8 | ) 9 | 10 | // CreateExam 创建新的 Exam 11 | func CreateExam(exam *model.Exam) (err error) { 12 | if exam.Name == "" { 13 | return errors.New("Exam 的 Name 不能为空") 14 | } 15 | 16 | if HasExam(exam.Name) { 17 | err = errors.New("创建 Exam 失败,名为 '" + exam.Name + "' 的 Exam 已存在,不能重复创建") 18 | return 19 | } 20 | 21 | _SaveExamConf(exam) 22 | 23 | tableName := GetExamTableName(exam.Name) 24 | model := &model.Score{} 25 | if err := lib.DB.Table(tableName).Migrator().CreateTable(model); err != nil { 26 | panic(err) 27 | } 28 | // TODO: tableName非法字符的处理 29 | lib.DB.Exec("CREATE INDEX `idx_name_" + tableName + "` ON `" + tableName + "` (name)") 30 | lib.DB.Exec("CREATE INDEX `idx_code_" + tableName + "` ON `" + tableName + "` (code)") 31 | lib.DB.Exec("CREATE INDEX `idx_school_" + tableName + "` ON `" + tableName + "` (school)") 32 | lib.DB.Exec("CREATE INDEX `idx_class_" + tableName + "` ON `" + tableName + "` (class)") 33 | return 34 | } 35 | 36 | // SaveExamConf 保存新的考试配置 37 | func _SaveExamConf(examConf *model.Exam) error { 38 | if examConf.Name == "" { 39 | return errors.New("examConf 的 Name 不能为空") 40 | } 41 | 42 | if !lib.DB.Migrator().HasTable(lib.ExamConfTb) { 43 | if err := lib.DB.Migrator().CreateTable(&model.Exam{}); err != nil { 44 | panic(err) 45 | } 46 | if err := lib.DB.Migrator().RenameTable(&model.Exam{}, lib.ExamConfTb); err != nil { 47 | panic(err) 48 | } 49 | } 50 | 51 | var findExamConf model.Exam 52 | _ = lib.DB.Table(lib.ExamConfTb).Find(&findExamConf, "Name = ?", examConf.Name) 53 | if (findExamConf != model.Exam{}) { 54 | // 若已存在,则删除旧数据 55 | if r := lib.DB.Table(lib.ExamConfTb).Delete(findExamConf); r.Error != nil { 56 | panic(r.Error) 57 | } 58 | } 59 | 60 | return lib.DB.Table(lib.ExamConfTb).Create(examConf).Error 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://user-images.githubusercontent.com/22412567/89914023-fb3a6e80-dc26-11ea-82ba-5ed80e2ffb69.jpg) 2 | 3 | # qwquiver 4 | 5 | > A website for exploring and analyzing exam results. 6 | 7 | ## Features 8 | - 多平台支持 (Win, macOS, Linux) 9 | - Material Design 风格 10 | - 多条件查询 11 | - 支持正则表达式 12 | - 数据模糊查询 13 | - 根据学校班级查询 14 | - 单科成绩 15 | - 多视角排名 16 | - 数据排序 17 | - 表格 18 | - 固定表头 19 | - 大屏幕展示 20 | - 字体大小手动调整 21 | - 自定义字段显示隐藏 22 | - 每页项目数量设定 23 | - 快速查看数据平均分 24 | - 下载为 `.xlsx` 表格文件 25 | - 在线直接打印 26 | - 考生数统计 27 | - 趋势,统计图 28 | - 历史成绩 29 | - 总分趋势 30 | - 平均分趋势 31 | - 多科对比 32 | - 命令行功能 33 | - 考试管理 34 | - 成绩导入(从电子表格文件) 35 | - 构建排名(高考录取人数排名风格) 36 | - 适配手机端 响应式页面 37 | 38 | ## Quick Start 39 | 40 | ```bash 41 | # 运行服务器 42 | $ qwquiver serve 43 | 44 | # 考试管理 45 | $ qwquiver exam 46 | 47 | # 列出所有考试 48 | $ qwquiver exam list 49 | 50 | # 导入工具帮助文档 51 | $ qwquiver import -h 52 | 53 | # 导入考试成绩 xlsx 54 | $ qwquiver import "20200811.xlsx" --exam-name "期末考试" --exam-conf '{"Grp":"高中","Label":"期末考试","Subj":["YW","SX","YY","WL","HX","SW"],"SubjFullScore":{"HX":100,"SW":100,"SX":150,"WL":100,"YW":150,"YY":150},"Date":"202008","Note":"备注"}' 55 | 56 | # 编辑考试数据 57 | $ qwquiver exam config set -h 58 | 59 | # 获取考试数据 60 | $ qwquiver exam config get -h 61 | ``` 62 | 63 | ## Build Setup 64 | 65 | ```bash 66 | # clone qwquiver project 67 | $ git clone --recurse-submodules https://github.com/qwqcode/qwquiver.git 68 | 69 | # install frontend dependencies 70 | $ cd frontend 71 | $ yarn install 72 | $ cd ../ 73 | 74 | # build qwquiver 75 | $ cd build 76 | $ ./build_frontend.sh # build frontend 77 | $ ./build_win.sh # build for windows 78 | ``` 79 | 80 | ## Screenshots 81 | 82 | ![](https://user-images.githubusercontent.com/22412567/89917968-0b088180-dc2c-11ea-882e-204382d49818.png) 83 | ![](https://user-images.githubusercontent.com/22412567/89917972-0ba11800-dc2c-11ea-9793-d584f1837361.png) 84 | ![](https://user-images.githubusercontent.com/22412567/89917976-0c39ae80-dc2c-11ea-8064-dc4a19f79bd8.png) 85 | ![](https://user-images.githubusercontent.com/22412567/89917979-0cd24500-dc2c-11ea-9139-cc751d95e07b.png) 86 | ![](https://user-images.githubusercontent.com/22412567/89917982-0d6adb80-dc2c-11ea-92c4-cb86774728c6.png) 87 | -------------------------------------------------------------------------------- /http/analyze.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | 8 | "github.com/labstack/echo/v4" 9 | "github.com/qwqcode/qwquiver/lib/exd" 10 | "github.com/qwqcode/qwquiver/lib/utils" 11 | "github.com/qwqcode/qwquiver/model" 12 | "github.com/sirupsen/logrus" 13 | "github.com/thoas/go-funk" 14 | "gopkg.in/oleiade/reflections.v1" 15 | ) 16 | 17 | // Get Api: /api/analyze 18 | func analyzeHandler(c echo.Context) error { 19 | examGrp := c.QueryParam("examGrp") 20 | 21 | whereJSONStr := c.QueryParam("where") 22 | var condList map[string]string 23 | if whereJSONStr != "" { 24 | if err := json.Unmarshal([]byte(whereJSONStr), &condList); err != nil { 25 | return RespError(c, "where 参数 JSON 解析失败") 26 | } 27 | } 28 | 29 | uncertain := false // 是否为不确定的数据 30 | examList := exd.GetExamsByGrp(examGrp) 31 | fields := []string{} 32 | for _, f := range model.SFieldSubj { 33 | fields = append(fields, model.ScoreFieldTransMap[f]) 34 | } 35 | 36 | classList := []string{} // 从数据中收集该姓名存在的班级 37 | 38 | // 获取每次考试的数据 39 | exams := []interface{}{} 40 | { 41 | for _, exam := range examList { 42 | queryPersonSc := []model.Score{} 43 | if rs := exd.FilterScores(exam.NewQuery(), condList, false).Find(&queryPersonSc); rs.Error != nil { 44 | logrus.Error("api.chart ", rs.Error) 45 | continue 46 | } 47 | if len(queryPersonSc) == 0 { 48 | continue 49 | } 50 | if len(queryPersonSc) > 1 { 51 | uncertain = true 52 | } 53 | 54 | sc := queryPersonSc[0] 55 | 56 | // 记录该姓名存在的班级 57 | if !funk.ContainsString(classList, sc.CLASS) { 58 | classList = append(classList, sc.CLASS) 59 | } 60 | 61 | // 统计此人此次考试的各科分数 62 | var subjects []string 63 | if exam.Subj != "" { 64 | if err := utils.JSONDecode(exam.Subj, &subjects); err != nil { 65 | continue 66 | } 67 | if len(subjects) == 0 { 68 | continue 69 | } 70 | } else { 71 | continue 72 | } 73 | 74 | var subjFullScore map[string]float64 75 | if err := utils.JSONDecode(exam.SubjFullScore, &subjFullScore); err != nil { 76 | continue 77 | } 78 | subjScores := map[string]interface{}{} 79 | for _, f := range subjects { 80 | scoreI, err := reflections.GetField(sc, f) 81 | if err != nil { 82 | continue 83 | } 84 | score := float64(scoreI.(float64)) 85 | if subjFullScore != nil && subjFullScore[f] > 0 { 86 | score = (score / subjFullScore[f]) * 100 // 转为百分制 87 | score, _ = strconv.ParseFloat(fmt.Sprintf("%.2f", score), 64) // 保留两位小数 88 | } 89 | 90 | subjScores[model.ScoreFieldTransMap[f]] = score 91 | } 92 | 93 | examKey := exam.Label 94 | if examKey == "" { 95 | examKey = exam.Name 96 | } 97 | subjScores["exam"] = examKey 98 | subjScores["date"] = exam.Date 99 | exams = append(exams, subjScores) 100 | } 101 | } 102 | 103 | result := Map{ 104 | "examGrp": examGrp, 105 | "name": condList["NAME"], 106 | "school": condList["SCHOOL"], 107 | "classList": classList, 108 | "exams": exams, 109 | "examCount": len(exams), 110 | "fieldList": fields, 111 | "uncertain": uncertain, 112 | } 113 | 114 | return RespData(c, result) 115 | } 116 | -------------------------------------------------------------------------------- /lib/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | ) 7 | 8 | // GetStructFields 获取结构体中的所有字段名 9 | func GetStructFields(t interface{}) []string { 10 | s := reflect.ValueOf(t).Elem() 11 | typeOfT := s.Type() 12 | 13 | names := []string{} 14 | for i := 0; i < s.NumField(); i++ { 15 | names = append(names, typeOfT.Field(i).Name) 16 | } 17 | 18 | return names 19 | } 20 | 21 | func JSONEncode(obj interface{}) (str string, err error) { 22 | data, err := json.Marshal(obj) 23 | if err != nil { 24 | return 25 | } 26 | str = string(data) 27 | return 28 | } 29 | 30 | func JSONDecode(str string, t interface{}) error { 31 | return json.Unmarshal([]byte(str), t) 32 | } 33 | 34 | // ContainsInt returns true if an int is present in a iteratee. 35 | func ContainsInt(s []int, v int) bool { 36 | for _, vv := range s { 37 | if vv == v { 38 | return true 39 | } 40 | } 41 | return false 42 | } 43 | 44 | // ContainsInt32 returns true if an int32 is present in a iteratee. 45 | func ContainsInt32(s []int32, v int32) bool { 46 | for _, vv := range s { 47 | if vv == v { 48 | return true 49 | } 50 | } 51 | return false 52 | } 53 | 54 | // ContainsInt64 returns true if an int64 is present in a iteratee. 55 | func ContainsInt64(s []int64, v int64) bool { 56 | for _, vv := range s { 57 | if vv == v { 58 | return true 59 | } 60 | } 61 | return false 62 | } 63 | 64 | // ContainsUInt returns true if an uint is present in a iteratee. 65 | func ContainsUInt(s []uint, v uint) bool { 66 | for _, vv := range s { 67 | if vv == v { 68 | return true 69 | } 70 | } 71 | return false 72 | } 73 | 74 | // ContainsUInt32 returns true if an uint32 is present in a iteratee. 75 | func ContainsUInt32(s []uint32, v uint32) bool { 76 | for _, vv := range s { 77 | if vv == v { 78 | return true 79 | } 80 | } 81 | return false 82 | } 83 | 84 | // ContainsUInt64 returns true if an uint64 is present in a iteratee. 85 | func ContainsUInt64(s []uint64, v uint64) bool { 86 | for _, vv := range s { 87 | if vv == v { 88 | return true 89 | } 90 | } 91 | return false 92 | } 93 | 94 | // ContainsString returns true if a string is present in a iteratee. 95 | func ContainsString(s []string, v string) bool { 96 | for _, vv := range s { 97 | if vv == v { 98 | return true 99 | } 100 | } 101 | return false 102 | } 103 | 104 | // ContainsFloat32 returns true if a float32 is present in a iteratee. 105 | func ContainsFloat32(s []float32, v float32) bool { 106 | for _, vv := range s { 107 | if vv == v { 108 | return true 109 | } 110 | } 111 | return false 112 | } 113 | 114 | // ContainsFloat64 returns true if a float64 is present in a iteratee. 115 | func ContainsFloat64(s []float64, v float64) bool { 116 | for _, vv := range s { 117 | if vv == v { 118 | return true 119 | } 120 | } 121 | return false 122 | } 123 | 124 | // SumInt32 sums a int32 iteratee and returns the sum of all elements 125 | func SumInt32(s []int32) (sum int32) { 126 | for _, v := range s { 127 | sum += v 128 | } 129 | return 130 | } 131 | 132 | // SumInt64 sums a int64 iteratee and returns the sum of all elements 133 | func SumInt64(s []int64) (sum int64) { 134 | for _, v := range s { 135 | sum += v 136 | } 137 | return 138 | } 139 | -------------------------------------------------------------------------------- /model/score.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // Score 成绩模型 4 | type Score struct { 5 | ID int `gorm:"primaryKey;autoIncrement"` // 编号 6 | NAME string `` // 姓名 7 | CODE string `` // 考号 8 | SCHOOL string `` // 学校 9 | CLASS string `` // 班级 10 | TOTAL float64 // 总分 11 | RANK int // 排名 12 | SCHOOL_RANK int // 校排名 13 | CLASS_RANK int // 校排名 14 | 15 | YW float64 // 语文 16 | SX float64 // 数学 17 | YY float64 // 英语 18 | 19 | WL float64 // 物理 20 | HX float64 // 化学 21 | SW float64 // 生物 22 | 23 | ZZ float64 // 政治 24 | LS float64 // 历史 25 | DL float64 // 地理 26 | 27 | LZ float64 // 理综 (物+化+生) 28 | WZ float64 // 文综 (政+历+地) 29 | 30 | ZK float64 // 主科 (语+数+英) 31 | LK float64 // 理科 (语数英+理综) 32 | WK float64 // 文科 (语数英+理综) 33 | 34 | ZK_RANK int // 主排 35 | LK_RANK int // 理排 36 | WK_RANK int // 文排 37 | LZ_RANK int // 理综排 38 | WZ_RANK int // 文综排 39 | YW_RANK int // 语文排 40 | SX_RANK int // 数学排 41 | YY_RANK int // 英语排 42 | WL_RANK int // 物理排 43 | HX_RANK int // 化学排 44 | SW_RANK int // 生物排 45 | ZZ_RANK int // 政治排 46 | LS_RANK int // 历史排 47 | DL_RANK int // 地理排 48 | 49 | ZK_SCHOOL_RANK int // 主班排 50 | LK_SCHOOL_RANK int // 理班排 51 | WK_SCHOOL_RANK int // 文班排 52 | LZ_SCHOOL_RANK int // 理综班排 53 | WZ_SCHOOL_RANK int // 文综班排 54 | YW_SCHOOL_RANK int // 语文班排 55 | SX_SCHOOL_RANK int // 数学班排 56 | YY_SCHOOL_RANK int // 英语班排 57 | WL_SCHOOL_RANK int // 物理班排 58 | HX_SCHOOL_RANK int // 化学班排 59 | SW_SCHOOL_RANK int // 生物班排 60 | ZZ_SCHOOL_RANK int // 政治班排 61 | LS_SCHOOL_RANK int // 历史班排 62 | DL_SCHOOL_RANK int // 地理班排 63 | 64 | ZK_CLASS_RANK int // 主班排 65 | LK_CLASS_RANK int // 理班排 66 | WK_CLASS_RANK int // 文班排 67 | LZ_CLASS_RANK int // 理综班排 68 | WZ_CLASS_RANK int // 文综班排 69 | YW_CLASS_RANK int // 语文班排 70 | SX_CLASS_RANK int // 数学班排 71 | YY_CLASS_RANK int // 英语班排 72 | WL_CLASS_RANK int // 物理班排 73 | HX_CLASS_RANK int // 化学班排 74 | SW_CLASS_RANK int // 生物班排 75 | ZZ_CLASS_RANK int // 政治班排 76 | LS_CLASS_RANK int // 历史班排 77 | DL_CLASS_RANK int // 地理班排 78 | } 79 | 80 | // ScoreFieldTransMap 字段名 => 中文名 81 | var ScoreFieldTransMap map[string]string = map[string]string{ 82 | "ID": "编号", "NAME": "姓名", "CODE": "考号", "SCHOOL": "学校", 83 | "CLASS": "班级", "TOTAL": "总分", "RANK": "排名", 84 | 85 | "YW": "语文", "SX": "数学", "YY": "英语", 86 | "WL": "物理", "HX": "化学", "SW": "生物", 87 | "ZZ": "政治", "LS": "历史", "DL": "地理", 88 | 89 | "LZ": "理综", "WZ": "文综", 90 | "ZK": "主科", "LK": "理科", "WK": "文科", 91 | 92 | "SCHOOL_RANK": "校排名", "CLASS_RANK": "班排名", 93 | } 94 | 95 | // SFieldSubj 所有学科字段名 96 | var SFieldSubj []string = []string{"YW", "SX", "YY", "WL", "HX", "SW", "ZZ", "LS", "DL"} 97 | 98 | // SFieldSubjZK 所有主要科目字段名 99 | var SFieldSubjZK []string = []string{"YW", "SX", "YY"} 100 | 101 | // SFieldSubjLK 所有理科字段名 102 | var SFieldSubjLK []string = []string{"WL", "HX", "SW"} 103 | 104 | // SFieldSubjWK 所有文科字段名 105 | var SFieldSubjWK []string = []string{"ZZ", "LS", "DL"} 106 | 107 | // SFieldExtRank 拓展排名字段 108 | var SFieldExtRank []string = []string{"ZK_RANK", "LK_RANK", "WK_RANK", "LZ_RANK", "WZ_RANK"} 109 | 110 | // SFieldRankAble 可进行排名的字段 111 | var SFieldRankAble []string = []string{"ZK", "LK", "WK", "LZ", "WZ", "YW", "SX", "YY", "WL", "HX", "SW", "ZZ", "LS", "DL"} 112 | 113 | // SFieldExtSum 拓展求和字段 114 | var SFieldExtSum []string = []string{"ZK", "LZ", "WZ", "LK", "WK"} 115 | -------------------------------------------------------------------------------- /cli/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/qwqcode/qwquiver/config" 8 | "github.com/qwqcode/qwquiver/lib" 9 | "github.com/rifflock/lfshook" 10 | "github.com/sirupsen/logrus" 11 | "github.com/spf13/cobra" 12 | v "github.com/spf13/viper" 13 | prefixed "github.com/x-cray/logrus-prefixed-formatter" 14 | ) 15 | 16 | var Banner = ` 17 | ____ __ ______ ___ __ _ _____ _____ 18 | / __ / | /| / / __ / / / /_/ | / / _ \/ ___/ 19 | / /_/ /| |/ |/ / /_/ / /_/ / /| |/ / __/ / 20 | \__, / |__/|__/\__, /\__,_/_/ |___/\___/_/ 21 | /_/ /_/ 22 | 23 | A website for exploring and analyzing exam results. 24 | 25 | More detail on https://github.com/qwqcode/qwquiver 26 | 27 | (c) 2020 qwqaq.com` 28 | 29 | var ( 30 | cfgFile string 31 | 32 | rootCmd = &cobra.Command{ 33 | Use: "qwquiver", 34 | Short: "A web-based exam results explorer.", 35 | Long: Banner, 36 | /* Run: func(cmd *cobra.Command, args []string) { 37 | // Do Stuff Here 38 | } */ 39 | } 40 | ) 41 | 42 | // Execute is execute cobra 43 | func Execute() { 44 | if err := rootCmd.Execute(); err != nil { 45 | fmt.Println(err) 46 | os.Exit(1) 47 | } 48 | } 49 | 50 | func init() { 51 | cobra.OnInitialize(initConfig) 52 | cobra.OnInitialize(initLog) 53 | cobra.OnInitialize(initDB) 54 | 55 | flagV(rootCmd, "dbFile", "./data/qwquiver.db", "数据文件路径") 56 | flagV(rootCmd, "logFile", "./data/qwquiver.log", "日志文件路径") 57 | rootCmd.SetVersionTemplate("qwquiver {{printf \"version %s\" .Version}}\n") 58 | rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "配置文件路径 (defaults are './.qwquiver', '$HOME/.qwquiver' or '/etc/qwquiver/.qwquiver')") 59 | 60 | // 功能注入 61 | for _, inject := range config.CMDInjections { 62 | inject(rootCmd) 63 | } 64 | } 65 | 66 | func initConfig() { 67 | config.Init(cfgFile) 68 | } 69 | 70 | func initLog() { 71 | // 初始化日志 72 | stdFormatter := &prefixed.TextFormatter{ 73 | DisableTimestamp: true, 74 | ForceFormatting: true, 75 | ForceColors: true, 76 | DisableColors: false, 77 | } // 命令行输出格式 78 | fileFormatter := &prefixed.TextFormatter{ 79 | FullTimestamp: true, 80 | TimestampFormat: "2006-01-02.15:04:05.000000", 81 | ForceFormatting: true, 82 | ForceColors: false, 83 | DisableColors: true, 84 | } // 文件输出格式 85 | 86 | // logrus.SetLevel(logrus.DebugLevel) 87 | logrus.SetFormatter(stdFormatter) 88 | logrus.SetOutput(os.Stdout) 89 | 90 | // 文件保存 91 | pathMap := lfshook.PathMap{ 92 | logrus.InfoLevel: config.Instance.LogFile, 93 | logrus.DebugLevel: config.Instance.LogFile, 94 | logrus.ErrorLevel: config.Instance.LogFile, 95 | } 96 | logrus.AddHook(lfshook.NewHook( 97 | pathMap, 98 | fileFormatter, 99 | )) 100 | } 101 | 102 | func initDB() { 103 | lib.OpenDb(config.Instance.DbFile) 104 | } 105 | 106 | //// 捷径函数 //// 107 | 108 | func flag(cmd *cobra.Command, name string, defaultVal interface{}, usage string) { 109 | f := cmd.PersistentFlags() 110 | switch y := defaultVal.(type) { 111 | case bool: 112 | f.Bool(name, y, usage) 113 | case int: 114 | f.Int(name, y, usage) 115 | case string: 116 | f.String(name, y, usage) 117 | } 118 | v.SetDefault(name, defaultVal) 119 | } 120 | 121 | func flagP(cmd *cobra.Command, name, shorthand string, defaultVal interface{}, usage string) { 122 | f := cmd.PersistentFlags() 123 | switch y := defaultVal.(type) { 124 | case bool: 125 | f.BoolP(name, shorthand, y, usage) 126 | case int: 127 | f.IntP(name, shorthand, y, usage) 128 | case string: 129 | f.StringP(name, shorthand, y, usage) 130 | } 131 | v.SetDefault(name, defaultVal) 132 | } 133 | 134 | func flagV(cmd *cobra.Command, name string, defaultVal interface{}, usage string) { 135 | flag(cmd, name, defaultVal, usage) 136 | v.BindPFlag(name, cmd.PersistentFlags().Lookup(name)) 137 | } 138 | 139 | func flagPV(cmd *cobra.Command, name, shorthand string, defaultVal interface{}, usage string) { 140 | flagP(cmd, name, shorthand, defaultVal, usage) 141 | v.BindPFlag(name, cmd.PersistentFlags().Lookup(name)) 142 | } 143 | -------------------------------------------------------------------------------- /lib/exd/exd.go: -------------------------------------------------------------------------------- 1 | package exd 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/araddon/dateparse" 10 | "github.com/qwqcode/qwquiver/lib" 11 | "github.com/qwqcode/qwquiver/model" 12 | "github.com/thoas/go-funk" 13 | "gorm.io/gorm" 14 | ) 15 | 16 | // GetAllExams 获取所有 Exam 17 | func GetAllExams() []model.Exam { 18 | exams := []model.Exam{} 19 | lib.DB.Table(lib.ExamConfTb).Find(&exams) 20 | 21 | // 根据时间降序 22 | sort.Slice(exams, func(i, j int) bool { 23 | if exams[i].Date == "" || exams[j].Date == "" { 24 | return false 25 | } 26 | t1, err := dateparse.ParseAny(exams[i].Date) 27 | t2, err := dateparse.ParseAny(exams[j].Date) 28 | if err != nil { 29 | return false 30 | } 31 | return t1.After(t2) 32 | }) 33 | 34 | return exams 35 | } 36 | 37 | // GetExam 获取 Exam 38 | func GetExam(name string) *model.Exam { 39 | var exam model.Exam 40 | lib.DB.Table(lib.ExamConfTb).First(&exam, "name = ?", name) 41 | return &exam 42 | } 43 | 44 | // GetExamName 获取 Exam 名(非 tableName) 45 | func GetExamName(name string) string { 46 | return strings.TrimPrefix(name, lib.ExamTbPf) 47 | } 48 | 49 | // GetExamTableName 获取 Exam 数据表名 50 | func GetExamTableName(name string) string { 51 | return lib.ExamTbPf + strings.TrimPrefix(name, lib.ExamTbPf) 52 | } 53 | 54 | // HasExam 判断 Exam 是否存在 55 | func HasExam(name string) bool { 56 | return lib.DB.Migrator().HasTable(GetExamTableName(name)) 57 | } 58 | 59 | // RemoveExam 删除 Exam 60 | func RemoveExam(name string) error { 61 | exam := GetExam(name) 62 | if exam == nil { 63 | return errors.New("未找到 Exam: " + name) 64 | } 65 | lib.DB.Table(lib.ExamConfTb).Delete(exam) 66 | return lib.DropTable(GetExamTableName(name)) 67 | } 68 | 69 | // FilterScores 查询指定的成绩数据 70 | func FilterScores(query *gorm.DB, rawMatchCond map[string]string, regMode bool) *gorm.DB { 71 | if regMode { 72 | i := 0 73 | for key, val := range rawMatchCond { 74 | if i == 0 { 75 | query = query.Where(key+` LIKE ?`, `%`+val+`%`) 76 | } else { 77 | query = query.Or(key+` LIKE ?`, `%`+val+`%`) 78 | } 79 | i++ 80 | } 81 | } else { 82 | query = query.Where(rawMatchCond) 83 | } 84 | 85 | return query 86 | } 87 | 88 | // FilterScoresByRegStr 模糊查询指定的成绩数据 89 | func FilterScoresByRegStr(query *gorm.DB, regStr string) *gorm.DB { 90 | return FilterScores(query, map[string]string{ 91 | "NAME": regStr, 92 | "SCHOOL": regStr, 93 | "CLASS": regStr, 94 | "CODE": regStr, 95 | }, true) 96 | } 97 | 98 | // GetAllExamGrps 获取所有 Exam 的 Grp 99 | func GetAllExamGrps() []string { 100 | grps := []string{} 101 | for _, exam := range GetAllExams() { 102 | if exam.Grp != "" { 103 | if !funk.ContainsString(grps, exam.Grp) { 104 | grps = append(grps, exam.Grp) // 追加不重复的 Grp 105 | } 106 | } 107 | } 108 | return grps 109 | } 110 | 111 | // GetExamsByGrp 获取指定 Grp 的所有 Exam 112 | func GetExamsByGrp(grp string) []model.Exam { 113 | exams := []model.Exam{} 114 | for _, exam := range GetAllExams() { 115 | if exam.Grp == grp { 116 | exams = append(exams, exam) 117 | } 118 | } 119 | return exams 120 | } 121 | 122 | // GetExamConfJSONStr 获取考试配置数据 JSON 字符串,beauty=是否格式化JSON 123 | func GetExamConfJSONStr(name string, beauty bool) string { 124 | examConf := GetExam(name) 125 | if examConf == nil { 126 | return "" 127 | } 128 | if beauty { 129 | json, _ := json.MarshalIndent(examConf, "", " ") 130 | return string(json) 131 | } 132 | 133 | json, _ := json.Marshal(examConf) 134 | return string(json) 135 | } 136 | 137 | // UpdateExamConf 修改考试配置 138 | func UpdateExamConf(examConf *model.Exam) error { 139 | return lib.DB.Table(lib.ExamConfTb).Save(examConf).Error 140 | } 141 | 142 | // UpdateExamConfByJSON 修改考试配置,通过 JSON 数据 143 | func UpdateExamConfByJSON(examConf *model.Exam, jsonStr string) error { 144 | var nExamConf model.Exam 145 | // testData := []byte(`{"Name":"测试","Grp":"233","Label":"233","Subj":["DL"],"SubjFullScore":{"DL":100},"Date":"dd","Note":"ww"}`) 146 | if err := json.Unmarshal([]byte(jsonStr), &nExamConf); err != nil { 147 | panic(err) 148 | } 149 | 150 | examConf.Grp = nExamConf.Grp 151 | examConf.Label = nExamConf.Label 152 | examConf.Subj = nExamConf.Subj 153 | examConf.SubjFullScore = nExamConf.SubjFullScore 154 | examConf.Date = nExamConf.Date 155 | examConf.Note = nExamConf.Note 156 | // TODO ... 比较 dirty 的代码,不够 flexible,先将就这样 157 | 158 | return UpdateExamConf(examConf) 159 | } 160 | -------------------------------------------------------------------------------- /cli/cmd/exam.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/qwqcode/qwquiver/lib/exd" 8 | "github.com/sirupsen/logrus" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // examCmd represents the db command 13 | var examCmd = &cobra.Command{ 14 | Use: "exam [command]", 15 | Version: rootCmd.Version, 16 | Aliases: []string{}, 17 | Short: "考试管理", 18 | Long: `qwquiver 的考试数据管理工具`, 19 | Args: cobra.NoArgs, 20 | } 21 | 22 | var examListCmd = &cobra.Command{ 23 | Use: "list", 24 | Aliases: []string{"ls"}, 25 | Short: "列出所有 Exam", 26 | Run: examListRun, 27 | Args: cobra.NoArgs, 28 | } 29 | 30 | var examRemoveCmd = &cobra.Command{ 31 | Use: "remove [ExamName]", 32 | Aliases: []string{"rm", "remove", "del"}, 33 | Short: "删除指定的 Exam", 34 | Run: examRemoveRun, 35 | Args: cobra.RangeArgs(1, 1), 36 | } 37 | 38 | // $ qwquiver exam config 39 | var examConfigCmd = &cobra.Command{ 40 | Use: "config [Command]", 41 | Aliases: []string{"conf"}, 42 | Short: "配置指定的 Exam", 43 | Args: cobra.NoArgs, 44 | } 45 | 46 | // $ qwquiver exam config set 47 | var configSetCmd = &cobra.Command{ 48 | Use: "set [ExamName]", 49 | Short: "配置指定的 Exam", 50 | Args: cobra.RangeArgs(1, 2), 51 | Run: examConfSetRun, 52 | } 53 | 54 | // $ qwquiver exam config get 55 | var configGetCmd = &cobra.Command{ 56 | Use: "get [ExamName]", 57 | Short: "获取指定 Exam 的配置", 58 | Args: cobra.RangeArgs(1, 1), 59 | Run: examConfigGetRun, 60 | } 61 | 62 | func init() { 63 | rootCmd.AddCommand(examCmd) 64 | 65 | // list 66 | examCmd.AddCommand(examListCmd) 67 | 68 | // remove 69 | examCmd.AddCommand(examRemoveCmd) 70 | flagP(examRemoveCmd, "force", "f", false, "强制删除,没有任何提示") 71 | 72 | // config 73 | examCmd.AddCommand(examConfigCmd) 74 | 75 | // config set 76 | examConfigCmd.AddCommand(configSetCmd) 77 | flag(configSetCmd, "Grp", "", "修改 Exam Grp") 78 | flag(configSetCmd, "Label", "", "修改 Exam Label") 79 | flag(configSetCmd, "Subj", "", "修改 Exam Subj (JSON格式)") 80 | flag(configSetCmd, "SubjFullScore", "", "修改 Exam SubjFullScore (JSON格式)") 81 | flag(configSetCmd, "Date", "", "修改 Exam Date") 82 | flag(configSetCmd, "Note", "", "修改 Exam Note") 83 | flag(configSetCmd, "json", "", "直接通过 JSON 格式修改所有参数") 84 | 85 | // config get 86 | examConfigCmd.AddCommand(configGetCmd) 87 | flagP(configGetCmd, "inline", "i", false, "输出未格式化的 JSON 数据") 88 | } 89 | 90 | func examListRun(cmd *cobra.Command, args []string) { 91 | allExams := exd.GetAllExams() 92 | fmt.Printf("共有 %d 个 Exam\n", len(allExams)) 93 | for i, exam := range allExams { 94 | itemLen := exam.CountScores() 95 | fmt.Printf(" %d. %s (%d 条数据)\n", i+1, exam.Name, itemLen) 96 | } 97 | } 98 | 99 | func examRemoveRun(cmd *cobra.Command, args []string) { 100 | examName := args[0] 101 | 102 | if force, _ := cmd.Flags().GetBool("force"); !force { 103 | fmt.Println("请加上 flag '--force' 确认删除 '" + examName + "' 这个 Exam") 104 | return 105 | } 106 | 107 | err := exd.RemoveExam(examName) 108 | if err != nil { 109 | fmt.Println("删除 Exam 发生错误") 110 | fmt.Println(err) 111 | } 112 | } 113 | 114 | // config set 115 | func examConfSetRun(cmd *cobra.Command, args []string) { 116 | examName := args[0] 117 | if !exd.HasExam(examName) { 118 | logrus.Error("Exam '" + examName + "' 不存在") 119 | return 120 | } 121 | 122 | examConf := exd.GetExam(examName) 123 | 124 | jsonStr, _ := cmd.Flags().GetString("json") 125 | if jsonStr == "" { 126 | jsonStr = args[1] 127 | } 128 | 129 | if jsonStr != "" { 130 | if err := exd.UpdateExamConfByJSON(examConf, jsonStr); err != nil { 131 | logrus.Error("保存 ExamConf 发生错误 ", err) 132 | return 133 | } 134 | } else { 135 | // 非直接 JSON 修改模式 136 | if grp, _ := cmd.Flags().GetString("Grp"); grp != "" { 137 | examConf.Grp = grp 138 | } 139 | if label, _ := cmd.Flags().GetString("Label"); label != "" { 140 | examConf.Label = label 141 | } 142 | if subjStr, _ := cmd.Flags().GetString("Subj"); subjStr != "" { 143 | var subjList []string 144 | err := json.Unmarshal([]byte(subjStr), &subjList) 145 | if err == nil && subjList != nil { 146 | examConf.Subj = subjStr 147 | } else { 148 | logrus.Error("尝试解析 flag '--Subj' 的 JSON 数据时发生错误 ", err) 149 | } 150 | } 151 | if sfc, _ := cmd.Flags().GetString("SubjFullScore"); sfc != "" { 152 | var subjFullScore map[string]float64 153 | err := json.Unmarshal([]byte(sfc), &subjFullScore) 154 | if err == nil && subjFullScore != nil { 155 | examConf.SubjFullScore = sfc 156 | } else { 157 | logrus.Error("尝试解析 flag '--SubjFullScore' 的 JSON 数据时发生错误 ", err) 158 | } 159 | } 160 | if date, _ := cmd.Flags().GetString("Date"); date != "" { 161 | examConf.Date = date 162 | } 163 | if note, _ := cmd.Flags().GetString("Note"); note != "" { 164 | examConf.Note = note 165 | } 166 | // TODO ... 比较 dirty 的代码,先将就这样 167 | exd.UpdateExamConf(examConf) 168 | } 169 | 170 | examConfJSON := exd.GetExamConfJSONStr(examName, true) 171 | fmt.Println("ExamConf 已更新") 172 | fmt.Println(string(examConfJSON)) 173 | } 174 | 175 | // config get 176 | func examConfigGetRun(cmd *cobra.Command, args []string) { 177 | examName := args[0] 178 | if !exd.HasExam(examName) { 179 | logrus.Error("Exam '" + examName + "' 不存在") 180 | return 181 | } 182 | 183 | examConfJSON := "NULL" 184 | if isInlineJSON, _ := cmd.Flags().GetBool("inline"); isInlineJSON { 185 | examConfJSON = exd.GetExamConfJSONStr(examName, false) 186 | } else { 187 | examConfJSON = exd.GetExamConfJSONStr(examName, true) 188 | } 189 | 190 | fmt.Println(string(examConfJSON)) 191 | } 192 | -------------------------------------------------------------------------------- /http/query.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "math" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/labstack/echo/v4" 11 | "github.com/qwqcode/qwquiver/lib/exd" 12 | "github.com/qwqcode/qwquiver/model" 13 | "gorm.io/gorm" 14 | ) 15 | 16 | // query API 的公共参数 17 | type queryAPICommonParms struct { 18 | examName string 19 | whereJSONStr string 20 | pageStr string 21 | pageSizeStr string 22 | sortJSONStr string 23 | initConf Map 24 | 25 | exam *model.Exam 26 | query *gorm.DB 27 | condList map[string]string 28 | sortList map[string]int 29 | dataDesc string 30 | subjectList []string 31 | } 32 | 33 | func getQueryAPICommonParms(c echo.Context) *queryAPICommonParms { 34 | p := &queryAPICommonParms{ 35 | examName: c.QueryParam("exam"), 36 | whereJSONStr: c.QueryParam("where"), 37 | pageStr: c.QueryParam("page"), 38 | pageSizeStr: c.QueryParam("pageSize"), 39 | sortJSONStr: c.QueryParam("sort"), 40 | } 41 | 42 | isInitReq := c.QueryParam("init") != "" 43 | if isInitReq { 44 | // 若为初始化请求 45 | examMap := exd.GetAllExams() 46 | examGrpList := exd.GetAllExams() 47 | fieldTransDict := model.ScoreFieldTransMap 48 | 49 | if len(examMap) == 0 { 50 | RespError(c, "未找到任何考试数据,请导入数据") 51 | return nil 52 | } 53 | 54 | if p.examName == "" { 55 | p.examName = examMap[0].Name // 设置默认 exam 56 | p.exam = &examMap[0] 57 | } else { 58 | p.exam = exd.GetExam(p.examName) 59 | } 60 | 61 | p.initConf = Map{ 62 | "examMap": examMap, 63 | "examGrpList": examGrpList, 64 | "fieldTransDict": fieldTransDict, 65 | } 66 | } else { 67 | p.exam = exd.GetExam(p.examName) 68 | } 69 | 70 | if p.exam == nil { 71 | RespError(c, "Exam 不存在") 72 | return nil 73 | } 74 | 75 | p.query = p.exam.NewQuery() 76 | 77 | // JSON 解析 78 | if p.whereJSONStr != "" { // Note: json 不允许出现 Number 类型的 Value (eg.{"Class":1} 必须为 {"Class":"1"}) 79 | if err := json.Unmarshal([]byte(p.whereJSONStr), &p.condList); err != nil { 80 | RespError(c, "where 参数 JSON 解析失败", err.Error()) 81 | return nil 82 | } 83 | } 84 | if p.sortJSONStr != "" { 85 | if err := json.Unmarshal([]byte(p.sortJSONStr), &p.sortList); err != nil { 86 | RespError(c, "sort 参数 JSON 解析失败", err.Error()) 87 | return nil 88 | } 89 | } 90 | 91 | // 数据内容描述 92 | if p.condList == nil { 93 | p.dataDesc = "全部考生成绩" 94 | } else if len(p.condList) == 1 && p.condList["NAME"] != "" { 95 | p.dataDesc = fmt.Sprintf(`数据满足 “%s” 的考生成绩`, p.condList["NAME"]) 96 | } else if p.condList["CLASS"] == "" && p.condList["SCHOOL"] != "" { 97 | p.dataDesc = fmt.Sprintf(`%s · 全校成绩`, p.condList["SCHOOL"]) 98 | } else if p.condList["CLASS"] != "" && p.condList["SCHOOL"] != "" { 99 | p.dataDesc = fmt.Sprintf(`%s %s · 班级成绩`, p.condList["SCHOOL"], p.condList["CLASS"]) 100 | } 101 | 102 | // 查询条件 103 | if p.condList == nil || len(p.condList) == 0 { 104 | // 全部数据 105 | 106 | } else { 107 | if len(p.condList) == 1 && p.condList["NAME"] != "" { 108 | // 模糊查询 109 | p.query = exd.FilterScoresByRegStr(p.query, p.condList["NAME"]) 110 | } else { 111 | // 精确查询 112 | p.query = exd.FilterScores(p.query, p.condList, false) 113 | } 114 | } 115 | 116 | // 排序规则 117 | if p.sortList == nil || len(p.sortList) == 0 { 118 | p.sortList = map[string]int{"TOTAL": -1} 119 | } 120 | for key, t := range p.sortList { 121 | if t == 1 { 122 | p.query = p.query.Order(key + ` asc`) // TODO: sql注入风险 待测试 123 | } else if t == -1 { 124 | p.query = p.query.Order(key + ` desc`) 125 | } 126 | break 127 | } 128 | 129 | // 考试科目 130 | var err error 131 | p.subjectList, err = p.exam.GetSubjects() 132 | if err != nil { 133 | RespError(c, "考试科目数据获取失败", err.Error()) 134 | return nil 135 | } 136 | 137 | return p 138 | } 139 | 140 | // 查询 get api/query 141 | func queryHandler(c echo.Context) error { 142 | p := getQueryAPICommonParms(c) 143 | if p == nil { 144 | return nil 145 | } 146 | 147 | // 分页操作 148 | var total int64 // 数据总数 149 | p.query.Count(&total) 150 | 151 | var page, pageSize int // 页码 152 | page, _ = strconv.Atoi(p.pageStr) 153 | if page <= 0 { 154 | page = 1 155 | } 156 | pageSize, _ = strconv.Atoi(p.pageSizeStr) 157 | if pageSize <= 0 { 158 | pageSize = 50 159 | } 160 | 161 | var lastPage int // 最后一页 162 | var offset int // 数据偏移量 163 | lastPage = int(math.Ceil(float64(total) / float64(pageSize))) 164 | offset = (page - 1) * pageSize 165 | 166 | // 响应数据 167 | scList := []model.Score{} 168 | p.query.Offset(offset).Limit(pageSize).Find(&scList) 169 | 170 | pageResult := Map{ 171 | "examName": p.examName, 172 | "dataDesc": p.dataDesc, 173 | "page": page, 174 | "pageSize": pageSize, 175 | "total": total, 176 | "lastPage": lastPage, 177 | "subjectList": p.subjectList, 178 | "list": scList, 179 | "examConf": p.exam, 180 | "sortList": p.sortList, 181 | "condList": p.condList, 182 | } 183 | 184 | if p.initConf != nil { 185 | pageResult["initConf"] = p.initConf 186 | } 187 | 188 | return RespData(c, pageResult) 189 | } 190 | 191 | // 求均值 get api/query/avg 192 | func queryAvgHandler(c echo.Context) error { 193 | p := getQueryAPICommonParms(c) 194 | if p == nil { 195 | return nil 196 | } 197 | 198 | avgKeys := []string{"TOTAL", "LZ", "WZ", "ZK"} 199 | avgKeys = append(avgKeys, p.subjectList...) 200 | 201 | avgSQL := "" 202 | for _, k := range avgKeys { 203 | avgSQL += "avg(" + k + ") as " + k + "," 204 | } 205 | avgSQL = strings.TrimRight(avgSQL, ",") 206 | 207 | var result []map[string]interface{} 208 | p.query.Select(avgSQL).Find(&result) 209 | 210 | avgList := result[0] 211 | return RespData(c, Map{ 212 | "avgList": avgList, 213 | }) 214 | } 215 | -------------------------------------------------------------------------------- /lib/exd/excel_importer.go: -------------------------------------------------------------------------------- 1 | package exd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "path/filepath" 7 | "reflect" 8 | "sort" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/360EntSecGroup-Skylar/excelize" 13 | "github.com/cheggaaa/pb" 14 | "github.com/oleiade/reflections" 15 | "github.com/qwqcode/qwquiver/lib" 16 | "github.com/qwqcode/qwquiver/lib/utils" 17 | "github.com/qwqcode/qwquiver/model" 18 | "github.com/sirupsen/logrus" 19 | "github.com/thoas/go-funk" 20 | ) 21 | 22 | // ImportExcel 数据导入 23 | func ImportExcel(examName string, filename string, examConfJSON string) { 24 | Exam := &model.Exam{} 25 | 26 | examName = strings.TrimSpace(examName) 27 | filename = strings.TrimSpace(filename) 28 | 29 | if filename == "" { 30 | logrus.Error("ExcelImporter: Excel 文件路径不能为空") 31 | return 32 | } 33 | 34 | if fileExt := filepath.Ext(filename); fileExt != ".xlsx" { 35 | logrus.Error("ExcelImporter: 仅支持导入 .xlsx 文件,'" + fileExt + "' 格式不被支持") 36 | return 37 | } 38 | 39 | if examName == "" { // 若 examName 为空,默认使用 Excel 文件名 40 | fileBasename := filepath.Base(filename) 41 | examName = strings.TrimSuffix(fileBasename, filepath.Ext(fileBasename)) 42 | } 43 | 44 | // 查询是否相同 examName 的 bucket 已存在 45 | if HasExam(examName) { 46 | logrus.Error("ExcelImporter: 名称为 '" + examName + "' 的数据表已存在,无法重复导入;您可以执行 `qwquiver exam` 对现有 Exam 进行删除操作") 47 | return 48 | } 49 | 50 | examTable := GetExamTableName(examName) 51 | 52 | // examConf 处理 53 | var jExamConf model.Exam 54 | if examConfJSON != "" { 55 | if err := json.Unmarshal([]byte(examConfJSON), &jExamConf); err != nil { 56 | logrus.Error("ExcelImporter: 解析 examConf JSON 发生错误 ", err) 57 | return 58 | } 59 | Exam = &jExamConf 60 | } 61 | Exam.Name = examName 62 | 63 | logrus.Info("数据导入任务") 64 | logrus.Info("ExamName='" + examName + "', ExcelFile='" + filename + "'") 65 | 66 | file, err := excelize.OpenFile(filename) 67 | if err != nil { 68 | logrus.Error("ExcelImporter: Excel 打开失败 ", err) 69 | return 70 | } 71 | 72 | rows := file.GetRows(file.GetSheetName(1)) 73 | if len(rows) < 1 { 74 | logrus.Error("ExcelImporter: Excel 数据不能为空") 75 | return 76 | } 77 | 78 | optionalFields := utils.GetStructFields(&model.Score{}) 79 | optionalFieldsTr := (funk.Values(model.ScoreFieldTransMap)).([]string) // 中文字段名 80 | 81 | logrus.Debugln("字段KEY ", optionalFields) 82 | logrus.Debugln("字段名 ", optionalFieldsTr) 83 | 84 | fmt.Println() 85 | fmt.Println(" - 表头 =", rows[0]) 86 | 87 | // 读取表头 88 | fieldPos := map[string]int{} // 字段名 => Position 89 | fieldList := []string{} // 所有字段 90 | for pos, val := range rows[0] { 91 | if funk.ContainsString(optionalFields, val) { 92 | fieldPos[val] = pos 93 | fieldList = append(fieldList, val) 94 | } else if funk.ContainsString(optionalFieldsTr, val) { 95 | // 若表头为中文名,则先翻译为原始字段名 96 | val, _ := funk.FindKey(model.ScoreFieldTransMap, func(trN string) bool { 97 | return trN == val 98 | }) 99 | if val != "" { 100 | fieldPos[val.(string)] = pos 101 | fieldList = append(fieldList, val.(string)) 102 | } 103 | } 104 | } 105 | subjList := funk.IntersectString(fieldList, model.SFieldSubj) // 考试科目 106 | 107 | subjListJSON, _ := utils.JSONEncode(subjList) 108 | Exam.Subj = subjListJSON 109 | 110 | fmt.Println(" - 表头 POSITION =", fieldPos) 111 | fmt.Println(" - 考试科目 =", subjList) 112 | fmt.Println() 113 | 114 | scList := &[](*model.Score){} 115 | schoolList := map[string][]string{} // 学校&班级列表 116 | 117 | // 读取数据 118 | rankAbleFn := []string{} // 可排序的字段 字段名 119 | for _, row := range rows[1:] { 120 | sc := &model.Score{} 121 | 122 | for name, pos := range fieldPos { 123 | // 将表格值添加到 sc 中 124 | rf := reflect.ValueOf(sc).Elem().FieldByName(name) 125 | switch reflect.ValueOf(model.Score{}).FieldByName(name).Kind() { 126 | case reflect.String: 127 | rf.SetString(row[pos]) 128 | case reflect.Float64: 129 | val, _ := strconv.ParseFloat(row[pos], 64) 130 | rf.SetFloat(val) 131 | case reflect.Int: 132 | val, _ := strconv.Atoi(row[pos]) 133 | rf.SetInt(int64(val)) 134 | } 135 | } 136 | 137 | // 记录学校班级 138 | if sc.SCHOOL != "" { 139 | if schoolList[sc.SCHOOL] == nil { 140 | schoolList[sc.SCHOOL] = []string{} 141 | } 142 | if sc.CLASS != "" && !funk.ContainsString(schoolList[sc.SCHOOL], sc.CLASS) { 143 | schoolList[sc.SCHOOL] = append(schoolList[sc.SCHOOL], sc.CLASS) 144 | } 145 | } 146 | 147 | // 求和 148 | sc.ZK = sc.YW + sc.SX + sc.YY // 主科 149 | sc.LZ = sc.WL + sc.HX + sc.SW // 理综 150 | sc.WZ = sc.ZZ + sc.LS + sc.DL // 文综 151 | sc.LK = sc.ZK + sc.LZ // 理科 152 | sc.WK = sc.ZK + sc.WZ // 文科 153 | sc.TOTAL = sc.ZK + sc.LZ + sc.WZ // 总分 154 | 155 | // 收集可排序的字段 156 | for _, field := range model.SFieldRankAble { 157 | fieldVal, err := reflections.GetField(sc, field) 158 | if err == nil && fieldVal.(float64) > 0 { // 字段值存在 159 | if !funk.Contains(rankAbleFn, field) { 160 | rankAbleFn = append(rankAbleFn, field) 161 | } 162 | } 163 | } 164 | 165 | *scList = append(*scList, sc) 166 | } 167 | logrus.Info("总分数据已生成") 168 | fmt.Println() 169 | fmt.Println(" - 排名执行字段 =", rankAbleFn) 170 | fmt.Println(" 开始生成排名数据...") 171 | fmt.Println() 172 | 173 | // 生成排名数据 func 174 | Rank := func(scList []*model.Score, rankByF string, outputF string) { 175 | nScList := make([]*model.Score, len(scList)) 176 | copy(nScList, scList) 177 | 178 | // 成绩从大到小排序 179 | sort.Slice(nScList, func(i, j int) bool { 180 | iv := reflect.ValueOf(nScList[i]).Elem().FieldByName(rankByF).Float() // 当前元素 181 | jv := reflect.ValueOf(nScList[j]).Elem().FieldByName(rankByF).Float() // 下一个元素 182 | 183 | return iv > jv // 从大到小,降序;当前元素是否大于下一个元素,当前元素是否排前面 (true/false) 184 | }) 185 | 186 | // 建立排名 187 | var tRank int = 1 188 | var tNum float64 = -1 189 | var tSameNum int = 1 190 | for _, sc := range nScList { 191 | num := reflect.ValueOf(sc).Elem().FieldByName(rankByF).Float() 192 | setRankData := func(rankVal int) { 193 | for _, rawSc := range scList { 194 | if *rawSc == *sc { 195 | reflect.ValueOf(rawSc).Elem().FieldByName(outputF).SetInt(int64(rankVal)) 196 | break 197 | } 198 | } 199 | // fmt.Println(rawSc.(*model.Score).Name, " ", rankVal) 200 | } 201 | 202 | if tNum == -1 { // 最高分初始化 203 | tNum = num 204 | setRankData(tRank) 205 | continue 206 | } 207 | 208 | if num == tNum { 209 | tSameNum++ 210 | } else if num < tNum { 211 | tRank = tRank + tSameNum 212 | tNum = num 213 | tSameNum = 1 214 | } 215 | setRankData(tRank) 216 | } 217 | } 218 | 219 | // 执行排名 220 | Rank(*scList, "TOTAL", "RANK") 221 | for _, rankByF := range rankAbleFn { 222 | Rank(*scList, rankByF, rankByF+"_RANK") // 相对于全部数据的排名 223 | } 224 | 225 | for school, classes := range schoolList { 226 | // 相对于学校的排名 227 | scListRtSchool := funk.Filter(*scList, func(sc *model.Score) bool { 228 | return sc.SCHOOL == school 229 | }).([]*model.Score) 230 | Rank(scListRtSchool, "TOTAL", "SCHOOL_RANK") 231 | for _, rankByF := range rankAbleFn { 232 | Rank(scListRtSchool, rankByF, rankByF+"_SCHOOL_RANK") 233 | } 234 | 235 | // 相对于班级的排名 236 | for _, class := range classes { 237 | scListRtClass := funk.Filter(*scList, func(sc *model.Score) bool { 238 | return sc.SCHOOL == school && sc.CLASS == class 239 | }).([]*model.Score) 240 | Rank(scListRtClass, "TOTAL", "CLASS_RANK") 241 | for _, rankByF := range rankAbleFn { 242 | Rank(scListRtClass, rankByF, rankByF+"_CLASS_RANK") 243 | } 244 | } 245 | } 246 | 247 | logrus.Info("排名数据已生成") 248 | 249 | // 成绩从高到低排序 250 | sort.Slice(*scList, func(i, j int) bool { 251 | return (*scList)[i].TOTAL > (*scList)[j].TOTAL 252 | }) 253 | 254 | fmt.Println() 255 | 256 | consoleOutput := func() { 257 | for _, sc := range *scList { 258 | s := "" 259 | for _, f := range utils.GetStructFields(&model.Score{}) { 260 | rf := reflect.ValueOf(sc).Elem().FieldByName(f) 261 | s += model.ScoreFieldTransMap[f] + ": " 262 | switch reflect.ValueOf(model.Score{}).FieldByName(f).Kind() { 263 | case reflect.String: 264 | s += rf.String() 265 | case reflect.Float64: 266 | s += fmt.Sprintf("%.6f", rf.Float()) 267 | case reflect.Int: 268 | s += fmt.Sprintf("%d", rf.Int()) 269 | } 270 | s += ", " 271 | } 272 | fmt.Println(s) 273 | } 274 | } 275 | if false { 276 | consoleOutput() 277 | } 278 | 279 | // 处理附加数据 280 | 281 | // 尝试获取每科的最高分数 282 | TryGetFullScore := func() (subjFullScore map[string]float64) { 283 | subjFullScore = map[string]float64{} 284 | RecordOnce := func(subj string, score float64) { 285 | if subj == "" || score <= 0 { 286 | return 287 | } 288 | 289 | var predScore float64 = 0 290 | if score > 110 { 291 | predScore = 150 292 | } else if score > 100 { 293 | predScore = 110 294 | } else if score > 90 { 295 | predScore = 100 296 | } else if score > 80 { 297 | predScore = 90 298 | } else if score > 50 { 299 | predScore = 60 300 | } else if score > 40 { 301 | predScore = 50 302 | } 303 | 304 | if predScore > subjFullScore[subj] { 305 | subjFullScore[subj] = predScore 306 | } 307 | } 308 | 309 | for _, sc := range *scList { 310 | for _, subj := range model.SFieldSubj { 311 | score := reflect.ValueOf(sc).Elem().FieldByName(subj).Float() 312 | RecordOnce(subj, score) 313 | } 314 | } 315 | 316 | return 317 | } 318 | 319 | var subjFullScore map[string]float64 320 | if err := utils.JSONDecode(Exam.SubjFullScore, &subjFullScore); err != nil { 321 | subjFullScore = map[string]float64{} 322 | } 323 | 324 | for subj, fullScore := range TryGetFullScore() { 325 | if fullScore != 0 && subjFullScore[subj] == 0 { 326 | subjFullScore[subj] = fullScore 327 | } 328 | } 329 | subjFullScoreJSON, _ := utils.JSONEncode(subjFullScore) 330 | Exam.SubjFullScore = subjFullScoreJSON 331 | 332 | logrus.Info("学科最高分数已获取") 333 | 334 | // 将数据导入数据库 335 | if err := CreateExam(Exam); err != nil { 336 | logrus.Error("创建 Exam 发生错误 ", err) 337 | panic(err) 338 | } 339 | 340 | fmt.Print("\n") 341 | saveBar := pb.StartNew(len(*scList)) 342 | 343 | // 开始遍历导入数据库 344 | // TODO: https://gorm.io/zh_CN/docs/transactions.html 可改用 transactions 345 | saveErr := []error{} 346 | itemCount := 0 347 | for _, sc := range *scList { 348 | if rs := lib.DB.Table(examTable).Create(sc); rs.Error != nil { 349 | logrus.Error(rs.Error) 350 | saveErr = append(saveErr, rs.Error) 351 | } 352 | saveBar.Add(1) 353 | itemCount++ 354 | } 355 | 356 | saveBar.Finish() 357 | fmt.Print("\n\n") 358 | logrus.Info("成功导入 ", len(*scList), " 条数据") 359 | 360 | // 错误处理 361 | if len(saveErr) > 0 { 362 | logrus.Error("ExcelImporter: 保存 Score 过程中发生错误") 363 | return 364 | } 365 | 366 | fmt.Print("\n\n") 367 | fmt.Println("ExamConf = '" + GetExamConfJSONStr(examName, false) + "'") 368 | fmt.Print("\n\n") 369 | fmt.Println("您可以执行以下命令,对 Exam 进行修改:") 370 | fmt.Println(" - 更改配置:qwquiver exam config set \"" + examName + "\" -h") 371 | fmt.Println(" - 获取配置:qwquiver exam config get \"" + examName + "\" -i") 372 | fmt.Println(" - 执行删除:qwquiver exam remove \"" + examName + "\" --force") 373 | 374 | fmt.Println() 375 | logrus.Info("数据导入任务执行完毕") 376 | 377 | // 尝试读取数据库数据 378 | //var queryScores []model.Score 379 | //lib.GetScoreBucket(examName).All(&queryScores) 380 | // fmt.Println(len(queryScores)) 381 | } 382 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/360EntSecGroup-Skylar/excelize v1.4.1 h1:l55mJb6rkkaUzOpSsgEeKYtS6/0gHwBYyfo5Jcjv/Ks= 3 | github.com/360EntSecGroup-Skylar/excelize v1.4.1/go.mod h1:vnax29X2usfl7HHkBrX5EvSCJcmH3dT9luvxzu8iGAE= 4 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 5 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 6 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= 7 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 8 | github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1 h1:TEBmxO80TM04L8IuMWk77SGL1HomBmKTdzdJLLWznxI= 9 | github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI= 10 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 11 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 12 | github.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo= 13 | github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30= 14 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 15 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 16 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 17 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 18 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 19 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 21 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 23 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 24 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 25 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 26 | github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= 27 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 28 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 29 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 30 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 31 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 32 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 33 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 34 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 35 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 36 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 37 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 38 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 39 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 40 | github.com/golang/protobuf v1.4.1 h1:ZFgWrT+bLgsYPirOnRfKLYJLvssAegOj/hgyMFdJZe0= 41 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 42 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 43 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 44 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 45 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 46 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 47 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 48 | github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= 49 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 50 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 51 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 52 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 53 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 54 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 55 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 56 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 57 | github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= 58 | github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 59 | github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= 60 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 61 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 62 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 63 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 64 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 65 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 66 | github.com/labstack/echo/v4 v4.1.16 h1:8swiwjE5Jkai3RPfZoahp8kjVCRNq+y7Q0hPji2Kz0o= 67 | github.com/labstack/echo/v4 v4.1.16/go.mod h1:awO+5TzAjvL8XpibdsfXxPgHr+orhtXZJZIQCVjogKI= 68 | github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= 69 | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= 70 | github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= 71 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 72 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 73 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 74 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 75 | github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= 76 | github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 77 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 78 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 79 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 80 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 81 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 82 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 83 | github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= 84 | github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 85 | github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA= 86 | github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= 87 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= 88 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 89 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 90 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 91 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 92 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 93 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= 94 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= 95 | github.com/mozillazg/go-pinyin v0.18.0 h1:hQompXO23/0ohH8YNjvfsAITnCQImCiR/Fny8EhIeW0= 96 | github.com/mozillazg/go-pinyin v0.18.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc= 97 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 98 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 99 | github.com/oleiade/reflections v1.0.0 h1:0ir4pc6v8/PJ0yw5AEtMddfXpWBXg9cnG7SgSoJuCgY= 100 | github.com/oleiade/reflections v1.0.0/go.mod h1:RbATFBbKYkVdqmSFtx13Bb/tVhR0lgOBXunWTZKeL4w= 101 | github.com/onrik/logrus v0.7.0 h1:Zw7pup8/kzvYVAazuAg1jI+w7p+b3cl7DGbhwDHRz7M= 102 | github.com/onrik/logrus v0.7.0/go.mod h1:qfe9NeZVAJfIxviw3cYkZo3kvBtLoPRJriAO8zl7qTk= 103 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 104 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 105 | github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= 106 | github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 107 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 108 | github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= 109 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 110 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 111 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 112 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 113 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 114 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 115 | github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo= 116 | github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM= 117 | github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= 118 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 119 | github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= 120 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 121 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 122 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 123 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 124 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 125 | github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= 126 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 127 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 128 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 129 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 130 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 131 | github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M= 132 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 133 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 134 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 135 | github.com/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 136 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 137 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 138 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 139 | github.com/thoas/go-funk v0.7.0 h1:GmirKrs6j6zJbhJIficOsz2aAI7700KsU/5YrdHRM1Y= 140 | github.com/thoas/go-funk v0.7.0/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= 141 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 142 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 143 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 144 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 145 | github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 146 | github.com/valyala/fasttemplate v1.2.0 h1:y3yXRCoDvC2HTtIHvL2cc7Zd+bqA+zqDO6oQzsJO07E= 147 | github.com/valyala/fasttemplate v1.2.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 148 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= 149 | github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= 150 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 151 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 152 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 153 | golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 154 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 155 | golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig= 156 | golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 157 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 158 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 159 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 160 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 161 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 162 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 163 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 164 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 165 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 166 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 167 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 168 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 169 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 170 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 171 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 172 | golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= 173 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 174 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 175 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 176 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 177 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 178 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 179 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 180 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 181 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 182 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 183 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 184 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 185 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 186 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 187 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 188 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 189 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 190 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 191 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 192 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 193 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 194 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw= 195 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 196 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 197 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 198 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 199 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 200 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 201 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 202 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 203 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 204 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 205 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 206 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 207 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 208 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 209 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 210 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 211 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 212 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 213 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 214 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 215 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 216 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 217 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 218 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 219 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 220 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 221 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 222 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 223 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 224 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 225 | google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= 226 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 227 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 228 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 229 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 230 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 231 | gopkg.in/oleiade/reflections.v1 v1.0.0 h1:nV9NFaFd5bXKjilVvPvA+/V/tNQk1pOEEc9gGWDkj+s= 232 | gopkg.in/oleiade/reflections.v1 v1.0.0/go.mod h1:SpA8pv+LUnF0FbB2hyRxc8XSng78D6iLBZ11PDb8Z5g= 233 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 234 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 235 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 236 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 237 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 238 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 239 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 240 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 241 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= 242 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 243 | gorm.io/driver/sqlite v1.1.0 h1:PVykhVHGz4/rA5ZriLQKSbY/+jh6VD9LU1ERdX/l+fU= 244 | gorm.io/driver/sqlite v1.1.0/go.mod h1:hm2olEcl8Tmsc6eZyxYSeznnsDaMqamBvEXLNtBg4cI= 245 | gorm.io/gorm v1.9.19 h1:NMrwpxOZIHWJEFzZ0MM8PdYlcXyKLaXTHWfpDEDdBNg= 246 | gorm.io/gorm v1.9.19/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= 247 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 248 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 249 | --------------------------------------------------------------------------------