diff --git a/README.md b/README.md index aa00445..685ee1f 100644 --- a/README.md +++ b/README.md @@ -10,23 +10,27 @@ $ go get -u git.listensoft.net/tool/lxutils ``` ## Get a value -Get searches json for the specified path. A path is in dot syntax, such as "name.last" or "age". When the value is found it's returned immediately. +Go. ```go package main import ( + "fmt" "git.listensoft.net/tool/lxutils" - "git.listensoft.net/tool/lxutils/lxlog" + "git.listensoft.net/tool/lxutils/lxrun" "git.listensoft.net/tool/lxutils/lxzap" + "github.com/gin-gonic/gin" ) func main() { lxutils.Aa() - lxlog.InitLog() - err := lxzap.InitLogger(lxzap.LogConfig{}) - if err != nil { - panic(err.Error()) - } + lxrun.Run("prod", "8989", func() { + //可执行定时器等等 + fmt.Println("run cron、mq") + }, router, lxzap.LogConfig{}) +} +func router(router *gin.Engine) { + } ``` \ No newline at end of file diff --git a/go.mod b/go.mod index 173ed9c..fad6006 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,9 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/gomodule/redigo v1.8.9 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -33,5 +35,6 @@ require ( golang.org/x/text v0.9.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/mysql v1.5.2 // indirect gorm.io/gorm v1.25.5 // indirect ) diff --git a/go.sum b/go.sum index 96475a2..30be144 100644 --- a/go.sum +++ b/go.sum @@ -18,9 +18,13 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= +github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -85,6 +89,9 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs= +gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8= +gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/lxCommon/common.go b/lxCommon/common.go new file mode 100644 index 0000000..935ce98 --- /dev/null +++ b/lxCommon/common.go @@ -0,0 +1,84 @@ +package lxCommon + +import ( + "fmt" + "github.com/gin-gonic/gin" + "net/http" +) + +type errorCode struct { + SUCCESS int + ERROR int + NotFound int + LoginError int + LoginTimeout int + InActive int +} + +// ErrorCode 错误码 +var ErrorCode = errorCode{ + SUCCESS: 0, + ERROR: 1, + NotFound: 404, + LoginError: 1000, //用户名或密码错误 + LoginTimeout: 1001, //登录超时 + InActive: 1002, //未激活账号 +} + +func SendErrJSON(msg string, args ...interface{}) { + if len(args) == 0 { + panic("缺少 *gin.Context") + } + var c *gin.Context + var errNo = ErrorCode.ERROR + if len(args) == 1 { + theCtx, ok := args[0].(*gin.Context) + if !ok { + panic("缺少 *gin.Context") + } + c = theCtx + } else if len(args) == 2 { + theErrNo, ok := args[0].(int) + if !ok { + panic("errNo不正确") + } + errNo = theErrNo + theCtx, ok := args[1].(*gin.Context) + if !ok { + panic("缺少 *gin.Context") + } + c = theCtx + } + fmt.Println(msg) // todo for debug, added by leek, 应该放到业务里决定是否打印错误日志 还是统一放到这里? + c.JSON(http.StatusOK, gin.H{ + "errNo": errNo, + "msg": msg, + "data": gin.H{}, + }) + // 终止请求链 + c.Abort() +} + +func Ok(c *gin.Context, data interface{}) { + c.JSON(http.StatusOK, gin.H{ + "errNo": ErrorCode.SUCCESS, + "msg": "success", + "data": data, + }) +} + +func HandleParamError(c *gin.Context, err error) bool { + if err != nil { + SendErrJSON("参数错误:"+err.Error(), c) + return true + } + return false +} + +func HandleError(c *gin.Context, err error) bool { + if err != nil { + SendErrJSON(err.Error(), c) + return true + } + return false +} diff --git a/lxDb/db.go b/lxDb/db.go new file mode 100644 index 0000000..d520a70 --- /dev/null +++ b/lxDb/db.go @@ -0,0 +1,75 @@ +package lxDb + +import ( + "fmt" + "git.listensoft.net/tool/lxutils/lxzap" + "go.uber.org/zap" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "gorm.io/gorm/schema" + "os" +) + +// DB2.0 数据库连接 gorm2.0 +var DB *gorm.DB //gs 数据库 +type DbConfig struct { + Host string + Port string + User string + Password string + Database string + Charset string +} + +func InitDB(env string, conf DbConfig) { + //dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local" + //newLogger := logger.New( + // //zap.NewStdLog(), + // log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer + // logger.Config{ + // SlowThreshold: 200 * time.Millisecond, // Slow SQL threshold + // LogLevel: logger.Info, // Log level + // IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger + // Colorful: false, // Disable color + // }, + //) + if conf.Host == "" { + DB = nil + fmt.Println("未配置Db连接") + return + } + if conf.Charset == "" { + conf.Charset = "utf8mb4" + } + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local", + conf.User, conf.Password, conf.Host, conf.Port, conf.Database, conf.Charset) + if env == "dev" { + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), + NamingStrategy: schema.NamingStrategy{ + SingularTable: true, // 使用单数表名,启用该选项,此时,`User` 的表名应该是 `user` + }, + }) + if err != nil { + fmt.Println(err.Error()) + os.Exit(-1) + } + DB = db + } else { + logger2 := lxzap.NewGormZap(zap.L()) + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ + //Logger: logger.Default.LogMode(logger.Info), + Logger: logger2, + NamingStrategy: schema.NamingStrategy{ + SingularTable: true, // 使用单数表名,启用该选项,此时,`User` 的表名应该是 `user` + }, + }) + if err != nil { + fmt.Println(err.Error()) + os.Exit(-1) + } + DB = db + } + //db.AutoMigrate(TaskData{}, Task{}, Version{}) +} diff --git a/lxDb/model.go b/lxDb/model.go new file mode 100644 index 0000000..34cdfa3 --- /dev/null +++ b/lxDb/model.go @@ -0,0 +1,291 @@ +package lxDb + +import ( + "fmt" + "git.listensoft.net/tool/lxutils/lxUtil" + "gorm.io/gorm" + "reflect" + "strings" + "time" +) + +type BaseModel struct { + ID uint `json:"id" gorm:"primaryKey"` // old version is: gorm:"primary_key", TODO: 兼容旧的 有没有影响? + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + CreatedBy uint `json:"createdBy"` + UpdatedBy uint `json:"updatedBy"` +} + +// BaseModel2 带操作人ID的BaseModel +type BaseModel2 struct { + BaseModel + CreatedBy uint `json:"createdBy"` + UpdatedBy uint `json:"updatedBy"` + //DeletedBy uint `json:"-"` +} + +//func (m BaseModel)AsID (id uint) { +// m.ID = id +//} + +// NewModel 返回被 uid 填充的 model +//func NewModel(uid uint) Model { +// return Model{CreatedBy: uid, UpdatedBy: uid} +//} + +// Condition 查询条件 +type Condition struct { + Field string // 字段 + Operator string // 操作类型 + Arg interface{} // 值 +} + +// PaginationQuery 查询条件 +// TODO: 考虑在包之外使用方法修改属性而不是直接操作属性 +// TODO: 考虑要明确指定不分页查询, 否则就强制分页, 以免误查大量数据, 但是给非前端调用带来麻烦 +// TODO: 要明确指定是不分页查询, 默认“下一页”不查询total. 考虑前端使用的友好性,及可配置化(config) +type PaginationQuery struct { + Conditions []Condition `json:"-"` // 查询条件 + OrderBy string `json:"orderBy"` // 排序 会被直接拼接到 sql 里 // TODO: 这个会保留吗, 支持特殊的自定义排序. + Orders []string `json:"orders"` // 排序 会被直接拼接到 sql 里 // TODO: 支持多条件排序 string 还是对象? 还未做 + Offset int `json:"page"` // 第几页 + Limit int `json:"limit"` // 一页几条数据 or size ? // TODO: + NoTotal bool `json:"noTotal"` // 不需要记录条数, true: 不查询Total + Total int `json:"total"` // 记录条数 + Summary string `json:"summary"` // 汇总字段,如: "inAmt,inCount" + SummarySql []string `json:"-"` // 汇总子sql, 如: ["SUM(int_amt) AS inAmt", "SUM(int_count) AS inCount"] + SummaryResult map[string]interface{} `json:"summaryResult"` // 汇总结果 +} + +// Eq equal 等于 +func (q *PaginationQuery) Eq(field string, arg interface{}) { + q.Conditions = append(q.Conditions, Condition{Field: field, Operator: "eq", Arg: arg}) +} + +// Ne not equal 不等于 +func (q *PaginationQuery) Ne(field string, arg interface{}) { + q.Conditions = append(q.Conditions, Condition{Field: field, Operator: "ne", Arg: arg}) +} + +// Lt less than 小于 +func (q *PaginationQuery) Lt(field string, arg interface{}) { + q.Conditions = append(q.Conditions, Condition{Field: field, Operator: "lt", Arg: arg}) +} + +// Le less equal 小于等于 +func (q *PaginationQuery) Le(field string, arg interface{}) { + q.Conditions = append(q.Conditions, Condition{Field: field, Operator: "le", Arg: arg}) +} + +// Gt greater than 大于 +func (q *PaginationQuery) Gt(field string, arg interface{}) { + q.Conditions = append(q.Conditions, Condition{Field: field, Operator: "gt", Arg: arg}) +} + +// Ge greater equal 大于等于 +func (q *PaginationQuery) Ge(field string, arg interface{}) { + q.Conditions = append(q.Conditions, Condition{Field: field, Operator: "ge", Arg: arg}) +} + +// Like 包含 LIKE '%arg%' +func (q *PaginationQuery) Like(field string, arg interface{}) { + q.Conditions = append(q.Conditions, Condition{Field: field, Operator: "like", Arg: arg}) +} + +// Like2 自指定包含方式 LIKE 'arg'. arg是灵活的, 如: 前匹配abs%, 后匹配%abc, 通配符'_1_' +func (q *PaginationQuery) Like2(field string, value interface{}) { + q.Conditions = append(q.Conditions, Condition{Field: field, Operator: "like2", Arg: value}) +} + +func (q *PaginationQuery) NotLike(field string, value interface{}) { + q.Conditions = append(q.Conditions, Condition{Field: field, Operator: "notlike", Arg: value}) +} + +// In IN (...) +func (q *PaginationQuery) In(field string, arg interface{}) { + q.Conditions = append(q.Conditions, Condition{Field: field, Operator: "in", Arg: arg}) +} + +// Between BETWEEN v1 AND v2 +func (q *PaginationQuery) Between(field string, v1, v2 interface{}) { + q.Conditions = append(q.Conditions, Condition{Field: field, Operator: "between", Arg: [2]interface{}{v1, v2}}) +} + +// Or OR +func (q *PaginationQuery) Or(field string, arg interface{}) { + q.Conditions = append(q.Conditions, Condition{Field: field, Operator: "or", Arg: arg}) +} + +// Ors Deprecated: replaced by And 自定义条件, 如: (length(field) = 4 OR length(field) = 6) +func (q *PaginationQuery) Ors(condition string) { + q.And("(" + condition + ")") +} + +// And 自定义条件, 如: (length(field) = 4 OR length(field) = 6) +func (q *PaginationQuery) And(condition string) { + q.Conditions = append(q.Conditions, Condition{Field: condition, Operator: "and"}) +} + +// Group GROUP BY +func (q *PaginationQuery) Group(field string) { + q.Conditions = append(q.Conditions, Condition{Field: field, Operator: "group"}) +} + +// Join JOIN table_name ON +func (q *PaginationQuery) Join(joinSql string, args ...interface{}) { + q.Conditions = append(q.Conditions, Condition{Field: joinSql, Operator: "join", Arg: args}) +} + +// Select 要查询的字段. 可以将字段都写在第一个参数, 也可以作为多个参数传递. ("aa, bb, cc") == ("aa", "bb", "cc") +func (q *PaginationQuery) Select(query string, args ...interface{}) { + q.Conditions = append(q.Conditions, Condition{Field: query, Operator: "select", Arg: args}) +} + +// Build 构造查询条件 +func (q *PaginationQuery) Build(tx *gorm.DB) *gorm.DB { + for _, con := range q.Conditions { + switch con.Operator { + case "eq": + field := fmt.Sprintf("%s = ?", con.Field) + tx = tx.Where(field, con.Arg) + case "ne": + field := fmt.Sprintf("%s <> ?", con.Field) + tx = tx.Where(field, con.Arg) + case "lt": + field := fmt.Sprintf("%s < ?", con.Field) + tx = tx.Where(field, con.Arg) + case "le": + field := fmt.Sprintf("%s <= ?", con.Field) + tx = tx.Where(field, con.Arg) + case "gt": + field := fmt.Sprintf("%s > ?", con.Field) + tx = tx.Where(field, con.Arg) + case "ge": + field := fmt.Sprintf("%s >= ?", con.Field) + tx = tx.Where(field, con.Arg) + case "like": + field := fmt.Sprintf("%s LIKE ?", con.Field) // field LIKE '%arg%' + tx = tx.Where(field, fmt.Sprintf("%%%v%%", con.Arg)) + case "like2": + field := fmt.Sprintf("%s LIKE ?", con.Field) // field LIKE 'arg', support: 'abc%', '%abc', '_1_' + tx = tx.Where(field, con.Arg) + case "notlike": + field := fmt.Sprintf("%s NOT LIKE ?", con.Field) + tx = tx.Where(field, fmt.Sprintf("%%%v%%", con.Arg)) + case "in": + field := fmt.Sprintf("%s IN (?)", con.Field) + tx = tx.Where(field, con.Arg) + case "between": + field := fmt.Sprintf("%s BETWEEN ? AND ?", con.Field) + value1, value2, ok := split(con.Arg) + if ok { + tx = tx.Where(field, value1, value2) + } + case "or": + field := fmt.Sprintf("%s = ?", con.Field) + tx = tx.Or(field, con.Arg) + case "and": + tx = tx.Where(con.Field) // 自定义条件, 如: (length(field) = 4 OR length(field) = 6) + case "group": + tx = tx.Group(con.Field) + case "join": + //args := cast.ToStringSlice(con.Arg) // not work, cast.ToSlice not work too + tx = tx.Joins(con.Field, lxUtil.ToSlice1(con.Arg)...) + case "select": + tx = tx.Select(con.Field, lxUtil.ToSlice1(con.Arg)...) + } + } + return tx +} + +// BuildRawWhere 构造原生SQL的查询条件 +func (q *PaginationQuery) BuildRawWhere() (where string, args []interface{}) { + var sb strings.Builder + for _, con := range q.Conditions { + switch con.Operator { + case "eq": + sb.WriteString(fmt.Sprintf(" AND %s = ?", con.Field)) + args = append(args, con.Arg) + case "ne": + sb.WriteString(fmt.Sprintf(" AND %s <> ?", con.Field)) + args = append(args, con.Arg) + case "lt": + sb.WriteString(fmt.Sprintf(" AND %s < ?", con.Field)) + args = append(args, con.Arg) + case "le": + sb.WriteString(fmt.Sprintf(" AND %s <= ?", con.Field)) + args = append(args, con.Arg) + case "gt": + sb.WriteString(fmt.Sprintf(" AND %s > ?", con.Field)) + args = append(args, con.Arg) + case "ge": + sb.WriteString(fmt.Sprintf(" AND %s >= ?", con.Field)) + args = append(args, con.Arg) + case "like": + sb.WriteString(fmt.Sprintf(" AND %s LIKE ?", con.Field)) // field LIKE '%arg%' + args = append(args, fmt.Sprintf("%%%v%%", con.Arg)) + case "like2": + sb.WriteString(fmt.Sprintf(" AND %s LIKE ?", con.Field)) // field LIKE 'arg', support: 'abc%', '%abc', '_1_' + args = append(args, con.Arg) + case "notlike": + sb.WriteString(fmt.Sprintf(" AND %s NOT LIKE ?", con.Field)) + args = append(args, fmt.Sprintf("%%%v%%", con.Arg)) + case "in": + sb.WriteString(fmt.Sprintf(" AND %s IN (?)", con.Field)) + args = append(args, con.Arg) + case "between": + value1, value2, ok := split(con.Arg) + if ok { + sb.WriteString(fmt.Sprintf(" AND %s BETWEEN ? AND ?", con.Field)) + args = append(args, value1, value2) + } + case "or": + sb.WriteString(fmt.Sprintf(" OR %s = ?", con.Field)) + args = append(args, con.Arg) + case "and": + sb.WriteString(fmt.Sprintf(" AND %s", con.Field)) // 自定义条件, 如: (length(field) = 4 OR length(field) = 6) + case "group": + sb.WriteString(fmt.Sprintf(" GROUP BY %s", con.Field)) + + //case "join": + // if con.Arg == nil { + // tx = tx.Joins(con.Field) + // } else { + // tx = tx.Joins(con.Field, con.Arg) + // } + //case "select": + // if con.Arg == nil { + // tx = tx.Select(con.Field) + // } else { + // tx = tx.Select(con.Field, con.Arg) + // } + } + } + where = sb.String() + if args == nil { + args = make([]interface{}, 0) + } + return +} + +// 参数拆分 // TODO: 不知道有没有更好的实现方式 +// TODO: 加异常处理, 如应该两个值只传了一个值 +//func split(value interface{}) (interface{}, interface{}) { +// values := value.([2]interface{}) +// return values[0], values[1] +//} + +// 参数拆分 +func split(value interface{}) (v1 interface{}, v2 interface{}, ok bool) { + rv := reflect.ValueOf(value) + // 取值 + for rv.Kind() == reflect.Ptr { + rv = rv.Elem() + } + // 校验是否为长度为 2 的数组或切片 + if (rv.Kind() != reflect.Slice && rv.Kind() != reflect.Array) || rv.Len() != 2 { + return + } + return rv.Index(0).Interface(), rv.Index(1).Interface(), true +} diff --git a/lxDb/redis.go b/lxDb/redis.go new file mode 100644 index 0000000..8cf0ec0 --- /dev/null +++ b/lxDb/redis.go @@ -0,0 +1,47 @@ +package lxDb + +import ( + "fmt" + "github.com/gomodule/redigo/redis" + "os" + "time" +) + +// RedisPool Redis连接池 +var RedisPool *redis.Pool + +type RedisConfig struct { + URL string + MaxIdle int + MaxActive int + Password string +} + +func InitRedis(conf RedisConfig) { + if conf.URL == "" { + RedisPool = nil + fmt.Println("未配置Redis连接") + return + } + + RedisPool = &redis.Pool{ + MaxIdle: conf.MaxIdle, + MaxActive: conf.MaxActive, + IdleTimeout: 240 * time.Second, + Wait: true, + Dial: func() (redis.Conn, error) { + c, err := redis.Dial("tcp", conf.URL, redis.DialPassword(conf.Password)) + if err != nil { + return nil, err + } + return c, nil + }, + } + c := RedisPool.Get() + defer c.Close() + _, err := redis.String(c.Do("PING")) // Redis Ping: PONG + if err != nil { + fmt.Println("error while connecting redis:", err) + os.Exit(-1) + } +} diff --git a/lxDb/sql.go b/lxDb/sql.go new file mode 100644 index 0000000..39ff53a --- /dev/null +++ b/lxDb/sql.go @@ -0,0 +1,444 @@ +package lxDb + +import ( + "errors" + "fmt" + "git.listensoft.net/tool/lxutils/lxUtil" + "gorm.io/gorm" + "strings" +) + +// 带事务的, 和不带事务的 说明: +// 如果需要支持事务请调aaaTx方法, 并传开启事务的DB + +// OneById 通过ID查询一条 +func OneById(m interface{}, id uint) (err error) { + return OneByIdTx(DB, m, id) +} + +// OneByIdTx 通过ID查询一条 +func OneByIdTx(tx *gorm.DB, m interface{}, id uint) (err error) { + if id == 0 { + return errors.New("主键ID不能为空") + } + if tx.Take(m, id).Error != nil { // or First ? + return errors.New("未找到记录") + } + return nil +} + +// One 查询一条 使用m的有值属性作为条件, m必须是Model结构体. 条件为空时报错. +func One(m interface{}) (err error) { + return OneTx(DB, m) +} + +// OneTx 查询一条 使用m的有值属性作为条件, m必须是Model结构体. 条件为空时报错. +func OneTx(tx *gorm.DB, m interface{}) error { + return oneTx(tx, m, "") +} + +func oneTx(tx *gorm.DB, m interface{}, Type string) (err error) { + //reflectVal := reflect.ValueOf(m) + //t := reflect.Indirect(reflectVal).Type() + //newObj := reflect.New(t) + if lxUtil.IsZeroOfUnderlyingType(m) { + return errors.New("条件不能为空") + } + + // 这里有一个特别的情况, 如果m.id有值, 生成sql的where里id条件出现两次, 但是不影响效果 + if Type == "first" { // 第一个 + err = tx.Where(m).First(m).Error + } else if Type == "last" { // 最后一个 + err = tx.Where(m).Last(m).Error + } else { // 就是一个 + err = tx.Where(m).Take(m).Error + } + if err != nil { + return err + } + return nil +} + +// First 查询第一条 使用m的有值属性作为条件, m必须是Model结构体. 条件为空时报错. +func First(m interface{}) (err error) { + return FirstTx(DB, m) +} + +// FirstTx 查询第一条 使用m的有值属性作为条件, m必须是Model结构体. 条件为空时报错. +func FirstTx(tx *gorm.DB, m interface{}) error { + return oneTx(tx, m, "first") +} + +// Last 查询最后一条 使用m的有值属性作为条件, m必须是Model结构体. 条件为空时报错. +func Last(m interface{}) (err error) { + return LastTx(DB, m) +} + +// LastTx 查询最后一条 使用m的有值属性作为条件, m必须是Model结构体. 条件为空时报错. +func LastTx(tx *gorm.DB, m interface{}) error { + return oneTx(tx, m, "last") +} + +// One 查询一条. 这种方式时不行的, 实际上对应的表是 base_model +//func (m *BaseModel) One() (err error) { +// //if DB.Where(m).First(one).RecordNotFound() { +// if DB.Where(m).First(m).Error != nil { +// return errors.New("resource is not found") +// } +// return nil +//} + +// Create 新增 +func Create(m interface{}) error { + return CreateTx(DB, m) +} + +// CreateTx 带事务的, 新增 +func CreateTx(tx *gorm.DB, m interface{}) error { + return tx.Create(m).Error +} + +// Update 更新一条数据的单个字段, m.ID必须有值 +func Update(m interface{}, field string, value interface{}) error { + return UpdateTx(DB, m, field, value) +} + +// UpdateTx 带事务的, 更新一条数据的单个字段, m.ID必须有值 +func UpdateTx(tx *gorm.DB, m interface{}, field string, value interface{}) error { + db := tx.Model(m).Update(field, value) + if err := db.Error; err != nil { + return err + } + if db.RowsAffected != 1 { + return errors.New("id is invalid and resource is not found") + } + return nil +} + +// Updates 更新一条数据的多个字段, m.ID必须有值 +// FIXME: 只会更新非零值的字段!! +// 尽量不要让m和fields是同一个对象, fields只赋值需要更新的字段 +// TODO 支持更新成零值, 支持结构体 map, 放在表结构体里? 不通过ID更新? +// TODO: fields 里不能ID有值, 否则也更新ID, 这是不行的 +func Updates(m interface{}, fields interface{}) error { + return UpdatesTx(DB, m, fields) +} + +// UpdatesTx 带事务的, 更新一条数据的多个字段, m.ID必须有值 +func UpdatesTx(tx *gorm.DB, m interface{}, fields interface{}) error { + db := tx.Model(m).Updates(fields) + if err := db.Error; err != nil { + return err + } + if db.RowsAffected != 1 { + return errors.New("id is invalid and resource is not found") + } + return nil +} + +// Delete 删除, m.ID必须有值. +func Delete(m interface{}) error { + //func Delete(m interface{}, conds ...interface{}) error { // 不做这种支持, 由ma.DB.Delete()去支持 + return DeleteTx(DB, m) + //return DeleteTx(nil, m, conds...) +} + +// DeleteTx 删除, m.ID必须有值. tx不为空就是带事务的 +func DeleteTx(tx *gorm.DB, m interface{}) error { + //func DeleteTx(tx *gorm.DB, m interface{}, conds ...interface{}) error { + db := tx.Delete(m) + //db := tx.Delete(m, conds...) + if err := db.Error; err != nil { + return err + } + if db.RowsAffected != 1 { + return errors.New("未找到要删除的数据") + } + return nil +} + +// List 查询数据列表, m是库表结构体, m的有值属性会作为查询条件, 且必须有条件, list里个体的类型可以与m的类型不同 +func List(m interface{}, list interface{}) (err error) { + return ListTx(DB, m, list) +} + +// ListTx 查询数据列表, m是库表结构体, m的有值属性会作为查询条件, 且必须有条件, list里个体的类型可以与m的类型不同 +func ListTx(tx *gorm.DB, m interface{}, list interface{}) (err error) { + if lxUtil.IsZeroOfUnderlyingType(m) { + return errors.New("条件不能为空") + } + if tx.Model(m).Where(m).Find(list).Error != nil { + // if tx.Where(m).Find(list).Error != nil { + return errors.New("查询出现错误") + } + return nil +} + +// ListAll 查询所有数据, m是库表结构体, m的有值属性不会作为查询条件, list里个体的类型可以与m的类型不同 +func ListAll(m interface{}, list interface{}) (err error) { + return ListAllTx(DB, m, list) +} + +// ListAllTx 查询所有数据, m是库表结构体, m的有值属性不会作为查询条件, list里个体的类型可以与m的类型不同 +func ListAllTx(tx *gorm.DB, m interface{}, list interface{}) (err error) { + if tx.Model(m).Find(list).Error != nil { + // if tx.Where(m).Find(list).Error != nil { + return errors.New("查询出现错误") + } + return nil +} + +// Query 查询数据列表, 支持分页、总数、汇总, 见PaginationQuery属性. PaginationQuery不可为nil +func Query(m interface{}, list interface{}, q *PaginationQuery) (err error) { + QueryTx(DB, m, list, q) + return +} + +// QueryTx 带事务的查询数据列表. tx为空就是不带事务, 否则认为是开启了事务. 你应该避免使用次方法, 而是使用Query +func QueryTx(tx *gorm.DB, m interface{}, list interface{}, q *PaginationQuery) (err error) { + // !! 关于会话 新的Statement实例 及复用 ref: https://gorm.io/zh_CN/docs/method_chaining.html + + if q == nil { + err = errors.New("paginationQuery不可为nil") + return + } + + tx = tx.Model(m) + + // 注意: count, 查询list, summary的顺序不能变. + + tx = q.Build(tx) // 构造查询条件 + + // 记录条数 + if needDoCount(q) { + var total int64 + tx = tx.Count(&total) + q.Total = int(total) + if total == 0 { // 如果查了记录条数并且是0, 就不需要查记录和汇总了 + return + } + } + + if q.OrderBy != "" { + tx = tx.Order(lxUtil.FieldToColumn(q.OrderBy)) // TODO: q.OrderBy是字符串,可能多个字段 会有问题吗 + //tx = tx.Order(q.OrderBy) + } + + if q.Offset > 0 { + tx = tx.Offset((q.Offset - 1) * q.Limit) + } + if q.Limit > 0 { + tx = tx.Limit(q.Limit) + } + + // 获取查询值 + err = tx.Find(list).Error + + // 获取汇总信息, 如果不需要查记录条数就不再查汇总 + if needDoCount(q) { + if q.Summary != "" && len(q.SummarySql) == 0 { + q.SummarySql = fieldsToSumSql(q.Summary) + } + if len(q.Summary) != 0 { + tx = tx.Offset(-1) // 需要去除offset, 否则结果可能为空, 注意: 设置0不起作用. + var summary = make(map[string]interface{}) + //tx.Order("") // FIXME: 怎么去掉order by, sum是不需要order by的, 影响性能. + tx.Select(q.SummarySql).Take(&summary) + + // []byte 转 string. 不太合理, 应该返回int或float + for k, v := range summary { + if bs, ok := v.([]byte); ok { + summary[k] = string(bs) + } + } + q.SummaryResult = summary + } + } + + return +} + +//// SqlOne 原生SQL查询一个 +//func SqlOne(sql string, m interface{}) { +// DB.Raw(sql, m) +//} +// +//// SqlList 原生SQL查询列表 +//func SqlList() { +// +//} + +// SqlQuery 原生SQL查询列表, 支持分页. PaginationQuery可为nil +func SqlQuery(sql string, list interface{}, q *PaginationQuery, params ...interface{}) (err error) { + return SqlQueryTx(DB, sql, list, q, params...) +} + +func SqlQueryTx(tx *gorm.DB, sql string, list interface{}, q *PaginationQuery, params ...interface{}) (err error) { + var builder strings.Builder + builder.WriteString(sql) + + if params == nil { + params = make([]interface{}, 0) + } + + // 条件字段 + if q != nil { + where, args := q.BuildRawWhere() + if hasWhere(sql) { // 原SQL已有WHERE子句 + builder.WriteString(where) // 去掉where 前头的and or .. + } else { // 原SQL没有WHERE子句 + if strings.HasPrefix(where, " AND ") { + where = strings.Replace(where, " AND ", " WHERE ", 1) + builder.WriteString(where) + } else if strings.HasPrefix(where, " OR ") { + where = strings.Replace(where, " OR ", " WHERE ", 1) + builder.WriteString(where) + } else { + builder.WriteString(where) // "" 或者 " GROUP BY ..." + } + } + if len(args) > 0 { + params = append(params, args...) + } + + // 半成品 sql 用于查询其他信息 + var sql2 = builder.String() + + // 记录条数 + if needDoCount(q) { + var total int64 + //tx = tx.Count(&total) + tx.Raw("SELECT COUNT(*) as total FROM ("+sql2+") aaaa", params...).Take(&total) + q.Total = int(total) + if total == 0 { // 如果查了记录条数并且是0, 就不需要查记录和汇总了 + return + } + + // 获取汇总信息 // TODO: 汇总应该放到查询列表的后面 + if q.Summary != "" && len(q.SummarySql) == 0 { + q.SummarySql = fieldsToSumSql(q.Summary) + } + if len(q.Summary) != 0 { + tx = tx.Offset(-1) // 需要去除offset, 否则结果可能为空, 注意: 设置0不起作用. + var summary = make(map[string]interface{}) + //tx.Order("") // FIXME: 怎么去掉order by, sum是不需要order by的, 影响性能. + //tx.Select(q.SummarySql).Take(&summary) // 不适合rawsql? + + tx.Raw("SELECT "+strings.Join(q.SummarySql, ", ")+" FROM ("+sql2+") ssss", params...).Take(&summary) + + // []byte 转 string. 不太合理, 应该返回int或float + for k, v := range summary { + if bs, ok := v.([]byte); ok { + summary[k] = string(bs) + } + } + q.SummaryResult = summary + } + } + + // 排序处理 + if q.OrderBy != "" { + s := fmt.Sprintf(" ORDER BY %s", lxUtil.FieldToColumn(q.OrderBy)) // TODO: q.OrderBy是字符串,可能多个字段 会有问题吗 + builder.WriteString(s) + } + + // 偏移量处理 + if q.Limit > 0 { + if q.Offset > 0 { + offset := (q.Offset - 1) * q.Limit + s := fmt.Sprintf(" LIMIT %d, %d", offset, q.Limit) + builder.WriteString(s) + } else { + s := fmt.Sprintf(" LIMIT %d", q.Limit) + builder.WriteString(s) + } + } + } + + //tx.Raw(builder.String(), params...).Scan(list) // FIXME: unsupported data type: &[] why? + tx.Raw(builder.String(), params...).Find(list) // Find与Scan区别: list传入[]时, 查询为空的情况下, Find返回的是[], 而Scan返回的是nil. + // ref: What is the difference between Find and Scan: https://github.com/go-gorm/gorm/issues/4218 + + return +} + +// 是否需要查询记录条数 +func needDoCount(q *PaginationQuery) bool { + if q.NoTotal { + return false + } + + if q.Limit == 0 { // 不限制条数, 等同于查所有记录, 这时候就不需要查记录条数 + return false + } + + //return q.Offset <= 1 // todo lcs 为什么要这样写 第二页都没了 + return true +} + +// utils ----------------- + +// "inAmt,inCount" -> ["SUM(int_amt) AS inAmt", "SUM(int_count) AS inCount"] +func fieldsToSumSql(fields string) (sumSqls []string) { + strs := strings.Split(strings.TrimSpace(fields), ",") + for _, str := range strs { + field := strings.TrimSpace(str) + if field != "" { + sumSqls = append(sumSqls, "SUM("+lxUtil.FieldToColumn(field)+") AS "+field+"") + } + } + return +} + +// SELECT...FROM...[WHERE] 句式的 SQL 中是否存在 WHERE 子句 +func hasWhere(sql string) bool { + deep := 0 // 括号嵌套层数 + step := 0 // "where" 匹配进度 + + // 遍历 sql 忽略 ' ( ` 判断是否存在 where + for i := 0; i < len(sql); i++ { + switch sql[i] { + case '(': + deep++ + case ')': + deep-- + case 96: // "`" + // 下一个 "`" 的下标 + // 忽略其他字符 + for i = i + 1; i < len(sql); i++ { + if sql[i] == 96 { + break + } + } + case 39: // "'" + // 下一个 "'" 的下标 + // 忽略其他字符 + for i = i + 1; i < len(sql); i++ { + if sql[i] == 39 { + break + } + } + default: + if deep != 0 { + continue + } + if step == 5 { + return true + } + if sql[i] == where[step][0] || sql[i] == where[step][1] { + step++ + } else { + step = 0 + } + } + } + return false +} + +var where = []string{ + "Ww", + "Hh", + "Ee", + "Rr", + "Ee", +} diff --git a/lxUtil/lxutils.go b/lxUtil/lxutils.go new file mode 100644 index 0000000..aabaa78 --- /dev/null +++ b/lxUtil/lxutils.go @@ -0,0 +1,83 @@ +package lxUtil + +import ( + "reflect" + "strings" +) + +// IsZeroOfUnderlyingType 判断是否零值对象, 考虑到了model是指针的情况 +func IsZeroOfUnderlyingType(model interface{}) bool { + // ref: https://www.coder.work/article/200965 + rawType := reflect.TypeOf(model) + if rawType.Kind() == reflect.Ptr { // 指针 + rawType = rawType.Elem() + return reflect.DeepEqual(model, reflect.New(rawType).Interface()) + } + return reflect.DeepEqual(model, reflect.Zero(rawType).Interface()) +} + +// ToSlice 为什么不能被 cast.ToSlice 替代? +func ToSlice1(v interface{}) []interface{} { + var out []interface{} + rv := reflect.ValueOf(v) + if rv.Kind() == reflect.Slice { + for i := 0; i < rv.Len(); i++ { + out = append(out, rv.Index(i).Interface()) + } + } else { + out = append(out, v) + } + return out +} + +// FieldToColumn inAmt -> in_amt 结构体属性转成库表字段 +func FieldToColumn(s string) string { + return CamelCaseToSnakeCase(s) +} + +// CamelCaseToSnakeCase 小驼峰转下划线 +// mesLine -> mes_line +// mesLine-lineCode-lineDesc -> mes_line-line_code-line_desc +func CamelCaseToSnakeCase(s string) string { + if s = strings.TrimSpace(s); s == "" { + return "" + } + + var ( + buf strings.Builder + prevUpper, nextUpper, nextNumber bool + curUpper = s[0] <= 'Z' && s[0] >= 'A' + ) + + for i, r := range s[:len(s)-1] { + nextUpper = 'A' <= s[i+1] && s[i+1] <= 'Z' + nextNumber = '0' <= s[i+1] && s[i+1] <= '9' + + if curUpper { + if prevUpper && (nextUpper || nextNumber) { + buf.WriteRune(r + 32) + } else { + if i > 0 && s[i-1] != '_' && s[i+1] != '_' { + buf.WriteByte('_') + } + buf.WriteRune(r + 32) + } + } else { + buf.WriteRune(r) + } + + prevUpper = curUpper + curUpper = nextUpper + } + + if curUpper { + if !prevUpper && len(s) > 1 { + buf.WriteByte('_') + } + buf.WriteByte(s[len(s)-1] + 32) + } else { + buf.WriteByte(s[len(s)-1]) + } + + return buf.String() +} diff --git a/lxrun/run.go b/lxrun/run.go index d7920b7..9687a90 100644 --- a/lxrun/run.go +++ b/lxrun/run.go @@ -3,6 +3,7 @@ package lxrun import ( "context" "fmt" + "git.listensoft.net/tool/lxutils/lxDb" "git.listensoft.net/tool/lxutils/lxlog" "git.listensoft.net/tool/lxutils/lxzap" "github.com/gin-gonic/gin" @@ -14,7 +15,7 @@ import ( "time" ) -func Run(env string, port string, fun func(), r func(router *gin.Engine), zapConfig lxzap.LogConfig) { +func Run(env string, port string, fun func(), r func(router *gin.Engine), zapConfig lxzap.LogConfig, dbConf lxDb.DbConfig, redisConf lxDb.RedisConfig) { _ = lxlog.InitLog() err := lxzap.InitLogger(zapConfig) if err != nil { @@ -27,6 +28,8 @@ func Run(env string, port string, fun func(), r func(router *gin.Engine), zapCon } else { app.Use(lxzap.GinLogger(), lxzap.GinRecovery(true)) } + lxDb.InitDB(env, dbConf) + lxDb.InitRedis(redisConf) fun() r(app) srv := http.Server{