commit 7083ed381c488d1401a8832b1d7d34a1eb6e885a Author: dengbiao Date: Thu Aug 15 18:37:09 2024 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a8f686 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +# Test binary, built with `go test -c` +*.test +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +.idea +.vscode +*.log +.DS_Store +Thumbs.db +*.swp +*.swn +*.swo +*.swm +*.7z +*.zip +*.rar +*.tar +*.tar.gz +go.sum +/etc/cfg.yaml +images +test/test.json +etc/cfg.yml +t.json +t1.json +t2.json +t3.json +t.go +wait-for-it.sh +test.go +xorm +test.csv +nginx.conf +.devcontainer +.devcontainer/Dockerfile +.devcontainer/sources.list +/t1.go +/tmp/* +.idea/* +/.idea/modules.xml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..650d62d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# 多重构建,减少镜像大小 +# 构建:使用golang:1.15版本 +FROM registry.cn-shenzhen.aliyuncs.com/fnuoos-prd/golang:1.18.4 as build + + +# 容器环境变量添加,会覆盖默认的变量值 +ENV GO111MODULE=on +ENV GOPROXY=https://goproxy.cn,direct +ENV TZ="Asia/Shanghai" +# 设置工作区 +WORKDIR /go/release + +# 把全部文件添加到/go/release目录 +ADD . . + +# 编译:把main.go编译成可执行的二进制文件,命名为zyos +RUN GOOS=linux CGO_ENABLED=0 GOARCH=amd64 go build -tags netgo -ldflags="-s -w" -installsuffix cgo -o zyos main.go + +FROM ubuntu:xenial as prod +LABEL maintainer="wuhanqin" +ENV TZ="Asia/Shanghai" + +COPY static/html static/html +# 时区纠正 +RUN rm -f /etc/localtime \ + && ln -sv /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ + && echo "Asia/Shanghai" > /etc/timezone +# 在build阶段复制可执行的go二进制文件app +COPY --from=build /go/release/zyos ./zyos + +COPY --from=build /go/release/etc/cfg.yml /var/zyos/cfg.yml + +# 启动服务 +CMD ["./zyos","-c","/var/zyos/cfg.yml"] + diff --git a/Dockerfile-task b/Dockerfile-task new file mode 100644 index 0000000..0e1539c --- /dev/null +++ b/Dockerfile-task @@ -0,0 +1,34 @@ +# 多重构建,减少镜像大小 +# 构建:使用golang:1.15版本 +FROM registry.cn-shenzhen.aliyuncs.com/fnuoos-prd/golang:1.18.4 as build + +# 容器环境变量添加,会覆盖默认的变量值 +ENV GO111MODULE=on +ENV GOPROXY=https://goproxy.cn,direct +ENV TZ="Asia/Shanghai" +# 设置工作区 +WORKDIR /go/release + +# 把全部文件添加到/go/release目录 +ADD . . + +# 编译:把main.go编译成可执行的二进制文件,命名为zyos +RUN GOOS=linux CGO_ENABLED=0 GOARCH=amd64 go build -tags netgo -ldflags="-s -w" -installsuffix cgo -o zyos_mall_task cmd/task/main.go + +FROM ubuntu:xenial as prod +LABEL maintainer="wuhanqin" +ENV TZ="Asia/Shanghai" + +COPY static/html static/html +# 时区纠正 +RUN rm -f /etc/localtime \ + && ln -sv /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ + && echo "Asia/Shanghai" > /etc/timezone +# 在build阶段复制可执行的go二进制文件app +COPY --from=build /go/release/zyos_mall_task ./zyos_mall_task + +COPY --from=build /go/release/etc/task.yml /var/zyos/task.yml + +# 启动服务 +CMD ["./zyos_mall_task","-c","/var/zyos/task.yml"] + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e7e30c2 --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +.PHONY: build clean tool lint help + +APP=applet + +all: build + +build: + go build -o ./bin/$(APP) ./cmd/main.go + +lite: + go build -ldflags "-s -w" -o ./bin/$(APP) ./cmd/main.go + +install: + #@go build -v . + go install ./cmd/... + +tool: + go vet ./...; true + gofmt -w . + +lint: + golint ./... + +clean: + rm -rf go-gin-example + go clean -i . + +help: + @echo "make: compile packages and dependencies" + @echo "make tool: run specified go tool" + @echo "make lint: golint ./..." + @echo "make clean: remove object files and cached files" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..03195c4 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# 广告站长平台 + +## 要看 nginx.conf 和 wap conf + +## 层级介绍 + +- hdl 做接收数据的报错, 数据校验 +- svc 做数据处理的报错, 数据转换 +- lib 只抛出错误给hdl或者svc进行处理, 不做数据校验 +- db 可以处理db错误,其它错误返回给svc进行处理 +- mw 中间件 +- md 结构体 + +#### 介绍 +基于gin的接口小程序 + +#### 软件架构 + +软件架构说明 + +#### 安装教程 + +1. xxxx +2. xxxx +3. xxxx + +#### 使用说明 + +1. xxxx +2. xxxx +3. xxxx + +#### 参与贡献 + +1. Fork 本仓库 +2. 新建 Feat_xxx 分支 +3. 提交代码 +4. 新建 Pull Request + +## swagger + +``` +// 参考:https://segmentfault.com/a/1190000013808421 +// 安装命令行 +go get -u github.com/swaggo/swag/cmd/swag +// 生成 +swag init +``` \ No newline at end of file diff --git a/app/cfg/cfg_app.go b/app/cfg/cfg_app.go new file mode 100644 index 0000000..9dbbea4 --- /dev/null +++ b/app/cfg/cfg_app.go @@ -0,0 +1,44 @@ +package cfg + +import ( + "time" +) + +type Config struct { + Debug bool `yaml:"debug"` + Prd bool `yaml:"prd"` + CurlDebug bool `yaml:"curldebug"` + SrvAddr string `yaml:"srv_addr"` + RedisAddr string `yaml:"redis_addr"` + DB DBCfg `yaml:"db"` + Log LogCfg `yaml:"log"` + Local bool +} + +// 数据库配置结构体 +type DBCfg struct { + Host string `yaml:"host"` //ip及端口 + Name string `yaml:"name"` //库名 + User string `yaml:"user"` //用户 + Psw string `yaml:"psw"` //密码 + ShowLog bool `yaml:"show_log"` //是否显示SQL语句 + MaxLifetime time.Duration `yaml:"max_lifetime"` + MaxOpenConns int `yaml:"max_open_conns"` + MaxIdleConns int `yaml:"max_idle_conns"` + Path string `yaml:"path"` //日志文件存放路径 +} + +// 日志配置结构体 +type LogCfg struct { + AppName string `yaml:"app_name" ` + Level string `yaml:"level"` + IsStdOut bool `yaml:"is_stdout"` + TimeFormat string `yaml:"time_format"` // second, milli, nano, standard, iso, + Encoding string `yaml:"encoding"` // console, json + + IsFileOut bool `yaml:"is_file_out"` + FileDir string `yaml:"file_dir"` + FileName string `yaml:"file_name"` + FileMaxSize int `yaml:"file_max_size"` + FileMaxAge int `yaml:"file_max_age"` +} diff --git a/app/cfg/cfg_cache_key.go b/app/cfg/cfg_cache_key.go new file mode 100644 index 0000000..c091909 --- /dev/null +++ b/app/cfg/cfg_cache_key.go @@ -0,0 +1,3 @@ +package cfg + +// 统一管理缓存 diff --git a/app/cfg/init_cache.go b/app/cfg/init_cache.go new file mode 100644 index 0000000..873657f --- /dev/null +++ b/app/cfg/init_cache.go @@ -0,0 +1,9 @@ +package cfg + +import ( + "applet/app/utils/cache" +) + +func InitCache() { + cache.NewRedis(RedisAddr) +} diff --git a/app/cfg/init_cfg.go b/app/cfg/init_cfg.go new file mode 100644 index 0000000..4fdbab6 --- /dev/null +++ b/app/cfg/init_cfg.go @@ -0,0 +1,50 @@ +package cfg + +import ( + "flag" + "io/ioutil" + + "gopkg.in/yaml.v2" +) + +// 配置文件数据,全局变量 +var ( + Debug bool + Prd bool + CurlDebug bool + SrvAddr string + RedisAddr string + DB *DBCfg + Log *LogCfg + Local bool +) + +// 初始化配置文件,将cfg.yml读入到内存 +func InitCfg() { + //用指定的名称、默认值、使用信息注册一个string类型flag。 + path := flag.String("c", "etc/cfg.yml", "config file") + //解析命令行参数写入注册的flag里。 + //解析之后,flag的值可以直接使用。 + flag.Parse() + var ( + c []byte + err error + conf *Config + ) + if c, err = ioutil.ReadFile(*path); err != nil { + panic(err) + } + //yaml.Unmarshal反序列化映射到Config + if err = yaml.Unmarshal(c, &conf); err != nil { + panic(err) + } + //数据读入内存 + Prd = conf.Prd + Debug = conf.Debug + Local = conf.Local + CurlDebug = conf.CurlDebug + DB = &conf.DB + Log = &conf.Log + RedisAddr = conf.RedisAddr + SrvAddr = conf.SrvAddr +} diff --git a/app/cfg/init_log.go b/app/cfg/init_log.go new file mode 100644 index 0000000..0f31eb5 --- /dev/null +++ b/app/cfg/init_log.go @@ -0,0 +1,20 @@ +package cfg + +import "applet/app/utils/logx" + +func InitLog() { + logx.InitDefaultLogger(&logx.LogConfig{ + AppName: Log.AppName, + Level: Log.Level, + StacktraceLevel: "error", + IsStdOut: Log.IsStdOut, + TimeFormat: Log.TimeFormat, + Encoding: Log.Encoding, + IsFileOut: Log.IsFileOut, + FileDir: Log.FileDir, + FileName: Log.FileName, + FileMaxSize: Log.FileMaxSize, + FileMaxAge: Log.FileMaxAge, + Skip: 2, + }) +} diff --git a/app/cfg/init_task.go b/app/cfg/init_task.go new file mode 100644 index 0000000..ca4459c --- /dev/null +++ b/app/cfg/init_task.go @@ -0,0 +1,43 @@ +package cfg + +import ( + "flag" + "io/ioutil" + + "gopkg.in/yaml.v2" + + mc "applet/app/utils/cache/cache" + "applet/app/utils/logx" +) + +func InitTaskCfg() { + path := flag.String("c", "etc/task.yml", "config file") + flag.Parse() + var ( + c []byte + err error + conf *Config + ) + if c, err = ioutil.ReadFile(*path); err != nil { + panic(err) + } + if err = yaml.Unmarshal(c, &conf); err != nil { + panic(err) + } + Prd = conf.Prd + Debug = conf.Debug + DB = &conf.DB + Log = &conf.Log + RedisAddr = conf.RedisAddr + Local = conf.Local +} + +var MemCache mc.Cache + +func InitMemCache() { + var err error + MemCache, err = mc.NewCache("memory", `{"interval":60}`) + if err != nil { + logx.Fatal(err.Error()) + } +} diff --git a/app/db/dao/admin_dao.go b/app/db/dao/admin_dao.go new file mode 100644 index 0000000..742fdd4 --- /dev/null +++ b/app/db/dao/admin_dao.go @@ -0,0 +1,44 @@ +package dao + +import ( + "applet/app/db/model" + "xorm.io/xorm" +) + +type AdminDao interface { + CreateAdminId() (admId int, err error) + AdminDeleteBySession(session *xorm.Session, admId interface{}) (int64, error) + GetAdmin(id int) (m *model.Admin, err error) + GetAdminByUserName(userName string) (m *model.Admin, err error) + GetAdminRolePermission(admId int) (list []*AdminRolePermission, total int64, err error) + FindAdmin(username string, state, page, limit int) (list []model.Admin, total int64, err error) + UpdateAdmin(m *model.Admin, columns ...string) (int64, error) + AdminInsert(m *model.Admin) (int64, error) + FindAdminRolePermissionGroup(admId int) (list []*AdminRolePermissionGroup, total int64, err error) +} + +type AdminRolePermissionGroup struct { + model.Admin `xorm:"extends"` + model.AdminRole `xorm:"extends"` + model.Role `xorm:"extends"` + model.RolePermissionGroup `xorm:"extends"` + model.PermissionGroup `xorm:"extends"` +} + +func (AdminRolePermissionGroup) TableName() string { + return "admin" +} + +type AdminRolePermission struct { + model.Admin `xorm:"extends"` + model.AdminRole `xorm:"extends"` + model.Role `xorm:"extends"` + model.RolePermissionGroup `xorm:"extends"` + model.PermissionGroup `xorm:"extends"` + model.PermissionGroupPermission `xorm:"extends"` + model.Permission `xorm:"extends"` +} + +func (AdminRolePermission) TableName() string { + return "admin" +} diff --git a/app/db/dao/admin_role_dao.go b/app/db/dao/admin_role_dao.go new file mode 100644 index 0000000..d1ce742 --- /dev/null +++ b/app/db/dao/admin_role_dao.go @@ -0,0 +1,25 @@ +package dao + +import ( + "applet/app/db/model" + "xorm.io/xorm" +) + +type AdminRoleDao interface { + FindAdminRole(id int) (*[]model.AdminRole, error) + FindAdminRoleByRoleId(id int) (*[]model.AdminRole, error) + AdminDeleteBySessionForAdmId(session *xorm.Session, admId interface{}) (int64, error) + GetAdminRoleByRole(id int) (m *model.AdminRole, err error) + AdminRoleDeleteForRoleBySession(session *xorm.Session, roleId interface{}) (int64, error) + AdminRoleDeleteBySession(session *xorm.Session, id interface{}) (int64, error) + BatchAddAdminRoleBySession(session *xorm.Session, mm []*model.AdminRole) (int64, error) +} + +type AdminRole struct { + model.Admin `xorm:"extends"` + model.AdminRole `xorm:"extends"` +} + +func (AdminRole) TableName() string { + return "admin_role" +} diff --git a/app/db/dao/permission_dao.go b/app/db/dao/permission_dao.go new file mode 100644 index 0000000..e94d561 --- /dev/null +++ b/app/db/dao/permission_dao.go @@ -0,0 +1,5 @@ +package dao + +type PermissionDao interface { + //TODO:: You can add specific method definitions here +} diff --git a/app/db/dao/permission_group_dao.go b/app/db/dao/permission_group_dao.go new file mode 100644 index 0000000..bcec860 --- /dev/null +++ b/app/db/dao/permission_group_dao.go @@ -0,0 +1,8 @@ +package dao + +import "applet/app/db/model" + +type PermissionGroupDao interface { + FindPermissionGroup() (*[]model.PermissionGroup, error) + FindPermissionGroupV2() (*[]model.PermissionGroup, error) +} diff --git a/app/db/dao/permission_group_permission_dao.go b/app/db/dao/permission_group_permission_dao.go new file mode 100644 index 0000000..3f32ce3 --- /dev/null +++ b/app/db/dao/permission_group_permission_dao.go @@ -0,0 +1,5 @@ +package dao + +type PermissionGroupPermissionDao interface { + //TODO:: You can add specific method definitions here +} diff --git a/app/db/dao/role_dao.go b/app/db/dao/role_dao.go new file mode 100644 index 0000000..0a65841 --- /dev/null +++ b/app/db/dao/role_dao.go @@ -0,0 +1,25 @@ +package dao + +import ( + "applet/app/db/model" + "xorm.io/xorm" +) + +type RoleDao interface { + FindRole() (*[]model.Role, error) + GetRole() (m *model.Role, err error) + UpdateRole(m *model.Role, columns ...string) (int64, error) + RoleDeleteBySession(session *xorm.Session, id interface{}) (int64, error) + RoleInsert(m *model.Role) (int, error) + FindPermissionGroupByRole(roleId int) (list []*RolePermissionGroup, total int64, err error) +} + +type RolePermissionGroup struct { + model.Role `xorm:"extends"` + model.RolePermissionGroup `xorm:"extends"` + model.PermissionGroup `xorm:"extends"` +} + +func (RolePermissionGroup) TableName() string { + return "role" +} diff --git a/app/db/dao/role_permission_group_dao.go b/app/db/dao/role_permission_group_dao.go new file mode 100644 index 0000000..2c2000d --- /dev/null +++ b/app/db/dao/role_permission_group_dao.go @@ -0,0 +1,11 @@ +package dao + +import ( + "applet/app/db/model" + "xorm.io/xorm" +) + +type RolePermissionGroupDao interface { + RolePermissionGroupDeleteForRoleBySession(session *xorm.Session, roleId interface{}) (int64, error) + BatchAddRolePermissionGroupBySession(session *xorm.Session, mm []*model.RolePermissionGroup) (int64, error) +} diff --git a/app/db/db.go b/app/db/db.go new file mode 100644 index 0000000..00630e8 --- /dev/null +++ b/app/db/db.go @@ -0,0 +1,112 @@ +package db + +import ( + "database/sql" + "fmt" + "os" + "time" + + _ "github.com/go-sql-driver/mysql" //必须导入mysql驱动,否则会panic + "xorm.io/xorm" + "xorm.io/xorm/log" + + "applet/app/cfg" + "applet/app/utils/logx" +) + +var Db *xorm.Engine + +//根据DB配置文件初始化数据库 +func InitDB(c *cfg.DBCfg) error { + var ( + err error + f *os.File + ) + //创建Orm引擎 + if Db, err = xorm.NewEngine("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4", c.User, c.Psw, c.Host, c.Name)); err != nil { + return err + } + Db.SetConnMaxLifetime(c.MaxLifetime * time.Second) //设置最长连接时间 + Db.SetMaxOpenConns(c.MaxOpenConns) //设置最大打开连接数 + Db.SetMaxIdleConns(c.MaxIdleConns) //设置连接池的空闲数大小 + if err = Db.Ping(); err != nil { //尝试ping数据库 + return err + } + if c.ShowLog { //根据配置文件设置日志 + Db.ShowSQL(true) //设置是否打印sql + Db.Logger().SetLevel(0) //设置日志等级 + //修改日志文件存放路径文件名是%s.log + path := fmt.Sprintf(c.Path, c.Name) + f, err = os.OpenFile(path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0777) + if err != nil { + os.RemoveAll(c.Path) + if f, err = os.OpenFile(c.Path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0777); err != nil { + return err + } + } + logger := log.NewSimpleLogger(f) + logger.ShowSQL(true) + Db.SetLogger(logger) + } + return nil +} + +/********************************************* 公用方法 *********************************************/ + +// 数据批量插入 +func DbInsertBatch(Db *xorm.Engine, m ...interface{}) error { + if len(m) == 0 { + return nil + } + id, err := Db.Insert(m...) + if id == 0 || err != nil { + return logx.Warn("cannot insert data :", err) + } + return nil +} + +// QueryNativeString 查询原生sql +func QueryNativeString(Db *xorm.Engine, sql string, args ...interface{}) ([]map[string]string, error) { + results, err := Db.SQL(sql, args...).QueryString() + return results, err +} + +// UpdateComm common update +func UpdateComm(Db *xorm.Engine, id interface{}, model interface{}) (int64, error) { + row, err := Db.ID(id).Update(model) + return row, err +} + +// InsertComm common insert +func InsertComm(Db *xorm.Engine, model interface{}) (int64, error) { + row, err := Db.InsertOne(model) + return row, err +} + +// ExecuteOriginalSql 执行原生sql +func ExecuteOriginalSql(session *xorm.Session, sql string) (sql.Result, error) { + result, err := session.Exec(sql) + if err != nil { + _ = logx.Warn(err) + return nil, err + } + return result, nil +} + +// GetComm +// payload *model +// return *model,has,err +func GetComm(Db *xorm.Engine, model interface{}) (interface{}, bool, error) { + has, err := Db.Get(model) + if err != nil { + _ = logx.Warn(err) + return nil, false, err + } + return model, has, nil +} + +// InsertCommWithSession common insert +func InsertCommWithSession(session *xorm.Session, model interface{}) (int64, error) { + row, err := session.InsertOne(model) + return row, err +} diff --git a/app/db/db_sys_cfg.go b/app/db/db_sys_cfg.go new file mode 100644 index 0000000..6f49814 --- /dev/null +++ b/app/db/db_sys_cfg.go @@ -0,0 +1,82 @@ +package db + +import ( + "applet/app/db/model" + "applet/app/md" + "applet/app/utils/cache" + "applet/app/utils/logx" + "fmt" + "xorm.io/xorm" +) + +// 系统配置get +func SysCfgGetAll(Db *xorm.Engine) (*[]model.SysCfg, error) { + var cfgList []model.SysCfg + if err := Db.Cols("key,val,memo").Find(&cfgList); err != nil { + return nil, logx.Error(err) + } + return &cfgList, nil +} + +// 获取一条记录 +func SysCfgGetOne(Db *xorm.Engine, key string) (*model.SysCfg, error) { + var cfgList model.SysCfg + if has, err := Db.Where("`key`=?", key).Get(&cfgList); err != nil || has == false { + return nil, logx.Error(err) + } + return &cfgList, nil +} + +// 返回最后插入id +func SysCfgInsert(Db *xorm.Engine, key, val, memo string) bool { + cfg := model.SysCfg{Key: key, Val: val, Memo: memo} + _, err := Db.InsertOne(&cfg) + if err != nil { + logx.Error(err) + return false + } + return true +} + +func SysCfgUpdate(Db *xorm.Engine, key, val, memo string) bool { + cfg := model.SysCfg{Key: key, Val: val, Memo: memo} + _, err := Db.Where("`key`=?", key).Cols("val,memo").Update(&cfg) + if err != nil { + logx.Error(err) + return false + } + return true +} +func SysCfgGetWithDb(eg *xorm.Engine, masterId string, HKey string) string { + cacheKey := fmt.Sprintf(md.AppCfgCacheKey, masterId) + HKey + get, err := cache.GetString(cacheKey) + if err != nil || get == "" { + cfg, err := SysCfgGetOne(eg, HKey) + if err != nil || cfg == nil { + _ = logx.Error(err) + return "" + } + + // key是否存在 + cacheKeyExist := false + if cache.Exists(cacheKey) { + cacheKeyExist = true + } + + // 设置缓存 + _, err = cache.SetEx(cacheKey, cfg.Val, 30) + if err != nil { + _ = logx.Error(err) + return "" + } + if !cacheKeyExist { // 如果是首次设置 设置过期时间 + _, err := cache.Expire(cacheKey, md.CfgCacheTime) + if err != nil { + _ = logx.Error(err) + return "" + } + } + return cfg.Val + } + return get +} diff --git a/app/db/dbs.go b/app/db/dbs.go new file mode 100644 index 0000000..9f02c9f --- /dev/null +++ b/app/db/dbs.go @@ -0,0 +1,104 @@ +package db + +import ( + "fmt" + "os" + "time" + + "xorm.io/xorm" + "xorm.io/xorm/log" + + "applet/app/cfg" + "applet/app/db/model" + "applet/app/utils/logx" +) + +var DBs map[string]*xorm.Engine + +// 每个站长都要有自己的syscfg 缓存, 键是站长id,值是缓存名 +// var SysCfgMapKey map[string]string + +func NewDB(c *cfg.DBCfg) (*xorm.Engine, error) { + db, err := xorm.NewEngine("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4", c.User, c.Psw, c.Host, c.Name)) + if err != nil { + return nil, err + } + db.SetConnMaxLifetime(c.MaxLifetime * time.Second) + db.SetMaxOpenConns(c.MaxOpenConns) + db.SetMaxIdleConns(c.MaxIdleConns) + err = db.Ping() + if err != nil { + return nil, err + } + if c.ShowLog { + db.ShowSQL(true) + db.Logger().SetLevel(log.LOG_DEBUG) + f, err := os.OpenFile(c.Path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0777) + if err != nil { + os.RemoveAll(c.Path) + if f, err = os.OpenFile(c.Path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0777); err != nil { + return nil, err + } + } + logger := log.NewSimpleLogger(f) + logger.ShowSQL(true) + db.SetLogger(logger) + } + return db, nil +} + +// InitDBs is 初始化多数据库 +func InitDBs(ch chan int) { + // 初始化多数据库 + var tables *[]model.DbMapping + InitMapDbs(cfg.DB, cfg.Prd) + ch <- 1 + // 每10s 查询一次模板数据库的db mapping 表,如果有新增数据库记录,则添加到 DBs中 + ticker := time.NewTicker(time.Duration(time.Second * 120)) + for range ticker.C { + if cfg.Prd { + tables = GetAllDatabasePrd() //默认获取全部 + } else { + tables = GetAllDatabaseDev() //默认获取全部 + } + if tables == nil { + logx.Warn("no database tables data") + continue + } + for _, item := range *tables { + _, ok := DBs[item.DbMasterId] + if !ok { + // 不在db.DBs 则添加进去 + dbCfg := cfg.DBCfg{ + Name: item.DbName, + ShowLog: cfg.DB.ShowLog, + MaxLifetime: cfg.DB.MaxLifetime, + MaxOpenConns: cfg.DB.MaxOpenConns, + MaxIdleConns: cfg.DB.MaxIdleConns, + Path: fmt.Sprintf(cfg.DB.Path, item.DbName), + } + if item.DbHost != "" && item.DbUsername != "" && item.DbPassword != "" { + dbCfg.Host = item.DbHost + dbCfg.User = item.DbUsername + dbCfg.Psw = item.DbPassword + } else { + dbCfg.Host = cfg.DB.Host + dbCfg.User = cfg.DB.User + dbCfg.Psw = cfg.DB.Psw + } + e, err := NewDB(&dbCfg) + if err != nil || e == nil { + logx.Warnf("db engine can't create, please check config, params[host:%s, user:%s, psw: %s, name: %s], err: %v", dbCfg.Host, dbCfg.User, dbCfg.Psw, dbCfg.Name, err) + } else { + DBs[item.DbMasterId] = e + } + } + // 如果 被禁用则删除 + if item.DeletedAt == 1 { + logx.Infof("%s have been removed", item.DbMasterId) + delete(DBs, item.DbMasterId) + } + } + } + +} diff --git a/app/db/dbs_map.go b/app/db/dbs_map.go new file mode 100644 index 0000000..6a8e453 --- /dev/null +++ b/app/db/dbs_map.go @@ -0,0 +1,194 @@ +package db + +import ( + "errors" + "fmt" + + "xorm.io/xorm" + + "applet/app/cfg" + "applet/app/db/model" + "applet/app/utils/logx" +) + +func MapBaseExists() (bool, error) { + return Db.IsTableExist("db_mapping") +} + +func InitMapDbs(c *cfg.DBCfg, prd bool) { + var tables *[]model.DbMapping + exists, err := MapBaseExists() + if !exists || err != nil { + logx.Fatalf("db_mapping not exists : %v", err) + } + // tables := MapAllDatabases(debug) + if prd { + tables = GetAllDatabasePrd() //debug 获取生产 + } else { + tables = GetAllDatabaseDev() //debug 获取开发 + } + + if tables == nil { + logx.Fatal("no database tables data") + } + var e *xorm.Engine + DBs = map[string]*xorm.Engine{} + for _, v := range *tables { + if v.DbName != "" && v.DeletedAt == 0 && v.DbName != c.Name { + dbCfg := cfg.DBCfg{ + Name: v.DbName, + ShowLog: c.ShowLog, + MaxLifetime: c.MaxLifetime, + MaxOpenConns: c.MaxOpenConns, + MaxIdleConns: c.MaxIdleConns, + Path: fmt.Sprintf(c.Path, v.DbName), + } + if v.DbHost != "" && v.DbUsername != "" && v.DbPassword != "" { + dbCfg.Host = v.DbHost + dbCfg.User = v.DbUsername + dbCfg.Psw = v.DbPassword + } else { + dbCfg.Host = c.Host + dbCfg.User = c.User + dbCfg.Psw = c.Psw + } + e, err = NewDB(&dbCfg) + if err != nil || e == nil { + logx.Warnf("db engine can't create, please check config, params[host:%s, user:%s, psw: %s, name: %s], err: %v", dbCfg.Host, dbCfg.User, dbCfg.Psw, dbCfg.Name, err) + } else { + DBs[v.DbMasterId] = e + } + } + } +} + +func MapAllDatabases(debug bool) *[]model.DbMapping { + sql := "`db_name` != ?" + if debug { + sql = "`db_name` = ?" + } + var m []model.DbMapping + if err := Db.Where(sql, cfg.DB.Name).Find(&m); err != nil || len(m) == 0 { + logx.Warn(err) + return nil + } + return &m +} + +// GetAllDatabasePrd is 获取生成库 所有db 除了 deleted_at = 1 的 +func GetAllDatabasePrd() *[]model.DbMapping { + var m []model.DbMapping + if err := Db.Where("deleted_at != ? AND is_dev = '0' ", 1).Find(&m); err != nil || len(m) == 0 { + logx.Warn(err) + return nil + } + return &m +} + +// GetAllDatabaseDev is 获取开发库 所有db 除了 deleted_at = 1 的 +func GetAllDatabaseDev() *[]model.DbMapping { + var m []model.DbMapping + var err error + fmt.Println("cfg.Local is: ", cfg.Local) + if cfg.Local { // 本地调试 加快速度 + fmt.Println("notice:LOCAL TEST, only masterId:** 123456 ** available!") + err = Db.Where("deleted_at != ? AND is_dev = '1' AND db_master_id=?", 1, 123456).Find(&m) + } else { + err = Db.Where("deleted_at != ? AND is_dev = '1' ", 1).Find(&m) + } + + //err := Db.Where("deleted_at != ? AND is_dev = '1' and db_master_id='123456'", 1).Find(&m) + if err != nil || len(m) == 0 { + logx.Warn(err) + return nil + } + return &m +} + +//GetDatabaseByMasterID is 根据站长id 获取对应的的数据库信息 +func GetDatabaseByMasterID(Db *xorm.Engine, id string) (*model.DbMapping, error) { + var m model.DbMapping + has, err := Db.Where("db_master_id=?", id).Get(&m) + if !has { + return nil, errors.New("Not Found DB data by " + id) + } + if err != nil { + return nil, err + } + if m.DbHost == "" { + m.DbHost = cfg.DB.Host + m.DbUsername = cfg.DB.User + m.DbPassword = cfg.DB.Psw + } + return &m, nil +} + +//SessionGetDatabaseByMasterID is 根据站长id 获取对应的的数据库信息 +func SessionGetDatabaseByMasterID(Db *xorm.Session, id string) (*model.DbMapping, error) { + var m model.DbMapping + has, err := Db.Where("db_master_id=?", id).Get(&m) + if !has { + return nil, errors.New("Not Found DB data by " + id) + } + if err != nil { + return nil, err + } + if m.DbHost == "" { + m.DbHost = cfg.DB.Host + m.DbName = cfg.DB.Name + m.DbUsername = cfg.DB.User + m.DbPassword = cfg.DB.Psw + } + return &m, nil +} + +// 获取自动任务队列 +func MapCrontabCfg(eg *xorm.Engine) *[]model.SysCfg { + var c []model.SysCfg + // 数据库查询如果有下划线会认为是一个任意字符 + if err := eg.Where("`key` LIKE 'mall_cron\\_%' AND val != ''").Cols("`key`,`val`").Find(&c); err != nil || len(c) == 0 { + logx.Warn(err) + return nil + } + return &c +} + +// 获取官方域名 +func GetOfficialDomainInfoByType(Db *xorm.Engine, masterId, key string) (string, error) { + type SysCfg struct { + K string + V string + Memo string + } + var domainBase SysCfg + + has, err := Db.Where("k=?", "domain_base").Get(&domainBase) + if err != nil { + return "", err + } + if has == false { + return "", errors.New("can not find key by : domain_base") + } + + if key == "wap" { + return "h5." + masterId + "." + domainBase.V, nil + } + + if key == "api" { + var apiDomain SysCfg + has, err = Db.Where("k=?", "domain_api_base").Get(&apiDomain) + if err != nil { + return "", err + } + if has == false { + return "", errors.New("can not find key by : domain_api_base") + } + return apiDomain.V, nil + } + + if key == "admin" { + return "admin." + masterId + "." + domainBase.V, nil + } + // 默认返回H5的 + return "h5." + masterId + "." + domainBase.V, nil +} diff --git a/app/db/implement/admin_implement.go b/app/db/implement/admin_implement.go new file mode 100644 index 0000000..b6eaca1 --- /dev/null +++ b/app/db/implement/admin_implement.go @@ -0,0 +1,115 @@ +package implement + +import ( + "applet/app/db/dao" + "applet/app/db/model" + "applet/app/utils/logx" + "reflect" + "xorm.io/xorm" +) + +func NewAdminDb(engine *xorm.Engine) dao.AdminDao { + return &AdminDb{Db: engine} +} + +type AdminDb struct { + Db *xorm.Engine +} + +func (a AdminDb) CreateAdminId() (admId int, err error) { + m := new(model.Admin) + has, err := a.Db.Desc("adm_id").Get(m) + if err != nil { + return 0, logx.Error(err) + } + if has == false { + return 0, nil + } + admId = m.AdmId + 1 + return admId, nil +} + +func (a AdminDb) AdminDeleteBySession(session *xorm.Session, admId interface{}) (int64, error) { + if reflect.TypeOf(admId).Kind() == reflect.Slice { + return session.In("adm_id", admId).Delete(model.Admin{}) + } else { + return session.Where("id = ?", admId).Delete(model.Admin{}) + } +} + +func (a AdminDb) GetAdmin(id int) (m *model.Admin, err error) { + m = new(model.Admin) + has, err := a.Db.Where("adm_id =?", id).Get(m) + if err != nil { + return nil, logx.Error(err) + } + if has == false { + return nil, nil + } + return m, nil +} + +func (a AdminDb) GetAdminByUserName(userName string) (m *model.Admin, err error) { + m = new(model.Admin) + has, err := a.Db.Where("username =?", userName).Get(m) + if err != nil { + return nil, logx.Error(err) + } + if has == false { + return nil, nil + } + return m, nil +} + +func (a AdminDb) GetAdminRolePermission(admId int) (list []*dao.AdminRolePermission, total int64, err error) { + total, err = a.Db.Where("admin.adm_id =?", admId). + Join("LEFT", "admin_role", "admin.adm_id = admin_role.adm_id"). + Join("LEFT", "role", "admin_role.role_id = role.id"). + Join("LEFT", "role_permission_group", "role.id = role_permission_group.role_id"). + Join("LEFT", "permission_group", "role_permission_group.group_id = permission_group.id"). + Join("LEFT", "permission_group_permission", "permission_group.id = permission_group_permission.group_id"). + Join("LEFT", "permission", "permission_group_permission.permission_id = permission.id"). + FindAndCount(&list) + return +} + +func (a AdminDb) FindAdmin(username string, state, page, limit int) (list []model.Admin, total int64, err error) { + sess := a.Db.Desc("adm_id").Limit(limit, (page-1)*limit) + if username != "" { + sess.And("username like ?", "%"+username+"%") + } + if state != 0 { + sess.And("state = ?", state) + } + total, err = sess.FindAndCount(&list) + if err != nil { + return nil, 0, err + } + return +} + +func (a AdminDb) UpdateAdmin(m *model.Admin, columns ...string) (int64, error) { + affected, err := a.Db.Where("adm_id =?", m.AdmId).Cols(columns...).Update(m) + if err != nil { + return 0, err + } + return affected, nil +} + +func (a AdminDb) AdminInsert(m *model.Admin) (int64, error) { + insertAffected, err := a.Db.InsertOne(m) + if err != nil { + return 0, err + } + return insertAffected, nil +} + +func (a AdminDb) FindAdminRolePermissionGroup(admId int) (list []*dao.AdminRolePermissionGroup, total int64, err error) { + total, err = a.Db.Where("admin.adm_id =?", admId). + Join("LEFT", "admin_role", "admin.adm_id = admin_role.adm_id"). + Join("LEFT", "role", "admin_role.role_id = role.id"). + //Join("LEFT", "role_permission_group", "role.id = role_permission_group.role_id"). + //Join("LEFT", "permission_group", "role_permission_group.group_id = permission_group.id"). + FindAndCount(&list) + return +} diff --git a/app/db/implement/admin_role_implement.go b/app/db/implement/admin_role_implement.go new file mode 100644 index 0000000..d3964f7 --- /dev/null +++ b/app/db/implement/admin_role_implement.go @@ -0,0 +1,77 @@ +package implement + +import ( + "applet/app/db/dao" + "applet/app/db/model" + "applet/app/utils/logx" + "reflect" + "xorm.io/xorm" +) + +func NewAdminRoleDb(engine *xorm.Engine) dao.AdminRoleDao { + return &AdminRoleDb{Db: engine} +} + +type AdminRoleDb struct { + Db *xorm.Engine +} + +func (a AdminRoleDb) FindAdminRole(id int) (*[]model.AdminRole, error) { + var m []model.AdminRole + if err := a.Db.Where("adm_id =?", id).Find(&m); err != nil { + return nil, logx.Error(err) + } + return &m, nil +} + +func (a AdminRoleDb) FindAdminRoleByRoleId(id int) (*[]model.AdminRole, error) { + var m []model.AdminRole + if err := a.Db.Where("role_id =?", id).Find(&m); err != nil { + return nil, logx.Error(err) + } + return &m, nil +} + +func (a AdminRoleDb) AdminDeleteBySessionForAdmId(session *xorm.Session, admId interface{}) (int64, error) { + if reflect.TypeOf(admId).Kind() == reflect.Slice { + return session.In("adm_id", admId).Delete(model.AdminRole{}) + } else { + return session.Where("adm_id = ?", admId).Delete(model.AdminRole{}) + } +} + +func (a AdminRoleDb) GetAdminRoleByRole(id int) (m *model.AdminRole, err error) { + m = new(model.AdminRole) + has, err := a.Db.Where("role_id =?", id).Get(m) + if err != nil { + return nil, logx.Error(err) + } + if has == false { + return nil, nil + } + return m, nil +} + +func (a AdminRoleDb) AdminRoleDeleteForRoleBySession(session *xorm.Session, roleId interface{}) (int64, error) { + if reflect.TypeOf(roleId).Kind() == reflect.Slice { + return session.In("role_id", roleId).Delete(model.AdminRole{}) + } else { + return session.Where("role_id = ?", roleId).Delete(model.AdminRole{}) + } +} + +func (a AdminRoleDb) AdminRoleDeleteBySession(session *xorm.Session, id interface{}) (int64, error) { + if reflect.TypeOf(id).Kind() == reflect.Slice { + return session.In("id", id).Delete(model.AdminRole{}) + } else { + return session.Where("adm_id = ?", id).Delete(model.AdminRole{}) + } +} + +func (a AdminRoleDb) BatchAddAdminRoleBySession(session *xorm.Session, mm []*model.AdminRole) (int64, error) { + affected, err := session.Insert(mm) + if err != nil { + return 0, err + } + return affected, nil +} diff --git a/app/db/implement/permission_group_implement.go b/app/db/implement/permission_group_implement.go new file mode 100644 index 0000000..b1126d1 --- /dev/null +++ b/app/db/implement/permission_group_implement.go @@ -0,0 +1,33 @@ +package implement + +import ( + "applet/app/db/dao" + "applet/app/db/model" + "applet/app/enum" + "applet/app/utils/logx" + "xorm.io/xorm" +) + +func NewPermissionGroupDb(engine *xorm.Engine) dao.PermissionGroupDao { + return &PermissionGroupDb{Db: engine} +} + +type PermissionGroupDb struct { + Db *xorm.Engine +} + +func (p PermissionGroupDb) FindPermissionGroup() (*[]model.PermissionGroup, error) { + var m []model.PermissionGroup + if err := p.Db.Where("state = ?", enum.PermissionGroupStateForNormal).Asc("id").Find(&m); err != nil { + return nil, logx.Error(err) + } + return &m, nil +} + +func (p PermissionGroupDb) FindPermissionGroupV2() (*[]model.PermissionGroup, error) { + var m []model.PermissionGroup + if err := p.Db.Asc("id").Find(&m); err != nil { + return nil, logx.Error(err) + } + return &m, nil +} diff --git a/app/db/implement/permission_group_permission_implement.go b/app/db/implement/permission_group_permission_implement.go new file mode 100644 index 0000000..1bd1f48 --- /dev/null +++ b/app/db/implement/permission_group_permission_implement.go @@ -0,0 +1,14 @@ +package implement + +import ( + "applet/app/db/dao" + "xorm.io/xorm" +) + +type PermissionGroupPermissionDb struct { + Db *xorm.Engine +} + +func NewPermissionGroupPermissionDb(engine *xorm.Engine) dao.PermissionGroupPermissionDao { + return &PermissionGroupPermissionDb{Db: engine} +} diff --git a/app/db/implement/permission_implement.go b/app/db/implement/permission_implement.go new file mode 100644 index 0000000..0cfad56 --- /dev/null +++ b/app/db/implement/permission_implement.go @@ -0,0 +1,14 @@ +package implement + +import ( + "applet/app/db/dao" + "xorm.io/xorm" +) + +type PermissionDb struct { + Db *xorm.Engine +} + +func NewPermissionDb(engine *xorm.Engine) dao.PermissionDao { + return &PermissionDb{Db: engine} +} diff --git a/app/db/implement/role_implement.go b/app/db/implement/role_implement.go new file mode 100644 index 0000000..8dd9020 --- /dev/null +++ b/app/db/implement/role_implement.go @@ -0,0 +1,73 @@ +package implement + +import ( + "applet/app/db/dao" + "applet/app/db/model" + "applet/app/utils/logx" + "reflect" + "xorm.io/xorm" +) + +func NewRoleDb(engine *xorm.Engine, id int) dao.RoleDao { + return &RoleDb{ + Db: engine, + Id: id, + } +} + +type RoleDb struct { + Db *xorm.Engine + Id int `json:"id"` +} + +func (r RoleDb) FindRole() (*[]model.Role, error) { + var m []model.Role + if err := r.Db.Desc("id").Find(&m); err != nil { + return nil, logx.Error(err) + } + return &m, nil +} + +func (r RoleDb) GetRole() (m *model.Role, err error) { + m = new(model.Role) + has, err := r.Db.Where("id = ?", r.Id).Get(m) + if err != nil { + return nil, logx.Error(err) + } + if has == false { + return nil, nil + } + return m, nil +} + +func (r RoleDb) UpdateRole(m *model.Role, columns ...string) (int64, error) { + affected, err := r.Db.Where("id =?", m.Id).Cols(columns...).Update(m) + if err != nil { + return 0, err + } + return affected, nil +} + +func (r RoleDb) RoleDeleteBySession(session *xorm.Session, id interface{}) (int64, error) { + if reflect.TypeOf(id).Kind() == reflect.Slice { + return session.In("id", id).Delete(model.Role{}) + } else { + return session.Where("id = ?", id).Delete(model.Role{}) + } +} + +func (r RoleDb) RoleInsert(m *model.Role) (int, error) { + _, err := r.Db.InsertOne(m) + if err != nil { + return 0, err + } + return m.Id, nil +} + +func (r RoleDb) FindPermissionGroupByRole(roleId int) (list []*dao.RolePermissionGroup, total int64, err error) { + total, err = r.Db.Where("role.id =?", roleId). + Join("LEFT", "role_permission_group", "role_permission_group.role_id = role.id"). + Join("LEFT", "permission_group", "permission_group.id = role_permission_group.group_id"). + FindAndCount(&list) + return +} diff --git a/app/db/implement/role_permission_group_implement.go b/app/db/implement/role_permission_group_implement.go new file mode 100644 index 0000000..b9aeb70 --- /dev/null +++ b/app/db/implement/role_permission_group_implement.go @@ -0,0 +1,32 @@ +package implement + +import ( + "applet/app/db/dao" + "applet/app/db/model" + "reflect" + "xorm.io/xorm" +) + +func NewRolePermissionGroupDb(engine *xorm.Engine) dao.RolePermissionGroupDao { + return &RolePermissionGroupDb{Db: engine} +} + +type RolePermissionGroupDb struct { + Db *xorm.Engine +} + +func (r RolePermissionGroupDb) RolePermissionGroupDeleteForRoleBySession(session *xorm.Session, roleId interface{}) (int64, error) { + if reflect.TypeOf(roleId).Kind() == reflect.Slice { + return session.In("role_id", roleId).Delete(model.RolePermissionGroup{}) + } else { + return session.Where("role_id = ?", roleId).Delete(model.RolePermissionGroup{}) + } +} + +func (r RolePermissionGroupDb) BatchAddRolePermissionGroupBySession(session *xorm.Session, mm []*model.RolePermissionGroup) (int64, error) { + affected, err := session.Insert(mm) + if err != nil { + return 0, err + } + return affected, nil +} diff --git a/app/db/model/admin.go b/app/db/model/admin.go new file mode 100644 index 0000000..c5f78da --- /dev/null +++ b/app/db/model/admin.go @@ -0,0 +1,14 @@ +package model + +type Admin struct { + AdmId int `json:"adm_id" xorm:"not null comment('管理员id') INT(11)"` + Username string `json:"username" xorm:"not null default '' comment('用户名') VARCHAR(255)"` + Password string `json:"password" xorm:"not null default '' comment('密码') VARCHAR(255)"` + State int `json:"state" xorm:"not null default 1 comment('状态(1:正常 2:冻结)') TINYINT(1)"` + IsSuperAdministrator int `json:"is_super_administrator" xorm:"not null default 0 comment('是否为超级管理员(0:否 1:是)') TINYINT(1)"` + Memo string `json:"memo" xorm:"not null default '' comment('备注信息') VARCHAR(244)"` + CreateAt string `json:"create_at" xorm:"not null default 'CURRENT_TIMESTAMP' DATETIME"` + UpdateAt string `json:"update_at" xorm:"not null default 'CURRENT_TIMESTAMP' DATETIME"` + OpenId string `json:"open_id" xorm:"not null default '' comment('微信openid') VARCHAR(255)"` + UnionId string `json:"union_id" xorm:"not null default '' comment('微信用户id') VARCHAR(255)"` +} diff --git a/app/db/model/admin_role.go b/app/db/model/admin_role.go new file mode 100644 index 0000000..5a35baa --- /dev/null +++ b/app/db/model/admin_role.go @@ -0,0 +1,10 @@ +package model + +type AdminRole struct { + Id int `json:"id" xorm:"not null pk autoincr INT(11)"` + AdmId int `json:"adm_id" xorm:"not null default 0 comment('管理员id') INT(11)"` + RoleId int `json:"role_id" xorm:"not null default 0 comment('角色id') INT(11)"` + State int `json:"state" xorm:"not null default 1 comment('状态(1:正常 2:冻结)') TINYINT(1)"` + CreateAt string `json:"create_at" xorm:"not null default 'CURRENT_TIMESTAMP' DATETIME"` + UpdateAt string `json:"update_at" xorm:"not null default 'CURRENT_TIMESTAMP' DATETIME"` +} diff --git a/app/db/model/db_mapping.go b/app/db/model/db_mapping.go new file mode 100644 index 0000000..2e0abaa --- /dev/null +++ b/app/db/model/db_mapping.go @@ -0,0 +1,14 @@ +package model + +type DbMapping struct { + DbMasterId string `json:"db_master_id" xorm:"not null pk comment('站长id') VARCHAR(16)"` + DbHost string `json:"db_host" xorm:"not null default '' comment('数据库连接(带port)') VARCHAR(255)"` + DbUsername string `json:"db_username" xorm:"not null default '' comment('数据库用户名') VARCHAR(255)"` + DbPassword string `json:"db_password" xorm:"not null default '' comment('数据库用户名密码') VARCHAR(255)"` + DbName string `json:"db_name" xorm:"not null comment('数据库名') VARCHAR(255)"` + ExternalMysql string `json:"external_mysql" xorm:"not null default '0' comment('是否外部mysql(0是内部,1是外部)') VARCHAR(255)"` + IsDev int `json:"is_dev" xorm:"not null default 0 comment('开发库是1,0是生产库') TINYINT(1)"` + CreatedAt string `json:"created_at" xorm:"not null default 'CURRENT_TIMESTAMP' comment('创建时间') TIMESTAMP"` + UpdatedAt string `json:"updated_at" xorm:"not null default 'CURRENT_TIMESTAMP' comment('更新时间') TIMESTAMP"` + DeletedAt int `json:"deleted_at" xorm:"not null default 0 comment('是否已删除') TINYINT(1)"` +} diff --git a/app/db/model/permission.go b/app/db/model/permission.go new file mode 100644 index 0000000..82e00e2 --- /dev/null +++ b/app/db/model/permission.go @@ -0,0 +1,10 @@ +package model + +type Permission struct { + Id int `json:"id" xorm:"not null pk autoincr INT(11)"` + Name string `json:"name" xorm:"not null default '' comment('名称') VARCHAR(255)"` + Action string `json:"action" xorm:"not null default '' comment('路由') unique VARCHAR(255)"` + State int `json:"state" xorm:"not null default 1 comment('状态(1:正常 2:废弃)') TINYINT(1)"` + CreateAt string `json:"create_at" xorm:"not null default 'CURRENT_TIMESTAMP' DATETIME"` + UpdateAt string `json:"update_at" xorm:"not null default 'CURRENT_TIMESTAMP' DATETIME"` +} diff --git a/app/db/model/permission_group.go b/app/db/model/permission_group.go new file mode 100644 index 0000000..ac927e3 --- /dev/null +++ b/app/db/model/permission_group.go @@ -0,0 +1,11 @@ +package model + +type PermissionGroup struct { + Id int `json:"id" xorm:"not null pk autoincr INT(11)"` + ParentId int `json:"parent_id" xorm:"not null default 0 comment('父级id') INT(11)"` + Key string `json:"key" xorm:"not null default '' VARCHAR(255)"` + Name string `json:"name" xorm:"not null default '' comment('名称') VARCHAR(255)"` + State int `json:"state" xorm:"not null default 1 comment('状态(1:正常 2:废弃)') TINYINT(1)"` + CreateAt string `json:"create_at" xorm:"not null default 'CURRENT_TIMESTAMP' DATETIME"` + UpdateAt string `json:"update_at" xorm:"not null default 'CURRENT_TIMESTAMP' DATETIME"` +} diff --git a/app/db/model/permission_group_permission.go b/app/db/model/permission_group_permission.go new file mode 100644 index 0000000..7d1bad7 --- /dev/null +++ b/app/db/model/permission_group_permission.go @@ -0,0 +1,9 @@ +package model + +type PermissionGroupPermission struct { + Id int `json:"id" xorm:"not null pk autoincr INT(11)"` + GroupId int `json:"group_id" xorm:"not null default 0 comment('权限组id') INT(11)"` + PermissionId int `json:"permission_id" xorm:"not null default 0 comment('权限id') INT(11)"` + CreateAt string `json:"create_at" xorm:"not null default 'CURRENT_TIMESTAMP' DATETIME"` + UpdateAt string `json:"update_at" xorm:"not null default 'CURRENT_TIMESTAMP' DATETIME"` +} diff --git a/app/db/model/role.go b/app/db/model/role.go new file mode 100644 index 0000000..4b46b20 --- /dev/null +++ b/app/db/model/role.go @@ -0,0 +1,10 @@ +package model + +type Role struct { + Id int `json:"id" xorm:"not null pk autoincr INT(11)"` + Name string `json:"name" xorm:"not null default '' comment('名称') VARCHAR(255)"` + State int `json:"state" xorm:"not null default 1 comment('状态(1:正常 2:冻结)') TINYINT(1)"` + Memo string `json:"memo" xorm:"not null default '' comment('备注') VARCHAR(255)"` + CreateAt string `json:"create_at" xorm:"not null default 'CURRENT_TIMESTAMP' DATETIME"` + UpdateAt string `json:"update_at" xorm:"not null default 'CURRENT_TIMESTAMP' DATETIME"` +} diff --git a/app/db/model/role_permission_group.go b/app/db/model/role_permission_group.go new file mode 100644 index 0000000..b9d04a1 --- /dev/null +++ b/app/db/model/role_permission_group.go @@ -0,0 +1,9 @@ +package model + +type RolePermissionGroup struct { + Id int `json:"id" xorm:"not null pk autoincr INT(11)"` + RoleId int `json:"role_id" xorm:"not null default 0 comment('角色id') INT(11)"` + GroupId int `json:"group_id" xorm:"not null default 0 comment('权限组id') INT(11)"` + CreateAt string `json:"create_at" xorm:"not null default 'CURRENT_TIMESTAMP' DATETIME"` + UpdateAt string `json:"update_at" xorm:"not null default 'CURRENT_TIMESTAMP' DATETIME"` +} diff --git a/app/db/model/sys_cfg.go b/app/db/model/sys_cfg.go new file mode 100644 index 0000000..22d906b --- /dev/null +++ b/app/db/model/sys_cfg.go @@ -0,0 +1,7 @@ +package model + +type SysCfg struct { + Key string `json:"key" xorm:"not null pk comment('键') VARCHAR(127)"` + Val string `json:"val" xorm:"comment('值') TEXT"` + Memo string `json:"memo" xorm:"not null default '' comment('备注') VARCHAR(255)"` +} diff --git a/app/db/super/dao/medium_list_dao.go b/app/db/super/dao/medium_list_dao.go new file mode 100644 index 0000000..0cd3bf2 --- /dev/null +++ b/app/db/super/dao/medium_list_dao.go @@ -0,0 +1,5 @@ +package dao + +type MediumListDao interface { + //TODO:: You can add specific method definitions here +} diff --git a/app/db/super/implement/medium_list_implement.go b/app/db/super/implement/medium_list_implement.go new file mode 100644 index 0000000..b9eab0d --- /dev/null +++ b/app/db/super/implement/medium_list_implement.go @@ -0,0 +1,14 @@ +package implement + +import ( + "applet/app/db/super/dao" + "xorm.io/xorm" +) + +func NewMediumListDb(engine *xorm.Engine) dao.MediumListDao { + return &MediumListDb{Db: engine} +} + +type MediumListDb struct { + Db *xorm.Engine +} diff --git a/app/db/super/model/medium_list.go b/app/db/super/model/medium_list.go new file mode 100644 index 0000000..7cdba50 --- /dev/null +++ b/app/db/super/model/medium_list.go @@ -0,0 +1,25 @@ +package model + +type MediumList struct { + Id int `json:"id" xorm:"not null pk autoincr INT(11)"` + Uuid int `json:"uuid" xorm:"not null comment('站长id') index unique(IDX_UUID_TYPE) INT(11)"` + MediumId int `json:"medium_id" xorm:"not null comment('媒体id') unique(IDX_UUID_TYPE) INT(11)"` + Kind int `json:"kind" xorm:"not null default 1 comment('类型(1:企业 2:个人)') TINYINT(1)"` + CompanyName string `json:"company_name" xorm:"not null default '' comment('公司名称') VARCHAR(255)"` + CompanyAbbreviation string `json:"company_abbreviation" xorm:"not null default '' comment('公司简称') VARCHAR(255)"` + UnifiedSocialCreditCode string `json:"unified_social_credit_code" xorm:"not null default '' comment('统一社会信用代码') VARCHAR(255)"` + CertificateType int `json:"certificate_type" xorm:"not null default 1 comment('证件类型') TINYINT(1)"` + BusinessLicenseImgUrl string `json:"business_license_img_url" xorm:"not null default '' comment('营业执照照片') VARCHAR(255)"` + LegalRepresentative string `json:"legal_representative" xorm:"not null default '' comment('法定代表人') CHAR(50)"` + CountryRegionId int `json:"country_region_id" xorm:"not null default 1 comment('国家地区id') TINYINT(3)"` + CountryRegion string `json:"country_region" xorm:"not null default '' comment('国家地区') CHAR(50)"` + RegisteredAddressProvinceId int `json:"registered_address_province_id" xorm:"not null default 0 comment('注册地址-省份id') INT(11)"` + RegisteredAddressCityId int `json:"registered_address_city_id" xorm:"not null default 0 comment('注册地址-市id') INT(11)"` + RegisteredAddressCountyId int `json:"registered_address_county_id" xorm:"not null default 0 comment('注册地址-县/区id') INT(11)"` + RegisteredAddress string `json:"registered_address" xorm:"not null default '' comment('注册地址') VARCHAR(255)"` + BusinessLicenseAddress string `json:"business_license_address" xorm:"not null default '' comment('营业执照地址') VARCHAR(255)"` + CertificateValidity string `json:"certificate_validity" xorm:"not null default '0000-00-00' comment('证件有效期') CHAR(50)"` + State int `json:"state" xorm:"not null default 0 comment('状态(0:待审核 1:审核通过 2:审核拒绝)') TINYINT(1)"` + CreateAt string `json:"create_at" xorm:"not null default 'CURRENT_TIMESTAMP' DATETIME"` + UpdateAt string `json:"update_at" xorm:"not null default 'CURRENT_TIMESTAMP' DATETIME"` +} diff --git a/app/e/code.go b/app/e/code.go new file mode 100644 index 0000000..cc8be46 --- /dev/null +++ b/app/e/code.go @@ -0,0 +1,236 @@ +package e + +const ( + // 200 因为部分第三方接口不能返回错误头,因此在此定义部分错误 + ERR_FILE_SAVE = 200001 + // 400 系列 + ERR_BAD_REQUEST = 400000 + ERR_INVALID_ARGS = 400001 + ERR_API_RESPONSE = 400002 + ERR_NO_DATA = 400003 + ERR_MOBILE_NIL = 400004 + ERR_MOBILE_MATH = 400005 + ERR_FILE_EXT = 400006 + ERR_FILE_MAX_SIZE = 400007 + ERR_SIGN = 400008 + ERR_PASSWORD_MATH = 400009 + ERR_PROVIDER_RESPONSE = 400010 + ERR_AES_ENCODE = 400011 + ERR_ADMIN_API = 400012 + ERR_QINIUAPI_RESPONSE = 400013 + ERR_URL_TURNCHAIN = 400014 + + // 401 未授权 + ERR_UNAUTHORIZED = 401000 + ERR_NOT_AUTH = 401001 + ERR_SMS_AUTH = 401002 + ERR_TOKEN_AUTH = 401003 + ERR_TOKEN_FORMAT = 401004 + ERR_TOKEN_GEN = 401005 + ERR_CACHE_SET = 401006 + // 403 禁止 + ERR_FORBIDEN = 403000 + ERR_PLATFORM = 403001 + ERR_MOBILE_EXIST = 403002 + ERR_USER_NO_EXIST = 403003 + ERR_MOBILE_NO_EXIST = 403004 + ERR_FORBIDEN_VALID = 403005 + ERR_RELATE_ERR = 403006 + ERR_REPEAT_RELATE = 403007 + ERR_MOB_FORBIDEN = 403008 + ERR_MOB_SMS_NO_AVA = 403009 + ERR_USER_IS_REG = 403010 + ERR_MASTER_ID = 403011 + ERR_CASH_OUT_TIME = 403012 + ERR_CASH_OUT_FEE = 403013 + ERR_CASH_OUT_USER_NOT_FOUND = 403014 + ERR_CASH_OUT_FAIL = 403015 + ERR_CASH_OUT_TIMES = 403016 + ERR_CASH_OUT_MINI = 403017 + ERR_CASH_OUT_MUT = 403018 + ERR_CASH_OUT_NOT_DECIMAL = 403019 + ERR_CASH_OUT_NOT_DAY_AVA = 403020 + ERR_USER_LEVEL_PAY_CHECK_TASK_NO_DONE = 403021 + ERR_USER_LEVEL_PAY_CHECK_NO_CROSS = 403022 + ERR_USER_LEVEL_ORD_EXP = 403023 + ERR_IS_BIND_THIRDPARTY = 403024 + ERR_USER_LEVEL_UPDATE_CHECK_TASK_NO_DONE = 403025 + ERR_USER_LEVEL_UPDATE_CHECK_NOT_FOUND_ORDER = 403026 + ERR_USER_LEVEL_UPDATE_REPEAT = 403027 + ERR_USER_NO_ACTIVE = 403028 + ERR_USER_IS_BAN = 403029 + ERR_ALIPAY_SETTING = 403030 + ERR_ALIPAY_ORDERTYPE = 403031 + ERR_CLIPBOARD_UNSUP = 403032 + ERR_SYSUNION_CONFIG = 403033 + ERR_WECAHT_MINI = 403034 + ERR_WECAHT_MINI_CACHE = 403035 + ERR_WECAHT_MINI_DECODE = 403036 + ERR_WECHAT_MINI_ACCESSTOKEN = 403037 + ERR_CURRENT_VIP_LEVEL_AUDITING = 403038 + ERR_LEVEL_RENEW_SHOULD_KEEP_CURRENT = 403039 + ERR_LEVEL_UPGRADE_APPLY_AUDITTING = 403040 + ERR_LEVEL_TASK_PAY_TYPE = 403041 + ERR_BALANCE_NOT_ENOUGH = 403042 + ERR_ADMIN_PUSH = 403043 + ERR_PLAN = 403044 + ERR_MOB_CONFIG = 403045 + ERR_BAlANCE_PAY_ORDERTYPE = 403046 + ERR_PHONE_EXISTED = 403047 + ERR_NOT_RESULT = 403048 + ERR_REVIEW = 403049 + ERR_USER_LEVEL_HAS_PAID = 403050 + ERR_USER_BIND_OWN = 403051 + ERR_PARENTUID_ERR = 403052 + ERR_USER_DEL = 403053 + ERR_SEARCH_ERR = 403054 + ERR_LEVEL_REACH_TOP = 403055 + ERR_USER_CHECK_ERR = 403056 + ERR_PASSWORD_ERR = 403057 + // 404 + ERR_USER_NOTFOUND = 404001 + ERR_SUP_NOTFOUND = 404002 + ERR_LEVEL_MAP = 404003 + ERR_MOD_NOTFOUND = 404004 + ERR_CLIPBOARD_PARSE = 404005 + ERR_NOT_FAN = 404006 + ERR_USER_LEVEL = 404007 + ERR_LACK_PAY_CFG = 404008 + ERR_NOT_LEVEL_TASK = 404009 + ERR_ITEM_NOT_FOUND = 404010 + ERR_WX_CHECKFILE_NOTFOUND = 404011 + + // 429 请求频繁 + ERR_TOO_MANY_REQUESTS = 429000 + // 500 系列 + ERR = 500000 + ERR_UNMARSHAL = 500001 + ERR_UNKNOWN = 500002 + ERR_SMS = 500003 + ERR_ARKID_REGISTER = 500004 + ERR_ARKID_WHITELIST = 500005 + ERR_ARKID_LOGIN = 500006 + ERR_CFG = 500007 + ERR_DB_ORM = 500008 + ERR_CFG_CACHE = 500009 + ERR_ZHIMENG_CONVERT_ERR = 500010 + ERR_ALIPAY_ERR = 500011 + ERR_ALIPAY_ORDER_ERR = 500012 + ERR_PAY_ERR = 500013 + ERR_IS_BIND_THIRDOTHER = 500014 +) + +var MsgFlags = map[int]string{ + // 200 + ERR_FILE_SAVE: "文件保存失败", + // 400 + ERR_BAD_REQUEST: "请求失败", + ERR_INVALID_ARGS: "请求参数错误", + ERR_API_RESPONSE: "API错误", + ERR_QINIUAPI_RESPONSE: "七牛请求API错误", + ERR_URL_TURNCHAIN: "转链失败", + ERR_NO_DATA: "暂无数据", + ERR_MOBILE_NIL: "电话号码不能为空", + ERR_MOBILE_MATH: "电话号码输入有误", + ERR_FILE_MAX_SIZE: "文件上传大小超限", + ERR_FILE_EXT: "文件类型不支持", + ERR_SIGN: "签名校验失败", + ERR_PROVIDER_RESPONSE: "提供商接口错误", + ERR_AES_ENCODE: "加解密错误", + ERR_ADMIN_API: "后台接口请求失败", + // 401 + ERR_NOT_AUTH: "请登录后操作", + ERR_SMS_AUTH: "验证码过期或无效", + ERR_UNAUTHORIZED: "验证用户失败", + ERR_TOKEN_FORMAT: "Token格式不对", + ERR_TOKEN_GEN: "生成Token失败", + ERR_CACHE_SET: "生成缓存失败", + // 403 + ERR_FORBIDEN: "禁止访问", + ERR_PLATFORM: "平台不支持", + ERR_MOBILE_EXIST: "该号码已注册过", + ERR_USER_NO_EXIST: "用户没有注册或账号密码不正确", + ERR_PASSWORD_ERR: "输入两次密码不一致", + ERR_RELATE_ERR: "推荐人不能是自己的粉丝", + ERR_PARENTUID_ERR: "推荐人不存在", + ERR_TOKEN_AUTH: "登录信息失效,请重新登录", + ERR_MOB_SMS_NO_AVA: "短信余额不足或智盟短信配置失败", + ERR_USER_IS_REG: "用户已注册", + ERR_MASTER_ID: "找不到对应站长的数据库", + ERR_CASH_OUT_TIME: "非可提现时间段", + ERR_CASH_OUT_USER_NOT_FOUND: "收款账号不存在", + ERR_CASH_OUT_FAIL: "提现失败", + ERR_CASH_OUT_FEE: "提现金额必须大于手续费", + ERR_CASH_OUT_TIMES: "当日提现次数已达上线", + ERR_CASH_OUT_MINI: "申请提现金额未达到最低金额要求", + ERR_CASH_OUT_MUT: "申请提现金额未达到整数倍要求", + ERR_CASH_OUT_NOT_DECIMAL: "提现申请金额只能是整数", + ERR_CASH_OUT_NOT_DAY_AVA: "不在可提现日期范围内", + ERR_USER_LEVEL_PAY_CHECK_TASK_NO_DONE: "请先完成其他任务", + ERR_USER_LEVEL_PAY_CHECK_NO_CROSS: "无法跨越升级", + ERR_USER_LEVEL_ORD_EXP: "付费订单已失效", + ERR_IS_BIND_THIRDPARTY: "该用户已经绑定了", + ERR_IS_BIND_THIRDOTHER: "该账号已经被绑定了", + ERR_USER_LEVEL_UPDATE_CHECK_TASK_NO_DONE: "请完成指定任务", + ERR_USER_LEVEL_UPDATE_CHECK_NOT_FOUND_ORDER: "没有找到对应的订单", + ERR_USER_LEVEL_UPDATE_REPEAT: "不允许重复升级", + ERR_USER_NO_ACTIVE: "账户没激活", + ERR_USER_IS_BAN: "账户已被冻结", + ERR_SYSUNION_CONFIG: "联盟设置错误,请检查配置", + ERR_WECAHT_MINI: "小程序响应错误,请检查小程序配置", + ERR_WECAHT_MINI_CACHE: "获取小程序缓存失败", + ERR_WECAHT_MINI_DECODE: "小程序解密失败", + ERR_WECHAT_MINI_ACCESSTOKEN: "无法获取accesstoekn", + ERR_CURRENT_VIP_LEVEL_AUDITING: "当前等级正在审核中", + ERR_LEVEL_RENEW_SHOULD_KEEP_CURRENT: "续费只能在当前等级续费", + ERR_LEVEL_UPGRADE_APPLY_AUDITTING: "已有申请正在审核中,暂时不能申请", + ERR_LEVEL_TASK_PAY_TYPE: "任务付费类型错误", + ERR_BALANCE_NOT_ENOUGH: "余额不足", + ERR_ADMIN_PUSH: "后台MOB推送错误", + ERR_PLAN: "分拥方案出错", + ERR_MOB_CONFIG: "Mob 配置错误", + ERR_BAlANCE_PAY_ORDERTYPE: "无效余额支付订单类型", + ERR_PHONE_EXISTED: "手机号码已存在", + ERR_NOT_RESULT: "已加载完毕", + ERR_REVIEW: "审核模板错误", + ERR_USER_LEVEL_HAS_PAID: "该等级已经付过款", + // 404 + ERR_USER_NOTFOUND: "用户不存在", + ERR_USER_DEL: "账号被删除,如有疑问请联系客服", + ERR_SUP_NOTFOUND: "上级用户不存在", + ERR_LEVEL_MAP: "无等级映射关系", + ERR_MOD_NOTFOUND: "没有找到对应模块", + ERR_CLIPBOARD_PARSE: "无法解析剪切板内容", + ERR_NOT_FAN: "没有粉丝", + ERR_CLIPBOARD_UNSUP: "不支持该平台", + ERR_USER_LEVEL: "该等级已不存在", + ERR_LACK_PAY_CFG: "支付配置不完整", + ERR_NOT_LEVEL_TASK: "等级任务查找错误", + ERR_ITEM_NOT_FOUND: "找不到对应商品", + ERR_WX_CHECKFILE_NOTFOUND: "找不到微信校验文件", + ERR_USER_BIND_OWN: "不能填写自己的邀请码", + // 429 + ERR_TOO_MANY_REQUESTS: "请求频繁,请稍后重试", + // 500 内部错误 + ERR: "接口错误", + ERR_SMS: "短信发送出错", + ERR_CFG: "服务器配置错误", + ERR_UNMARSHAL: "JSON解码错误", + ERR_UNKNOWN: "未知错误", + ERR_ARKID_LOGIN: "登录失败", + ERR_MOBILE_NO_EXIST: "该用户未设定手机号", + ERR_FORBIDEN_VALID: "验证码错误", + ERR_CFG_CACHE: "获取配置缓存失败", + ERR_DB_ORM: "数据操作失败", + ERR_REPEAT_RELATE: "重复关联", + ERR_ZHIMENG_CONVERT_ERR: "智盟转链失败", + ERR_MOB_FORBIDEN: "Mob调用失败", + ERR_ALIPAY_ERR: "支付宝参数错误", + ERR_ALIPAY_SETTING: "请在后台正确配置支付宝", + ERR_ALIPAY_ORDERTYPE: "无效支付宝订单类型", + ERR_ALIPAY_ORDER_ERR: "订单创建错误", + ERR_PAY_ERR: "未找到支付方式", + ERR_SEARCH_ERR: "暂无该分类商品", + ERR_LEVEL_REACH_TOP: "已经是最高等级", + ERR_USER_CHECK_ERR: "校验失败", +} diff --git a/app/e/error.go b/app/e/error.go new file mode 100644 index 0000000..2564174 --- /dev/null +++ b/app/e/error.go @@ -0,0 +1,72 @@ +package e + +import ( + "fmt" + "path" + "runtime" +) + +type E struct { + Code int // 错误码 + msg string // 报错代码 + st string // 堆栈信息 +} + +func NewErrCode(code int) error { + if msg, ok := MsgFlags[code]; ok { + return E{code, msg, stack(3)} + } + return E{ERR_UNKNOWN, "unknown", stack(3)} +} + +func NewErr(code int, msg string) error { + return E{code, msg, stack(3)} +} + +func NewErrf(code int, msg string, args ...interface{}) error { + return E{code, fmt.Sprintf(msg, args), stack(3)} +} + +func (e E) Error() string { + return e.msg +} + +func stack(skip int) string { + stk := make([]uintptr, 32) + str := "" + l := runtime.Callers(skip, stk[:]) + for i := 0; i < l; i++ { + f := runtime.FuncForPC(stk[i]) + name := f.Name() + file, line := f.FileLine(stk[i]) + str += fmt.Sprintf("\n%-30s[%s:%d]", name, path.Base(file), line) + } + return str +} + +// ErrorIsAccountBan is 检查这个账号是否被禁用的错误 +func ErrorIsAccountBan(e error) bool { + err, ok := e.(E) + if ok && err.Code == 403029 { + return true + } + return false +} + +// ErrorIsAccountNoActive is 检查这个账号是否被禁用的错误 +func ErrorIsAccountNoActive(e error) bool { + err, ok := e.(E) + if ok && err.Code == 403028 { + return true + } + return false +} + +// ErrorIsUserDel is 检查这个账号是否被删除 +func ErrorIsUserDel(e error) bool { + err, ok := e.(E) + if ok && err.Code == 403053 { + return true + } + return false +} diff --git a/app/e/msg.go b/app/e/msg.go new file mode 100644 index 0000000..ed226ae --- /dev/null +++ b/app/e/msg.go @@ -0,0 +1,110 @@ +package e + +import ( + "applet/app/utils" + "encoding/json" + "net/http" + + "github.com/gin-gonic/gin" + + "applet/app/utils/logx" +) + +// GetMsg get error information based on Code +// 因为这里code是自己控制的, 因此没考虑报错信息 +func GetMsg(code int) (int, string) { + if msg, ok := MsgFlags[code]; ok { + return code / 1000, msg + } + if http.StatusText(code) == "" { + code = 200 + } + return code, MsgFlags[ERR_BAD_REQUEST] +} + +// 成功输出, fields 是额外字段, 与code, msg同级 +func OutSuc(c *gin.Context, data interface{}, fields map[string]interface{}) { + res := gin.H{ + "code": 1, + "msg": "ok", + "data": data, + } + if fields != nil { + for k, v := range fields { + res[k] = v + } + } + if utils.GetApiVersion(c) > 0 { //加了签名校验只返回加密的字符串 + jsonData, _ := json.Marshal(res) + str := utils.ResultAes(c, jsonData) + c.Writer.WriteString(str) + } else { + c.AbortWithStatusJSON(200, res) + } +} + +func OutSucPure(c *gin.Context, data interface{}, fields map[string]interface{}) { + res := gin.H{ + "code": 1, + "msg": "ok", + "data": data, + } + if fields != nil { + for k, v := range fields { + res[k] = v + } + } + c.Abort() + c.PureJSON(200, res) +} + +// 错误输出 +func OutErr(c *gin.Context, code int, err ...interface{}) { + statusCode, msg := GetMsg(code) + if len(err) > 0 && err[0] != nil { + e := err[0] + switch v := e.(type) { + case E: + statusCode = v.Code / 1000 + msg = v.Error() + logx.Error(v.msg + ": " + v.st) // 记录堆栈信息 + case error: + logx.Error(v) + break + case string: + msg = v + case int: + if _, ok := MsgFlags[v]; ok { + msg = MsgFlags[v] + } + } + } + if utils.GetApiVersion(c) > 0 { //加了签名校验只返回加密的字符串 + jsonData, _ := json.Marshal(gin.H{ + "code": code, + "msg": msg, + "data": []struct{}{}, + }) + str := utils.ResultAes(c, jsonData) + if code > 100000 { + code = int(utils.FloatFormat(float64(code/1000), 0)) + } + c.Status(500) + c.Writer.WriteString(str) + } else { + c.AbortWithStatusJSON(statusCode, gin.H{ + "code": code, + "msg": msg, + "data": []struct{}{}, + }) + } +} + +// 重定向 +func OutRedirect(c *gin.Context, code int, loc string) { + if code < 301 || code > 308 { + code = 303 + } + c.Redirect(code, loc) + c.Abort() +} diff --git a/app/e/set_cache.go b/app/e/set_cache.go new file mode 100644 index 0000000..45337a1 --- /dev/null +++ b/app/e/set_cache.go @@ -0,0 +1,8 @@ +package e + +func SetCache(cacheTime int64) map[string]interface{} { + if cacheTime == 0 { + return map[string]interface{}{"cache_time": cacheTime} + } + return map[string]interface{}{"cache_time": cacheTime} +} diff --git a/app/enum/enum_admin.go b/app/enum/enum_admin.go new file mode 100644 index 0000000..e69c56d --- /dev/null +++ b/app/enum/enum_admin.go @@ -0,0 +1,37 @@ +package enum + +type AdminState int32 + +const ( + AdminStateForNormal = 1 + AdminStateForFreeze = 2 +) + +func (gt AdminState) String() string { + switch gt { + case AdminStateForNormal: + return "正常" + case AdminStateForFreeze: + return "冻结" + default: + return "未知" + } +} + +type IsSuperAdministrator int32 + +const ( + IsSuperAdministratorTure = 1 + IsSuperAdministratorFalse = 2 +) + +func (gt IsSuperAdministrator) String() string { + switch gt { + case IsSuperAdministratorTure: + return "超管" + case IsSuperAdministratorFalse: + return "非超管" + default: + return "未知" + } +} diff --git a/app/enum/enum_permission_group.go b/app/enum/enum_permission_group.go new file mode 100644 index 0000000..eda844a --- /dev/null +++ b/app/enum/enum_permission_group.go @@ -0,0 +1,19 @@ +package enum + +type PermissionGroupState int32 + +const ( + PermissionGroupStateForNormal = 1 + PermissionGroupStateForDiscard = 2 +) + +func (gt PermissionGroupState) String() string { + switch gt { + case PermissionGroupStateForNormal: + return "正常" + case PermissionGroupStateForDiscard: + return "废弃" + default: + return "未知" + } +} diff --git a/app/enum/enum_role.go b/app/enum/enum_role.go new file mode 100644 index 0000000..bd8763c --- /dev/null +++ b/app/enum/enum_role.go @@ -0,0 +1,19 @@ +package enum + +type RoleState int32 + +const ( + RoleStateForNormal = 1 + RoleStateForFreeze = 2 +) + +func (gt RoleState) String() string { + switch gt { + case RoleStateForNormal: + return "正常" + case RoleStateForFreeze: + return "冻结" + default: + return "未知" + } +} diff --git a/app/hdl/hdl_demo.go b/app/hdl/hdl_demo.go new file mode 100644 index 0000000..5e1df8b --- /dev/null +++ b/app/hdl/hdl_demo.go @@ -0,0 +1,23 @@ +package hdl + +import ( + "applet/app/e" + "applet/app/svc" + "github.com/gin-gonic/gin" +) + +func Demo(c *gin.Context) { + var args interface{} + err := c.ShouldBindJSON(&args) + if err != nil { + err = svc.HandleValidateErr(err) + err1 := err.(e.E) + e.OutErr(c, err1.Code, err1.Error()) + return + } + + e.OutSuc(c, map[string]interface{}{ + "args": args, + }, nil) + return +} diff --git a/app/hdl/hdl_login.go b/app/hdl/hdl_login.go new file mode 100644 index 0000000..cd33878 --- /dev/null +++ b/app/hdl/hdl_login.go @@ -0,0 +1,60 @@ +package hdl + +import ( + "applet/app/db" + "applet/app/db/implement" + "applet/app/e" + "applet/app/lib/validate" + "applet/app/md" + "applet/app/svc" + "applet/app/utils" + "fmt" + "github.com/gin-gonic/gin" +) + +// Login 登陆 +// @Summary 登陆 +// @Tags ADMIN +// @Description 登入 +// @Accept json +// @Produce json +// @Param req body md2.LoginReq true "用户名密码" +// @Success 200 {object} md2.LoginResponse "token" +// @Failure 400 {object} md2.Response "具体错误" +// @Router /admin/login [post] +func Login(c *gin.Context) { + var req md.LoginReq + err := c.ShouldBindJSON(&req) + if err != nil { + err = validate.HandleValidateErr(err) + err1 := err.(e.E) + e.OutErr(c, err1.Code, err1.Error()) + return + } + engine := db.DBs[svc.GetMasterId(c)] + adminDb := implement.NewAdminDb(engine) + admin, err := adminDb.GetAdminByUserName(req.UserName) + if err != nil { + e.OutErr(c, e.ERR_DB_ORM, err) + return + } + if admin == nil { + e.OutErr(c, e.ERR_NO_DATA, "账号不存在!") + return + } + if utils.Md5(req.PassWord) != admin.Password { + e.OutErr(c, e.ERR_INVALID_ARGS, "密码错误") + return + } + ip := utils.GetIP(c.Request) + key := fmt.Sprintf(md.JwtTokenKey, ip, utils.AnyToString(admin.AdmId)) + token, err := svc.HandleLoginToken(key, admin) + if err != nil { + e.OutErr(c, e.ERR, err.Error()) + return + } + e.OutSuc(c, md.LoginResponse{ + Token: token, + }, nil) + return +} diff --git a/app/hdl/hdl_role.go b/app/hdl/hdl_role.go new file mode 100644 index 0000000..2dc9add --- /dev/null +++ b/app/hdl/hdl_role.go @@ -0,0 +1,649 @@ +package hdl + +import ( + "applet/app/db" + "applet/app/db/implement" + "applet/app/db/model" + "applet/app/e" + "applet/app/enum" + "applet/app/lib/validate" + "applet/app/md" + "applet/app/svc" + "applet/app/utils" + "github.com/gin-gonic/gin" + "time" +) + +// PermissionGroupList +// @Summary 权限组列表 +// @Tags ADMIN-权限管理 +// @Description 权限组列表 +// @param Authorization header string true "验证参数Bearer和token空格拼接" +// @Accept json +// @Produce json +// @param adm_id query string true "管理员id" +// @Success 200 {string} "具体看返回内容" +// @Failure 400 {object} md.Response "具体错误" +// @Router /admin/role/permissionGroupList [GET] +func PermissionGroupList(c *gin.Context) { + roleId := c.DefaultQuery("role_id", "") + engine := db.DBs[svc.GetMasterId(c)] + qrcodeWithBatchRecordsDb := implement.NewPermissionGroupDb(engine) + groupList, err := qrcodeWithBatchRecordsDb.FindPermissionGroup() + if err != nil { + e.OutErr(c, e.ERR_DB_ORM, err.Error()) + return + } + + roleDb := implement.NewRoleDb(engine, utils.StrToInt(roleId)) + list, _, err := roleDb.FindPermissionGroupByRole(utils.StrToInt(roleId)) + if err != nil { + e.OutErr(c, e.ERR_DB_ORM, err.Error()) + return + } + var isHasPermissionGroupId []string + for _, v := range list { + isHasPermissionGroupId = append(isHasPermissionGroupId, utils.IntToStr(v.PermissionGroup.Id)) + } + + var tempRespMap = map[string]*md.PermissionGroupListResp{} + var tempRespMapKeys []string + for _, v := range *groupList { + isCheck := false + if utils.InArr(utils.IntToStr(v.Id), isHasPermissionGroupId) { + isCheck = true + } + tempRespMap[utils.IntToStr(v.Id)] = &md.PermissionGroupListResp{ + Id: v.Id, + Name: v.Name, + Key: v.Key, + State: v.State, + ParentId: v.ParentId, + CreateAt: v.CreateAt, + UpdateAt: v.UpdateAt, + IsCheck: isCheck, + } + tempRespMapKeys = append(tempRespMapKeys, utils.IntToStr(v.Id)) + } + for _, v := range tempRespMap { + if v.ParentId != 0 && tempRespMap[utils.IntToStr(v.ParentId)].ParentId != 0 { + tempRespMap[utils.IntToStr(v.ParentId)].SubPermissionGroupList = append(tempRespMap[utils.IntToStr(v.ParentId)].SubPermissionGroupList, *v) + } + } + for _, v := range tempRespMap { + if v.ParentId != 0 && tempRespMap[utils.IntToStr(v.ParentId)].ParentId == 0 { + tempRespMap[utils.IntToStr(v.ParentId)].SubPermissionGroupList = append(tempRespMap[utils.IntToStr(v.ParentId)].SubPermissionGroupList, *v) + } + } + + var resp []*md.PermissionGroupListResp + for _, v := range tempRespMapKeys { + if tempRespMap[v].ParentId == 0 { + resp = append(resp, tempRespMap[v]) + } + } + + e.OutSuc(c, map[string]interface{}{ + "list": resp, + "state": []map[string]interface{}{ + { + "name": enum.PermissionGroupState(enum.PermissionGroupStateForNormal).String(), + "value": enum.PermissionGroupStateForNormal, + }, + { + "name": enum.PermissionGroupState(enum.PermissionGroupStateForDiscard).String(), + "value": enum.PermissionGroupStateForDiscard, + }, + }, + }, nil) + return +} + +// RoleList +// @Summary 角色列表 +// @Tags ADMIN-权限管理 +// @Description 角色列表 +// @param Authorization header string true "验证参数Bearer和token空格拼接" +// @Accept json +// @Produce json +// @Success 200 {string} "具体看返回内容" +// @Failure 400 {object} md.Response "具体错误" +// @Router /admin/role/roleList [GET] +func RoleList(c *gin.Context) { + engine := db.DBs[svc.GetMasterId(c)] + roleDb := implement.NewRoleDb(engine, 0) + roleList, err := roleDb.FindRole() + if err != nil { + e.OutErr(c, e.ERR_DB_ORM, err.Error()) + return + } + + adminRoleDb := implement.NewAdminRoleDb(engine) + adminDb := implement.NewAdminDb(engine) + var result []*md.RoleListResp + for _, v := range *roleList { + var temp md.RoleListResp + temp.Data = v + adminRoles, err1 := adminRoleDb.FindAdminRoleByRoleId(v.Id) + if err1 != nil { + e.OutErr(c, e.ERR_DB_ORM, err1.Error()) + return + } + for _, adminRole := range *adminRoles { + admin, err2 := adminDb.GetAdmin(adminRole.AdmId) + if err2 != nil { + e.OutErr(c, e.ERR_DB_ORM, err2.Error()) + return + } + temp.AdminList = append(temp.AdminList, struct { + Name string `json:"name"` + }{ + Name: admin.Username, + }) + } + result = append(result, &temp) + } + e.OutSuc(c, map[string]interface{}{ + "list": result, + "state": []map[string]interface{}{ + { + "name": enum.RoleState(enum.RoleStateForNormal).String(), + "value": enum.RoleStateForNormal, + }, + { + "name": enum.RoleState(enum.RoleStateForFreeze).String(), + "value": enum.RoleStateForFreeze, + }, + }, + }, nil) + return +} + +// AddRole +// @Summary 添加角色 +// @Tags ADMIN-权限管理 +// @Description 添加角色 +// @param Authorization header string true "验证参数Bearer和token空格拼接" +// @Accept json +// @Produce json +// @Param args body md.AddRoleReq true "请求参数" +// @Success 200 {string} "success" +// @Failure 400 {object} md.Response "具体错误" +// @Router /admin/role/addRole [POST] +func AddRole(c *gin.Context) { + var req md.AddRoleReq + err := c.ShouldBindJSON(&req) + if err != nil { + err = validate.HandleValidateErr(err) + err1 := err.(e.E) + e.OutErr(c, err1.Code, err1.Error()) + return + } + engine := db.DBs[svc.GetMasterId(c)] + roleDb := implement.NewRoleDb(engine, 0) + now := time.Now() + _, err = roleDb.RoleInsert(&model.Role{ + Name: req.Name, + State: enum.RoleStateForNormal, + Memo: req.Memo, + CreateAt: now.Format("2006-01-02 15:04:05"), + UpdateAt: now.Format("2006-01-02 15:04:05"), + }) + if err != nil { + e.OutErr(c, e.ERR_DB_ORM, err.Error()) + return + } + + e.OutSuc(c, "success", nil) + return +} + +// UpdateRole +// @Summary 修改角色 +// @Tags ADMIN-权限管理 +// @Description 修改角色 +// @param Authorization header string true "验证参数Bearer和token空格拼接" +// @Accept json +// @Produce json +// @Param args body md.UpdateRoleReq true "请求参数" +// @Success 200 {string} "success" +// @Failure 400 {object} md.Response "具体错误" +// @Router /admin/role/updateRole [POST] +func UpdateRole(c *gin.Context) { + var req md.UpdateRoleReq + err := c.ShouldBindJSON(&req) + if err != nil { + err = validate.HandleValidateErr(err) + err1 := err.(e.E) + e.OutErr(c, err1.Code, err1.Error()) + return + } + engine := db.DBs[svc.GetMasterId(c)] + roleDb := implement.NewRoleDb(engine, req.RoleId) + role, err := roleDb.GetRole() + if err != nil { + e.OutErr(c, e.ERR_DB_ORM, err.Error()) + return + } + if role == nil { + e.OutErr(c, e.ERR_NO_DATA, "未查询到相应记录") + return + } + role.Name = req.Name + role.Memo = req.Memo + _, err = roleDb.UpdateRole(role, "name", "memo") + if err != nil { + e.OutErr(c, e.ERR_DB_ORM, err.Error()) + return + } + e.OutSuc(c, "success", nil) + return +} + +// RoleBindPermissionGroup +// @Summary 角色绑定权限组 +// @Tags ADMIN-权限管理 +// @Description 角色绑定权限组 +// @param Authorization header string true "验证参数Bearer和token空格拼接" +// @Accept json +// @Produce json +// @Param args body md.RoleBindPermissionGroupReq true "请求参数" +// @Success 200 {string} "success" +// @Failure 400 {object} md.Response "具体错误" +// @Router /admin/role/roleBindPermissionGroup [POST] +func RoleBindPermissionGroup(c *gin.Context) { + var req md.RoleBindPermissionGroupReq + err := c.ShouldBindJSON(&req) + if err != nil { + err = validate.HandleValidateErr(err) + err1 := err.(e.E) + e.OutErr(c, err1.Code, err1.Error()) + return + } + err = svc.RoleBindPermissionGroup(c, req) + if err != nil { + e.OutErr(c, e.ERR, err.Error()) + return + } + + e.OutSuc(c, "success", nil) + return +} + +// UpdateRoleState +// @Summary 修改角色状态 +// @Tags ADMIN-权限管理 +// @Description 修改角色状态 +// @param Authorization header string true "验证参数Bearer和token空格拼接" +// @Accept json +// @Produce json +// @Param args body md.UpdateRoleStateReq true "请求参数" +// @Success 200 {string} "success" +// @Failure 400 {object} md.Response "具体错误" +// @Router /admin/role/updateRole [POST] +func UpdateRoleState(c *gin.Context) { + var req md.UpdateRoleStateReq + err := c.ShouldBindJSON(&req) + if err != nil { + err = validate.HandleValidateErr(err) + err1 := err.(e.E) + e.OutErr(c, err1.Code, err1.Error()) + return + } + engine := db.DBs[svc.GetMasterId(c)] + roleDb := implement.NewRoleDb(engine, req.RoleId) + role, err := roleDb.GetRole() + if err != nil { + e.OutErr(c, e.ERR_DB_ORM, err.Error()) + return + } + if role == nil { + e.OutErr(c, e.ERR_NO_DATA, "未查询到相应记录") + return + } + role.State = req.State + _, err = roleDb.UpdateRole(role, "state") + if err != nil { + e.OutErr(c, e.ERR_DB_ORM, err.Error()) + return + } + e.OutSuc(c, "success", nil) + return +} + +// DeleteRole +// @Summary 删除角色 +// @Tags ADMIN-权限管理 +// @Description 删除角色 +// @param Authorization header string true "验证参数Bearer和token空格拼接" +// @Accept json +// @Produce json +// @Param args body md.UpdateRoleStateReq true "请求参数" +// @Success 200 {string} "success" +// @Failure 400 {object} md.Response "具体错误" +// @Router /admin/role/deleteRole/{$id} [DELETE] +func DeleteRole(c *gin.Context) { + id := c.Param("id") + engine := db.DBs[svc.GetMasterId(c)] + roleDb := implement.NewRoleDb(engine, utils.StrToInt(id)) + role, err := roleDb.GetRole() + if err != nil { + e.OutErr(c, e.ERR_DB_ORM, err.Error()) + return + } + if role == nil { + e.OutErr(c, e.ERR_NO_DATA, "未查询到相应记录") + return + } + + err = svc.DeleteRole(c, utils.StrToInt(id)) + if err != nil { + e.OutErr(c, e.ERR, err.Error()) + return + } + + e.OutSuc(c, "success", nil) + return +} + +// AdminList +// @Summary 管理员列表 +// @Tags ADMIN-权限管理 +// @Description 管理员列表 +// @param Authorization header string true "验证参数Bearer和token空格拼接" +// @Accept json +// @Produce json +// @Param args body md.AdminListReq true "请求参数" +// @Success 200 {string} "具体看返回内容" +// @Failure 400 {object} md.Response "具体错误" +// @Router /admin/role/adminList [POST] +func AdminList(c *gin.Context) { + var req md.AdminListReq + err := c.ShouldBindJSON(&req) + if err != nil { + err = validate.HandleValidateErr(err) + err1 := err.(e.E) + e.OutErr(c, err1.Code, err1.Error()) + return + } + if req.Limit == 0 { + req.Limit = 10 + } + if req.Page == 0 { + req.Page = 10 + } + engine := db.DBs[svc.GetMasterId(c)] + adminDb := implement.NewAdminDb(engine) + adminList, total, err := adminDb.FindAdmin(req.UserName, req.State, req.Page, req.Limit) + if err != nil { + e.OutErr(c, e.ERR_DB_ORM, err.Error()) + return + } + var result []md.AdminListResp + for _, v := range adminList { + permissionGroupList, _, err1 := adminDb.FindAdminRolePermissionGroup(v.AdmId) + if err1 != nil { + e.OutErr(c, e.ERR_DB_ORM, err1.Error()) + return + } + var roleList []string + for _, v1 := range permissionGroupList { + roleList = append(roleList, v1.Role.Name) + } + + result = append(result, md.AdminListResp{ + AdmId: v.AdmId, + Username: v.Username, + State: v.State, + IsSuperAdministrator: v.IsSuperAdministrator, + Memo: v.Memo, + CreateAt: v.CreateAt, + UpdateAt: v.UpdateAt, + RoleList: roleList, + }) + } + + e.OutSuc(c, map[string]interface{}{ + "list": result, + "total": total, + "state": []map[string]interface{}{ + { + "name": enum.RoleState(enum.RoleStateForNormal).String(), + "value": enum.RoleStateForNormal, + }, + { + "name": enum.RoleState(enum.RoleStateForFreeze).String(), + "value": enum.RoleStateForFreeze, + }, + }, + }, nil) + return +} + +// UpdateAdminState +// @Summary 修改管理员状态 +// @Tags ADMIN-权限管理 +// @Description 修改管理员状态 +// @param Authorization header string true "验证参数Bearer和token空格拼接" +// @Accept json +// @Produce json +// @Param args body md.UpdateAdminStateReq true "请求参数" +// @Success 200 {string} "success" +// @Failure 400 {object} md.Response "具体错误" +// @Router /admin/role/updateAdminState [POST] +func UpdateAdminState(c *gin.Context) { + var req md.UpdateAdminStateReq + err := c.ShouldBindJSON(&req) + if err != nil { + err = validate.HandleValidateErr(err) + err1 := err.(e.E) + e.OutErr(c, err1.Code, err1.Error()) + return + } + + engine := db.DBs[svc.GetMasterId(c)] + admDb := implement.NewAdminDb(engine) + admin, err := admDb.GetAdmin(req.AdmId) + if err != nil { + e.OutErr(c, e.ERR_DB_ORM, err.Error()) + return + } + if admin == nil { + e.OutErr(c, e.ERR_NO_DATA, "未查询到相应记录") + return + } + admin.State = req.State + _, err = admDb.UpdateAdmin(admin, "state") + if err != nil { + e.OutErr(c, e.ERR_DB_ORM, err.Error()) + return + } + e.OutSuc(c, "success", nil) + return +} + +// UpdateAdmin +// @Summary 修改管理员信息 +// @Tags ADMIN-权限管理 +// @Description 修改管理员信息 +// @param Authorization header string true "验证参数Bearer和token空格拼接" +// @Accept json +// @Produce json +// @Param args body md.UpdateAdminReq true "请求参数" +// @Success 200 {string} "success" +// @Failure 400 {object} md.Response "具体错误" +// @Router /admin/role/updateAdmin [POST] +func UpdateAdmin(c *gin.Context) { + var req md.UpdateAdminReq + err := c.ShouldBindJSON(&req) + if err != nil { + err = validate.HandleValidateErr(err) + err1 := err.(e.E) + e.OutErr(c, err1.Code, err1.Error()) + return + } + engine := db.DBs[svc.GetMasterId(c)] + admDb := implement.NewAdminDb(engine) + admin, err := admDb.GetAdmin(req.AdmId) + if err != nil { + e.OutErr(c, e.ERR_DB_ORM, err.Error()) + return + } + if admin == nil { + e.OutErr(c, e.ERR_NO_DATA, "未查询到相应记录") + return + } + admin.Username = req.Username + admin.Memo = req.Memo + admin.Password = utils.Md5(req.Password) + _, err = admDb.UpdateAdmin(admin, "username", "memo", "password") + if err != nil { + e.OutErr(c, e.ERR_DB_ORM, err.Error()) + return + } + e.OutSuc(c, "success", nil) + return +} + +// AddAdmin +// @Summary 新增管理员 +// @Tags ADMIN-权限管理 +// @Description 新增管理员 +// @param Authorization header string true "验证参数Bearer和token空格拼接" +// @Accept json +// @Produce json +// @Param args body md.AddAdminReq true "请求参数" +// @Success 200 {string} "success" +// @Failure 400 {object} md.Response "具体错误" +// @Router /admin/role/addAdmin [POST] +func AddAdmin(c *gin.Context) { + var req md.AddAdminReq + err := c.ShouldBindJSON(&req) + if err != nil { + err = validate.HandleValidateErr(err) + err1 := err.(e.E) + e.OutErr(c, err1.Code, err1.Error()) + return + } + engine := db.DBs[svc.GetMasterId(c)] + admDb := implement.NewAdminDb(engine) + isHasAdmin, err := admDb.GetAdminByUserName(req.Username) + if err != nil { + e.OutErr(c, e.ERR_DB_ORM, err.Error()) + return + } + if isHasAdmin != nil { + e.OutErr(c, e.ERR, "当前用户名已存在,请勿重复添加") + return + } + + admId, err := admDb.CreateAdminId() + if err != nil { + e.OutErr(c, e.ERR_DB_ORM, err.Error()) + return + } + admin := model.Admin{ + AdmId: admId, + Username: req.Username, + Password: utils.Md5(req.Password), + State: enum.AdminStateForNormal, + IsSuperAdministrator: 0, + Memo: req.Memo, + CreateAt: time.Now().Format("2006-01-02 15:04:05"), + UpdateAt: time.Now().Format("2006-01-02 15:04:05"), + } + _, err = admDb.AdminInsert(&admin) + if err != nil { + e.OutErr(c, e.ERR_DB_ORM, err.Error()) + return + } + e.OutSuc(c, "success", nil) + return +} + +// DeleteAdmin +// @Summary 删除管理员 +// @Tags ADMIN-权限管理 +// @Description 删除管理员 +// @param Authorization header string true "验证参数Bearer和token空格拼接" +// @Accept json +// @Produce json +// @Success 200 {string} "success" +// @Failure 400 {object} md.Response "具体错误" +// @Router /admin/role/deleteAdmin/{$adm_id} [DELETE] +func DeleteAdmin(c *gin.Context) { + admId := c.Param("adm_id") + err := svc.AdminDelete(c, []int{utils.StrToInt(admId)}) + if err != nil { + e.OutErr(c, e.ERR_DB_ORM, err.Error()) + return + } + e.OutSuc(c, "success", nil) + return +} + +// BindAdminRole +// @Summary 管理员绑定角色 +// @Tags ADMIN-权限管理 +// @Description 管理员绑定角色 +// @param Authorization header string true "验证参数Bearer和token空格拼接" +// @Accept json +// @Produce json +// @Param args body md.BindAdminRoleReq true "请求参数" +// @Success 200 {string} "success" +// @Failure 400 {object} md.Response "具体错误" +// @Router /admin/role/bindAdminRole/ [POST] +func BindAdminRole(c *gin.Context) { + var req md.BindAdminRoleReq + err := c.ShouldBindJSON(&req) + if err != nil { + err = validate.HandleValidateErr(err) + err1 := err.(e.E) + e.OutErr(c, err1.Code, err1.Error()) + return + } + err = svc.BindAdminRole(c, req) + if err != nil { + e.OutErr(c, e.ERR, err.Error()) + return + } + e.OutSuc(c, "success", nil) + return +} + +// AdminInfo +// @Summary 管理员信息 +// @Tags ADMIN-权限管理 +// @Description 管理员信息 +// @param Authorization header string true "验证参数Bearer和token空格拼接" +// @Accept json +// @Produce json +// @param adm_id query string true "管理员id" +// @Success 200 {string} "具体看返回内容" +// @Failure 400 {object} md.Response "具体错误" +// @Router /admin/role/adminInfo [GET] +func AdminInfo(c *gin.Context) { + admId := c.DefaultQuery("adm_id", "") + engine := db.DBs[svc.GetMasterId(c)] + admDb := implement.NewAdminDb(engine) + admin, err := admDb.GetAdmin(utils.StrToInt(admId)) + if err != nil { + e.OutErr(c, e.ERR_DB_ORM, err.Error()) + return + } + admin.Password = "" + e.OutSuc(c, map[string]interface{}{ + "info": admin, + "state": []map[string]interface{}{ + { + "name": enum.RoleState(enum.RoleStateForNormal).String(), + "value": enum.RoleStateForNormal, + }, + { + "name": enum.RoleState(enum.RoleStateForFreeze).String(), + "value": enum.RoleStateForFreeze, + }, + }, + }, nil) + return +} diff --git a/app/lib/alipay/api.go b/app/lib/alipay/api.go new file mode 100644 index 0000000..9b54aac --- /dev/null +++ b/app/lib/alipay/api.go @@ -0,0 +1,262 @@ +package alipay + +import ( + "applet/app/cfg" + "applet/app/pay/md" + "applet/app/utils/logx" + "fmt" + + "github.com/iGoogle-ink/gopay" + "github.com/iGoogle-ink/gopay/alipay" +) + +// TradeAppPay is 支付宝APP支付 +// 抖音头条小程序使用APP调起 +func TradeAppPay(appID, priKey, subject, orderID, amount, notiURL, RSA, PKCS string, paySet *md.PayData) (string, error) { + //初始化支付宝客户端 + // appID 是在支付宝申请的APPID + // priKey 是支付宝私钥 + // subject 是支付订单的主题 + // orderID 是智莺这边生成的订单id + // amount 是付费金额 + // notiURL 通知地址url + // passback_params 回调通知参数 + + client := alipay.NewClient(appID, priKey, true) + client.DebugSwitch = gopay.DebugOn + //判断密钥的类型 + rsa_type := alipay.RSA2 + pkcs_type := alipay.PKCS1 + if RSA == "1" { + rsa_type = alipay.RSA + } + if PKCS == "1" { + pkcs_type = alipay.PKCS8 + } + if paySet.PayAliUseType == "1" { + rsa_type = alipay.RSA2 + pkcs_type = alipay.PKCS8 + } + //配置公共参数 + client.SetCharset("utf-8"). + SetSignType(rsa_type). + SetPrivateKeyType(pkcs_type) + if notiURL != "" { + client.SetNotifyUrl(notiURL) + } + //新支付宝支付 + if paySet.PayAliUseType == "1" { + appCertSN, err := alipay.GetCertSN(cfg.WxappletFilepath.URL + "/" + paySet.PayAppCertSn) + fmt.Println("-应用-") + fmt.Println(appCertSN) + if err != nil { + fmt.Println(err) + return "", err + } + if appCertSN == "" { + fmt.Println(err) + return "", err + } + client.SetAppCertSN(appCertSN) + //aliPayRootCertSN, err := alipay.GetCertSN(cfg.WxappletFilepath.URL + "/" + paySet.PayAlipayRootCertSn) + aliPayRootCertSN := "687b59193f3f462dd5336e5abf83c5d8_02941eef3187dddf3d3b83462e1dfcf6" + client.SetAliPayRootCertSN(aliPayRootCertSN) + aliPayPublicCertSN, err := alipay.GetCertSN(cfg.WxappletFilepath.URL + "/" + paySet.PayAlipayrsaPublicKey) + fmt.Println("-公钥-") + fmt.Println(aliPayPublicCertSN) + + if err != nil { + fmt.Println(err) + return "", err + } + if aliPayPublicCertSN == "" { + fmt.Println(err) + return "", err + } + client.SetAliPayPublicCertSN(aliPayPublicCertSN) + } + fmt.Println(client) + //请求参数 + body := make(gopay.BodyMap) + body.Set("subject", subject) + body.Set("body", subject) + body.Set("out_trade_no", orderID) + body.Set("total_amount", amount) + body.Set("timeout_express", "30m") + + // body.Set("passback_params", orderID) + //手机APP支付参数请求 + payParam, err := client.TradeAppPay(body) + if err != nil { + return "", logx.Warn(err) + } + return payParam, nil +} + +// TradeAppPay is 支付宝H5支付 +func TradeWapPay(appID, priKey, subject, orderID, amount, notiURL, RSA, PKCS, page_url string, paySet *md.PayData) (string, error) { + fmt.Println("notifyURL is:>>>>>>>>>>", notiURL) + //aliPayPublicKey := "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1wn1sU/8Q0rYLlZ6sq3enrPZw2ptp6FecHR2bBFLjJ+sKzepROd0bKddgj+Mr1ffr3Ej78mLdWV8IzLfpXUi945DkrQcOUWLY0MHhYVG2jSs/qzFfpzmtut2Cl2TozYpE84zom9ei06u2AXLMBkU6VpznZl+R4qIgnUfByt3Ix5b3h4Cl6gzXMAB1hJrrrCkq+WvWb3Fy0vmk/DUbJEz8i8mQPff2gsHBE1nMPvHVAMw1GMk9ImB4PxucVek4ZbUzVqxZXphaAgUXFK2FSFU+Q+q1SPvHbUsjtIyL+cLA6H/6ybFF9Ffp27Y14AHPw29+243/SpMisbGcj2KD+evBwIDAQAB" + privateKey := priKey + //判断密钥的类型 + rsa_type := alipay.RSA2 + pkcs_type := alipay.PKCS1 + if RSA == "1" { + rsa_type = alipay.RSA + } + if PKCS == "1" { + pkcs_type = alipay.PKCS8 + } + if paySet.PayAliUseType == "1" { + rsa_type = alipay.RSA2 + pkcs_type = alipay.PKCS8 + } + //初始化支付宝客户端 + // appId:应用ID + // privateKey:应用秘钥 + // isProd:是否是正式环境 + client := alipay.NewClient(appID, privateKey, true) + //配置公共参数 + client.SetCharset("utf-8"). + SetSignType(rsa_type). + SetPrivateKeyType(pkcs_type). + SetReturnUrl(page_url). + SetNotifyUrl(notiURL) + //新支付宝支付 + if paySet.PayAliUseType == "1" { + appCertSN, err := alipay.GetCertSN(cfg.WxappletFilepath.URL + "/" + paySet.PayAppCertSn) + if err != nil { + fmt.Println("appCertSn_err:>>>>>>>>", err) + return "", err + } + if appCertSN == "" { + fmt.Println(err) + return "", err + } + client.SetAppCertSN(appCertSN) + //aliPayRootCertSN, err := alipay.GetCertSN(cfg.WxappletFilepath.URL + "/" + paySet.PayAlipayRootCertSn) + //if err != nil { + // + // fmt.Println("rootcertsn_err:>>>>>>>>>>", err) + // fmt.Println("rootcertsn_err:>>>>>>>>>>", cfg.WxappletFilepath.URL) + // fmt.Println("rootcertsn_err:>>>>>>>>>>", paySet.PayAlipayRootCertSn) + // return "", err + //} + //if aliPayRootCertSN == "" { + // fmt.Println(err) + // return "", err + //} + aliPayRootCertSN := "687b59193f3f462dd5336e5abf83c5d8_02941eef3187dddf3d3b83462e1dfcf6" + client.SetAliPayRootCertSN(aliPayRootCertSN) + aliPayPublicCertSN, err := alipay.GetCertSN(cfg.WxappletFilepath.URL + "/" + paySet.PayAlipayrsaPublicKey) + if err != nil { + fmt.Println("publicCertSn_err:>>>>>>>>>>>", err) + return "", err + } + if aliPayPublicCertSN == "" { + fmt.Println(err) + return "", err + } + client.SetAliPayPublicCertSN(aliPayPublicCertSN) + } + //请求参数 + body := make(gopay.BodyMap) + body.Set("subject", subject) + body.Set("out_trade_no", orderID) + // quit_url is 用户付款中途退出返回商户网站的地址 + body.Set("quit_url", notiURL) + body.Set("total_amount", amount) + // product_code is 销售产品码,商家和支付宝签约的产品码 + body.Set("product_code", "QUICK_WAP_WAY") + //手机网站支付请求 + payUrl, err := client.TradeWapPay(body) + if err != nil { + return "", logx.Warn(err) + + } + return payUrl, nil +} + +// TradeAppPay is 支付宝小程序本身支付 +func TradeCreate(appID, priKey, subject, orderID, amount, notiURL, RSA, PKCS string, paySet *md.PayData) (*alipay.TradeCreateResponse, error) { + //aliPayPublicKey := "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1wn1sU/8Q0rYLlZ6sq3enrPZw2ptp6FecHR2bBFLjJ+sKzepROd0bKddgj+Mr1ffr3Ej78mLdWV8IzLfpXUi945DkrQcOUWLY0MHhYVG2jSs/qzFfpzmtut2Cl2TozYpE84zom9ei06u2AXLMBkU6VpznZl+R4qIgnUfByt3Ix5b3h4Cl6gzXMAB1hJrrrCkq+WvWb3Fy0vmk/DUbJEz8i8mQPff2gsHBE1nMPvHVAMw1GMk9ImB4PxucVek4ZbUzVqxZXphaAgUXFK2FSFU+Q+q1SPvHbUsjtIyL+cLA6H/6ybFF9Ffp27Y14AHPw29+243/SpMisbGcj2KD+evBwIDAQAB" + privateKey := priKey + rsa_type := alipay.RSA2 + pkcs_type := alipay.PKCS1 + if RSA == "1" { + rsa_type = alipay.RSA + } + if PKCS == "1" { + pkcs_type = alipay.PKCS8 + } + if paySet.PayAliUseType == "1" { + rsa_type = alipay.RSA2 + pkcs_type = alipay.PKCS8 + } + //初始化支付宝客户端 + // appId:应用ID + // privateKey:应用私钥,支持PKCS1和PKCS8 + // isProd:是否是正式环境 + client := alipay.NewClient(appID, privateKey, true) + //配置公共参数 + client.SetCharset("utf-8"). + SetSignType(rsa_type). + SetPrivateKeyType(pkcs_type). + SetNotifyUrl(notiURL) + if paySet.PayAliUseType == "1" { + appCertSN, err := alipay.GetCertSN(cfg.WxappletFilepath.URL + "/" + paySet.PayAppCertSn) + if err != nil { + fmt.Println(err) + return nil, err + } + if appCertSN == "" { + fmt.Println(err) + return nil, err + } + client.SetAppCertSN(appCertSN) + //aliPayRootCertSN, err := alipay.GetCertSN(cfg.WxappletFilepath.URL + "/" + paySet.PayAlipayRootCertSn) + //if err != nil { + // fmt.Println(err) + // return nil, err + //} + //if aliPayRootCertSN == "" { + // fmt.Println(err) + // return nil, err + //} + aliPayRootCertSN := "687b59193f3f462dd5336e5abf83c5d8_02941eef3187dddf3d3b83462e1dfcf6" + client.SetAliPayRootCertSN(aliPayRootCertSN) + aliPayPublicCertSN, err := alipay.GetCertSN(cfg.WxappletFilepath.URL + "/" + paySet.PayAlipayrsaPublicKey) + if err != nil { + fmt.Println(err) + return nil, err + } + if aliPayPublicCertSN == "" { + fmt.Println(err) + return nil, err + } + client.SetAliPayPublicCertSN(aliPayPublicCertSN) + } + //请求参数 + body := make(gopay.BodyMap) + body.Set("subject", subject) + // 支付宝小程序支付时 buyer_id 为必传参数,需要提前获取,获取方法如下两种 + // 1、alipay.SystemOauthToken() 返回取值:rsp.SystemOauthTokenResponse.UserId + // 2、client.SystemOauthToken() 返回取值:aliRsp.SystemOauthTokenResponse.UserId + buyer_id, err := client.SystemOauthToken(body) + if err != nil { + return nil, logx.Warn(err) + } + body.Set("buyer_id", buyer_id) + body.Set("out_trade_no", orderID) + body.Set("total_amount", amount) + //创建订单 + aliRsp, err := client.TradeCreate(body) + + if err != nil { + return nil, logx.Warn(err) + } + logx.Warn("aliRsp:", *aliRsp) + logx.Warn("aliRsp.TradeNo:", aliRsp.Response.TradeNo) + return aliRsp, nil + +} diff --git a/app/lib/auth/auth.go b/app/lib/auth/auth.go new file mode 100644 index 0000000..f2744e3 --- /dev/null +++ b/app/lib/auth/auth.go @@ -0,0 +1,39 @@ +package auth + +import ( + "errors" + "github.com/dgrijalva/jwt-go" + "time" +) + +// GenToken 生成JWT +func GenToken(admId int, username string) (string, error) { + // 创建一个我们自己的声明 + c := JWTUser{ + AdmId: admId, + Username: username, + StandardClaims: jwt.StandardClaims{ + ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), // 过期时间 + Issuer: "advertisement", // 签发人 + }, + } + // 使用指定的签名方法创建签名对象 + token := jwt.NewWithClaims(jwt.SigningMethodHS256, c) + // 使用指定的secret签名并获得完整的编码后的字符串token + return token.SignedString(Secret) +} + +// ParseToken 解析JWT +func ParseToken(tokenString string) (*JWTUser, error) { + // 解析token + token, err := jwt.ParseWithClaims(tokenString, &JWTUser{}, func(token *jwt.Token) (i interface{}, err error) { + return Secret, nil + }) + if err != nil { + return nil, err + } + if claims, ok := token.Claims.(*JWTUser); ok && token.Valid { // 校验token + return claims, nil + } + return nil, errors.New("invalid token") +} diff --git a/app/lib/auth/base.go b/app/lib/auth/base.go new file mode 100644 index 0000000..9874b96 --- /dev/null +++ b/app/lib/auth/base.go @@ -0,0 +1,19 @@ +package auth + +import ( + "time" + + "github.com/dgrijalva/jwt-go" +) + +// TokenExpireDuration is jwt 过期时间 +const TokenExpireDuration = time.Hour * 24 + +var Secret = []byte("micro_group_admin") + +// JWTUser 如果想要保存更多信息,都可以添加到这个结构体中 +type JWTUser struct { + AdmId int `json:"adm_id"` + Username string `json:"username"` + jwt.StandardClaims +} diff --git a/app/lib/qiniu/bucket_create.go b/app/lib/qiniu/bucket_create.go new file mode 100644 index 0000000..28d8106 --- /dev/null +++ b/app/lib/qiniu/bucket_create.go @@ -0,0 +1,16 @@ +package qiniu + +import ( + "github.com/qiniu/api.v7/v7/auth" + "github.com/qiniu/api.v7/v7/storage" +) + +func BucketCreate() error { + mac := auth.New(AK, SK) + cfg := storage.Config{ + // 是否使用https域名进行资源管理 + UseHTTPS: false, + } + bucketManager := storage.NewBucketManager(mac, &cfg) + return bucketManager.CreateBucket("", storage.RIDHuanan) +} diff --git a/app/lib/qiniu/bucket_delete.go b/app/lib/qiniu/bucket_delete.go new file mode 100644 index 0000000..6d41521 --- /dev/null +++ b/app/lib/qiniu/bucket_delete.go @@ -0,0 +1,18 @@ +package qiniu + +import ( + "github.com/qiniu/api.v7/v7/auth" + "github.com/qiniu/api.v7/v7/storage" +) + +func BucketDelete(bucketName string) error { + mac := auth.New(AK, SK) + + cfg := storage.Config{ + // 是否使用https域名进行资源管理 + UseHTTPS: false, + } + + bucketManager := storage.NewBucketManager(mac, &cfg) + return bucketManager.DropBucket(bucketName) +} diff --git a/app/lib/qiniu/bucket_get_domain.go b/app/lib/qiniu/bucket_get_domain.go new file mode 100644 index 0000000..f4cee3a --- /dev/null +++ b/app/lib/qiniu/bucket_get_domain.go @@ -0,0 +1,18 @@ +package qiniu + +import ( + "github.com/qiniu/api.v7/v7/auth" + "github.com/qiniu/api.v7/v7/storage" +) + +func BucketGetDomain(bucketName string) (string, error) { + mac := auth.New(AK, SK) + + cfg := storage.Config{UseHTTPS: false} + bucketManager := storage.NewBucketManager(mac, &cfg) + b, err := bucketManager.ListBucketDomains(bucketName) + if err != nil { + return "", err + } + return b[0].Domain, nil +} diff --git a/app/lib/qiniu/init.go b/app/lib/qiniu/init.go new file mode 100644 index 0000000..1d4346a --- /dev/null +++ b/app/lib/qiniu/init.go @@ -0,0 +1,22 @@ +package qiniu + +import ( + "applet/app/utils" +) + +var ( + AK = "MmxNdai23egjNUHjdzEVaTPdPCIbWzENz9BQuak3" + SK = "mElaFlM9O16rXp-ihoQdJ9KOH56naKm3MoyQBA59" + BUCKET = "dev-fnuoos" // 桶子名称 + BUCKET_SCHEME = "http" + BUCKET_REGION = "up-z2.qiniup.com" + Expires uint64 = 3600 +) + +func Init(ak, sk, bucket, region, scheme string) { + AK, SK, BUCKET, BUCKET_REGION, BUCKET_SCHEME = ak, sk, bucket, region, scheme +} + +func Sign(t string) string { + return utils.Md5(AK + SK + t) +} diff --git a/app/lib/qiniu/req_img_upload.go b/app/lib/qiniu/req_img_upload.go new file mode 100644 index 0000000..d24f69b --- /dev/null +++ b/app/lib/qiniu/req_img_upload.go @@ -0,0 +1,55 @@ +package qiniu + +import ( + "time" + + "github.com/qiniu/api.v7/v7/auth/qbox" + _ "github.com/qiniu/api.v7/v7/conf" + "github.com/qiniu/api.v7/v7/storage" + + "applet/app/md" + "applet/app/utils" +) + +// 请求图片上传地址信息 +func ReqImgUpload(f *md.FileCallback, callbackUrl string) interface{} { + if ext := utils.FileExt(f.FileName); ext == "png" || ext == "jpg" || ext == "jpeg" || ext == "gif" || ext == "bmp" || ext == "webp" { + f.Width = "$(imageInfo.width)" + f.Height = "$(imageInfo.height)" + } + f.Provider = "qiniu" + f.FileSize = "$(fsize)" + f.Hash = "$(etag)" + f.Bucket = "$(bucket)" + f.Mime = "$(mimeType)" + f.Time = utils.Int64ToStr(time.Now().Unix()) + f.Sign = Sign(f.Time) + putPolicy := storage.PutPolicy{ + Scope: BUCKET + ":" + f.FileName, // 使用覆盖方式时候必须请求里面有key,否则报错 + Expires: Expires, + ForceSaveKey: true, + SaveKey: f.FileName, + MimeLimit: "image/*", // 只允许上传图片 + CallbackURL: callbackUrl, + CallbackBody: utils.SerializeStr(f), + CallbackBodyType: "application/json", + } + return &struct { + Method string `json:"method"` + Key string `json:"key"` + Host string `json:"host"` + Token string `json:"token"` + }{Key: f.FileName, Method: "POST", Host: BUCKET_SCHEME + "://" + BUCKET_REGION, Token: putPolicy.UploadToken(qbox.NewMac(AK, SK))} +} + +/* +form表单上传 +地址 : http://upload-z2.qiniup.com +header + - Content-Type : multipart/form-data + +body : + - key : 文件名 + - token : 生成token + - file : 待上传文件 +*/ diff --git a/app/lib/validate/validate_comm.go b/app/lib/validate/validate_comm.go new file mode 100644 index 0000000..9305d9e --- /dev/null +++ b/app/lib/validate/validate_comm.go @@ -0,0 +1,33 @@ +package validate + +import ( + "applet/app/e" + "applet/app/utils" + "applet/app/utils/logx" + "encoding/json" + "fmt" + "github.com/go-playground/validator/v10" +) + +func HandleValidateErr(err error) error { + switch err.(type) { + case *json.UnmarshalTypeError: + return e.NewErr(e.ERR_UNMARSHAL, "参数格式错误") + case validator.ValidationErrors: + errs := err.(validator.ValidationErrors) + transMsgMap := errs.Translate(utils.ValidatorTrans) // utils.ValidatorTrans \app\utils\validator_err_trans.go::ValidatorTransInit初始化获得 + transMsgOne := transMsgMap[GetOneKeyOfMapString(transMsgMap)] + return e.NewErr(e.ERR_INVALID_ARGS, transMsgOne) + default: + _ = logx.Error(err) + return e.NewErr(e.ERR, fmt.Sprintf("validate request params, err:%v\n", err)) + } +} + +// GetOneKeyOfMapString 取出Map的一个key +func GetOneKeyOfMapString(collection map[string]string) string { + for k := range collection { + return k + } + return "" +} diff --git a/app/lib/wxpay/.gitignore b/app/lib/wxpay/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/app/lib/wxpay/api.go b/app/lib/wxpay/api.go new file mode 100644 index 0000000..16d5408 --- /dev/null +++ b/app/lib/wxpay/api.go @@ -0,0 +1,305 @@ +package wxpay + +import ( + "applet/app/utils" + "applet/app/utils/logx" + "fmt" + "github.com/iGoogle-ink/gopay" + "github.com/iGoogle-ink/gopay/pkg/util" + "github.com/iGoogle-ink/gopay/wechat" + v3 "github.com/iGoogle-ink/gopay/wechat/v3" + "strconv" + "time" +) + +func NewClient(appId, mchId, apiKey string, isProd bool) *wechat.Client { + // 初始化微信客户端 + // appId:应用ID + // mchId:商户ID + // apiKey:API秘钥值 + // isProd:是否是正式环境 + client := wechat.NewClient(appId, mchId, apiKey, isProd) + // 打开Debug开关,输出请求日志,默认关闭 + client.DebugSwitch = gopay.DebugOn + // 设置国家:不设置默认 中国国内 + // wechat.China:中国国内 + // wechat.China2:中国国内备用 + // wechat.SoutheastAsia:东南亚 + // wechat.Other:其他国家 + client.SetCountry(wechat.China) + // 添加微信证书 Path 路径 + // certFilePath:apiclient_cert.pem 路径 + // keyFilePath:apiclient_key.pem 路径 + // pkcs12FilePath:apiclient_cert.p12 路径 + // 返回err + //client.AddCertFilePath() + + // 添加微信证书内容 Content + // certFileContent:apiclient_cert.pem 内容 + // keyFileContent:apiclient_key.pem 内容 + // pkcs12FileContent:apiclient_cert.p12 内容 + // 返回err + //client.AddCertFileContent() + return client +} + +// TradeAppPay is 微信APP支付 +func TradeAppPay(client *wechat.Client, subject, orderID, amount, notifyUrl string) (map[string]string, error) { + // 初始化 BodyMap + bm := make(gopay.BodyMap) + bm.Set("nonce_str", util.GetRandomString(32)). + Set("body", subject). + Set("out_trade_no", orderID). + Set("total_fee", amount). + Set("spbill_create_ip", "127.0.0.1"). + Set("notify_url", notifyUrl). + Set("trade_type", wechat.TradeType_App). + Set("sign_type", wechat.SignType_MD5) + /*.Set("openid", "o0Df70H2Q0fY8JXh1aFPIRyOBgu8")*/ + // 预下单 + wxRsp, err := client.UnifiedOrder(bm) + if err != nil { + _ = logx.Warn(err) + return nil, err + } + _, err = wechat.VerifySign(client.ApiKey, wechat.SignType_MD5, wxRsp) + if err != nil { + _ = logx.Warn(err) + return nil, err + } + //if !ok { + // return nil, errors.New("验签失败") + //} + timeStamp := strconv.FormatInt(time.Now().Unix(), 10) + paySign := wechat.GetAppPaySign(client.AppId, client.MchId, wxRsp.NonceStr, wxRsp.PrepayId, wechat.SignType_MD5, timeStamp, client.ApiKey) + res := map[string]string{ + "appid": client.AppId, + "partnerid": client.MchId, + "prepayid": wxRsp.PrepayId, + "sign": paySign, + "package": "Sign=WXPay", + "noncestr": wxRsp.NonceStr, + "timestamp": timeStamp, + } + return res, nil +} + +// TradeAppPay is 微信H5支付 +func TradeH5Pay(client *wechat.Client, subject, orderID, amount, notifyUrl, ip string) (map[string]string, error) { + // 初始化 BodyMap + bm := make(gopay.BodyMap) + bm.Set("nonce_str", util.GetRandomString(32)). + Set("body", subject). + Set("out_trade_no", orderID). + Set("total_fee", amount). + //Set("spbill_create_ip", "121.196.29.49"). + Set("spbill_create_ip", ip). + Set("notify_url", notifyUrl). + Set("trade_type", wechat.TradeType_H5). + Set("sign_type", wechat.SignType_MD5). + SetBodyMap("scene_info", func(bm gopay.BodyMap) { + bm.SetBodyMap("h5_info", func(bm gopay.BodyMap) { + bm.Set("type", "Wap") + bm.Set("wap_url", "https://www.fumm.cc") + bm.Set("wap_name", "zyos") + }) + }) + /*.Set("openid", "o0Df70H2Q0fY8JXh1aFPIRyOBgu8")*/ + // 预下单 + fmt.Println(bm) + wxRsp, err := client.UnifiedOrder(bm) + fmt.Println(wxRsp) + if err != nil { + _ = logx.Warn(err) + return nil, err + } + _, err = wechat.VerifySign(client.ApiKey, wechat.SignType_MD5, wxRsp) + if err != nil { + _ = logx.Warn(err) + return nil, err + } + timeStamp := strconv.FormatInt(time.Now().Unix(), 10) + packages := "prepay_id=" + wxRsp.PrepayId + paySign := wechat.GetH5PaySign(client.AppId, wxRsp.NonceStr, packages, wechat.SignType_MD5, timeStamp, client.ApiKey) + fmt.Println("paySign===", paySign) + r := map[string]string{ + "redirect_url": wxRsp.MwebUrl, + } + return r, nil +} + +// TradeMiniProgPay is 微信小程序支付 ☑️ +func TradeMiniProgPay(client *wechat.Client, subject, orderID, amount, notifyUrl, openid string) (map[string]string, error) { + // 初始化 BodyMap + bm := make(gopay.BodyMap) + bm.Set("nonce_str", util.GetRandomString(32)). + Set("body", subject). + Set("openid", openid). + Set("out_trade_no", orderID). + Set("total_fee", amount). + Set("spbill_create_ip", "127.0.0.1"). + Set("notify_url", notifyUrl). + Set("trade_type", wechat.TradeType_Mini). + Set("sign_type", wechat.SignType_MD5) + // 预下单 + wxRsp, err := client.UnifiedOrder(bm) + if err != nil { + _ = logx.Warn(err) + return nil, err + } + timeStamp := strconv.FormatInt(time.Now().Unix(), 10) + packages := "prepay_id=" + wxRsp.PrepayId + paySign := wechat.GetMiniPaySign(client.AppId, wxRsp.NonceStr, packages, wechat.SignType_MD5, timeStamp, client.ApiKey) + res := map[string]string{ + "appId": client.AppId, + "paySign": paySign, + "signType": wechat.SignType_MD5, + "package": packages, + "nonceStr": wxRsp.NonceStr, + "timeStamp": timeStamp, + } + return res, nil +} + +// TradeAppPayV3 is 微信APP支付v3 +func TradeAppPayV3(client *v3.ClientV3, subject, orderID, amount, notifyUrl string) (map[string]string, error) { + // 初始化 BodyMap + amountNew := utils.AnyToFloat64(amount) * 100 + bm := make(gopay.BodyMap) + bm.Set("nonce_str", util.GetRandomString(32)). + Set("body", subject). + Set("out_trade_no", orderID). + Set("total_fee", amountNew). + Set("spbill_create_ip", "127.0.0.1"). + Set("notify_url", notifyUrl). + Set("trade_type", wechat.TradeType_App). + Set("sign_type", wechat.SignType_MD5) + /*.Set("openid", "o0Df70H2Q0fY8JXh1aFPIRyOBgu8")*/ + //// 预下单 + //wxRsp, err := v3.UnifiedOrder(bm) + //if err != nil { + // _ = logx.Warn(err) + // return nil, err + //} + //_, err = wechat.VerifySign(client.ApiKey, wechat.SignType_MD5, wxRsp) + //if err != nil { + // _ = logx.Warn(err) + // return nil, err + //} + ////if !ok { + //// return nil, errors.New("验签失败") + ////} + //timeStamp := strconv.FormatInt(time.Now().Unix(), 10) + //paySign := wechat.GetAppPaySign(client.AppId, client.MchId, wxRsp.NonceStr, wxRsp.PrepayId, wechat.SignType_MD5, timeStamp, client.ApiKey) + //res := map[string]string{ + // "appid": client.AppId, + // "partnerid": client.MchId, + // "prepayid": wxRsp.PrepayId, + // "sign": paySign, + // "package": "Sign=WXPay", + // "noncestr": wxRsp.NonceStr, + // "timestamp": timeStamp, + //} + //return res, nil + return nil, nil +} + +//// TradeJSAPIPay is 微信JSAPI支付 +func TradeJSAPIPay(client *wechat.Client, subject, orderID, amount, notifyUrl, openid string) (map[string]string, error) { + // 初始化 BodyMap + bm := make(gopay.BodyMap) + bm.Set("nonce_str", util.GetRandomString(32)). + Set("body", subject). + Set("out_trade_no", orderID). + Set("total_fee", amount). + Set("spbill_create_ip", "121.196.29.49"). + Set("notify_url", notifyUrl). + Set("trade_type", wechat.TradeType_JsApi). + Set("sign_type", wechat.SignType_MD5). + Set("openid", openid). + SetBodyMap("scene_info", func(bm gopay.BodyMap) { + bm.SetBodyMap("h5_info", func(bm gopay.BodyMap) { + bm.Set("type", "Wap") + bm.Set("wap_url", "https://www.fumm.cc") + bm.Set("wap_name", "zyos") + }) + }) + // 预下单 + wxRsp, err := client.UnifiedOrder(bm) + if err != nil { + _ = logx.Warn(err) + return nil, err + } + _, err = wechat.VerifySign(client.ApiKey, wechat.SignType_MD5, wxRsp) + if err != nil { + _ = logx.Warn(err) + return nil, err + } + //if !ok { + // return nil, errors.New("验签失败") + //} + timeStamp := strconv.FormatInt(time.Now().Unix(), 10) + //paySign := wechat.GetAppPaySign(client.AppId, client.MchId, wxRsp.NonceStr, wxRsp.PrepayId, wechat.SignType_MD5, timeStamp, client.ApiKey) + packages := "prepay_id=" + wxRsp.PrepayId + paySign := wechat.GetJsapiPaySign(client.AppId, wxRsp.NonceStr, packages, wechat.SignType_MD5, timeStamp, client.ApiKey) + + logx.Info("wxRsp.PrepayId:" + wxRsp.PrepayId) + logx.Info("wxRsp.PrepayId:" + wxRsp.PrepayId) + logx.Info("wxRsp.PrepayId:" + openid) + res := map[string]string{ + "appid": client.AppId, + "partnerid": client.MchId, + "prepayid": wxRsp.PrepayId, + "sign": paySign, + "package": "prepay_id=" + wxRsp.PrepayId, + "noncestr": wxRsp.NonceStr, + "timestamp": timeStamp, + } + return res, nil +} + +// TradeH5PayV3 is 微信H5支付v3 +func TradeH5PayV3(client *wechat.Client, subject, orderID, amount, notifyUrl string) (string, error) { + // 初始化 BodyMap + bm := make(gopay.BodyMap) + bm.Set("nonce_str", util.GetRandomString(32)). + Set("body", subject). + Set("out_trade_no", orderID). + Set("total_fee", amount). + Set("spbill_create_ip", "127.0.0.1"). + Set("notify_url", notifyUrl). + Set("trade_type", wechat.TradeType_App). + Set("device_info", "WEB"). + Set("sign_type", wechat.SignType_MD5). + SetBodyMap("scene_info", func(bm gopay.BodyMap) { + bm.SetBodyMap("h5_info", func(bm gopay.BodyMap) { + bm.Set("type", "Wap") + bm.Set("wap_url", "https://www.fumm.cc") + bm.Set("wap_name", "H5测试支付") + }) + }) /*.Set("openid", "o0Df70H2Q0fY8JXh1aFPIRyOBgu8")*/ + // 预下单 + wxRsp, err := client.UnifiedOrder(bm) + if err != nil { + _ = logx.Warn(err) + return "", err + } + // ====APP支付 paySign==== + timeStamp := strconv.FormatInt(time.Now().Unix(), 10) + // 获取APP支付的 paySign + // 注意:package 参数因为是固定值,无需开发者再传入 + // appId:AppID + // partnerid:partnerid + // nonceStr:随机字符串 + // prepayId:统一下单成功后得到的值 + // signType:签名方式,务必与统一下单时用的签名方式一致 + // timeStamp:时间 + // apiKey:API秘钥值 + paySign := wechat.GetAppPaySign(client.AppId, client.MchId, wxRsp.NonceStr, wxRsp.PrepayId, wechat.SignType_MD5, timeStamp, client.ApiKey) + return paySign, nil +} + +// TradeMiniProgPayV3 is 微信小程序支付v3 +func TradeMiniProgPayV3(client *v3.ClientV3, subject, orderID, amount, notifyUrl string) (string, error) { + return "", nil +} diff --git a/app/md/app_redis_key.go b/app/md/app_redis_key.go new file mode 100644 index 0000000..ef19b51 --- /dev/null +++ b/app/md/app_redis_key.go @@ -0,0 +1,18 @@ +package md + +// 缓存key统一管理, %s格式化为masterId +const ( + JwtTokenKey = "%s:advertisement_jwt_token:%s" // jwt, 占位符:ip, admin:id + + JwtTokenCacheTime = 3600 * 24 + + AppCfgCacheKey = "%s:cfg_cache:%s" // 占位符: masterId, key的第一个字母 + + UserFinValidUpdateLock = "%s:user_fin_valid_update_lock:%s" // 用户余额更新锁(能拿到锁才能更新余额) + + AdminRolePermissionCacheTime = 3600 * 24 * 0.5 + + KEY_SYS_CFG_CACHE = "sys_cfg_cache" + + CfgCacheTime = 86400 +) diff --git a/app/md/md_login.go b/app/md/md_login.go new file mode 100644 index 0000000..16b0897 --- /dev/null +++ b/app/md/md_login.go @@ -0,0 +1,10 @@ +package md + +type LoginReq struct { + UserName string `json:"username" binding:"required" label:"登录账号"` + PassWord string `json:"password" binding:"required" label:"登录密码"` +} + +type LoginResponse struct { + Token string `json:"token"` +} diff --git a/app/md/md_role.go b/app/md/md_role.go new file mode 100644 index 0000000..9515e1b --- /dev/null +++ b/app/md/md_role.go @@ -0,0 +1,91 @@ +package md + +import ( + "applet/app/db/model" +) + +type RoleListResp struct { + Data model.Role `json:"data"` + AdminList []struct { + Name string `json:"name"` + } `json:"admin_list"` +} + +type UpdateRoleStateReq struct { + RoleId int `json:"role_id" binding:"required" label:"id"` + State int `json:"state" binding:"required" label:"状态"` +} + +type AddRoleReq struct { + Name string `json:"name" binding:"required" label:"名称"` + Memo string `json:"memo" binding:"required" label:"备注"` +} + +type UpdateRoleReq struct { + RoleId int `json:"role_id" binding:"required" label:"id"` + Name string `json:"name" binding:"required" label:"名称"` + Memo string `json:"memo" binding:"required" label:"备注"` +} + +type RoleBindPermissionGroupReq struct { + RoleId int `json:"role_id" binding:"required" label:"id"` + PermissionIds []int `json:"permission_ids" label:"权限组id"` +} + +type PermissionGroupListResp struct { + Id int `json:"id"` + Name string `json:"name"` //菜单名称 + Key string `json:"key"` //唯一标识符 + State int `json:"state"` + ParentId int `json:"parent_id"` //父级id,为0则代表没有父级 + CreateAt string `json:"create_at"` + UpdateAt string `json:"update_at"` + IsCheck bool `json:"is_check"` //是否用用 + SubPermissionGroupList []PermissionGroupListResp `json:"sub_permission_group_list"` //子集菜单 +} + +type AdminListReq struct { + Limit int `json:"limit"` + Page int `json:"page" ` + UserName string `json:"username"` + State int `json:"state"` +} + +type AdminListResp struct { + AdmId int `json:"adm_id"` + Username string `json:"username"` + State int `json:"state"` + IsSuperAdministrator int `json:"is_super_administrator"` + Memo string `json:"memo"` + CreateAt string `json:"create_at"` + UpdateAt string `json:"update_at"` + RoleList []string `json:"role_list"` +} + +type UpdateAdminStateReq struct { + AdmId int `json:"adm_id" binding:"required" label:"管理员id"` + State int `json:"state" binding:"required" label:"状态"` +} + +type AddAdminReq struct { + Username string `json:"username" binding:"required" label:"名称"` + Password string `json:"password" binding:"required" label:"密码"` + Memo string `json:"memo" label:"备注"` +} + +type UpdateAdminReq struct { + AdmId int `json:"adm_id" binding:"required" label:"管理员id"` + Username string `json:"username" binding:"required" label:"名称"` + Password string `json:"password" binding:"required" label:"密码"` + Memo string `json:"memo" label:"备注"` +} + +type BindAdminRoleReq struct { + AdmId int `json:"adm_id" binding:"required" label:"管理员id"` + RoleIds []int `json:"role_ids" label:"角色id"` +} + +type BindAdminWithEnterpriseReq struct { + AdmId int `json:"adm_id" binding:"required" label:"管理员id"` + Ids []int `json:"ids" label:"记录id"` +} diff --git a/app/md/md_white_uri.go b/app/md/md_white_uri.go new file mode 100644 index 0000000..b5c54d7 --- /dev/null +++ b/app/md/md_white_uri.go @@ -0,0 +1,5 @@ +package md + +var WhiteUri = []string{ + "/api/admin/comm/getMenuList", +} diff --git a/app/md/split_db.go b/app/md/split_db.go new file mode 100644 index 0000000..f60d962 --- /dev/null +++ b/app/md/split_db.go @@ -0,0 +1,42 @@ +package md + +import ( + "regexp" + + "xorm.io/xorm" +) + +type DbInfo struct { + User string + Psw string + Name string + Host string +} + +func SplitDbInfo(eg *xorm.Engine) *DbInfo { + if eg == nil { + return &DbInfo{ + User: "nil", + Psw: "nil", + Host: "nil", + Name: "nil", + } + } + pattern := `(\w+):(.*)@tcp\(([\w\.\-\:\_]+)\)\/(\w+)` + reg := regexp.MustCompile(pattern).FindStringSubmatch(eg.DataSourceName()) + + if len(reg) < 5 { + return &DbInfo{ + User: "unknown", + Psw: "unknown", + Host: "unknown", + Name: "unknown", + } + } + return &DbInfo{ + User: reg[1], + Psw: reg[2], + Host: reg[3], + Name: reg[4], + } +} diff --git a/app/mw/mw_access_log.go b/app/mw/mw_access_log.go new file mode 100644 index 0000000..84f6b52 --- /dev/null +++ b/app/mw/mw_access_log.go @@ -0,0 +1,31 @@ +package mw + +import ( + "time" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" + + "applet/app/utils/logx" +) + +// access log +func AccessLog(c *gin.Context) { + start := time.Now() + c.Next() + cost := time.Since(start) + + logx.Info(c.Request.URL.Path) + + logger := &zap.Logger{} + logger.Info(c.Request.URL.Path, + zap.Int("status", c.Writer.Status()), + zap.String("method", c.Request.Method), + zap.String("path", c.Request.URL.Path), + zap.String("query", c.Request.URL.RawQuery), + zap.String("ip", c.ClientIP()), + zap.String("user-agent", c.Request.UserAgent()), + zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()), + zap.Duration("cost", cost), + ) +} diff --git a/app/mw/mw_auth.go b/app/mw/mw_auth.go new file mode 100644 index 0000000..e065ad8 --- /dev/null +++ b/app/mw/mw_auth.go @@ -0,0 +1,26 @@ +package mw + +import ( + "applet/app/e" + "applet/app/svc" + "github.com/gin-gonic/gin" +) + +// 检查权限, 签名等等 +func Auth(c *gin.Context) { + admin, err := svc.CheckUser(c) + if err != nil { + switch err.(type) { + case e.E: + err1 := err.(e.E) + e.OutErr(c, err1.Code, err1.Error()) + return + default: + e.OutErr(c, e.ERR_TOKEN_AUTH, err.Error()) + return + } + } + // 将当前请求的username信息保存到请求的上下文c上 + c.Set("admin", admin) + c.Next() +} diff --git a/app/mw/mw_breaker.go b/app/mw/mw_breaker.go new file mode 100644 index 0000000..fefc078 --- /dev/null +++ b/app/mw/mw_breaker.go @@ -0,0 +1,30 @@ +package mw + +import ( + "errors" + "net/http" + "strconv" + + "github.com/afex/hystrix-go/hystrix" + "github.com/gin-gonic/gin" +) + +// 熔断器, 此组件需要在gin.Recovery中间之前进行调用, 否则可能会导致panic时候, 无法recovery, 正确顺序如下 +//r.Use(BreakerWrapper) +//r.Use(gin.Recovery()) +func Breaker(c *gin.Context) { + name := c.Request.Method + "-" + c.Request.RequestURI + hystrix.Do(name, func() error { + c.Next() + statusCode := c.Writer.Status() + if statusCode >= http.StatusInternalServerError { + return errors.New("status code " + strconv.Itoa(statusCode)) + } + return nil + }, func(e error) error { + if e == hystrix.ErrCircuitOpen { + c.String(http.StatusAccepted, "请稍后重试") //todo 修改报错方法 + } + return e + }) +} diff --git a/app/mw/mw_change_header.go b/app/mw/mw_change_header.go new file mode 100644 index 0000000..4a5aefa --- /dev/null +++ b/app/mw/mw_change_header.go @@ -0,0 +1,17 @@ +package mw + +import ( + "github.com/gin-gonic/gin" +) + +// 修改传过来的头部字段 +func ChangeHeader(c *gin.Context) { + appvserison := c.GetHeader("AppVersionName") + if appvserison == "" { + appvserison = c.GetHeader("app_version_name") + } + if appvserison != "" { + c.Request.Header.Add("app_version_name", appvserison) + } + c.Next() +} diff --git a/app/mw/mw_check_sign.go b/app/mw/mw_check_sign.go new file mode 100644 index 0000000..e3bf3c2 --- /dev/null +++ b/app/mw/mw_check_sign.go @@ -0,0 +1,34 @@ +package mw + +import ( + "applet/app/e" + "applet/app/utils" + "bytes" + "fmt" + "github.com/gin-gonic/gin" + "io/ioutil" +) + +// CheckSign is 中间件 用来检查签名 +func CheckSign(c *gin.Context) { + + bools := utils.SignCheck(c) + if bools == false { + e.OutErr(c, 400, e.NewErr(400, "签名校验错误,请求失败")) + return + } + c.Next() +} +func CheckBody(c *gin.Context) { + if utils.GetApiVersion(c) > 0 { + body, _ := ioutil.ReadAll(c.Request.Body) + fmt.Println(string(body)) + if string(body) != "" { + str := utils.ResultAesDecrypt(c, string(body)) + if str != "" { + c.Request.Body = ioutil.NopCloser(bytes.NewBuffer([]byte(str))) + } + } + } + c.Next() +} diff --git a/app/mw/mw_cors.go b/app/mw/mw_cors.go new file mode 100644 index 0000000..3433553 --- /dev/null +++ b/app/mw/mw_cors.go @@ -0,0 +1,29 @@ +package mw + +import ( + "github.com/gin-gonic/gin" +) + +// cors跨域 +func Cors(c *gin.Context) { + // 放行所有OPTIONS方法 + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + + origin := c.Request.Header.Get("Origin") // 请求头部 + if origin != "" { + c.Header("Access-Control-Allow-Origin", origin) // 这是允许访问来源域 + c.Header("Access-Control-Allow-Methods", "POST,GET,OPTIONS,PUT,DELETE,UPDATE") // 服务器支持的所有跨域请求的方法,为了避免浏览次请求的多次'预检'请求 + // header的类型 + c.Header("Access-Control-Allow-Headers", "Authorization,Content-Length,X-CSRF-Token,Token,session,X_Requested_With,Accept,Origin,Host,Connection,Accept-Encoding,Accept-Language,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Pragma,X-Mx-ReqToken") + // 允许跨域设置,可以返回其他子段 + // 跨域关键设置 让浏览器可以解析 + c.Header("Access-Control-Expose-Headers", "Content-Length,Access-Control-Allow-Origin,Access-Control-Allow-Headers,Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma,FooBar") + c.Header("Access-Control-Max-Age", "172800") // 缓存请求信息 单位为秒 + c.Header("Access-Control-Allow-Credentials", "false") // 跨域请求是否需要带cookie信息 默认设置为true + c.Set("Content-Type", "Application/json") // 设置返回格式是json + } + c.Next() +} diff --git a/app/mw/mw_csrf.go b/app/mw/mw_csrf.go new file mode 100644 index 0000000..b15619b --- /dev/null +++ b/app/mw/mw_csrf.go @@ -0,0 +1,136 @@ +package mw + +import ( + "crypto/sha1" + "encoding/base64" + "errors" + "io" + + "github.com/dchest/uniuri" + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" +) + +// csrf,xsrf检查 +const ( + csrfSecret = "csrfSecret" + csrfSalt = "csrfSalt" + csrfToken = "csrfToken" +) + +var defaultIgnoreMethods = []string{"GET", "HEAD", "OPTIONS"} + +var defaultErrorFunc = func(c *gin.Context) { + panic(errors.New("CSRF token mismatch")) +} + +var defaultTokenGetter = func(c *gin.Context) string { + r := c.Request + + if t := r.FormValue("_csrf"); len(t) > 0 { + return t + } else if t := r.URL.Query().Get("_csrf"); len(t) > 0 { + return t + } else if t := r.Header.Get("X-CSRF-TOKEN"); len(t) > 0 { + return t + } else if t := r.Header.Get("X-XSRF-TOKEN"); len(t) > 0 { + return t + } + + return "" +} + +// Options stores configurations for a CSRF middleware. +type Options struct { + Secret string + IgnoreMethods []string + ErrorFunc gin.HandlerFunc + TokenGetter func(c *gin.Context) string +} + +func tokenize(secret, salt string) string { + h := sha1.New() + io.WriteString(h, salt+"-"+secret) + hash := base64.URLEncoding.EncodeToString(h.Sum(nil)) + + return hash +} + +func inArray(arr []string, value string) bool { + inarr := false + + for _, v := range arr { + if v == value { + inarr = true + break + } + } + + return inarr +} + +// Middleware validates CSRF token. +func Middleware(options Options) gin.HandlerFunc { + ignoreMethods := options.IgnoreMethods + errorFunc := options.ErrorFunc + tokenGetter := options.TokenGetter + + if ignoreMethods == nil { + ignoreMethods = defaultIgnoreMethods + } + + if errorFunc == nil { + errorFunc = defaultErrorFunc + } + + if tokenGetter == nil { + tokenGetter = defaultTokenGetter + } + + return func(c *gin.Context) { + session := sessions.Default(c) + c.Set(csrfSecret, options.Secret) + + if inArray(ignoreMethods, c.Request.Method) { + c.Next() + return + } + + salt, ok := session.Get(csrfSalt).(string) + + if !ok || len(salt) == 0 { + errorFunc(c) + return + } + + token := tokenGetter(c) + + if tokenize(options.Secret, salt) != token { + errorFunc(c) + return + } + + c.Next() + } +} + +// GetToken returns a CSRF token. +func GetToken(c *gin.Context) string { + session := sessions.Default(c) + secret := c.MustGet(csrfSecret).(string) + + if t, ok := c.Get(csrfToken); ok { + return t.(string) + } + + salt, ok := session.Get(csrfSalt).(string) + if !ok { + salt = uniuri.New() + session.Set(csrfSalt, salt) + session.Save() + } + token := tokenize(secret, salt) + c.Set(csrfToken, token) + + return token +} diff --git a/app/mw/mw_db.go b/app/mw/mw_db.go new file mode 100644 index 0000000..fa89c70 --- /dev/null +++ b/app/mw/mw_db.go @@ -0,0 +1,30 @@ +package mw + +import ( + "applet/app/db" + "applet/app/e" + "applet/app/svc" + "errors" + "fmt" + "github.com/gin-gonic/gin" +) + +// DB is 中间件 用来检查master_id是否有对应的数据库engine +func DB(c *gin.Context) { + fmt.Println(c.Request.Header) + masterID := svc.GetMasterId(c) + fmt.Println("master_id", masterID) + if masterID == "" { + e.OutErr(c, e.ERR_MASTER_ID, errors.New("not found master_id")) + return + } + + _, ok := db.DBs[masterID] + if !ok { + e.OutErr(c, e.ERR_MASTER_ID, errors.New("not found master_id in DBs")) + return + } + fmt.Println("master_id", masterID) + c.Set("mid", masterID) + c.Next() +} diff --git a/app/mw/mw_limiter.go b/app/mw/mw_limiter.go new file mode 100644 index 0000000..4eb5299 --- /dev/null +++ b/app/mw/mw_limiter.go @@ -0,0 +1,58 @@ +package mw + +import ( + "bytes" + "io/ioutil" + + "github.com/gin-gonic/gin" + + "applet/app/utils" + "applet/app/utils/cache" +) + +// 限流器 +func Limiter(c *gin.Context) { + limit := 100 // 限流次数 + ttl := 1 // 限流过期时间 + ip := c.ClientIP() + // 读取token或者ip + token := c.GetHeader("Authorization") + // 判断是否已经超出限额次数 + method := c.Request.Method + host := c.Request.Host + uri := c.Request.URL.String() + + buf := make([]byte, 2048) + num, _ := c.Request.Body.Read(buf) + body := buf[:num] + // Write body back + c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + Md5 := utils.Md5(ip + token + method + host + uri + string(body)) + if cache.Exists(Md5) { + c.AbortWithStatusJSON(429, gin.H{ + "code": 429, + "msg": "don't repeat the request", + "data": struct{}{}, + }) + return + } + // 2s后没返回自动释放 + go cache.SetEx(Md5, "0", ttl) + key := "LIMITER_" + ip + reqs, _ := cache.GetInt(key) + if reqs >= limit { + c.AbortWithStatusJSON(429, gin.H{ + "code": 429, + "msg": "too many requests", + "data": struct{}{}, + }) + return + } + if reqs > 0 { + go cache.Incr(key) + } else { + go cache.SetEx(key, 1, ttl) + } + c.Next() + go cache.Del(Md5) +} diff --git a/app/mw/mw_recovery.go b/app/mw/mw_recovery.go new file mode 100644 index 0000000..b32cc82 --- /dev/null +++ b/app/mw/mw_recovery.go @@ -0,0 +1,57 @@ +package mw + +import ( + "net" + "net/http" + "net/http/httputil" + "os" + "runtime/debug" + "strings" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +func Recovery(logger *zap.Logger, stack bool) gin.HandlerFunc { + return func(c *gin.Context) { + defer func() { + if err := recover(); err != nil { + var brokenPipe bool + if ne, ok := err.(*net.OpError); ok { + if se, ok := ne.Err.(*os.SyscallError); ok { + if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") { + brokenPipe = true + } + } + } + + httpRequest, _ := httputil.DumpRequest(c.Request, false) + if brokenPipe { + logger.Error(c.Request.URL.Path, + zap.Any("error", err), + zap.String("request", string(httpRequest)), + ) + // If the connection is dead, we can't write a status to it. + c.Error(err.(error)) + c.Abort() + return + } + + if stack { + logger.Error("[Recovery from panic]", + zap.Any("error", err), + zap.String("request", string(httpRequest)), + zap.String("stack", string(debug.Stack())), + ) + } else { + logger.Error("[Recovery from panic]", + zap.Any("error", err), + zap.String("request", string(httpRequest)), + ) + } + c.AbortWithStatus(http.StatusInternalServerError) + } + }() + c.Next() + } +} diff --git a/app/router/router.go b/app/router/router.go new file mode 100644 index 0000000..34ea96c --- /dev/null +++ b/app/router/router.go @@ -0,0 +1,74 @@ +package router + +import ( + "applet/app/cfg" + "applet/app/hdl" + "applet/app/mw" + _ "applet/docs" + "github.com/gin-gonic/gin" +) + +// 初始化路由 +func Init() *gin.Engine { + // debug, release, test 项目阶段 + mode := "release" + if cfg.Debug { + mode = "debug" + } + gin.SetMode(mode) + //创建一个新的启动器 + r := gin.New() + r.Use(mw.ChangeHeader) + + // 是否打印访问日志, 在非正式环境都打印 + if mode != "release" { + r.Use(gin.Logger()) + } + r.Use(gin.Recovery()) + // r.Use(mw.Limiter) + //r.LoadHTMLGlob("static/html/*") + + r.GET("/favicon.ico", func(c *gin.Context) { + c.Status(204) + }) + r.NoRoute(func(c *gin.Context) { + c.JSON(404, gin.H{"code": 404, "msg": "page not found", "data": []struct{}{}}) + }) + r.NoMethod(func(c *gin.Context) { + c.JSON(405, gin.H{"code": 405, "msg": "method not allowed", "data": []struct{}{}}) + }) + r.Use(mw.Cors) + route(r.Group("/api")) + return r +} + +func route(r *gin.RouterGroup) { + r.GET("/test", hdl.Demo) + + r.Use(mw.DB) // 以下接口需要用到数据库 + { + r.POST("/login", hdl.Login) + } + r.Use(mw.CheckBody) //body参数转换 + r.Use(mw.CheckSign) //签名校验 + + r.Use(mw.Auth) // 以下接口需要JWT验证 + rRole(r.Group("/role")) //权限管理 +} + +func rRole(r *gin.RouterGroup) { + r.GET("/roleList", hdl.RoleList) //角色列表 + r.POST("/addRole", hdl.AddRole) //角色添加 + r.POST("/roleBindPermissionGroup", hdl.RoleBindPermissionGroup) //角色绑定权限组 + r.POST("/updateRoleState", hdl.UpdateRoleState) //修改角色状态 + r.POST("/updateRole", hdl.UpdateRole) //修改角色状态 + r.DELETE("/deleteRole/:id", hdl.DeleteRole) //删除角色 + r.GET("/permissionGroupList", hdl.PermissionGroupList) //权限组列表 + r.POST("/adminList", hdl.AdminList) //管理员列表 + r.POST("/updateAdminState", hdl.UpdateAdminState) //修改管理员状态 + r.POST("/updateAdmin", hdl.UpdateAdmin) //修改管理员信息 + r.POST("/addAdmin", hdl.AddAdmin) //新增管理员 + r.DELETE("/deleteAdmin/:adm_id", hdl.DeleteAdmin) //删除管理员 + r.GET("/adminInfo", hdl.AdminInfo) //获取管理员信息 + r.POST("/bindAdminRole", hdl.BindAdminRole) //绑定角色 +} diff --git a/app/svc/svc_admin.go b/app/svc/svc_admin.go new file mode 100644 index 0000000..c773be0 --- /dev/null +++ b/app/svc/svc_admin.go @@ -0,0 +1,31 @@ +package svc + +import ( + "applet/app/db" + "applet/app/db/implement" + "github.com/gin-gonic/gin" +) + +func AdminDelete(c *gin.Context, admIds []int) (err error) { + engine := db.DBs[GetMasterId(c)] + session := engine.NewSession() + defer session.Close() + session.Begin() + //1、删除 `admin` + adminDb := implement.NewAdminDb(engine) + _, err = adminDb.AdminDeleteBySession(session, admIds) + if err != nil { + _ = session.Rollback() + return + } + + //2、删除 `admin_role` + adminRoleDb := implement.NewAdminRoleDb(engine) + _, err = adminRoleDb.AdminDeleteBySessionForAdmId(session, admIds) + if err != nil { + _ = session.Rollback() + return + } + + return session.Commit() +} diff --git a/app/svc/svc_auth.go b/app/svc/svc_auth.go new file mode 100644 index 0000000..ac7b4e8 --- /dev/null +++ b/app/svc/svc_auth.go @@ -0,0 +1,55 @@ +package svc + +import ( + "applet/app/db" + "applet/app/db/implement" + "applet/app/db/model" + "applet/app/lib/auth" + "errors" + "github.com/gin-gonic/gin" + "strings" +) + +func GetUser(c *gin.Context) *model.Admin { + user, _ := c.Get("admin") + if user == nil { + return &model.Admin{ + AdmId: 0, + Username: "", + Password: "", + State: 0, + IsSuperAdministrator: 0, + Memo: "", + CreateAt: "", + UpdateAt: "", + OpenId: "", + UnionId: "", + } + } + return user.(*model.Admin) +} + +func CheckUser(c *gin.Context) (*model.Admin, error) { + token := c.GetHeader("Authorization") + if token == "" { + return nil, errors.New("token not exist") + } + // 按空格分割 + parts := strings.SplitN(token, " ", 2) + if !(len(parts) == 2 && parts[0] == "Bearer") { + return nil, errors.New("token format error") + } + // parts[1]是获取到的tokenString,我们使用之前定义好的解析JWT的函数来解析它 + mc, err := auth.ParseToken(parts[1]) + if err != nil { + return nil, err + } + + // 获取admin + adminDb := implement.NewAdminDb(db.DBs[GetMasterId(c)]) + admin, err := adminDb.GetAdmin(mc.AdmId) + if err != nil { + return nil, err + } + return admin, nil +} diff --git a/app/svc/svc_db.go b/app/svc/svc_db.go new file mode 100644 index 0000000..99b1e0d --- /dev/null +++ b/app/svc/svc_db.go @@ -0,0 +1,11 @@ +package svc + +import ( + "applet/app/db" + "github.com/gin-gonic/gin" + "xorm.io/xorm" +) + +func MasterDb(c *gin.Context) *xorm.Engine { + return db.DBs[c.GetString("mid")] +} diff --git a/app/svc/svc_login.go b/app/svc/svc_login.go new file mode 100644 index 0000000..c4a44da --- /dev/null +++ b/app/svc/svc_login.go @@ -0,0 +1,33 @@ +package svc + +import ( + "applet/app/db/model" + "applet/app/lib/auth" + "applet/app/md" + "applet/app/utils/cache" + "applet/app/utils/logx" +) + +func HandleLoginToken(cacheKey string, admin *model.Admin) (string, error) { + // 获取之前生成的token + token, err := cache.GetString(cacheKey) + if err != nil { + _ = logx.Error(err) + } + // 没有获取到 + if err != nil || token == "" { + // 生成token + token, err = auth.GenToken(admin.AdmId, admin.Username) + if err != nil { + return "", err + } + // 缓存token + _, err = cache.SetEx(cacheKey, token, md.JwtTokenCacheTime) + if err != nil { + return "", err + } + return token, nil + } + + return token, nil +} diff --git a/app/svc/svc_master.go b/app/svc/svc_master.go new file mode 100644 index 0000000..ccd792a --- /dev/null +++ b/app/svc/svc_master.go @@ -0,0 +1,58 @@ +package svc + +import ( + "fmt" + "github.com/gin-gonic/gin" + "strconv" +) + +func GetMasterId(c *gin.Context) (masterId string) { + masterId = c.GetHeader("master_id") + if masterId != "" { + return + } + + masterId = c.GetString("master_id") + if masterId == "" { + //TODO::通过域名查找masterId + //host := c.Request.Host + + //fmt.Println("not found master_id found MasterId start") + //masterId = c.GetHeader("MasterId") + //if masterId == "" && c.GetHeader("Platform") == md.PLATFORM_WAP { // H5 要根据域名去获取mid + // hostList := strings.Split(c.Request.Host, ".") + // if isNumeric(hostList[0]) { + // masterId = hostList[0] + // } else if isNumeric(hostList[1]) { + // masterId = hostList[1] + // } else { + // // 自定义域名 + // masterId = svc.GetWebSiteDomainMasterId(baseDb, md.PLATFORM_WAP, c.Request.Host) + // } + //} + //if masterId == "" && c.GetHeader("Platform") == md.PLATFORM_PC { // H5 要根据域名去获取mid + // hostList := strings.Split(c.Request.Host, ".") + // if isNumeric(hostList[0]) { + // masterId = hostList[0] + // } else if isNumeric(hostList[1]) { + // masterId = hostList[1] + // } else { + // // 自定义域名 + // masterId = svc.GetWebSiteDomainMasterId(baseDb, md.PLATFORM_PC, c.Request.Host) + // } + //} + //if masterId == "" && c.GetHeader("Platform") == "" { // 无平台访问 + // hostList := strings.Split(c.Request.Host, ".") + // if isNumeric(hostList[0]) { + // masterId = hostList[0] + // } + //} + } + fmt.Println("master_id:::::::", masterId) + return +} + +func isNumeric(str string) bool { + _, err := strconv.ParseFloat(str, 64) + return err == nil +} diff --git a/app/svc/svc_redis_mutex_lock.go b/app/svc/svc_redis_mutex_lock.go new file mode 100644 index 0000000..396e15c --- /dev/null +++ b/app/svc/svc_redis_mutex_lock.go @@ -0,0 +1,85 @@ +package svc + +import ( + "applet/app/md" + "applet/app/utils" + "applet/app/utils/cache" + "errors" + "fmt" + "math/rand" + "reflect" + "time" +) + +const redisMutexLockExpTime = 15 + +// TryGetDistributedLock 分布式锁获取 +// requestId 用于标识请求客户端,可以是随机字符串,需确保唯一 +func TryGetDistributedLock(lockKey, requestId string, isNegative bool) bool { + if isNegative { // 多次尝试获取 + retry := 1 + for { + ok, err := cache.Do("SET", lockKey, requestId, "EX", redisMutexLockExpTime, "NX") + // 获取锁成功 + if err == nil && ok == "OK" { + return true + } + // 尝试多次没获取成功 + if retry > 10 { + return false + } + time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000))) + retry += 1 + } + } else { // 只尝试一次 + ok, err := cache.Do("SET", lockKey, requestId, "EX", redisMutexLockExpTime, "NX") + // 获取锁成功 + if err == nil && ok == "OK" { + return true + } + + return false + } +} + +// ReleaseDistributedLock 释放锁,通过比较requestId,用于确保客户端只释放自己的锁,使用lua脚本保证操作的原子型 +func ReleaseDistributedLock(lockKey, requestId string) (bool, error) { + luaScript := ` + if redis.call("get",KEYS[1]) == ARGV[1] + then + return redis.call("del",KEYS[1]) + else + return 0 + end` + + do, err := cache.Do("eval", luaScript, 1, lockKey, requestId) + fmt.Println(reflect.TypeOf(do)) + fmt.Println(do) + + if utils.AnyToInt64(do) == 1 { + return true, err + } else { + return false, err + } +} + +func GetDistributedLockRequestId(prefix string) string { + return prefix + utils.IntToStr(rand.Intn(100000000)) +} + +// HandleBalanceDistributedLock 处理余额更新时获取锁和释放锁 如果加锁成功,使用语句 ` defer cb() ` 释放锁 +func HandleBalanceDistributedLock(masterId, uid, requestIdPrefix string) (cb func(), err error) { + // 获取余额更新锁 + balanceLockKey := fmt.Sprintf(md.UserFinValidUpdateLock, masterId, uid) + requestId := GetDistributedLockRequestId(requestIdPrefix) + balanceLockOk := TryGetDistributedLock(balanceLockKey, requestId, true) + if !balanceLockOk { + return nil, errors.New("系统繁忙,请稍后再试") + } + + cb = func() { + _, _ = ReleaseDistributedLock(balanceLockKey, requestId) + } + + return cb, nil +} diff --git a/app/svc/svc_role.go b/app/svc/svc_role.go new file mode 100644 index 0000000..0a9b9da --- /dev/null +++ b/app/svc/svc_role.go @@ -0,0 +1,184 @@ +package svc + +import ( + "applet/app/db" + "applet/app/db/implement" + "applet/app/db/model" + "applet/app/md" + "applet/app/utils" + "applet/app/utils/cache" + "encoding/json" + "errors" + "github.com/gin-gonic/gin" + "regexp" + "strings" + "time" +) + +func CheckUserRole(c *gin.Context, cacheKey, uri string, admId int) (isHasPermission bool, err error) { + uri = utils.UriFilterExcludeQueryString(uri) //去除uri中?后的query参数 + isHasPermission = false + var rolePermission []string + var rolePermissionString string + rolePermissionString, _ = cache.GetString(cacheKey) + + //TODO::判断是否在白名单中 + if utils.InArr(uri, md.WhiteUri) { + isHasPermission = true + return + } + + if rolePermissionString != "" { + //if false { + if err = json.Unmarshal([]byte(rolePermissionString), &rolePermission); err != nil { + return + } + } else { + adminDb := implement.NewAdminDb(db.DBs[GetMasterId(c)]) + list, _, err1 := adminDb.GetAdminRolePermission(admId) + if err1 != nil { + return isHasPermission, err1 + } + for _, v := range list { + rolePermission = append(rolePermission, v.Permission.Action) + } + marshal, err1 := json.Marshal(rolePermission) + if err1 != nil { + return isHasPermission, err1 + } + rolePermissionString = string(marshal) + _, err = cache.SetEx(cacheKey, rolePermissionString, md.AdminRolePermissionCacheTime) + } + + if utils.InArr(uri, rolePermission) { + isHasPermission = true + } else { + //正则匹配占位符情况 + compileRegex := regexp.MustCompile("[0-9]+") + matchArr := compileRegex.FindAllString(uri, -1) + if len(matchArr) > 0 { + uri = strings.Replace(uri, matchArr[len(matchArr)-1], ":id", 1) + if utils.InArr(uri, rolePermission) { + isHasPermission = true + } + } + } + return +} + +func DeleteRole(c *gin.Context, roleId int) (err error) { + session := db.Db.NewSession() + defer session.Close() + session.Begin() + + //1、删除 `role` + roleDb := implement.NewRoleDb(db.DBs[GetMasterId(c)], roleId) + _, err = roleDb.RoleDeleteBySession(session, roleId) + if err != nil { + _ = session.Rollback() + return + } + + //2、删除 `role_permission_group` + rolePermissionGroupDb := implement.NewRolePermissionGroupDb(db.DBs[GetMasterId(c)]) + _, err = rolePermissionGroupDb.RolePermissionGroupDeleteForRoleBySession(session, roleId) + if err != nil { + _ = session.Rollback() + return + } + + //3、删除 `admin_role` + adminRoleDb := implement.NewAdminRoleDb(db.DBs[GetMasterId(c)]) + _, err = adminRoleDb.AdminRoleDeleteForRoleBySession(session, roleId) + if err != nil { + _ = session.Rollback() + return + } + + return session.Commit() +} + +func RoleBindPermissionGroup(c *gin.Context, req md.RoleBindPermissionGroupReq) (err error) { + session := db.Db.NewSession() + defer session.Close() + session.Begin() + //1、查询 `role` + roleDb := implement.NewRoleDb(db.DBs[GetMasterId(c)], req.RoleId) + role, err := roleDb.GetRole() + if err != nil { + return + } + if role == nil { + return errors.New("未查询到相应记录") + } + + //1、删除 `role_permission_group` + rolePermissionGroupDb := implement.NewRolePermissionGroupDb(db.DBs[GetMasterId(c)]) + _, err = rolePermissionGroupDb.RolePermissionGroupDeleteForRoleBySession(session, req.RoleId) + if err != nil { + _ = session.Rollback() + return + } + + //2、新增 `role_permission_group`` + var mm []*model.RolePermissionGroup + now := time.Now() + for _, v := range req.PermissionIds { + mm = append(mm, &model.RolePermissionGroup{ + RoleId: role.Id, + GroupId: v, + CreateAt: now.Format("2006-01-02 15:04:05"), + UpdateAt: now.Format("2006-01-02 15:04:05"), + }) + } + _, err = rolePermissionGroupDb.BatchAddRolePermissionGroupBySession(session, mm) + if err != nil { + _ = session.Rollback() + return + } + + return session.Commit() +} + +func BindAdminRole(c *gin.Context, req md.BindAdminRoleReq) (err error) { + session := db.Db.NewSession() + defer session.Close() + session.Begin() + //1、查询 `role` + adminDb := implement.NewAdminDb(db.DBs[GetMasterId(c)]) + role, err := adminDb.GetAdmin(req.AdmId) + if err != nil { + return + } + if role == nil { + return errors.New("未查询到相应记录") + } + + //1、删除 `admin_role` + adminRoleDb := implement.NewAdminRoleDb(db.DBs[GetMasterId(c)]) + _, err = adminRoleDb.AdminRoleDeleteBySession(session, req.AdmId) + if err != nil { + _ = session.Rollback() + return + } + + //2、新增 `删除 `admin_role`` + var mm []*model.AdminRole + now := time.Now() + for _, v := range req.RoleIds { + mm = append(mm, &model.AdminRole{ + AdmId: req.AdmId, + RoleId: v, + State: 1, + CreateAt: now.Format("2006-01-02 15:04:05"), + UpdateAt: now.Format("2006-01-02 15:04:05"), + }) + } + _, err = adminRoleDb.BatchAddAdminRoleBySession(session, mm) + if err != nil { + _ = session.Rollback() + return + } + + return session.Commit() +} diff --git a/app/svc/svc_sys_cfg_get.go b/app/svc/svc_sys_cfg_get.go new file mode 100644 index 0000000..e4a928a --- /dev/null +++ b/app/svc/svc_sys_cfg_get.go @@ -0,0 +1,100 @@ +package svc + +import ( + "github.com/gin-gonic/gin" + "xorm.io/xorm" + + "applet/app/db" + "applet/app/md" + "applet/app/utils/cache" +) + +// 单挑记录获取 +func SysCfgGet(c *gin.Context, key string) string { + mid := c.GetString("mid") + eg := db.DBs[mid] + return db.SysCfgGetWithDb(eg, mid, key) +} + +// 多条记录获取 +func SysCfgFind(c *gin.Context, keys ...string) map[string]string { + var masterId string + if c == nil { + masterId = "" + } else { + masterId = c.GetString("mid") + } + tmp := SysCfgFindComm(masterId, keys...) + return tmp +} +func SysCfgFindComm(masterId string, keys ...string) map[string]string { + var eg *xorm.Engine + if masterId == "" { + eg = db.Db + } else { + eg = db.DBs[masterId] + } + res := map[string]string{} + //TODO::判断keys长度(大于10个直接查数据库) + if len(keys) > 10 { + cfgList, _ := db.SysCfgGetAll(eg) + if cfgList == nil { + return nil + } + for _, v := range *cfgList { + res[v.Key] = v.Val + } + } else { + for _, key := range keys { + res[key] = db.SysCfgGetWithDb(eg, masterId, key) + } + } + return res +} + +// 多条记录获取 +func EgSysCfgFind(keys ...string) map[string]string { + var e *xorm.Engine + res := map[string]string{} + if len(res) == 0 { + cfgList, _ := db.SysCfgGetAll(e) + if cfgList == nil { + return nil + } + for _, v := range *cfgList { + res[v.Key] = v.Val + } + // 先不设置缓存 + // cache.SetJson(md.KEY_SYS_CFG_CACHE, res, 60) + } + if len(keys) == 0 { + return res + } + tmp := map[string]string{} + for _, v := range keys { + if val, ok := res[v]; ok { + tmp[v] = val + } else { + tmp[v] = "" + } + } + return tmp +} + +// 清理系统配置信息 +func SysCfgCleanCache() { + cache.Del(md.KEY_SYS_CFG_CACHE) +} + +// 写入系统设置 +func SysCfgSet(c *gin.Context, key, val, memo string) bool { + cfg, err := db.SysCfgGetOne(db.DBs[c.GetString("mid")], key) + if err != nil || cfg == nil { + return db.SysCfgInsert(db.DBs[c.GetString("mid")], key, val, memo) + } + if memo != "" && cfg.Memo != memo { + cfg.Memo = memo + } + SysCfgCleanCache() + return db.SysCfgUpdate(db.DBs[c.GetString("mid")], key, val, cfg.Memo) +} diff --git a/app/svc/svc_validate_common.go b/app/svc/svc_validate_common.go new file mode 100644 index 0000000..08e1e9d --- /dev/null +++ b/app/svc/svc_validate_common.go @@ -0,0 +1,26 @@ +package svc + +import ( + "applet/app/e" + "applet/app/utils" + "applet/app/utils/logx" + "encoding/json" + "fmt" + "github.com/go-playground/validator/v10" +) + +// HandleValidateErr 通用请求参数错误处理 +func HandleValidateErr(err error) error { + switch err.(type) { + case *json.UnmarshalTypeError: + return e.NewErr(e.ERR_UNMARSHAL, "参数格式错误") + case validator.ValidationErrors: + errs := err.(validator.ValidationErrors) + transMsgMap := errs.Translate(utils.ValidatorTrans) + transMsgOne := transMsgMap[utils.GetOneKeyOfMapString(transMsgMap)] + return e.NewErr(e.ERR_INVALID_ARGS, transMsgOne) + default: + _ = logx.Error(err) + return e.NewErr(e.ERR, fmt.Sprintf("validate request params, err:%v\n", err)) + } +} diff --git a/app/task/init.go b/app/task/init.go new file mode 100644 index 0000000..54b3536 --- /dev/null +++ b/app/task/init.go @@ -0,0 +1,117 @@ +package task + +import ( + taskMd "applet/app/task/md" + "time" + + "github.com/robfig/cron/v3" + "xorm.io/xorm" + + "applet/app/cfg" + "applet/app/db" + "applet/app/db/model" + "applet/app/md" + "applet/app/utils" + "applet/app/utils/logx" +) + +var ( + timer *cron.Cron + jobs = map[string]func(*xorm.Engine, string){} + baseEntryId cron.EntryID + entryIds []cron.EntryID + taskCfgList map[string]*[]model.SysCfg + ch = make(chan int, 30) + workerNum = 15 // 智盟跟单并发数量 + otherCh = make(chan int, 30) + otherWorkerNum = 18 // 淘宝, 苏宁, 考拉并发量 +) + +func Init() { + // 初始化任务列表 + initTasks() + var err error + timer = cron.New() + // reload为初始化数据库方法 + if baseEntryId, err = timer.AddFunc("@every 15m", reload); err != nil { + _ = logx.Fatal(err) + } +} + +func Run() { + reload() + timer.Start() + _ = logx.Info("auto tasks running...") +} + +func reload() { + // 重新初始化数据库 + db.InitMapDbs(cfg.DB, cfg.Prd) + + if len(taskCfgList) == 0 { + taskCfgList = map[string]*[]model.SysCfg{} + } + + // 获取所有站长的配置信息 + for dbName, v := range db.DBs { + if conf := db.MapCrontabCfg(v); conf != nil { + if cfg.Debug { + dbInfo := md.SplitDbInfo(v) + // 去掉模版库 + if dbName == "000000" { + continue + } + _ = logx.Debugf("【MasterId】%s, 【Host】%s, 【Name】%s, 【User】%s, 【prd】%v, 【Task】%v\n", dbName, dbInfo.Host, dbInfo.Name, dbInfo.User, cfg.Prd, utils.SerializeStr(*conf)) + } + taskCfgList[dbName] = conf + } + } + if len(taskCfgList) > 0 { + // 删除原有所有任务 + if len(entryIds) > 0 { + for _, v := range entryIds { + if v != baseEntryId { + timer.Remove(v) + } + } + entryIds = nil + } + var ( + entryId cron.EntryID + err error + ) + // 添加任务 + for dbName, v := range taskCfgList { + for _, vv := range *v { + if _, ok := jobs[vv.Key]; ok && vv.Val != "" { + // fmt.Println(vv.Val) + if entryId, err = timer.AddFunc(vv.Val, doTask(dbName, vv.Key)); err == nil { + entryIds = append(entryIds, entryId) + } + } + } + } + + } +} + +func doTask(dbName, fnName string) func() { + return func() { + begin := time.Now().Local() + jobs[fnName](db.DBs[dbName], dbName) + end := time.Now().Local() + logx.Infof( + "[%s] AutoTask <%s> started at <%s>, ended at <%s> duration <%s>", + dbName, + fnName, + begin.Format("2006-01-02 15:04:05.000"), + end.Format("2006-01-02 15:04:05.000"), + time.Duration(end.UnixNano()-begin.UnixNano()).String(), + ) + } +} + +// 增加自动任务队列 +func initTasks() { + jobs[taskMd.MallCronOrderCancel] = taskCancelOrder // 取消订单 +} diff --git a/app/task/md/cron_key.go b/app/task/md/cron_key.go new file mode 100644 index 0000000..b38ccc8 --- /dev/null +++ b/app/task/md/cron_key.go @@ -0,0 +1,5 @@ +package md + +const ( + MallCronOrderCancel = "mall_cron_order_cancel" // 取消订单任务 +) diff --git a/app/task/svc/svc_cancel_order.go b/app/task/svc/svc_cancel_order.go new file mode 100644 index 0000000..0c35b5f --- /dev/null +++ b/app/task/svc/svc_cancel_order.go @@ -0,0 +1,64 @@ +package svc + +import ( + "applet/app/db" + "applet/app/utils" + "applet/app/utils/logx" + "errors" + "fmt" + "time" + "xorm.io/xorm" +) + +func CancelOrder(eg *xorm.Engine, dbName string) { + fmt.Println("cancel order...") + defer func() { + if err := recover(); err != nil { + _ = logx.Error(err) + } + }() + + timeStr, err := getCancelCfg(eg, dbName) + if err != nil { + fmt.Println(err.Error()) + return + } + + now := time.Now() + // x 分钟后取消订单 + expTime := now.Add(-time.Hour * time.Duration(utils.StrToInt64(timeStr))) + expTimeStr := utils.Time2String(expTime, "") + + page := 1 + + for { + isEmpty, err := handleOnePage(eg, dbName, expTimeStr) + if err != nil { + _ = logx.Error(err) + break + } + if isEmpty { + break + } + + if page > 100 { + break + } + + page += 1 + + } +} + +func handleOnePage(eg *xorm.Engine, dbName, expTimeStr string) (isEmpty bool, err error) { + return false, nil +} + +func getCancelCfg(eg *xorm.Engine, masterId string) (string, error) { + cfg := db.SysCfgGetWithDb(eg, masterId, "order_expiration_time") + + if cfg == "" { + return "", errors.New("order_expiration_time no found") + } + return cfg, nil +} diff --git a/app/task/task_cancel_order.go b/app/task/task_cancel_order.go new file mode 100644 index 0000000..2e45bbb --- /dev/null +++ b/app/task/task_cancel_order.go @@ -0,0 +1,23 @@ +package task + +import ( + "applet/app/task/svc" + "math/rand" + "time" + "xorm.io/xorm" +) + +// 取消订单 +func taskCancelOrder(eg *xorm.Engine, dbName string) { + for { + if len(ch) > workerNum { + time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000))) + } else { + goto START + } + } +START: + ch <- 1 + svc.CancelOrder(eg, dbName) + <-ch +} diff --git a/app/utils/aes.go b/app/utils/aes.go new file mode 100644 index 0000000..8f5aaac --- /dev/null +++ b/app/utils/aes.go @@ -0,0 +1,123 @@ +package utils + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "fmt" +) + +func AesEncrypt(rawData, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + blockSize := block.BlockSize() + rawData = PKCS5Padding(rawData, blockSize) + // rawData = ZeroPadding(rawData, block.BlockSize()) + blockMode := cipher.NewCBCEncrypter(block, key[:blockSize]) + encrypted := make([]byte, len(rawData)) + // 根据CryptBlocks方法的说明,如下方式初始化encrypted也可以 + // encrypted := rawData + blockMode.CryptBlocks(encrypted, rawData) + return encrypted, nil +} + +func AesDecrypt(encrypted, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + blockSize := block.BlockSize() + blockMode := cipher.NewCBCDecrypter(block, key[:blockSize]) + rawData := make([]byte, len(encrypted)) + // rawData := encrypted + blockMode.CryptBlocks(rawData, encrypted) + rawData = PKCS5UnPadding(rawData) + // rawData = ZeroUnPadding(rawData) + return rawData, nil +} + +func ZeroPadding(cipherText []byte, blockSize int) []byte { + padding := blockSize - len(cipherText)%blockSize + padText := bytes.Repeat([]byte{0}, padding) + return append(cipherText, padText...) +} + +func ZeroUnPadding(rawData []byte) []byte { + length := len(rawData) + unPadding := int(rawData[length-1]) + return rawData[:(length - unPadding)] +} + +func PKCS5Padding(cipherText []byte, blockSize int) []byte { + padding := blockSize - len(cipherText)%blockSize + padText := bytes.Repeat([]byte{byte(padding)}, padding) + return append(cipherText, padText...) +} + +func PKCS5UnPadding(rawData []byte) []byte { + length := len(rawData) + // 去掉最后一个字节 unPadding 次 + unPadding := int(rawData[length-1]) + return rawData[:(length - unPadding)] +} + +// 填充0 +func zeroFill(key *string) { + l := len(*key) + if l != 16 && l != 24 && l != 32 { + if l < 16 { + *key = *key + fmt.Sprintf("%0*d", 16-l, 0) + } else if l < 24 { + *key = *key + fmt.Sprintf("%0*d", 24-l, 0) + } else if l < 32 { + *key = *key + fmt.Sprintf("%0*d", 32-l, 0) + } else { + *key = string([]byte(*key)[:32]) + } + } +} + +type AesCrypt struct { + Key []byte + Iv []byte +} + +func (a *AesCrypt) Encrypt(data []byte) ([]byte, error) { + aesBlockEncrypt, err := aes.NewCipher(a.Key) + if err != nil { + println(err.Error()) + return nil, err + } + + content := pKCS5Padding(data, aesBlockEncrypt.BlockSize()) + cipherBytes := make([]byte, len(content)) + aesEncrypt := cipher.NewCBCEncrypter(aesBlockEncrypt, a.Iv) + aesEncrypt.CryptBlocks(cipherBytes, content) + return cipherBytes, nil +} + +func (a *AesCrypt) Decrypt(src []byte) (data []byte, err error) { + decrypted := make([]byte, len(src)) + var aesBlockDecrypt cipher.Block + aesBlockDecrypt, err = aes.NewCipher(a.Key) + if err != nil { + println(err.Error()) + return nil, err + } + aesDecrypt := cipher.NewCBCDecrypter(aesBlockDecrypt, a.Iv) + aesDecrypt.CryptBlocks(decrypted, src) + return pKCS5Trimming(decrypted), nil +} + +func pKCS5Padding(cipherText []byte, blockSize int) []byte { + padding := blockSize - len(cipherText)%blockSize + padText := bytes.Repeat([]byte{byte(padding)}, padding) + return append(cipherText, padText...) +} + +func pKCS5Trimming(encrypt []byte) []byte { + padding := encrypt[len(encrypt)-1] + return encrypt[:len(encrypt)-int(padding)] +} diff --git a/app/utils/base64.go b/app/utils/base64.go new file mode 100644 index 0000000..ee16553 --- /dev/null +++ b/app/utils/base64.go @@ -0,0 +1,95 @@ +package utils + +import ( + "encoding/base64" + "fmt" +) + +const ( + Base64Std = iota + Base64Url + Base64RawStd + Base64RawUrl +) + +func Base64StdEncode(str interface{}) string { + return Base64Encode(str, Base64Std) +} + +func Base64StdDecode(str interface{}) string { + return Base64Decode(str, Base64Std) +} + +func Base64UrlEncode(str interface{}) string { + return Base64Encode(str, Base64Url) +} + +func Base64UrlDecode(str interface{}) string { + return Base64Decode(str, Base64Url) +} + +func Base64RawStdEncode(str interface{}) string { + return Base64Encode(str, Base64RawStd) +} + +func Base64RawStdDecode(str interface{}) string { + return Base64Decode(str, Base64RawStd) +} + +func Base64RawUrlEncode(str interface{}) string { + return Base64Encode(str, Base64RawUrl) +} + +func Base64RawUrlDecode(str interface{}) string { + return Base64Decode(str, Base64RawUrl) +} + +func Base64Encode(str interface{}, encode int) string { + newEncode := base64Encode(encode) + if newEncode == nil { + return "" + } + switch v := str.(type) { + case string: + return newEncode.EncodeToString([]byte(v)) + case []byte: + return newEncode.EncodeToString(v) + } + return newEncode.EncodeToString([]byte(fmt.Sprint(str))) +} + +func Base64Decode(str interface{}, encode int) string { + var err error + var b []byte + newEncode := base64Encode(encode) + if newEncode == nil { + return "" + } + switch v := str.(type) { + case string: + b, err = newEncode.DecodeString(v) + case []byte: + b, err = newEncode.DecodeString(string(v)) + default: + return "" + } + if err != nil { + return "" + } + return string(b) +} + +func base64Encode(encode int) *base64.Encoding { + switch encode { + case Base64Std: + return base64.StdEncoding + case Base64Url: + return base64.URLEncoding + case Base64RawStd: + return base64.RawStdEncoding + case Base64RawUrl: + return base64.RawURLEncoding + default: + return nil + } +} diff --git a/app/utils/boolean.go b/app/utils/boolean.go new file mode 100644 index 0000000..d64c876 --- /dev/null +++ b/app/utils/boolean.go @@ -0,0 +1,26 @@ +package utils + +import "reflect" + +// 检验一个值是否为空 +func Empty(val interface{}) bool { + v := reflect.ValueOf(val) + switch v.Kind() { + case reflect.String, reflect.Array: + return v.Len() == 0 + case reflect.Map, reflect.Slice: + return v.Len() == 0 || v.IsNil() + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + } + + return reflect.DeepEqual(val, reflect.Zero(v.Type()).Interface()) +} \ No newline at end of file diff --git a/app/utils/cache/base.go b/app/utils/cache/base.go new file mode 100644 index 0000000..64648dd --- /dev/null +++ b/app/utils/cache/base.go @@ -0,0 +1,421 @@ +package cache + +import ( + "errors" + "fmt" + "strconv" + "time" +) + +const ( + redisDialTTL = 10 * time.Second + redisReadTTL = 3 * time.Second + redisWriteTTL = 3 * time.Second + redisIdleTTL = 10 * time.Second + redisPoolTTL = 10 * time.Second + redisPoolSize int = 512 + redisMaxIdleConn int = 64 + redisMaxActive int = 512 +) + +var ( + ErrNil = errors.New("nil return") + ErrWrongArgsNum = errors.New("args num error") + ErrNegativeInt = errors.New("redis cluster: unexpected value for Uint64") +) + +// 以下为提供类型转换 + +func Int(reply interface{}, err error) (int, error) { + if err != nil { + return 0, err + } + switch reply := reply.(type) { + case int: + return reply, nil + case int8: + return int(reply), nil + case int16: + return int(reply), nil + case int32: + return int(reply), nil + case int64: + x := int(reply) + if int64(x) != reply { + return 0, strconv.ErrRange + } + return x, nil + case uint: + n := int(reply) + if n < 0 { + return 0, strconv.ErrRange + } + return n, nil + case uint8: + return int(reply), nil + case uint16: + return int(reply), nil + case uint32: + n := int(reply) + if n < 0 { + return 0, strconv.ErrRange + } + return n, nil + case uint64: + n := int(reply) + if n < 0 { + return 0, strconv.ErrRange + } + return n, nil + case []byte: + data := string(reply) + if len(data) == 0 { + return 0, ErrNil + } + + n, err := strconv.ParseInt(data, 10, 0) + return int(n), err + case string: + if len(reply) == 0 { + return 0, ErrNil + } + + n, err := strconv.ParseInt(reply, 10, 0) + return int(n), err + case nil: + return 0, ErrNil + case error: + return 0, reply + } + return 0, fmt.Errorf("redis cluster: unexpected type for Int, got type %T", reply) +} + +func Int64(reply interface{}, err error) (int64, error) { + if err != nil { + return 0, err + } + switch reply := reply.(type) { + case int: + return int64(reply), nil + case int8: + return int64(reply), nil + case int16: + return int64(reply), nil + case int32: + return int64(reply), nil + case int64: + return reply, nil + case uint: + n := int64(reply) + if n < 0 { + return 0, strconv.ErrRange + } + return n, nil + case uint8: + return int64(reply), nil + case uint16: + return int64(reply), nil + case uint32: + return int64(reply), nil + case uint64: + n := int64(reply) + if n < 0 { + return 0, strconv.ErrRange + } + return n, nil + case []byte: + data := string(reply) + if len(data) == 0 { + return 0, ErrNil + } + + n, err := strconv.ParseInt(data, 10, 64) + return n, err + case string: + if len(reply) == 0 { + return 0, ErrNil + } + + n, err := strconv.ParseInt(reply, 10, 64) + return n, err + case nil: + return 0, ErrNil + case error: + return 0, reply + } + return 0, fmt.Errorf("redis cluster: unexpected type for Int64, got type %T", reply) +} + +func Uint64(reply interface{}, err error) (uint64, error) { + if err != nil { + return 0, err + } + switch reply := reply.(type) { + case uint: + return uint64(reply), nil + case uint8: + return uint64(reply), nil + case uint16: + return uint64(reply), nil + case uint32: + return uint64(reply), nil + case uint64: + return reply, nil + case int: + if reply < 0 { + return 0, ErrNegativeInt + } + return uint64(reply), nil + case int8: + if reply < 0 { + return 0, ErrNegativeInt + } + return uint64(reply), nil + case int16: + if reply < 0 { + return 0, ErrNegativeInt + } + return uint64(reply), nil + case int32: + if reply < 0 { + return 0, ErrNegativeInt + } + return uint64(reply), nil + case int64: + if reply < 0 { + return 0, ErrNegativeInt + } + return uint64(reply), nil + case []byte: + data := string(reply) + if len(data) == 0 { + return 0, ErrNil + } + + n, err := strconv.ParseUint(data, 10, 64) + return n, err + case string: + if len(reply) == 0 { + return 0, ErrNil + } + + n, err := strconv.ParseUint(reply, 10, 64) + return n, err + case nil: + return 0, ErrNil + case error: + return 0, reply + } + return 0, fmt.Errorf("redis cluster: unexpected type for Uint64, got type %T", reply) +} + +func Float64(reply interface{}, err error) (float64, error) { + if err != nil { + return 0, err + } + + var value float64 + err = nil + switch v := reply.(type) { + case float32: + value = float64(v) + case float64: + value = v + case int: + value = float64(v) + case int8: + value = float64(v) + case int16: + value = float64(v) + case int32: + value = float64(v) + case int64: + value = float64(v) + case uint: + value = float64(v) + case uint8: + value = float64(v) + case uint16: + value = float64(v) + case uint32: + value = float64(v) + case uint64: + value = float64(v) + case []byte: + data := string(v) + if len(data) == 0 { + return 0, ErrNil + } + value, err = strconv.ParseFloat(string(v), 64) + case string: + if len(v) == 0 { + return 0, ErrNil + } + value, err = strconv.ParseFloat(v, 64) + case nil: + err = ErrNil + case error: + err = v + default: + err = fmt.Errorf("redis cluster: unexpected type for Float64, got type %T", v) + } + + return value, err +} + +func Bool(reply interface{}, err error) (bool, error) { + if err != nil { + return false, err + } + switch reply := reply.(type) { + case bool: + return reply, nil + case int64: + return reply != 0, nil + case []byte: + data := string(reply) + if len(data) == 0 { + return false, ErrNil + } + + return strconv.ParseBool(data) + case string: + if len(reply) == 0 { + return false, ErrNil + } + + return strconv.ParseBool(reply) + case nil: + return false, ErrNil + case error: + return false, reply + } + return false, fmt.Errorf("redis cluster: unexpected type for Bool, got type %T", reply) +} + +func Bytes(reply interface{}, err error) ([]byte, error) { + if err != nil { + return nil, err + } + switch reply := reply.(type) { + case []byte: + if len(reply) == 0 { + return nil, ErrNil + } + return reply, nil + case string: + data := []byte(reply) + if len(data) == 0 { + return nil, ErrNil + } + return data, nil + case nil: + return nil, ErrNil + case error: + return nil, reply + } + return nil, fmt.Errorf("redis cluster: unexpected type for Bytes, got type %T", reply) +} + +func String(reply interface{}, err error) (string, error) { + if err != nil { + return "", err + } + + value := "" + err = nil + switch v := reply.(type) { + case string: + if len(v) == 0 { + return "", ErrNil + } + + value = v + case []byte: + if len(v) == 0 { + return "", ErrNil + } + + value = string(v) + case int: + value = strconv.FormatInt(int64(v), 10) + case int8: + value = strconv.FormatInt(int64(v), 10) + case int16: + value = strconv.FormatInt(int64(v), 10) + case int32: + value = strconv.FormatInt(int64(v), 10) + case int64: + value = strconv.FormatInt(v, 10) + case uint: + value = strconv.FormatUint(uint64(v), 10) + case uint8: + value = strconv.FormatUint(uint64(v), 10) + case uint16: + value = strconv.FormatUint(uint64(v), 10) + case uint32: + value = strconv.FormatUint(uint64(v), 10) + case uint64: + value = strconv.FormatUint(v, 10) + case float32: + value = strconv.FormatFloat(float64(v), 'f', -1, 32) + case float64: + value = strconv.FormatFloat(v, 'f', -1, 64) + case bool: + value = strconv.FormatBool(v) + case nil: + err = ErrNil + case error: + err = v + default: + err = fmt.Errorf("redis cluster: unexpected type for String, got type %T", v) + } + + return value, err +} + +func Strings(reply interface{}, err error) ([]string, error) { + if err != nil { + return nil, err + } + switch reply := reply.(type) { + case []interface{}: + result := make([]string, len(reply)) + for i := range reply { + if reply[i] == nil { + continue + } + switch subReply := reply[i].(type) { + case string: + result[i] = subReply + case []byte: + result[i] = string(subReply) + default: + return nil, fmt.Errorf("redis cluster: unexpected element type for String, got type %T", reply[i]) + } + } + return result, nil + case []string: + return reply, nil + case nil: + return nil, ErrNil + case error: + return nil, reply + } + return nil, fmt.Errorf("redis cluster: unexpected type for Strings, got type %T", reply) +} + +func Values(reply interface{}, err error) ([]interface{}, error) { + if err != nil { + return nil, err + } + switch reply := reply.(type) { + case []interface{}: + return reply, nil + case nil: + return nil, ErrNil + case error: + return nil, reply + } + return nil, fmt.Errorf("redis cluster: unexpected type for Values, got type %T", reply) +} diff --git a/app/utils/cache/cache/cache.go b/app/utils/cache/cache/cache.go new file mode 100644 index 0000000..e43c5f0 --- /dev/null +++ b/app/utils/cache/cache/cache.go @@ -0,0 +1,107 @@ +package cache + +import ( + "fmt" + "time" +) + +var c Cache + +type Cache interface { + // get cached value by key. + Get(key string) interface{} + // GetMulti is a batch version of Get. + GetMulti(keys []string) []interface{} + // set cached value with key and expire time. + Put(key string, val interface{}, timeout time.Duration) error + // delete cached value by key. + Delete(key string) error + // increase cached int value by key, as a counter. + Incr(key string) error + // decrease cached int value by key, as a counter. + Decr(key string) error + // check if cached value exists or not. + IsExist(key string) bool + // clear all cache. + ClearAll() error + // start gc routine based on config string settings. + StartAndGC(config string) error +} + +// Instance is a function create a new Cache Instance +type Instance func() Cache + +var adapters = make(map[string]Instance) + +// Register makes a cache adapter available by the adapter name. +// If Register is called twice with the same name or if driver is nil, +// it panics. +func Register(name string, adapter Instance) { + if adapter == nil { + panic("cache: Register adapter is nil") + } + if _, ok := adapters[name]; ok { + panic("cache: Register called twice for adapter " + name) + } + adapters[name] = adapter +} + +// NewCache Create a new cache driver by adapter name and config string. +// config need to be correct JSON as string: {"interval":360}. +// it will start gc automatically. +func NewCache(adapterName, config string) (adapter Cache, err error) { + instanceFunc, ok := adapters[adapterName] + if !ok { + err = fmt.Errorf("cache: unknown adapter name %q (forgot to import?)", adapterName) + return + } + adapter = instanceFunc() + err = adapter.StartAndGC(config) + if err != nil { + adapter = nil + } + return +} + +func InitCache(adapterName, config string) (err error) { + instanceFunc, ok := adapters[adapterName] + if !ok { + err = fmt.Errorf("cache: unknown adapter name %q (forgot to import?)", adapterName) + return + } + c = instanceFunc() + err = c.StartAndGC(config) + if err != nil { + c = nil + } + return +} + +func Get(key string) interface{} { + return c.Get(key) +} + +func GetMulti(keys []string) []interface{} { + return c.GetMulti(keys) +} +func Put(key string, val interface{}, ttl time.Duration) error { + return c.Put(key, val, ttl) +} +func Delete(key string) error { + return c.Delete(key) +} +func Incr(key string) error { + return c.Incr(key) +} +func Decr(key string) error { + return c.Decr(key) +} +func IsExist(key string) bool { + return c.IsExist(key) +} +func ClearAll() error { + return c.ClearAll() +} +func StartAndGC(cfg string) error { + return c.StartAndGC(cfg) +} diff --git a/app/utils/cache/cache/conv.go b/app/utils/cache/cache/conv.go new file mode 100644 index 0000000..6b700ae --- /dev/null +++ b/app/utils/cache/cache/conv.go @@ -0,0 +1,86 @@ +package cache + +import ( + "fmt" + "strconv" +) + +// GetString convert interface to string. +func GetString(v interface{}) string { + switch result := v.(type) { + case string: + return result + case []byte: + return string(result) + default: + if v != nil { + return fmt.Sprint(result) + } + } + return "" +} + +// GetInt convert interface to int. +func GetInt(v interface{}) int { + switch result := v.(type) { + case int: + return result + case int32: + return int(result) + case int64: + return int(result) + default: + if d := GetString(v); d != "" { + value, _ := strconv.Atoi(d) + return value + } + } + return 0 +} + +// GetInt64 convert interface to int64. +func GetInt64(v interface{}) int64 { + switch result := v.(type) { + case int: + return int64(result) + case int32: + return int64(result) + case int64: + return result + default: + + if d := GetString(v); d != "" { + value, _ := strconv.ParseInt(d, 10, 64) + return value + } + } + return 0 +} + +// GetFloat64 convert interface to float64. +func GetFloat64(v interface{}) float64 { + switch result := v.(type) { + case float64: + return result + default: + if d := GetString(v); d != "" { + value, _ := strconv.ParseFloat(d, 64) + return value + } + } + return 0 +} + +// GetBool convert interface to bool. +func GetBool(v interface{}) bool { + switch result := v.(type) { + case bool: + return result + default: + if d := GetString(v); d != "" { + value, _ := strconv.ParseBool(d) + return value + } + } + return false +} diff --git a/app/utils/cache/cache/file.go b/app/utils/cache/cache/file.go new file mode 100644 index 0000000..5c4e366 --- /dev/null +++ b/app/utils/cache/cache/file.go @@ -0,0 +1,241 @@ +package cache + +import ( + "bytes" + "crypto/md5" + "encoding/gob" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "strconv" + "time" +) + +// FileCacheItem is basic unit of file cache adapter. +// it contains data and expire time. +type FileCacheItem struct { + Data interface{} + LastAccess time.Time + Expired time.Time +} + +// FileCache Config +var ( + FileCachePath = "cache" // cache directory + FileCacheFileSuffix = ".bin" // cache file suffix + FileCacheDirectoryLevel = 2 // cache file deep level if auto generated cache files. + FileCacheEmbedExpiry time.Duration // cache expire time, default is no expire forever. +) + +// FileCache is cache adapter for file storage. +type FileCache struct { + CachePath string + FileSuffix string + DirectoryLevel int + EmbedExpiry int +} + +// NewFileCache Create new file cache with no config. +// the level and expiry need set in method StartAndGC as config string. +func NewFileCache() Cache { + // return &FileCache{CachePath:FileCachePath, FileSuffix:FileCacheFileSuffix} + return &FileCache{} +} + +// StartAndGC will start and begin gc for file cache. +// the config need to be like {CachePath:"/cache","FileSuffix":".bin","DirectoryLevel":2,"EmbedExpiry":0} +func (fc *FileCache) StartAndGC(config string) error { + + var cfg map[string]string + json.Unmarshal([]byte(config), &cfg) + if _, ok := cfg["CachePath"]; !ok { + cfg["CachePath"] = FileCachePath + } + if _, ok := cfg["FileSuffix"]; !ok { + cfg["FileSuffix"] = FileCacheFileSuffix + } + if _, ok := cfg["DirectoryLevel"]; !ok { + cfg["DirectoryLevel"] = strconv.Itoa(FileCacheDirectoryLevel) + } + if _, ok := cfg["EmbedExpiry"]; !ok { + cfg["EmbedExpiry"] = strconv.FormatInt(int64(FileCacheEmbedExpiry.Seconds()), 10) + } + fc.CachePath = cfg["CachePath"] + fc.FileSuffix = cfg["FileSuffix"] + fc.DirectoryLevel, _ = strconv.Atoi(cfg["DirectoryLevel"]) + fc.EmbedExpiry, _ = strconv.Atoi(cfg["EmbedExpiry"]) + + fc.Init() + return nil +} + +// Init will make new dir for file cache if not exist. +func (fc *FileCache) Init() { + if ok, _ := exists(fc.CachePath); !ok { // todo : error handle + _ = os.MkdirAll(fc.CachePath, os.ModePerm) // todo : error handle + } +} + +// get cached file name. it's md5 encoded. +func (fc *FileCache) getCacheFileName(key string) string { + m := md5.New() + io.WriteString(m, key) + keyMd5 := hex.EncodeToString(m.Sum(nil)) + cachePath := fc.CachePath + switch fc.DirectoryLevel { + case 2: + cachePath = filepath.Join(cachePath, keyMd5[0:2], keyMd5[2:4]) + case 1: + cachePath = filepath.Join(cachePath, keyMd5[0:2]) + } + + if ok, _ := exists(cachePath); !ok { // todo : error handle + _ = os.MkdirAll(cachePath, os.ModePerm) // todo : error handle + } + + return filepath.Join(cachePath, fmt.Sprintf("%s%s", keyMd5, fc.FileSuffix)) +} + +// Get value from file cache. +// if non-exist or expired, return empty string. +func (fc *FileCache) Get(key string) interface{} { + fileData, err := FileGetContents(fc.getCacheFileName(key)) + if err != nil { + return "" + } + var to FileCacheItem + GobDecode(fileData, &to) + if to.Expired.Before(time.Now()) { + return "" + } + return to.Data +} + +// GetMulti gets values from file cache. +// if non-exist or expired, return empty string. +func (fc *FileCache) GetMulti(keys []string) []interface{} { + var rc []interface{} + for _, key := range keys { + rc = append(rc, fc.Get(key)) + } + return rc +} + +// Put value into file cache. +// timeout means how long to keep this file, unit of ms. +// if timeout equals FileCacheEmbedExpiry(default is 0), cache this item forever. +func (fc *FileCache) Put(key string, val interface{}, timeout time.Duration) error { + gob.Register(val) + + item := FileCacheItem{Data: val} + if timeout == FileCacheEmbedExpiry { + item.Expired = time.Now().Add((86400 * 365 * 10) * time.Second) // ten years + } else { + item.Expired = time.Now().Add(timeout) + } + item.LastAccess = time.Now() + data, err := GobEncode(item) + if err != nil { + return err + } + return FilePutContents(fc.getCacheFileName(key), data) +} + +// Delete file cache value. +func (fc *FileCache) Delete(key string) error { + filename := fc.getCacheFileName(key) + if ok, _ := exists(filename); ok { + return os.Remove(filename) + } + return nil +} + +// Incr will increase cached int value. +// fc value is saving forever unless Delete. +func (fc *FileCache) Incr(key string) error { + data := fc.Get(key) + var incr int + if reflect.TypeOf(data).Name() != "int" { + incr = 0 + } else { + incr = data.(int) + 1 + } + fc.Put(key, incr, FileCacheEmbedExpiry) + return nil +} + +// Decr will decrease cached int value. +func (fc *FileCache) Decr(key string) error { + data := fc.Get(key) + var decr int + if reflect.TypeOf(data).Name() != "int" || data.(int)-1 <= 0 { + decr = 0 + } else { + decr = data.(int) - 1 + } + fc.Put(key, decr, FileCacheEmbedExpiry) + return nil +} + +// IsExist check value is exist. +func (fc *FileCache) IsExist(key string) bool { + ret, _ := exists(fc.getCacheFileName(key)) + return ret +} + +// ClearAll will clean cached files. +// not implemented. +func (fc *FileCache) ClearAll() error { + return nil +} + +// check file exist. +func exists(path string) (bool, error) { + _, err := os.Stat(path) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} + +// FileGetContents Get bytes to file. +// if non-exist, create this file. +func FileGetContents(filename string) (data []byte, e error) { + return ioutil.ReadFile(filename) +} + +// FilePutContents Put bytes to file. +// if non-exist, create this file. +func FilePutContents(filename string, content []byte) error { + return ioutil.WriteFile(filename, content, os.ModePerm) +} + +// GobEncode Gob encodes file cache item. +func GobEncode(data interface{}) ([]byte, error) { + buf := bytes.NewBuffer(nil) + enc := gob.NewEncoder(buf) + err := enc.Encode(data) + if err != nil { + return nil, err + } + return buf.Bytes(), err +} + +// GobDecode Gob decodes file cache item. +func GobDecode(data []byte, to *FileCacheItem) error { + buf := bytes.NewBuffer(data) + dec := gob.NewDecoder(buf) + return dec.Decode(&to) +} + +func init() { + Register("file", NewFileCache) +} diff --git a/app/utils/cache/cache/memory.go b/app/utils/cache/cache/memory.go new file mode 100644 index 0000000..0cc5015 --- /dev/null +++ b/app/utils/cache/cache/memory.go @@ -0,0 +1,239 @@ +package cache + +import ( + "encoding/json" + "errors" + "sync" + "time" +) + +var ( + // DefaultEvery means the clock time of recycling the expired cache items in memory. + DefaultEvery = 60 // 1 minute +) + +// MemoryItem store memory cache item. +type MemoryItem struct { + val interface{} + createdTime time.Time + lifespan time.Duration +} + +func (mi *MemoryItem) isExpire() bool { + // 0 means forever + if mi.lifespan == 0 { + return false + } + return time.Now().Sub(mi.createdTime) > mi.lifespan +} + +// MemoryCache is Memory cache adapter. +// it contains a RW locker for safe map storage. +type MemoryCache struct { + sync.RWMutex + dur time.Duration + items map[string]*MemoryItem + Every int // run an expiration check Every clock time +} + +// NewMemoryCache returns a new MemoryCache. +func NewMemoryCache() Cache { + cache := MemoryCache{items: make(map[string]*MemoryItem)} + return &cache +} + +// Get cache from memory. +// if non-existed or expired, return nil. +func (bc *MemoryCache) Get(name string) interface{} { + bc.RLock() + defer bc.RUnlock() + if itm, ok := bc.items[name]; ok { + if itm.isExpire() { + return nil + } + return itm.val + } + return nil +} + +// GetMulti gets caches from memory. +// if non-existed or expired, return nil. +func (bc *MemoryCache) GetMulti(names []string) []interface{} { + var rc []interface{} + for _, name := range names { + rc = append(rc, bc.Get(name)) + } + return rc +} + +// Put cache to memory. +// if lifespan is 0, it will be forever till restart. +func (bc *MemoryCache) Put(name string, value interface{}, lifespan time.Duration) error { + bc.Lock() + defer bc.Unlock() + bc.items[name] = &MemoryItem{ + val: value, + createdTime: time.Now(), + lifespan: lifespan, + } + return nil +} + +// Delete cache in memory. +func (bc *MemoryCache) Delete(name string) error { + bc.Lock() + defer bc.Unlock() + if _, ok := bc.items[name]; !ok { + return errors.New("key not exist") + } + delete(bc.items, name) + if _, ok := bc.items[name]; ok { + return errors.New("delete key error") + } + return nil +} + +// Incr increase cache counter in memory. +// it supports int,int32,int64,uint,uint32,uint64. +func (bc *MemoryCache) Incr(key string) error { + bc.RLock() + defer bc.RUnlock() + itm, ok := bc.items[key] + if !ok { + return errors.New("key not exist") + } + switch itm.val.(type) { + case int: + itm.val = itm.val.(int) + 1 + case int32: + itm.val = itm.val.(int32) + 1 + case int64: + itm.val = itm.val.(int64) + 1 + case uint: + itm.val = itm.val.(uint) + 1 + case uint32: + itm.val = itm.val.(uint32) + 1 + case uint64: + itm.val = itm.val.(uint64) + 1 + default: + return errors.New("item val is not (u)int (u)int32 (u)int64") + } + return nil +} + +// Decr decrease counter in memory. +func (bc *MemoryCache) Decr(key string) error { + bc.RLock() + defer bc.RUnlock() + itm, ok := bc.items[key] + if !ok { + return errors.New("key not exist") + } + switch itm.val.(type) { + case int: + itm.val = itm.val.(int) - 1 + case int64: + itm.val = itm.val.(int64) - 1 + case int32: + itm.val = itm.val.(int32) - 1 + case uint: + if itm.val.(uint) > 0 { + itm.val = itm.val.(uint) - 1 + } else { + return errors.New("item val is less than 0") + } + case uint32: + if itm.val.(uint32) > 0 { + itm.val = itm.val.(uint32) - 1 + } else { + return errors.New("item val is less than 0") + } + case uint64: + if itm.val.(uint64) > 0 { + itm.val = itm.val.(uint64) - 1 + } else { + return errors.New("item val is less than 0") + } + default: + return errors.New("item val is not int int64 int32") + } + return nil +} + +// IsExist check cache exist in memory. +func (bc *MemoryCache) IsExist(name string) bool { + bc.RLock() + defer bc.RUnlock() + if v, ok := bc.items[name]; ok { + return !v.isExpire() + } + return false +} + +// ClearAll will delete all cache in memory. +func (bc *MemoryCache) ClearAll() error { + bc.Lock() + defer bc.Unlock() + bc.items = make(map[string]*MemoryItem) + return nil +} + +// StartAndGC start memory cache. it will check expiration in every clock time. +func (bc *MemoryCache) StartAndGC(config string) error { + var cf map[string]int + json.Unmarshal([]byte(config), &cf) + if _, ok := cf["interval"]; !ok { + cf = make(map[string]int) + cf["interval"] = DefaultEvery + } + dur := time.Duration(cf["interval"]) * time.Second + bc.Every = cf["interval"] + bc.dur = dur + go bc.vacuum() + return nil +} + +// check expiration. +func (bc *MemoryCache) vacuum() { + bc.RLock() + every := bc.Every + bc.RUnlock() + + if every < 1 { + return + } + for { + <-time.After(bc.dur) + if bc.items == nil { + return + } + if keys := bc.expiredKeys(); len(keys) != 0 { + bc.clearItems(keys) + } + } +} + +// expiredKeys returns key list which are expired. +func (bc *MemoryCache) expiredKeys() (keys []string) { + bc.RLock() + defer bc.RUnlock() + for key, itm := range bc.items { + if itm.isExpire() { + keys = append(keys, key) + } + } + return +} + +// clearItems removes all the items which key in keys. +func (bc *MemoryCache) clearItems(keys []string) { + bc.Lock() + defer bc.Unlock() + for _, key := range keys { + delete(bc.items, key) + } +} + +func init() { + Register("memory", NewMemoryCache) +} diff --git a/app/utils/cache/redis.go b/app/utils/cache/redis.go new file mode 100644 index 0000000..eaf62fb --- /dev/null +++ b/app/utils/cache/redis.go @@ -0,0 +1,412 @@ +package cache + +import ( + "encoding/json" + "errors" + "log" + "strings" + "time" + + redigo "github.com/gomodule/redigo/redis" +) + +// configuration +type Config struct { + Server string + Password string + MaxIdle int // Maximum number of idle connections in the pool. + + // Maximum number of connections allocated by the pool at a given time. + // When zero, there is no limit on the number of connections in the pool. + MaxActive int + + // Close connections after remaining idle for this duration. If the value + // is zero, then idle connections are not closed. Applications should set + // the timeout to a value less than the server's timeout. + IdleTimeout time.Duration + + // If Wait is true and the pool is at the MaxActive limit, then Get() waits + // for a connection to be returned to the pool before returning. + Wait bool + KeyPrefix string // prefix to all keys; example is "dev environment name" + KeyDelimiter string // delimiter to be used while appending keys; example is ":" + KeyPlaceholder string // placeholder to be parsed using given arguments to obtain a final key; example is "?" +} + +var pool *redigo.Pool +var conf *Config + +func NewRedis(addr string) { + if addr == "" { + panic("\nredis connect string cannot be empty\n") + } + pool = &redigo.Pool{ + MaxIdle: redisMaxIdleConn, + IdleTimeout: redisIdleTTL, + MaxActive: redisMaxActive, + // MaxConnLifetime: redisDialTTL, + Wait: true, + Dial: func() (redigo.Conn, error) { + c, err := redigo.Dial("tcp", addr, + redigo.DialConnectTimeout(redisDialTTL), + redigo.DialReadTimeout(redisReadTTL), + redigo.DialWriteTimeout(redisWriteTTL), + ) + if err != nil { + log.Println("Redis Dial failed: ", err) + return nil, err + } + return c, err + }, + TestOnBorrow: func(c redigo.Conn, t time.Time) error { + _, err := c.Do("PING") + if err != nil { + log.Println("Unable to ping to redis server:", err) + } + return err + }, + } + conn := pool.Get() + defer conn.Close() + if conn.Err() != nil { + println("\nredis connect " + addr + " error: " + conn.Err().Error()) + } else { + println("\nredis connect " + addr + " success!\n") + } +} + +func Do(cmd string, args ...interface{}) (reply interface{}, err error) { + conn := pool.Get() + defer conn.Close() + return conn.Do(cmd, args...) +} + +func GetPool() *redigo.Pool { + return pool +} + +func ParseKey(key string, vars []string) (string, error) { + arr := strings.Split(key, conf.KeyPlaceholder) + actualKey := "" + if len(arr) != len(vars)+1 { + return "", errors.New("redis/connection.go: Insufficient arguments to parse key") + } else { + for index, val := range arr { + if index == 0 { + actualKey = arr[index] + } else { + actualKey += vars[index-1] + val + } + } + } + return getPrefixedKey(actualKey), nil +} + +func getPrefixedKey(key string) string { + return conf.KeyPrefix + conf.KeyDelimiter + key +} +func StripEnvKey(key string) string { + return strings.TrimLeft(key, conf.KeyPrefix+conf.KeyDelimiter) +} +func SplitKey(key string) []string { + return strings.Split(key, conf.KeyDelimiter) +} +func Expire(key string, ttl int) (interface{}, error) { + return Do("EXPIRE", key, ttl) +} +func Persist(key string) (interface{}, error) { + return Do("PERSIST", key) +} + +func Del(key string) (interface{}, error) { + return Do("DEL", key) +} +func Set(key string, data interface{}) (interface{}, error) { + // set + return Do("SET", key, data) +} +func SetNX(key string, data interface{}) (interface{}, error) { + return Do("SETNX", key, data) +} +func SetEx(key string, data interface{}, ttl int) (interface{}, error) { + return Do("SETEX", key, ttl, data) +} + +func SetJson(key string, data interface{}, ttl int) bool { + c, err := json.Marshal(data) + if err != nil { + return false + } + if ttl < 1 { + _, err = Set(key, c) + } else { + _, err = SetEx(key, c, ttl) + } + if err != nil { + return false + } + return true +} + +func GetJson(key string, dst interface{}) error { + b, err := GetBytes(key) + if err != nil { + return err + } + if err = json.Unmarshal(b, dst); err != nil { + return err + } + return nil +} + +func Get(key string) (interface{}, error) { + // get + return Do("GET", key) +} +func GetTTL(key string) (time.Duration, error) { + ttl, err := redigo.Int64(Do("TTL", key)) + return time.Duration(ttl) * time.Second, err +} +func GetBytes(key string) ([]byte, error) { + return redigo.Bytes(Do("GET", key)) +} +func GetString(key string) (string, error) { + return redigo.String(Do("GET", key)) +} +func GetStringMap(key string) (map[string]string, error) { + return redigo.StringMap(Do("GET", key)) +} +func GetInt(key string) (int, error) { + return redigo.Int(Do("GET", key)) +} +func GetInt64(key string) (int64, error) { + return redigo.Int64(Do("GET", key)) +} +func GetStringLength(key string) (int, error) { + return redigo.Int(Do("STRLEN", key)) +} +func ZAdd(key string, score float64, data interface{}) (interface{}, error) { + return Do("ZADD", key, score, data) +} +func ZAddNX(key string, score float64, data interface{}) (interface{}, error) { + return Do("ZADD", key, "NX", score, data) +} +func ZRem(key string, data interface{}) (interface{}, error) { + return Do("ZREM", key, data) +} +func ZRange(key string, start int, end int, withScores bool) ([]interface{}, error) { + if withScores { + return redigo.Values(Do("ZRANGE", key, start, end, "WITHSCORES")) + } + return redigo.Values(Do("ZRANGE", key, start, end)) +} +func ZRemRangeByScore(key string, start int64, end int64) ([]interface{}, error) { + return redigo.Values(Do("ZREMRANGEBYSCORE", key, start, end)) +} +func ZCard(setName string) (int64, error) { + return redigo.Int64(Do("ZCARD", setName)) +} +func ZScan(setName string) (int64, error) { + return redigo.Int64(Do("ZCARD", setName)) +} +func SAdd(setName string, data interface{}) (interface{}, error) { + return Do("SADD", setName, data) +} +func SCard(setName string) (int64, error) { + return redigo.Int64(Do("SCARD", setName)) +} +func SIsMember(setName string, data interface{}) (bool, error) { + return redigo.Bool(Do("SISMEMBER", setName, data)) +} +func SMembers(setName string) ([]string, error) { + return redigo.Strings(Do("SMEMBERS", setName)) +} +func SRem(setName string, data interface{}) (interface{}, error) { + return Do("SREM", setName, data) +} +func HSet(key string, HKey string, data interface{}) (interface{}, error) { + return Do("HSET", key, HKey, data) +} + +func HGet(key string, HKey string) (interface{}, error) { + return Do("HGET", key, HKey) +} + +func HMGet(key string, hashKeys ...string) ([]interface{}, error) { + ret, err := Do("HMGET", key, hashKeys) + if err != nil { + return nil, err + } + reta, ok := ret.([]interface{}) + if !ok { + return nil, errors.New("result not an array") + } + return reta, nil +} + +func HMSet(key string, hashKeys []string, vals []interface{}) (interface{}, error) { + if len(hashKeys) == 0 || len(hashKeys) != len(vals) { + var ret interface{} + return ret, errors.New("bad length") + } + input := []interface{}{key} + for i, v := range hashKeys { + input = append(input, v, vals[i]) + } + return Do("HMSET", input...) +} + +func HGetString(key string, HKey string) (string, error) { + return redigo.String(Do("HGET", key, HKey)) +} +func HGetFloat(key string, HKey string) (float64, error) { + f, err := redigo.Float64(Do("HGET", key, HKey)) + return f, err +} +func HGetInt(key string, HKey string) (int, error) { + return redigo.Int(Do("HGET", key, HKey)) +} +func HGetInt64(key string, HKey string) (int64, error) { + return redigo.Int64(Do("HGET", key, HKey)) +} +func HGetBool(key string, HKey string) (bool, error) { + return redigo.Bool(Do("HGET", key, HKey)) +} +func HDel(key string, HKey string) (interface{}, error) { + return Do("HDEL", key, HKey) +} + +func HGetAll(key string) (map[string]interface{}, error) { + vals, err := redigo.Values(Do("HGETALL", key)) + if err != nil { + return nil, err + } + num := len(vals) / 2 + result := make(map[string]interface{}, num) + for i := 0; i < num; i++ { + key, _ := redigo.String(vals[2*i], nil) + result[key] = vals[2*i+1] + } + return result, nil +} + +func FlushAll() bool { + res, _ := redigo.String(Do("FLUSHALL")) + if res == "" { + return false + } + return true +} + +// NOTE: Use this in production environment with extreme care. +// Read more here:https://redigo.io/commands/keys +func Keys(pattern string) ([]string, error) { + return redigo.Strings(Do("KEYS", pattern)) +} + +func HKeys(key string) ([]string, error) { + return redigo.Strings(Do("HKEYS", key)) +} + +func Exists(key string) bool { + count, err := redigo.Int(Do("EXISTS", key)) + if count == 0 || err != nil { + return false + } + return true +} + +func Incr(key string) (int64, error) { + return redigo.Int64(Do("INCR", key)) +} + +func Decr(key string) (int64, error) { + return redigo.Int64(Do("DECR", key)) +} + +func IncrBy(key string, incBy int64) (int64, error) { + return redigo.Int64(Do("INCRBY", key, incBy)) +} + +func DecrBy(key string, decrBy int64) (int64, error) { + return redigo.Int64(Do("DECRBY", key)) +} + +func IncrByFloat(key string, incBy float64) (float64, error) { + return redigo.Float64(Do("INCRBYFLOAT", key, incBy)) +} + +func DecrByFloat(key string, decrBy float64) (float64, error) { + return redigo.Float64(Do("DECRBYFLOAT", key, decrBy)) +} + +// use for message queue +func LPush(key string, data interface{}) (interface{}, error) { + // set + return Do("LPUSH", key, data) +} + +func LPop(key string) (interface{}, error) { + return Do("LPOP", key) +} + +func LPopString(key string) (string, error) { + return redigo.String(Do("LPOP", key)) +} +func LPopFloat(key string) (float64, error) { + f, err := redigo.Float64(Do("LPOP", key)) + return f, err +} +func LPopInt(key string) (int, error) { + return redigo.Int(Do("LPOP", key)) +} +func LPopInt64(key string) (int64, error) { + return redigo.Int64(Do("LPOP", key)) +} + +func RPush(key string, data interface{}) (interface{}, error) { + // set + return Do("RPUSH", key, data) +} + +func RPop(key string) (interface{}, error) { + return Do("RPOP", key) +} + +func RPopString(key string) (string, error) { + return redigo.String(Do("RPOP", key)) +} +func RPopFloat(key string) (float64, error) { + f, err := redigo.Float64(Do("RPOP", key)) + return f, err +} +func RPopInt(key string) (int, error) { + return redigo.Int(Do("RPOP", key)) +} +func RPopInt64(key string) (int64, error) { + return redigo.Int64(Do("RPOP", key)) +} + +func Scan(cursor int64, pattern string, count int64) (int64, []string, error) { + var items []string + var newCursor int64 + + values, err := redigo.Values(Do("SCAN", cursor, "MATCH", pattern, "COUNT", count)) + if err != nil { + return 0, nil, err + } + values, err = redigo.Scan(values, &newCursor, &items) + if err != nil { + return 0, nil, err + } + return newCursor, items, nil +} + +func LPushMax(key string, data ...interface{}) (interface{}, error) { + // set + return Do("LPUSH", key, data) +} + +func SelectDb(db int) (interface{}, error) { + return Do("SELECT", db) +} diff --git a/app/utils/cache/redis_cluster.go b/app/utils/cache/redis_cluster.go new file mode 100644 index 0000000..901f30c --- /dev/null +++ b/app/utils/cache/redis_cluster.go @@ -0,0 +1,622 @@ +package cache + +import ( + "strconv" + "time" + + "github.com/go-redis/redis" +) + +var pools *redis.ClusterClient + +func NewRedisCluster(addrs []string) error { + opt := &redis.ClusterOptions{ + Addrs: addrs, + PoolSize: redisPoolSize, + PoolTimeout: redisPoolTTL, + IdleTimeout: redisIdleTTL, + DialTimeout: redisDialTTL, + ReadTimeout: redisReadTTL, + WriteTimeout: redisWriteTTL, + } + pools = redis.NewClusterClient(opt) + if err := pools.Ping().Err(); err != nil { + return err + } + return nil +} + +func RCGet(key string) (interface{}, error) { + res, err := pools.Get(key).Result() + if err != nil { + return nil, convertError(err) + } + return []byte(res), nil +} +func RCSet(key string, value interface{}) error { + err := pools.Set(key, value, 0).Err() + return convertError(err) +} +func RCGetSet(key string, value interface{}) (interface{}, error) { + res, err := pools.GetSet(key, value).Result() + if err != nil { + return nil, convertError(err) + } + return []byte(res), nil +} +func RCSetNx(key string, value interface{}) (int64, error) { + res, err := pools.SetNX(key, value, 0).Result() + if err != nil { + return 0, convertError(err) + } + if res { + return 1, nil + } + return 0, nil +} +func RCSetEx(key string, value interface{}, timeout int64) error { + _, err := pools.Set(key, value, time.Duration(timeout)*time.Second).Result() + if err != nil { + return convertError(err) + } + return nil +} + +// nil表示成功,ErrNil表示数据库内已经存在这个key,其他表示数据库发生错误 +func RCSetNxEx(key string, value interface{}, timeout int64) error { + res, err := pools.SetNX(key, value, time.Duration(timeout)*time.Second).Result() + if err != nil { + return convertError(err) + } + if res { + return nil + } + return ErrNil +} +func RCMGet(keys ...string) ([]interface{}, error) { + res, err := pools.MGet(keys...).Result() + return res, convertError(err) +} + +// 为确保多个key映射到同一个slot,每个key最好加上hash tag,如:{test} +func RCMSet(kvs map[string]interface{}) error { + pairs := make([]string, 0, len(kvs)*2) + for k, v := range kvs { + val, err := String(v, nil) + if err != nil { + return err + } + pairs = append(pairs, k, val) + } + return convertError(pools.MSet(pairs).Err()) +} + +// 为确保多个key映射到同一个slot,每个key最好加上hash tag,如:{test} +func RCMSetNX(kvs map[string]interface{}) (bool, error) { + pairs := make([]string, 0, len(kvs)*2) + for k, v := range kvs { + val, err := String(v, nil) + if err != nil { + return false, err + } + pairs = append(pairs, k, val) + } + res, err := pools.MSetNX(pairs).Result() + return res, convertError(err) +} +func RCExpireAt(key string, timestamp int64) (int64, error) { + res, err := pools.ExpireAt(key, time.Unix(timestamp, 0)).Result() + if err != nil { + return 0, convertError(err) + } + if res { + return 1, nil + } + return 0, nil +} +func RCDel(keys ...string) (int64, error) { + args := make([]interface{}, 0, len(keys)) + for _, key := range keys { + args = append(args, key) + } + res, err := pools.Del(keys...).Result() + if err != nil { + return res, convertError(err) + } + return res, nil +} +func RCIncr(key string) (int64, error) { + res, err := pools.Incr(key).Result() + if err != nil { + return res, convertError(err) + } + return res, nil +} +func RCIncrBy(key string, delta int64) (int64, error) { + res, err := pools.IncrBy(key, delta).Result() + if err != nil { + return res, convertError(err) + } + return res, nil +} +func RCExpire(key string, duration int64) (int64, error) { + res, err := pools.Expire(key, time.Duration(duration)*time.Second).Result() + if err != nil { + return 0, convertError(err) + } + if res { + return 1, nil + } + return 0, nil +} +func RCExists(key string) (bool, error) { + res, err := pools.Exists(key).Result() + if err != nil { + return false, convertError(err) + } + if res > 0 { + return true, nil + } + return false, nil +} +func RCHGet(key string, field string) (interface{}, error) { + res, err := pools.HGet(key, field).Result() + if err != nil { + return nil, convertError(err) + } + return []byte(res), nil +} +func RCHLen(key string) (int64, error) { + res, err := pools.HLen(key).Result() + if err != nil { + return res, convertError(err) + } + return res, nil +} +func RCHSet(key string, field string, val interface{}) error { + value, err := String(val, nil) + if err != nil && err != ErrNil { + return err + } + _, err = pools.HSet(key, field, value).Result() + if err != nil { + return convertError(err) + } + return nil +} +func RCHDel(key string, fields ...string) (int64, error) { + args := make([]interface{}, 0, len(fields)+1) + args = append(args, key) + for _, field := range fields { + args = append(args, field) + } + res, err := pools.HDel(key, fields...).Result() + if err != nil { + return 0, convertError(err) + } + return res, nil +} + +func RCHMGet(key string, fields ...string) (interface{}, error) { + args := make([]interface{}, 0, len(fields)+1) + args = append(args, key) + for _, field := range fields { + args = append(args, field) + } + if len(fields) == 0 { + return nil, ErrNil + } + res, err := pools.HMGet(key, fields...).Result() + if err != nil { + return nil, convertError(err) + } + return res, nil +} +func RCHMSet(key string, kvs ...interface{}) error { + if len(kvs) == 0 { + return nil + } + if len(kvs)%2 != 0 { + return ErrWrongArgsNum + } + var err error + v := map[string]interface{}{} // todo change + v["field"], err = String(kvs[0], nil) + if err != nil && err != ErrNil { + return err + } + v["value"], err = String(kvs[1], nil) + if err != nil && err != ErrNil { + return err + } + pairs := make([]string, 0, len(kvs)-2) + if len(kvs) > 2 { + for _, kv := range kvs[2:] { + kvString, err := String(kv, nil) + if err != nil && err != ErrNil { + return err + } + pairs = append(pairs, kvString) + } + } + v["paris"] = pairs + _, err = pools.HMSet(key, v).Result() + if err != nil { + return convertError(err) + } + return nil +} + +func RCHKeys(key string) ([]string, error) { + res, err := pools.HKeys(key).Result() + if err != nil { + return res, convertError(err) + } + return res, nil +} +func RCHVals(key string) ([]interface{}, error) { + res, err := pools.HVals(key).Result() + if err != nil { + return nil, convertError(err) + } + rs := make([]interface{}, 0, len(res)) + for _, res := range res { + rs = append(rs, res) + } + return rs, nil +} +func RCHGetAll(key string) (map[string]string, error) { + vals, err := pools.HGetAll(key).Result() + if err != nil { + return nil, convertError(err) + } + return vals, nil +} +func RCHIncrBy(key, field string, delta int64) (int64, error) { + res, err := pools.HIncrBy(key, field, delta).Result() + if err != nil { + return res, convertError(err) + } + return res, nil +} +func RCZAdd(key string, kvs ...interface{}) (int64, error) { + args := make([]interface{}, 0, len(kvs)+1) + args = append(args, key) + args = append(args, kvs...) + if len(kvs) == 0 { + return 0, nil + } + if len(kvs)%2 != 0 { + return 0, ErrWrongArgsNum + } + zs := make([]redis.Z, len(kvs)/2) + for i := 0; i < len(kvs); i += 2 { + idx := i / 2 + score, err := Float64(kvs[i], nil) + if err != nil && err != ErrNil { + return 0, err + } + zs[idx].Score = score + zs[idx].Member = kvs[i+1] + } + res, err := pools.ZAdd(key, zs...).Result() + if err != nil { + return res, convertError(err) + } + return res, nil +} +func RCZRem(key string, members ...string) (int64, error) { + args := make([]interface{}, 0, len(members)) + args = append(args, key) + for _, member := range members { + args = append(args, member) + } + res, err := pools.ZRem(key, members).Result() + if err != nil { + return res, convertError(err) + } + return res, err +} + +func RCZRange(key string, min, max int64, withScores bool) (interface{}, error) { + res := make([]interface{}, 0) + if withScores { + zs, err := pools.ZRangeWithScores(key, min, max).Result() + if err != nil { + return nil, convertError(err) + } + for _, z := range zs { + res = append(res, z.Member, strconv.FormatFloat(z.Score, 'f', -1, 64)) + } + } else { + ms, err := pools.ZRange(key, min, max).Result() + if err != nil { + return nil, convertError(err) + } + for _, m := range ms { + res = append(res, m) + } + } + return res, nil +} +func RCZRangeByScoreWithScore(key string, min, max int64) (map[string]int64, error) { + opt := new(redis.ZRangeBy) + opt.Min = strconv.FormatInt(int64(min), 10) + opt.Max = strconv.FormatInt(int64(max), 10) + opt.Count = -1 + opt.Offset = 0 + vals, err := pools.ZRangeByScoreWithScores(key, *opt).Result() + if err != nil { + return nil, convertError(err) + } + res := make(map[string]int64, len(vals)) + for _, val := range vals { + key, err := String(val.Member, nil) + if err != nil && err != ErrNil { + return nil, err + } + res[key] = int64(val.Score) + } + return res, nil +} +func RCLRange(key string, start, stop int64) (interface{}, error) { + res, err := pools.LRange(key, start, stop).Result() + if err != nil { + return nil, convertError(err) + } + return res, nil +} +func RCLSet(key string, index int, value interface{}) error { + err := pools.LSet(key, int64(index), value).Err() + return convertError(err) +} +func RCLLen(key string) (int64, error) { + res, err := pools.LLen(key).Result() + if err != nil { + return res, convertError(err) + } + return res, nil +} +func RCLRem(key string, count int, value interface{}) (int, error) { + val, _ := value.(string) + res, err := pools.LRem(key, int64(count), val).Result() + if err != nil { + return int(res), convertError(err) + } + return int(res), nil +} +func RCTTl(key string) (int64, error) { + duration, err := pools.TTL(key).Result() + if err != nil { + return int64(duration.Seconds()), convertError(err) + } + return int64(duration.Seconds()), nil +} +func RCLPop(key string) (interface{}, error) { + res, err := pools.LPop(key).Result() + if err != nil { + return nil, convertError(err) + } + return res, nil +} +func RCRPop(key string) (interface{}, error) { + res, err := pools.RPop(key).Result() + if err != nil { + return nil, convertError(err) + } + return res, nil +} +func RCBLPop(key string, timeout int) (interface{}, error) { + res, err := pools.BLPop(time.Duration(timeout)*time.Second, key).Result() + if err != nil { + // 兼容redis 2.x + if err == redis.Nil { + return nil, ErrNil + } + return nil, err + } + return res[1], nil +} +func RCBRPop(key string, timeout int) (interface{}, error) { + res, err := pools.BRPop(time.Duration(timeout)*time.Second, key).Result() + if err != nil { + // 兼容redis 2.x + if err == redis.Nil { + return nil, ErrNil + } + return nil, convertError(err) + } + return res[1], nil +} +func RCLPush(key string, value ...interface{}) error { + args := make([]interface{}, 0, len(value)+1) + args = append(args, key) + args = append(args, value...) + vals := make([]string, 0, len(value)) + for _, v := range value { + val, err := String(v, nil) + if err != nil && err != ErrNil { + return err + } + vals = append(vals, val) + } + _, err := pools.LPush(key, vals).Result() // todo ... + if err != nil { + return convertError(err) + } + return nil +} +func RCRPush(key string, value ...interface{}) error { + args := make([]interface{}, 0, len(value)+1) + args = append(args, key) + args = append(args, value...) + vals := make([]string, 0, len(value)) + for _, v := range value { + val, err := String(v, nil) + if err != nil && err != ErrNil { + if err == ErrNil { + continue + } + return err + } + if val == "" { + continue + } + vals = append(vals, val) + } + _, err := pools.RPush(key, vals).Result() // todo ... + if err != nil { + return convertError(err) + } + return nil +} + +// 为确保srcKey跟destKey映射到同一个slot,srcKey和destKey需要加上hash tag,如:{test} +func RCBRPopLPush(srcKey string, destKey string, timeout int) (interface{}, error) { + res, err := pools.BRPopLPush(srcKey, destKey, time.Duration(timeout)*time.Second).Result() + if err != nil { + return nil, convertError(err) + } + return res, nil +} + +// 为确保srcKey跟destKey映射到同一个slot,srcKey和destKey需要加上hash tag,如:{test} +func RCRPopLPush(srcKey string, destKey string) (interface{}, error) { + res, err := pools.RPopLPush(srcKey, destKey).Result() + if err != nil { + return nil, convertError(err) + } + return res, nil +} +func RCSAdd(key string, members ...interface{}) (int64, error) { + args := make([]interface{}, 0, len(members)+1) + args = append(args, key) + args = append(args, members...) + ms := make([]string, 0, len(members)) + for _, member := range members { + m, err := String(member, nil) + if err != nil && err != ErrNil { + return 0, err + } + ms = append(ms, m) + } + res, err := pools.SAdd(key, ms).Result() // todo ... + if err != nil { + return res, convertError(err) + } + return res, nil +} +func RCSPop(key string) ([]byte, error) { + res, err := pools.SPop(key).Result() + if err != nil { + return nil, convertError(err) + } + return []byte(res), nil +} +func RCSIsMember(key string, member interface{}) (bool, error) { + m, _ := member.(string) + res, err := pools.SIsMember(key, m).Result() + if err != nil { + return res, convertError(err) + } + return res, nil +} +func RCSRem(key string, members ...interface{}) (int64, error) { + args := make([]interface{}, 0, len(members)+1) + args = append(args, key) + args = append(args, members...) + ms := make([]string, 0, len(members)) + for _, member := range members { + m, err := String(member, nil) + if err != nil && err != ErrNil { + return 0, err + } + ms = append(ms, m) + } + res, err := pools.SRem(key, ms).Result() // todo ... + if err != nil { + return res, convertError(err) + } + return res, nil +} +func RCSMembers(key string) ([]string, error) { + res, err := pools.SMembers(key).Result() + if err != nil { + return nil, convertError(err) + } + return res, nil +} +func RCScriptLoad(luaScript string) (interface{}, error) { + res, err := pools.ScriptLoad(luaScript).Result() + if err != nil { + return nil, convertError(err) + } + return res, nil +} +func RCEvalSha(sha1 string, numberKeys int, keysArgs ...interface{}) (interface{}, error) { + vals := make([]interface{}, 0, len(keysArgs)+2) + vals = append(vals, sha1, numberKeys) + vals = append(vals, keysArgs...) + keys := make([]string, 0, numberKeys) + args := make([]string, 0, len(keysArgs)-numberKeys) + for i, value := range keysArgs { + val, err := String(value, nil) + if err != nil && err != ErrNil { + return nil, err + } + if i < numberKeys { + keys = append(keys, val) + } else { + args = append(args, val) + } + } + res, err := pools.EvalSha(sha1, keys, args).Result() + if err != nil { + return nil, convertError(err) + } + return res, nil +} +func RCEval(luaScript string, numberKeys int, keysArgs ...interface{}) (interface{}, error) { + vals := make([]interface{}, 0, len(keysArgs)+2) + vals = append(vals, luaScript, numberKeys) + vals = append(vals, keysArgs...) + keys := make([]string, 0, numberKeys) + args := make([]string, 0, len(keysArgs)-numberKeys) + for i, value := range keysArgs { + val, err := String(value, nil) + if err != nil && err != ErrNil { + return nil, err + } + if i < numberKeys { + keys = append(keys, val) + } else { + args = append(args, val) + } + } + res, err := pools.Eval(luaScript, keys, args).Result() + if err != nil { + return nil, convertError(err) + } + return res, nil +} +func RCGetBit(key string, offset int64) (int64, error) { + res, err := pools.GetBit(key, offset).Result() + if err != nil { + return res, convertError(err) + } + return res, nil +} +func RCSetBit(key string, offset uint32, value int) (int, error) { + res, err := pools.SetBit(key, int64(offset), value).Result() + return int(res), convertError(err) +} +func RCGetClient() *redis.ClusterClient { + return pools +} +func convertError(err error) error { + if err == redis.Nil { + // 为了兼容redis 2.x,这里不返回 ErrNil,ErrNil在调用redis_cluster_reply函数时才返回 + return nil + } + return err +} diff --git a/app/utils/cache/redis_pool.go b/app/utils/cache/redis_pool.go new file mode 100644 index 0000000..ca38b3f --- /dev/null +++ b/app/utils/cache/redis_pool.go @@ -0,0 +1,324 @@ +package cache + +import ( + "errors" + "log" + "strings" + "time" + + redigo "github.com/gomodule/redigo/redis" +) + +type RedisPool struct { + *redigo.Pool +} + +func NewRedisPool(cfg *Config) *RedisPool { + return &RedisPool{&redigo.Pool{ + MaxIdle: cfg.MaxIdle, + IdleTimeout: cfg.IdleTimeout, + MaxActive: cfg.MaxActive, + Wait: cfg.Wait, + Dial: func() (redigo.Conn, error) { + c, err := redigo.Dial("tcp", cfg.Server) + if err != nil { + log.Println("Redis Dial failed: ", err) + return nil, err + } + if cfg.Password != "" { + if _, err := c.Do("AUTH", cfg.Password); err != nil { + c.Close() + log.Println("Redis AUTH failed: ", err) + return nil, err + } + } + return c, err + }, + TestOnBorrow: func(c redigo.Conn, t time.Time) error { + _, err := c.Do("PING") + if err != nil { + log.Println("Unable to ping to redis server:", err) + } + return err + }, + }} +} + +func (p *RedisPool) Do(cmd string, args ...interface{}) (reply interface{}, err error) { + conn := pool.Get() + defer conn.Close() + return conn.Do(cmd, args...) +} + +func (p *RedisPool) GetPool() *redigo.Pool { + return pool +} + +func (p *RedisPool) ParseKey(key string, vars []string) (string, error) { + arr := strings.Split(key, conf.KeyPlaceholder) + actualKey := "" + if len(arr) != len(vars)+1 { + return "", errors.New("redis/connection.go: Insufficient arguments to parse key") + } else { + for index, val := range arr { + if index == 0 { + actualKey = arr[index] + } else { + actualKey += vars[index-1] + val + } + } + } + return getPrefixedKey(actualKey), nil +} + +func (p *RedisPool) getPrefixedKey(key string) string { + return conf.KeyPrefix + conf.KeyDelimiter + key +} +func (p *RedisPool) StripEnvKey(key string) string { + return strings.TrimLeft(key, conf.KeyPrefix+conf.KeyDelimiter) +} +func (p *RedisPool) SplitKey(key string) []string { + return strings.Split(key, conf.KeyDelimiter) +} +func (p *RedisPool) Expire(key string, ttl int) (interface{}, error) { + return Do("EXPIRE", key, ttl) +} +func (p *RedisPool) Persist(key string) (interface{}, error) { + return Do("PERSIST", key) +} + +func (p *RedisPool) Del(key string) (interface{}, error) { + return Do("DEL", key) +} +func (p *RedisPool) Set(key string, data interface{}) (interface{}, error) { + // set + return Do("SET", key, data) +} +func (p *RedisPool) SetNX(key string, data interface{}) (interface{}, error) { + return Do("SETNX", key, data) +} +func (p *RedisPool) SetEx(key string, data interface{}, ttl int) (interface{}, error) { + return Do("SETEX", key, ttl, data) +} +func (p *RedisPool) Get(key string) (interface{}, error) { + // get + return Do("GET", key) +} +func (p *RedisPool) GetStringMap(key string) (map[string]string, error) { + // get + return redigo.StringMap(Do("GET", key)) +} + +func (p *RedisPool) GetTTL(key string) (time.Duration, error) { + ttl, err := redigo.Int64(Do("TTL", key)) + return time.Duration(ttl) * time.Second, err +} +func (p *RedisPool) GetBytes(key string) ([]byte, error) { + return redigo.Bytes(Do("GET", key)) +} +func (p *RedisPool) GetString(key string) (string, error) { + return redigo.String(Do("GET", key)) +} +func (p *RedisPool) GetInt(key string) (int, error) { + return redigo.Int(Do("GET", key)) +} +func (p *RedisPool) GetStringLength(key string) (int, error) { + return redigo.Int(Do("STRLEN", key)) +} +func (p *RedisPool) ZAdd(key string, score float64, data interface{}) (interface{}, error) { + return Do("ZADD", key, score, data) +} +func (p *RedisPool) ZRem(key string, data interface{}) (interface{}, error) { + return Do("ZREM", key, data) +} +func (p *RedisPool) ZRange(key string, start int, end int, withScores bool) ([]interface{}, error) { + if withScores { + return redigo.Values(Do("ZRANGE", key, start, end, "WITHSCORES")) + } + return redigo.Values(Do("ZRANGE", key, start, end)) +} +func (p *RedisPool) SAdd(setName string, data interface{}) (interface{}, error) { + return Do("SADD", setName, data) +} +func (p *RedisPool) SCard(setName string) (int64, error) { + return redigo.Int64(Do("SCARD", setName)) +} +func (p *RedisPool) SIsMember(setName string, data interface{}) (bool, error) { + return redigo.Bool(Do("SISMEMBER", setName, data)) +} +func (p *RedisPool) SMembers(setName string) ([]string, error) { + return redigo.Strings(Do("SMEMBERS", setName)) +} +func (p *RedisPool) SRem(setName string, data interface{}) (interface{}, error) { + return Do("SREM", setName, data) +} +func (p *RedisPool) HSet(key string, HKey string, data interface{}) (interface{}, error) { + return Do("HSET", key, HKey, data) +} + +func (p *RedisPool) HGet(key string, HKey string) (interface{}, error) { + return Do("HGET", key, HKey) +} + +func (p *RedisPool) HMGet(key string, hashKeys ...string) ([]interface{}, error) { + ret, err := Do("HMGET", key, hashKeys) + if err != nil { + return nil, err + } + reta, ok := ret.([]interface{}) + if !ok { + return nil, errors.New("result not an array") + } + return reta, nil +} + +func (p *RedisPool) HMSet(key string, hashKeys []string, vals []interface{}) (interface{}, error) { + if len(hashKeys) == 0 || len(hashKeys) != len(vals) { + var ret interface{} + return ret, errors.New("bad length") + } + input := []interface{}{key} + for i, v := range hashKeys { + input = append(input, v, vals[i]) + } + return Do("HMSET", input...) +} + +func (p *RedisPool) HGetString(key string, HKey string) (string, error) { + return redigo.String(Do("HGET", key, HKey)) +} +func (p *RedisPool) HGetFloat(key string, HKey string) (float64, error) { + f, err := redigo.Float64(Do("HGET", key, HKey)) + return float64(f), err +} +func (p *RedisPool) HGetInt(key string, HKey string) (int, error) { + return redigo.Int(Do("HGET", key, HKey)) +} +func (p *RedisPool) HGetInt64(key string, HKey string) (int64, error) { + return redigo.Int64(Do("HGET", key, HKey)) +} +func (p *RedisPool) HGetBool(key string, HKey string) (bool, error) { + return redigo.Bool(Do("HGET", key, HKey)) +} +func (p *RedisPool) HDel(key string, HKey string) (interface{}, error) { + return Do("HDEL", key, HKey) +} +func (p *RedisPool) HGetAll(key string) (map[string]interface{}, error) { + vals, err := redigo.Values(Do("HGETALL", key)) + if err != nil { + return nil, err + } + num := len(vals) / 2 + result := make(map[string]interface{}, num) + for i := 0; i < num; i++ { + key, _ := redigo.String(vals[2*i], nil) + result[key] = vals[2*i+1] + } + return result, nil +} + +// NOTE: Use this in production environment with extreme care. +// Read more here:https://redigo.io/commands/keys +func (p *RedisPool) Keys(pattern string) ([]string, error) { + return redigo.Strings(Do("KEYS", pattern)) +} + +func (p *RedisPool) HKeys(key string) ([]string, error) { + return redigo.Strings(Do("HKEYS", key)) +} + +func (p *RedisPool) Exists(key string) (bool, error) { + count, err := redigo.Int(Do("EXISTS", key)) + if count == 0 { + return false, err + } else { + return true, err + } +} + +func (p *RedisPool) Incr(key string) (int64, error) { + return redigo.Int64(Do("INCR", key)) +} + +func (p *RedisPool) Decr(key string) (int64, error) { + return redigo.Int64(Do("DECR", key)) +} + +func (p *RedisPool) IncrBy(key string, incBy int64) (int64, error) { + return redigo.Int64(Do("INCRBY", key, incBy)) +} + +func (p *RedisPool) DecrBy(key string, decrBy int64) (int64, error) { + return redigo.Int64(Do("DECRBY", key)) +} + +func (p *RedisPool) IncrByFloat(key string, incBy float64) (float64, error) { + return redigo.Float64(Do("INCRBYFLOAT", key, incBy)) +} + +func (p *RedisPool) DecrByFloat(key string, decrBy float64) (float64, error) { + return redigo.Float64(Do("DECRBYFLOAT", key, decrBy)) +} + +// use for message queue +func (p *RedisPool) LPush(key string, data interface{}) (interface{}, error) { + // set + return Do("LPUSH", key, data) +} + +func (p *RedisPool) LPop(key string) (interface{}, error) { + return Do("LPOP", key) +} + +func (p *RedisPool) LPopString(key string) (string, error) { + return redigo.String(Do("LPOP", key)) +} +func (p *RedisPool) LPopFloat(key string) (float64, error) { + f, err := redigo.Float64(Do("LPOP", key)) + return float64(f), err +} +func (p *RedisPool) LPopInt(key string) (int, error) { + return redigo.Int(Do("LPOP", key)) +} +func (p *RedisPool) LPopInt64(key string) (int64, error) { + return redigo.Int64(Do("LPOP", key)) +} + +func (p *RedisPool) RPush(key string, data interface{}) (interface{}, error) { + // set + return Do("RPUSH", key, data) +} + +func (p *RedisPool) RPop(key string) (interface{}, error) { + return Do("RPOP", key) +} + +func (p *RedisPool) RPopString(key string) (string, error) { + return redigo.String(Do("RPOP", key)) +} +func (p *RedisPool) RPopFloat(key string) (float64, error) { + f, err := redigo.Float64(Do("RPOP", key)) + return float64(f), err +} +func (p *RedisPool) RPopInt(key string) (int, error) { + return redigo.Int(Do("RPOP", key)) +} +func (p *RedisPool) RPopInt64(key string) (int64, error) { + return redigo.Int64(Do("RPOP", key)) +} + +func (p *RedisPool) Scan(cursor int64, pattern string, count int64) (int64, []string, error) { + var items []string + var newCursor int64 + + values, err := redigo.Values(Do("SCAN", cursor, "MATCH", pattern, "COUNT", count)) + if err != nil { + return 0, nil, err + } + values, err = redigo.Scan(values, &newCursor, &items) + if err != nil { + return 0, nil, err + } + + return newCursor, items, nil +} diff --git a/app/utils/cache/redis_pool_cluster.go b/app/utils/cache/redis_pool_cluster.go new file mode 100644 index 0000000..cd1911b --- /dev/null +++ b/app/utils/cache/redis_pool_cluster.go @@ -0,0 +1,617 @@ +package cache + +import ( + "strconv" + "time" + + "github.com/go-redis/redis" +) + +type RedisClusterPool struct { + client *redis.ClusterClient +} + +func NewRedisClusterPool(addrs []string) (*RedisClusterPool, error) { + opt := &redis.ClusterOptions{ + Addrs: addrs, + PoolSize: 512, + PoolTimeout: 10 * time.Second, + IdleTimeout: 10 * time.Second, + DialTimeout: 10 * time.Second, + ReadTimeout: 3 * time.Second, + WriteTimeout: 3 * time.Second, + } + c := redis.NewClusterClient(opt) + if err := c.Ping().Err(); err != nil { + return nil, err + } + return &RedisClusterPool{client: c}, nil +} + +func (p *RedisClusterPool) Get(key string) (interface{}, error) { + res, err := p.client.Get(key).Result() + if err != nil { + return nil, convertError(err) + } + return []byte(res), nil +} +func (p *RedisClusterPool) Set(key string, value interface{}) error { + err := p.client.Set(key, value, 0).Err() + return convertError(err) +} +func (p *RedisClusterPool) GetSet(key string, value interface{}) (interface{}, error) { + res, err := p.client.GetSet(key, value).Result() + if err != nil { + return nil, convertError(err) + } + return []byte(res), nil +} +func (p *RedisClusterPool) SetNx(key string, value interface{}) (int64, error) { + res, err := p.client.SetNX(key, value, 0).Result() + if err != nil { + return 0, convertError(err) + } + if res { + return 1, nil + } + return 0, nil +} +func (p *RedisClusterPool) SetEx(key string, value interface{}, timeout int64) error { + _, err := p.client.Set(key, value, time.Duration(timeout)*time.Second).Result() + if err != nil { + return convertError(err) + } + return nil +} + +// nil表示成功,ErrNil表示数据库内已经存在这个key,其他表示数据库发生错误 +func (p *RedisClusterPool) SetNxEx(key string, value interface{}, timeout int64) error { + res, err := p.client.SetNX(key, value, time.Duration(timeout)*time.Second).Result() + if err != nil { + return convertError(err) + } + if res { + return nil + } + return ErrNil +} +func (p *RedisClusterPool) MGet(keys ...string) ([]interface{}, error) { + res, err := p.client.MGet(keys...).Result() + return res, convertError(err) +} + +// 为确保多个key映射到同一个slot,每个key最好加上hash tag,如:{test} +func (p *RedisClusterPool) MSet(kvs map[string]interface{}) error { + pairs := make([]string, 0, len(kvs)*2) + for k, v := range kvs { + val, err := String(v, nil) + if err != nil { + return err + } + pairs = append(pairs, k, val) + } + return convertError(p.client.MSet(pairs).Err()) +} + +// 为确保多个key映射到同一个slot,每个key最好加上hash tag,如:{test} +func (p *RedisClusterPool) MSetNX(kvs map[string]interface{}) (bool, error) { + pairs := make([]string, 0, len(kvs)*2) + for k, v := range kvs { + val, err := String(v, nil) + if err != nil { + return false, err + } + pairs = append(pairs, k, val) + } + res, err := p.client.MSetNX(pairs).Result() + return res, convertError(err) +} +func (p *RedisClusterPool) ExpireAt(key string, timestamp int64) (int64, error) { + res, err := p.client.ExpireAt(key, time.Unix(timestamp, 0)).Result() + if err != nil { + return 0, convertError(err) + } + if res { + return 1, nil + } + return 0, nil +} +func (p *RedisClusterPool) Del(keys ...string) (int64, error) { + args := make([]interface{}, 0, len(keys)) + for _, key := range keys { + args = append(args, key) + } + res, err := p.client.Del(keys...).Result() + if err != nil { + return res, convertError(err) + } + return res, nil +} +func (p *RedisClusterPool) Incr(key string) (int64, error) { + res, err := p.client.Incr(key).Result() + if err != nil { + return res, convertError(err) + } + return res, nil +} +func (p *RedisClusterPool) IncrBy(key string, delta int64) (int64, error) { + res, err := p.client.IncrBy(key, delta).Result() + if err != nil { + return res, convertError(err) + } + return res, nil +} +func (p *RedisClusterPool) Expire(key string, duration int64) (int64, error) { + res, err := p.client.Expire(key, time.Duration(duration)*time.Second).Result() + if err != nil { + return 0, convertError(err) + } + if res { + return 1, nil + } + return 0, nil +} +func (p *RedisClusterPool) Exists(key string) (bool, error) { // todo (bool, error) + res, err := p.client.Exists(key).Result() + if err != nil { + return false, convertError(err) + } + if res > 0 { + return true, nil + } + return false, nil +} +func (p *RedisClusterPool) HGet(key string, field string) (interface{}, error) { + res, err := p.client.HGet(key, field).Result() + if err != nil { + return nil, convertError(err) + } + return []byte(res), nil +} +func (p *RedisClusterPool) HLen(key string) (int64, error) { + res, err := p.client.HLen(key).Result() + if err != nil { + return res, convertError(err) + } + return res, nil +} +func (p *RedisClusterPool) HSet(key string, field string, val interface{}) error { + value, err := String(val, nil) + if err != nil && err != ErrNil { + return err + } + _, err = p.client.HSet(key, field, value).Result() + if err != nil { + return convertError(err) + } + return nil +} +func (p *RedisClusterPool) HDel(key string, fields ...string) (int64, error) { + args := make([]interface{}, 0, len(fields)+1) + args = append(args, key) + for _, field := range fields { + args = append(args, field) + } + res, err := p.client.HDel(key, fields...).Result() + if err != nil { + return 0, convertError(err) + } + return res, nil +} + +func (p *RedisClusterPool) HMGet(key string, fields ...string) (interface{}, error) { + args := make([]interface{}, 0, len(fields)+1) + args = append(args, key) + for _, field := range fields { + args = append(args, field) + } + if len(fields) == 0 { + return nil, ErrNil + } + res, err := p.client.HMGet(key, fields...).Result() + if err != nil { + return nil, convertError(err) + } + return res, nil +} +func (p *RedisClusterPool) HMSet(key string, kvs ...interface{}) error { + if len(kvs) == 0 { + return nil + } + if len(kvs)%2 != 0 { + return ErrWrongArgsNum + } + var err error + v := map[string]interface{}{} // todo change + v["field"], err = String(kvs[0], nil) + if err != nil && err != ErrNil { + return err + } + v["value"], err = String(kvs[1], nil) + if err != nil && err != ErrNil { + return err + } + pairs := make([]string, 0, len(kvs)-2) + if len(kvs) > 2 { + for _, kv := range kvs[2:] { + kvString, err := String(kv, nil) + if err != nil && err != ErrNil { + return err + } + pairs = append(pairs, kvString) + } + } + v["paris"] = pairs + _, err = p.client.HMSet(key, v).Result() + if err != nil { + return convertError(err) + } + return nil +} + +func (p *RedisClusterPool) HKeys(key string) ([]string, error) { + res, err := p.client.HKeys(key).Result() + if err != nil { + return res, convertError(err) + } + return res, nil +} +func (p *RedisClusterPool) HVals(key string) ([]interface{}, error) { + res, err := p.client.HVals(key).Result() + if err != nil { + return nil, convertError(err) + } + rs := make([]interface{}, 0, len(res)) + for _, res := range res { + rs = append(rs, res) + } + return rs, nil +} +func (p *RedisClusterPool) HGetAll(key string) (map[string]string, error) { + vals, err := p.client.HGetAll(key).Result() + if err != nil { + return nil, convertError(err) + } + return vals, nil +} +func (p *RedisClusterPool) HIncrBy(key, field string, delta int64) (int64, error) { + res, err := p.client.HIncrBy(key, field, delta).Result() + if err != nil { + return res, convertError(err) + } + return res, nil +} +func (p *RedisClusterPool) ZAdd(key string, kvs ...interface{}) (int64, error) { + args := make([]interface{}, 0, len(kvs)+1) + args = append(args, key) + args = append(args, kvs...) + if len(kvs) == 0 { + return 0, nil + } + if len(kvs)%2 != 0 { + return 0, ErrWrongArgsNum + } + zs := make([]redis.Z, len(kvs)/2) + for i := 0; i < len(kvs); i += 2 { + idx := i / 2 + score, err := Float64(kvs[i], nil) + if err != nil && err != ErrNil { + return 0, err + } + zs[idx].Score = score + zs[idx].Member = kvs[i+1] + } + res, err := p.client.ZAdd(key, zs...).Result() + if err != nil { + return res, convertError(err) + } + return res, nil +} +func (p *RedisClusterPool) ZRem(key string, members ...string) (int64, error) { + args := make([]interface{}, 0, len(members)) + args = append(args, key) + for _, member := range members { + args = append(args, member) + } + res, err := p.client.ZRem(key, members).Result() + if err != nil { + return res, convertError(err) + } + return res, err +} + +func (p *RedisClusterPool) ZRange(key string, min, max int64, withScores bool) (interface{}, error) { + res := make([]interface{}, 0) + if withScores { + zs, err := p.client.ZRangeWithScores(key, min, max).Result() + if err != nil { + return nil, convertError(err) + } + for _, z := range zs { + res = append(res, z.Member, strconv.FormatFloat(z.Score, 'f', -1, 64)) + } + } else { + ms, err := p.client.ZRange(key, min, max).Result() + if err != nil { + return nil, convertError(err) + } + for _, m := range ms { + res = append(res, m) + } + } + return res, nil +} +func (p *RedisClusterPool) ZRangeByScoreWithScore(key string, min, max int64) (map[string]int64, error) { + opt := new(redis.ZRangeBy) + opt.Min = strconv.FormatInt(int64(min), 10) + opt.Max = strconv.FormatInt(int64(max), 10) + opt.Count = -1 + opt.Offset = 0 + vals, err := p.client.ZRangeByScoreWithScores(key, *opt).Result() + if err != nil { + return nil, convertError(err) + } + res := make(map[string]int64, len(vals)) + for _, val := range vals { + key, err := String(val.Member, nil) + if err != nil && err != ErrNil { + return nil, err + } + res[key] = int64(val.Score) + } + return res, nil +} +func (p *RedisClusterPool) LRange(key string, start, stop int64) (interface{}, error) { + res, err := p.client.LRange(key, start, stop).Result() + if err != nil { + return nil, convertError(err) + } + return res, nil +} +func (p *RedisClusterPool) LSet(key string, index int, value interface{}) error { + err := p.client.LSet(key, int64(index), value).Err() + return convertError(err) +} +func (p *RedisClusterPool) LLen(key string) (int64, error) { + res, err := p.client.LLen(key).Result() + if err != nil { + return res, convertError(err) + } + return res, nil +} +func (p *RedisClusterPool) LRem(key string, count int, value interface{}) (int, error) { + val, _ := value.(string) + res, err := p.client.LRem(key, int64(count), val).Result() + if err != nil { + return int(res), convertError(err) + } + return int(res), nil +} +func (p *RedisClusterPool) TTl(key string) (int64, error) { + duration, err := p.client.TTL(key).Result() + if err != nil { + return int64(duration.Seconds()), convertError(err) + } + return int64(duration.Seconds()), nil +} +func (p *RedisClusterPool) LPop(key string) (interface{}, error) { + res, err := p.client.LPop(key).Result() + if err != nil { + return nil, convertError(err) + } + return res, nil +} +func (p *RedisClusterPool) RPop(key string) (interface{}, error) { + res, err := p.client.RPop(key).Result() + if err != nil { + return nil, convertError(err) + } + return res, nil +} +func (p *RedisClusterPool) BLPop(key string, timeout int) (interface{}, error) { + res, err := p.client.BLPop(time.Duration(timeout)*time.Second, key).Result() + if err != nil { + // 兼容redis 2.x + if err == redis.Nil { + return nil, ErrNil + } + return nil, err + } + return res[1], nil +} +func (p *RedisClusterPool) BRPop(key string, timeout int) (interface{}, error) { + res, err := p.client.BRPop(time.Duration(timeout)*time.Second, key).Result() + if err != nil { + // 兼容redis 2.x + if err == redis.Nil { + return nil, ErrNil + } + return nil, convertError(err) + } + return res[1], nil +} +func (p *RedisClusterPool) LPush(key string, value ...interface{}) error { + args := make([]interface{}, 0, len(value)+1) + args = append(args, key) + args = append(args, value...) + vals := make([]string, 0, len(value)) + for _, v := range value { + val, err := String(v, nil) + if err != nil && err != ErrNil { + return err + } + vals = append(vals, val) + } + _, err := p.client.LPush(key, vals).Result() // todo ... + if err != nil { + return convertError(err) + } + return nil +} +func (p *RedisClusterPool) RPush(key string, value ...interface{}) error { + args := make([]interface{}, 0, len(value)+1) + args = append(args, key) + args = append(args, value...) + vals := make([]string, 0, len(value)) + for _, v := range value { + val, err := String(v, nil) + if err != nil && err != ErrNil { + if err == ErrNil { + continue + } + return err + } + if val == "" { + continue + } + vals = append(vals, val) + } + _, err := p.client.RPush(key, vals).Result() // todo ... + if err != nil { + return convertError(err) + } + return nil +} + +// 为确保srcKey跟destKey映射到同一个slot,srcKey和destKey需要加上hash tag,如:{test} +func (p *RedisClusterPool) BRPopLPush(srcKey string, destKey string, timeout int) (interface{}, error) { + res, err := p.client.BRPopLPush(srcKey, destKey, time.Duration(timeout)*time.Second).Result() + if err != nil { + return nil, convertError(err) + } + return res, nil +} + +// 为确保srcKey跟destKey映射到同一个slot,srcKey和destKey需要加上hash tag,如:{test} +func (p *RedisClusterPool) RPopLPush(srcKey string, destKey string) (interface{}, error) { + res, err := p.client.RPopLPush(srcKey, destKey).Result() + if err != nil { + return nil, convertError(err) + } + return res, nil +} +func (p *RedisClusterPool) SAdd(key string, members ...interface{}) (int64, error) { + args := make([]interface{}, 0, len(members)+1) + args = append(args, key) + args = append(args, members...) + ms := make([]string, 0, len(members)) + for _, member := range members { + m, err := String(member, nil) + if err != nil && err != ErrNil { + return 0, err + } + ms = append(ms, m) + } + res, err := p.client.SAdd(key, ms).Result() // todo ... + if err != nil { + return res, convertError(err) + } + return res, nil +} +func (p *RedisClusterPool) SPop(key string) ([]byte, error) { + res, err := p.client.SPop(key).Result() + if err != nil { + return nil, convertError(err) + } + return []byte(res), nil +} +func (p *RedisClusterPool) SIsMember(key string, member interface{}) (bool, error) { + m, _ := member.(string) + res, err := p.client.SIsMember(key, m).Result() + if err != nil { + return res, convertError(err) + } + return res, nil +} +func (p *RedisClusterPool) SRem(key string, members ...interface{}) (int64, error) { + args := make([]interface{}, 0, len(members)+1) + args = append(args, key) + args = append(args, members...) + ms := make([]string, 0, len(members)) + for _, member := range members { + m, err := String(member, nil) + if err != nil && err != ErrNil { + return 0, err + } + ms = append(ms, m) + } + res, err := p.client.SRem(key, ms).Result() // todo ... + if err != nil { + return res, convertError(err) + } + return res, nil +} +func (p *RedisClusterPool) SMembers(key string) ([]string, error) { + res, err := p.client.SMembers(key).Result() + if err != nil { + return nil, convertError(err) + } + return res, nil +} +func (p *RedisClusterPool) ScriptLoad(luaScript string) (interface{}, error) { + res, err := p.client.ScriptLoad(luaScript).Result() + if err != nil { + return nil, convertError(err) + } + return res, nil +} +func (p *RedisClusterPool) EvalSha(sha1 string, numberKeys int, keysArgs ...interface{}) (interface{}, error) { + vals := make([]interface{}, 0, len(keysArgs)+2) + vals = append(vals, sha1, numberKeys) + vals = append(vals, keysArgs...) + keys := make([]string, 0, numberKeys) + args := make([]string, 0, len(keysArgs)-numberKeys) + for i, value := range keysArgs { + val, err := String(value, nil) + if err != nil && err != ErrNil { + return nil, err + } + if i < numberKeys { + keys = append(keys, val) + } else { + args = append(args, val) + } + } + res, err := p.client.EvalSha(sha1, keys, args).Result() + if err != nil { + return nil, convertError(err) + } + return res, nil +} +func (p *RedisClusterPool) Eval(luaScript string, numberKeys int, keysArgs ...interface{}) (interface{}, error) { + vals := make([]interface{}, 0, len(keysArgs)+2) + vals = append(vals, luaScript, numberKeys) + vals = append(vals, keysArgs...) + keys := make([]string, 0, numberKeys) + args := make([]string, 0, len(keysArgs)-numberKeys) + for i, value := range keysArgs { + val, err := String(value, nil) + if err != nil && err != ErrNil { + return nil, err + } + if i < numberKeys { + keys = append(keys, val) + } else { + args = append(args, val) + } + } + res, err := p.client.Eval(luaScript, keys, args).Result() + if err != nil { + return nil, convertError(err) + } + return res, nil +} +func (p *RedisClusterPool) GetBit(key string, offset int64) (int64, error) { + res, err := p.client.GetBit(key, offset).Result() + if err != nil { + return res, convertError(err) + } + return res, nil +} +func (p *RedisClusterPool) SetBit(key string, offset uint32, value int) (int, error) { + res, err := p.client.SetBit(key, int64(offset), value).Result() + return int(res), convertError(err) +} +func (p *RedisClusterPool) GetClient() *redis.ClusterClient { + return pools +} diff --git a/app/utils/convert.go b/app/utils/convert.go new file mode 100644 index 0000000..a638d37 --- /dev/null +++ b/app/utils/convert.go @@ -0,0 +1,322 @@ +package utils + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "math" + "strconv" + "strings" +) + +func ToString(raw interface{}, e error) (res string) { + if e != nil { + return "" + } + return AnyToString(raw) +} + +func ToInt64(raw interface{}, e error) int64 { + if e != nil { + return 0 + } + return AnyToInt64(raw) +} + +func AnyToBool(raw interface{}) bool { + switch i := raw.(type) { + case float32, float64, int, int64, uint, uint8, uint16, uint32, uint64, int8, int16, int32: + return i != 0 + case []byte: + return i != nil + case string: + if i == "false" { + return false + } + return i != "" + case error: + return false + case nil: + return true + } + val := fmt.Sprint(raw) + val = strings.TrimLeft(val, "&") + if strings.TrimLeft(val, "{}") == "" { + return false + } + if strings.TrimLeft(val, "[]") == "" { + return false + } + // ptr type + b, err := json.Marshal(raw) + if err != nil { + return false + } + if strings.TrimLeft(string(b), "\"\"") == "" { + return false + } + if strings.TrimLeft(string(b), "{}") == "" { + return false + } + return true +} + +func AnyToInt64(raw interface{}) int64 { + switch i := raw.(type) { + case string: + res, _ := strconv.ParseInt(i, 10, 64) + return res + case []byte: + return BytesToInt64(i) + case int: + return int64(i) + case int64: + return i + case uint: + return int64(i) + case uint8: + return int64(i) + case uint16: + return int64(i) + case uint32: + return int64(i) + case uint64: + return int64(i) + case int8: + return int64(i) + case int16: + return int64(i) + case int32: + return int64(i) + case float32: + return int64(i) + case float64: + return int64(i) + case error: + return 0 + case bool: + if i { + return 1 + } + return 0 + } + return 0 +} + +func AnyToString(raw interface{}) string { + switch i := raw.(type) { + case []byte: + return string(i) + case int: + return strconv.FormatInt(int64(i), 10) + case int64: + return strconv.FormatInt(i, 10) + case float32: + return Float64ToStr(float64(i)) + case float64: + return Float64ToStr(i) + case uint: + return strconv.FormatInt(int64(i), 10) + case uint8: + return strconv.FormatInt(int64(i), 10) + case uint16: + return strconv.FormatInt(int64(i), 10) + case uint32: + return strconv.FormatInt(int64(i), 10) + case uint64: + return strconv.FormatInt(int64(i), 10) + case int8: + return strconv.FormatInt(int64(i), 10) + case int16: + return strconv.FormatInt(int64(i), 10) + case int32: + return strconv.FormatInt(int64(i), 10) + case string: + return i + case error: + return i.Error() + case bool: + return strconv.FormatBool(i) + } + return fmt.Sprintf("%#v", raw) +} + +func AnyToFloat64(raw interface{}) float64 { + switch i := raw.(type) { + case []byte: + f, _ := strconv.ParseFloat(string(i), 64) + return f + case int: + return float64(i) + case int64: + return float64(i) + case float32: + return float64(i) + case float64: + return i + case uint: + return float64(i) + case uint8: + return float64(i) + case uint16: + return float64(i) + case uint32: + return float64(i) + case uint64: + return float64(i) + case int8: + return float64(i) + case int16: + return float64(i) + case int32: + return float64(i) + case string: + f, _ := strconv.ParseFloat(i, 64) + return f + case bool: + if i { + return 1 + } + } + return 0 +} + +func ToByte(raw interface{}, e error) []byte { + if e != nil { + return []byte{} + } + switch i := raw.(type) { + case string: + return []byte(i) + case int: + return Int64ToBytes(int64(i)) + case int64: + return Int64ToBytes(i) + case float32: + return Float32ToByte(i) + case float64: + return Float64ToByte(i) + case uint: + return Int64ToBytes(int64(i)) + case uint8: + return Int64ToBytes(int64(i)) + case uint16: + return Int64ToBytes(int64(i)) + case uint32: + return Int64ToBytes(int64(i)) + case uint64: + return Int64ToBytes(int64(i)) + case int8: + return Int64ToBytes(int64(i)) + case int16: + return Int64ToBytes(int64(i)) + case int32: + return Int64ToBytes(int64(i)) + case []byte: + return i + case error: + return []byte(i.Error()) + case bool: + if i { + return []byte("true") + } + return []byte("false") + } + return []byte(fmt.Sprintf("%#v", raw)) +} + +func Int64ToBytes(i int64) []byte { + var buf = make([]byte, 8) + binary.BigEndian.PutUint64(buf, uint64(i)) + return buf +} + +func BytesToInt64(buf []byte) int64 { + return int64(binary.BigEndian.Uint64(buf)) +} + +func StrToInt(s string) int { + res, _ := strconv.Atoi(s) + return res +} + +func StrToInt64(s string) int64 { + res, _ := strconv.ParseInt(s, 10, 64) + return res +} + +func Float32ToByte(float float32) []byte { + bits := math.Float32bits(float) + bytes := make([]byte, 4) + binary.LittleEndian.PutUint32(bytes, bits) + + return bytes +} + +func ByteToFloat32(bytes []byte) float32 { + bits := binary.LittleEndian.Uint32(bytes) + return math.Float32frombits(bits) +} + +func Float64ToByte(float float64) []byte { + bits := math.Float64bits(float) + bytes := make([]byte, 8) + binary.LittleEndian.PutUint64(bytes, bits) + return bytes +} + +func ByteToFloat64(bytes []byte) float64 { + bits := binary.LittleEndian.Uint64(bytes) + return math.Float64frombits(bits) +} + +func Float64ToStr(f float64) string { + return strconv.FormatFloat(f, 'f', 2, 64) +} +func Float64ToStrPrec1(f float64) string { + return strconv.FormatFloat(f, 'f', 1, 64) +} + +func Float32ToStr(f float32) string { + return Float64ToStr(float64(f)) +} + +func StrToFloat64(s string) float64 { + res, err := strconv.ParseFloat(s, 64) + if err != nil { + return 0 + } + return res +} + +func StrToFloat32(s string) float32 { + res, err := strconv.ParseFloat(s, 32) + if err != nil { + return 0 + } + return float32(res) +} + +func StrToBool(s string) bool { + b, _ := strconv.ParseBool(s) + return b +} + +func BoolToStr(b bool) string { + if b { + return "true" + } + return "false" +} + +func FloatToInt64(f float64) int64 { + return int64(f) +} + +func IntToStr(i int) string { + return strconv.Itoa(i) +} + +func Int64ToStr(i int64) string { + return strconv.FormatInt(i, 10) +} diff --git a/app/utils/crypto.go b/app/utils/crypto.go new file mode 100644 index 0000000..56289c5 --- /dev/null +++ b/app/utils/crypto.go @@ -0,0 +1,19 @@ +package utils + +import ( + "crypto/md5" + "encoding/base64" + "fmt" +) + +func GetMd5(raw []byte) string { + h := md5.New() + h.Write(raw) + return fmt.Sprintf("%x", h.Sum(nil)) +} + +func GetBase64Md5(raw []byte) string { + h := md5.New() + h.Write(raw) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} diff --git a/app/utils/curl.go b/app/utils/curl.go new file mode 100644 index 0000000..0a45607 --- /dev/null +++ b/app/utils/curl.go @@ -0,0 +1,209 @@ +package utils + +import ( + "bytes" + "crypto/tls" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "sort" + "strings" + "time" +) + +var CurlDebug bool + +func CurlGet(router string, header map[string]string) ([]byte, error) { + return curl(http.MethodGet, router, nil, header) +} +func CurlGetJson(router string, body interface{}, header map[string]string) ([]byte, error) { + return curl_new(http.MethodGet, router, body, header) +} + +// 只支持form 与json 提交, 请留意body的类型, 支持string, []byte, map[string]string +func CurlPost(router string, body interface{}, header map[string]string) ([]byte, error) { + return curl(http.MethodPost, router, body, header) +} + +func CurlPut(router string, body interface{}, header map[string]string) ([]byte, error) { + return curl(http.MethodPut, router, body, header) +} + +// 只支持form 与json 提交, 请留意body的类型, 支持string, []byte, map[string]string +func CurlPatch(router string, body interface{}, header map[string]string) ([]byte, error) { + return curl(http.MethodPatch, router, body, header) +} + +// CurlDelete is curl delete +func CurlDelete(router string, body interface{}, header map[string]string) ([]byte, error) { + return curl(http.MethodDelete, router, body, header) +} + +func curl(method, router string, body interface{}, header map[string]string) ([]byte, error) { + var reqBody io.Reader + contentType := "application/json" + switch v := body.(type) { + case string: + reqBody = strings.NewReader(v) + case []byte: + reqBody = bytes.NewReader(v) + case map[string]string: + val := url.Values{} + for k, v := range v { + val.Set(k, v) + } + reqBody = strings.NewReader(val.Encode()) + contentType = "application/x-www-form-urlencoded" + case map[string]interface{}: + val := url.Values{} + for k, v := range v { + val.Set(k, v.(string)) + } + reqBody = strings.NewReader(val.Encode()) + contentType = "application/x-www-form-urlencoded" + } + if header == nil { + header = map[string]string{"Content-Type": contentType} + } + if _, ok := header["Content-Type"]; !ok { + header["Content-Type"] = contentType + } + resp, er := CurlReq(method, router, reqBody, header) + if er != nil { + return nil, er + } + res, err := ioutil.ReadAll(resp.Body) + if CurlDebug { + blob := SerializeStr(body) + if contentType != "application/json" { + blob = HttpBuild(body) + } + fmt.Printf("\n\n=====================\n[url]: %s\n[time]: %s\n[method]: %s\n[content-type]: %v\n[req_header]: %s\n[req_body]: %#v\n[resp_err]: %v\n[resp_header]: %v\n[resp_body]: %v\n=====================\n\n", + router, + time.Now().Format("2006-01-02 15:04:05.000"), + method, + contentType, + HttpBuildQuery(header), + blob, + err, + SerializeStr(resp.Header), + string(res), + ) + } + resp.Body.Close() + return res, err +} + +func curl_new(method, router string, body interface{}, header map[string]string) ([]byte, error) { + var reqBody io.Reader + contentType := "application/json" + + if header == nil { + header = map[string]string{"Content-Type": contentType} + } + if _, ok := header["Content-Type"]; !ok { + header["Content-Type"] = contentType + } + resp, er := CurlReq(method, router, reqBody, header) + if er != nil { + return nil, er + } + res, err := ioutil.ReadAll(resp.Body) + if CurlDebug { + blob := SerializeStr(body) + if contentType != "application/json" { + blob = HttpBuild(body) + } + fmt.Printf("\n\n=====================\n[url]: %s\n[time]: %s\n[method]: %s\n[content-type]: %v\n[req_header]: %s\n[req_body]: %#v\n[resp_err]: %v\n[resp_header]: %v\n[resp_body]: %v\n=====================\n\n", + router, + time.Now().Format("2006-01-02 15:04:05.000"), + method, + contentType, + HttpBuildQuery(header), + blob, + err, + SerializeStr(resp.Header), + string(res), + ) + } + resp.Body.Close() + return res, err +} + +func CurlReq(method, router string, reqBody io.Reader, header map[string]string) (*http.Response, error) { + req, _ := http.NewRequest(method, router, reqBody) + if header != nil { + for k, v := range header { + req.Header.Set(k, v) + } + } + // 绕过github等可能因为特征码返回503问题 + // https://www.imwzk.com/posts/2021-03-14-why-i-always-get-503-with-golang/ + defaultCipherSuites := []uint16{0xc02f, 0xc030, 0xc02b, 0xc02c, 0xcca8, 0xcca9, 0xc013, 0xc009, + 0xc014, 0xc00a, 0x009c, 0x009d, 0x002f, 0x0035, 0xc012, 0x000a} + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + CipherSuites: append(defaultCipherSuites[8:], defaultCipherSuites[:8]...), + }, + }, + // 获取301重定向 + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + return client.Do(req) +} + +// 组建get请求参数,sortAsc true为小到大,false为大到小,nil不排序 a=123&b=321 +func HttpBuildQuery(args map[string]string, sortAsc ...bool) string { + str := "" + if len(args) == 0 { + return str + } + if len(sortAsc) > 0 { + keys := make([]string, 0, len(args)) + for k := range args { + keys = append(keys, k) + } + if sortAsc[0] { + sort.Strings(keys) + } else { + sort.Sort(sort.Reverse(sort.StringSlice(keys))) + } + for _, k := range keys { + str += "&" + k + "=" + args[k] + } + } else { + for k, v := range args { + str += "&" + k + "=" + v + } + } + return str[1:] +} + +func HttpBuild(body interface{}, sortAsc ...bool) string { + params := map[string]string{} + if args, ok := body.(map[string]interface{}); ok { + for k, v := range args { + params[k] = AnyToString(v) + } + return HttpBuildQuery(params, sortAsc...) + } + if args, ok := body.(map[string]string); ok { + for k, v := range args { + params[k] = AnyToString(v) + } + return HttpBuildQuery(params, sortAsc...) + } + if args, ok := body.(map[string]int); ok { + for k, v := range args { + params[k] = AnyToString(v) + } + return HttpBuildQuery(params, sortAsc...) + } + return AnyToString(body) +} diff --git a/app/utils/debug.go b/app/utils/debug.go new file mode 100644 index 0000000..bb2e9d3 --- /dev/null +++ b/app/utils/debug.go @@ -0,0 +1,25 @@ +package utils + +import ( + "fmt" + "os" + "strconv" + "time" +) + +func Debug(args ...interface{}) { + s := "" + l := len(args) + if l < 1 { + fmt.Println("please input some data") + os.Exit(0) + } + i := 1 + for _, v := range args { + s += fmt.Sprintf("【"+strconv.Itoa(i)+"】: %#v\n", v) + i++ + } + s = "******************** 【DEBUG - " + time.Now().Format("2006-01-02 15:04:05") + "】 ********************\n" + s + "******************** 【DEBUG - END】 ********************\n" + fmt.Println(s) + os.Exit(0) +} diff --git a/app/utils/duplicate.go b/app/utils/duplicate.go new file mode 100644 index 0000000..17cea88 --- /dev/null +++ b/app/utils/duplicate.go @@ -0,0 +1,37 @@ +package utils + +func RemoveDuplicateString(elms []string) []string { + res := make([]string, 0, len(elms)) + temp := map[string]struct{}{} + for _, item := range elms { + if _, ok := temp[item]; !ok { + temp[item] = struct{}{} + res = append(res, item) + } + } + return res +} + +func RemoveDuplicateInt(elms []int) []int { + res := make([]int, 0, len(elms)) + temp := map[int]struct{}{} + for _, item := range elms { + if _, ok := temp[item]; !ok { + temp[item] = struct{}{} + res = append(res, item) + } + } + return res +} + +func RemoveDuplicateInt64(elms []int64) []int64 { + res := make([]int64, 0, len(elms)) + temp := map[int64]struct{}{} + for _, item := range elms { + if _, ok := temp[item]; !ok { + temp[item] = struct{}{} + res = append(res, item) + } + } + return res +} diff --git a/app/utils/file.go b/app/utils/file.go new file mode 100644 index 0000000..70a2927 --- /dev/null +++ b/app/utils/file.go @@ -0,0 +1,11 @@ +package utils + +import ( + "path" + "strings" +) + +// 获取文件后缀 +func FileExt(fname string) string { + return strings.ToLower(strings.TrimLeft(path.Ext(fname), ".")) +} diff --git a/app/utils/file_and_dir.go b/app/utils/file_and_dir.go new file mode 100644 index 0000000..93141f9 --- /dev/null +++ b/app/utils/file_and_dir.go @@ -0,0 +1,29 @@ +package utils + +import "os" + +// 判断所给路径文件、文件夹是否存在 +func Exists(path string) bool { + _, err := os.Stat(path) //os.Stat获取文件信息 + if err != nil { + if os.IsExist(err) { + return true + } + return false + } + return true +} + +// 判断所给路径是否为文件夹 +func IsDir(path string) bool { + s, err := os.Stat(path) + if err != nil { + return false + } + return s.IsDir() +} + +// 判断所给路径是否为文件 +func IsFile(path string) bool { + return !IsDir(path) +} diff --git a/app/utils/format.go b/app/utils/format.go new file mode 100644 index 0000000..997fe80 --- /dev/null +++ b/app/utils/format.go @@ -0,0 +1,59 @@ +package utils + +import ( + "math" +) + +func CouponFormat(data string) string { + switch data { + case "0.00", "0", "": + return "" + default: + return Int64ToStr(FloatToInt64(StrToFloat64(data))) + } +} +func CommissionFormat(data string) string { + if StrToFloat64(data) > 0 { + return data + } + + return "" +} + +func HideString(src string, hLen int) string { + str := []rune(src) + if hLen == 0 { + hLen = 4 + } + hideStr := "" + for i := 0; i < hLen; i++ { + hideStr += "*" + } + hideLen := len(str) / 2 + showLen := len(str) - hideLen + if hideLen == 0 || showLen == 0 { + return hideStr + } + subLen := showLen / 2 + if subLen == 0 { + return string(str[:showLen]) + hideStr + } + s := string(str[:subLen]) + s += hideStr + s += string(str[len(str)-subLen:]) + return s +} + +//SaleCountFormat is 格式化销量 +func SaleCountFormat(s string) string { + return s + "已售" +} + +// 小数格式化 +func FloatFormat(f float64, i int) float64 { + if i > 14 { + return f + } + p := math.Pow10(i) + return float64(int64((f+0.000000000000009)*p)) / p +} diff --git a/app/utils/ip.go b/app/utils/ip.go new file mode 100644 index 0000000..6ed8286 --- /dev/null +++ b/app/utils/ip.go @@ -0,0 +1,146 @@ +package utils + +import ( + "errors" + "math" + "net" + "net/http" + "strings" +) + +func GetIP(r *http.Request) string { + ip := ClientPublicIP(r) + if ip == "" { + ip = ClientIP(r) + } + if ip == "" { + ip = "0000" + } + return ip +} + +// HasLocalIPddr 检测 IP 地址字符串是否是内网地址 +// Deprecated: 此为一个错误名称错误拼写的函数,计划在将来移除,请使用 HasLocalIPAddr 函数 +func HasLocalIPddr(ip string) bool { + return HasLocalIPAddr(ip) +} + +// HasLocalIPAddr 检测 IP 地址字符串是否是内网地址 +func HasLocalIPAddr(ip string) bool { + return HasLocalIP(net.ParseIP(ip)) +} + +// HasLocalIP 检测 IP 地址是否是内网地址 +// 通过直接对比ip段范围效率更高,详见:https://github.com/thinkeridea/go-extend/issues/2 +func HasLocalIP(ip net.IP) bool { + if ip.IsLoopback() { + return true + } + + ip4 := ip.To4() + if ip4 == nil { + return false + } + + return ip4[0] == 10 || // 10.0.0.0/8 + (ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31) || // 172.16.0.0/12 + (ip4[0] == 169 && ip4[1] == 254) || // 169.254.0.0/16 + (ip4[0] == 192 && ip4[1] == 168) // 192.168.0.0/16 +} + +// ClientIP 尽最大努力实现获取客户端 IP 的算法。 +// 解析 X-Real-IP 和 X-Forwarded-For 以便于反向代理(nginx 或 haproxy)可以正常工作。 +func ClientIP(r *http.Request) string { + ip := strings.TrimSpace(strings.Split(r.Header.Get("X-Forwarded-For"), ",")[0]) + if ip != "" { + return ip + } + + ip = strings.TrimSpace(r.Header.Get("X-Real-Ip")) + if ip != "" { + return ip + } + + if ip, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)); err == nil { + return ip + } + + return "" +} + +// ClientPublicIP 尽最大努力实现获取客户端公网 IP 的算法。 +// 解析 X-Real-IP 和 X-Forwarded-For 以便于反向代理(nginx 或 haproxy)可以正常工作。 +func ClientPublicIP(r *http.Request) string { + var ip string + for _, ip = range strings.Split(r.Header.Get("X-Forwarded-For"), ",") { + if ip = strings.TrimSpace(ip); ip != "" && !HasLocalIPAddr(ip) { + return ip + } + } + + if ip = strings.TrimSpace(r.Header.Get("X-Real-Ip")); ip != "" && !HasLocalIPAddr(ip) { + return ip + } + + if ip = RemoteIP(r); !HasLocalIPAddr(ip) { + return ip + } + + return "" +} + +// RemoteIP 通过 RemoteAddr 获取 IP 地址, 只是一个快速解析方法。 +func RemoteIP(r *http.Request) string { + ip, _, _ := net.SplitHostPort(r.RemoteAddr) + return ip +} + +// IPString2Long 把ip字符串转为数值 +func IPString2Long(ip string) (uint, error) { + b := net.ParseIP(ip).To4() + if b == nil { + return 0, errors.New("invalid ipv4 format") + } + + return uint(b[3]) | uint(b[2])<<8 | uint(b[1])<<16 | uint(b[0])<<24, nil +} + +// Long2IPString 把数值转为ip字符串 +func Long2IPString(i uint) (string, error) { + if i > math.MaxUint32 { + return "", errors.New("beyond the scope of ipv4") + } + + ip := make(net.IP, net.IPv4len) + ip[0] = byte(i >> 24) + ip[1] = byte(i >> 16) + ip[2] = byte(i >> 8) + ip[3] = byte(i) + + return ip.String(), nil +} + +// IP2Long 把net.IP转为数值 +func IP2Long(ip net.IP) (uint, error) { + b := ip.To4() + if b == nil { + return 0, errors.New("invalid ipv4 format") + } + + return uint(b[3]) | uint(b[2])<<8 | uint(b[1])<<16 | uint(b[0])<<24, nil +} + +// Long2IP 把数值转为net.IP +func Long2IP(i uint) (net.IP, error) { + if i > math.MaxUint32 { + return nil, errors.New("beyond the scope of ipv4") + } + + ip := make(net.IP, net.IPv4len) + ip[0] = byte(i >> 24) + ip[1] = byte(i >> 16) + ip[2] = byte(i >> 8) + ip[3] = byte(i) + + return ip, nil +} diff --git a/app/utils/json.go b/app/utils/json.go new file mode 100644 index 0000000..998bcec --- /dev/null +++ b/app/utils/json.go @@ -0,0 +1,17 @@ +package utils + +import ( + "bytes" + "encoding/json" +) + +func JsonMarshal(interface{}) { + +} + +// 不科学计数法 +func JsonDecode(data []byte, v interface{}) error { + d := json.NewDecoder(bytes.NewReader(data)) + d.UseNumber() + return d.Decode(v) +} diff --git a/app/utils/logx/log.go b/app/utils/logx/log.go new file mode 100644 index 0000000..ca11223 --- /dev/null +++ b/app/utils/logx/log.go @@ -0,0 +1,245 @@ +package logx + +import ( + "os" + "strings" + "time" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type LogConfig struct { + AppName string `yaml:"app_name" json:"app_name" toml:"app_name"` + Level string `yaml:"level" json:"level" toml:"level"` + StacktraceLevel string `yaml:"stacktrace_level" json:"stacktrace_level" toml:"stacktrace_level"` + IsStdOut bool `yaml:"is_stdout" json:"is_stdout" toml:"is_stdout"` + TimeFormat string `yaml:"time_format" json:"time_format" toml:"time_format"` // second, milli, nano, standard, iso, + Encoding string `yaml:"encoding" json:"encoding" toml:"encoding"` // console, json + Skip int `yaml:"skip" json:"skip" toml:"skip"` + + IsFileOut bool `yaml:"is_file_out" json:"is_file_out" toml:"is_file_out"` + FileDir string `yaml:"file_dir" json:"file_dir" toml:"file_dir"` + FileName string `yaml:"file_name" json:"file_name" toml:"file_name"` + FileMaxSize int `yaml:"file_max_size" json:"file_max_size" toml:"file_max_size"` + FileMaxAge int `yaml:"file_max_age" json:"file_max_age" toml:"file_max_age"` +} + +var ( + l *LogX = defaultLogger() + conf *LogConfig +) + +// default logger setting +func defaultLogger() *LogX { + conf = &LogConfig{ + Level: "debug", + StacktraceLevel: "error", + IsStdOut: true, + TimeFormat: "standard", + Encoding: "console", + Skip: 2, + } + writers := []zapcore.WriteSyncer{os.Stdout} + lg, lv := newZapLogger(setLogLevel(conf.Level), setLogLevel(conf.StacktraceLevel), conf.Encoding, conf.TimeFormat, conf.Skip, zapcore.NewMultiWriteSyncer(writers...)) + zap.RedirectStdLog(lg) + return &LogX{logger: lg, atomLevel: lv} +} + +// initial standard log, if you don't init, it will use default logger setting +func InitDefaultLogger(cfg *LogConfig) { + var writers []zapcore.WriteSyncer + if cfg.IsStdOut || (!cfg.IsStdOut && !cfg.IsFileOut) { + writers = append(writers, os.Stdout) + } + if cfg.IsFileOut { + writers = append(writers, NewRollingFile(cfg.FileDir, cfg.FileName, cfg.FileMaxSize, cfg.FileMaxAge)) + } + + lg, lv := newZapLogger(setLogLevel(cfg.Level), setLogLevel(cfg.StacktraceLevel), cfg.Encoding, cfg.TimeFormat, cfg.Skip, zapcore.NewMultiWriteSyncer(writers...)) + zap.RedirectStdLog(lg) + if cfg.AppName != "" { + lg = lg.With(zap.String("app", cfg.AppName)) // 加上应用名称 + } + l = &LogX{logger: lg, atomLevel: lv} +} + +// create a new logger +func NewLogger(cfg *LogConfig) *LogX { + var writers []zapcore.WriteSyncer + if cfg.IsStdOut || (!cfg.IsStdOut && !cfg.IsFileOut) { + writers = append(writers, os.Stdout) + } + if cfg.IsFileOut { + writers = append(writers, NewRollingFile(cfg.FileDir, cfg.FileName, cfg.FileMaxSize, cfg.FileMaxAge)) + } + + lg, lv := newZapLogger(setLogLevel(cfg.Level), setLogLevel(cfg.StacktraceLevel), cfg.Encoding, cfg.TimeFormat, cfg.Skip, zapcore.NewMultiWriteSyncer(writers...)) + zap.RedirectStdLog(lg) + if cfg.AppName != "" { + lg = lg.With(zap.String("app", cfg.AppName)) // 加上应用名称 + } + return &LogX{logger: lg, atomLevel: lv} +} + +// create a new zaplog logger +func newZapLogger(level, stacktrace zapcore.Level, encoding, timeType string, skip int, output zapcore.WriteSyncer) (*zap.Logger, *zap.AtomicLevel) { + encCfg := zapcore.EncoderConfig{ + TimeKey: "T", + LevelKey: "L", + NameKey: "N", + CallerKey: "C", + MessageKey: "M", + StacktraceKey: "S", + LineEnding: zapcore.DefaultLineEnding, + EncodeCaller: zapcore.ShortCallerEncoder, + EncodeDuration: zapcore.NanosDurationEncoder, + EncodeLevel: zapcore.LowercaseLevelEncoder, + } + setTimeFormat(timeType, &encCfg) // set time type + atmLvl := zap.NewAtomicLevel() // set level + atmLvl.SetLevel(level) + encoder := zapcore.NewJSONEncoder(encCfg) // 确定encoder格式 + if encoding == "console" { + encoder = zapcore.NewConsoleEncoder(encCfg) + } + return zap.New(zapcore.NewCore(encoder, output, atmLvl), zap.AddCaller(), zap.AddStacktrace(stacktrace), zap.AddCallerSkip(skip)), &atmLvl +} + +// set log level +func setLogLevel(lvl string) zapcore.Level { + switch strings.ToLower(lvl) { + case "panic": + return zapcore.PanicLevel + case "fatal": + return zapcore.FatalLevel + case "error": + return zapcore.ErrorLevel + case "warn", "warning": + return zapcore.WarnLevel + case "info": + return zapcore.InfoLevel + default: + return zapcore.DebugLevel + } +} + +// set time format +func setTimeFormat(timeType string, z *zapcore.EncoderConfig) { + switch strings.ToLower(timeType) { + case "iso": // iso8601 standard + z.EncodeTime = zapcore.ISO8601TimeEncoder + case "sec": // only for unix second, without millisecond + z.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendInt64(t.Unix()) + } + case "second": // unix second, with millisecond + z.EncodeTime = zapcore.EpochTimeEncoder + case "milli", "millisecond": // millisecond + z.EncodeTime = zapcore.EpochMillisTimeEncoder + case "nano", "nanosecond": // nanosecond + z.EncodeTime = zapcore.EpochNanosTimeEncoder + default: // standard format + z.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(t.Format("2006-01-02 15:04:05.000")) + } + } +} + +func GetLevel() string { + switch l.atomLevel.Level() { + case zapcore.PanicLevel: + return "panic" + case zapcore.FatalLevel: + return "fatal" + case zapcore.ErrorLevel: + return "error" + case zapcore.WarnLevel: + return "warn" + case zapcore.InfoLevel: + return "info" + default: + return "debug" + } +} + +func SetLevel(lvl string) { + l.atomLevel.SetLevel(setLogLevel(lvl)) +} + +// temporary add call skip +func AddCallerSkip(skip int) *LogX { + l.logger.WithOptions(zap.AddCallerSkip(skip)) + return l +} + +// permanent add call skip +func AddDepth(skip int) *LogX { + l.logger = l.logger.WithOptions(zap.AddCallerSkip(skip)) + return l +} + +// permanent add options +func AddOptions(opts ...zap.Option) *LogX { + l.logger = l.logger.WithOptions(opts...) + return l +} + +func AddField(k string, v interface{}) { + l.logger.With(zap.Any(k, v)) +} + +func AddFields(fields map[string]interface{}) *LogX { + for k, v := range fields { + l.logger.With(zap.Any(k, v)) + } + return l +} + +// Normal log +func Debug(e interface{}, args ...interface{}) error { + return l.Debug(e, args...) +} +func Info(e interface{}, args ...interface{}) error { + return l.Info(e, args...) +} +func Warn(e interface{}, args ...interface{}) error { + return l.Warn(e, args...) +} +func Error(e interface{}, args ...interface{}) error { + return l.Error(e, args...) +} +func Panic(e interface{}, args ...interface{}) error { + return l.Panic(e, args...) +} +func Fatal(e interface{}, args ...interface{}) error { + return l.Fatal(e, args...) +} + +// Format logs +func Debugf(format string, args ...interface{}) error { + return l.Debugf(format, args...) +} +func Infof(format string, args ...interface{}) error { + return l.Infof(format, args...) +} +func Warnf(format string, args ...interface{}) error { + return l.Warnf(format, args...) +} +func Errorf(format string, args ...interface{}) error { + return l.Errorf(format, args...) +} +func Panicf(format string, args ...interface{}) error { + return l.Panicf(format, args...) +} +func Fatalf(format string, args ...interface{}) error { + return l.Fatalf(format, args...) +} + +func formatFieldMap(m FieldMap) []Field { + var res []Field + for k, v := range m { + res = append(res, zap.Any(k, v)) + } + return res +} diff --git a/app/utils/logx/output.go b/app/utils/logx/output.go new file mode 100644 index 0000000..ef33f0b --- /dev/null +++ b/app/utils/logx/output.go @@ -0,0 +1,105 @@ +package logx + +import ( + "bytes" + "io" + "os" + "path/filepath" + "time" + + "gopkg.in/natefinch/lumberjack.v2" +) + +// output interface +type WriteSyncer interface { + io.Writer + Sync() error +} + +// split writer +func NewRollingFile(dir, filename string, maxSize, MaxAge int) WriteSyncer { + s, err := os.Stat(dir) + if err != nil || !s.IsDir() { + os.RemoveAll(dir) + if err := os.MkdirAll(dir, 0766); err != nil { + panic(err) + } + } + return newLumberjackWriteSyncer(&lumberjack.Logger{ + Filename: filepath.Join(dir, filename), + MaxSize: maxSize, // megabytes, MB + MaxAge: MaxAge, // days + LocalTime: true, + Compress: false, + }) +} + +type lumberjackWriteSyncer struct { + *lumberjack.Logger + buf *bytes.Buffer + logChan chan []byte + closeChan chan interface{} + maxSize int +} + +func newLumberjackWriteSyncer(l *lumberjack.Logger) *lumberjackWriteSyncer { + ws := &lumberjackWriteSyncer{ + Logger: l, + buf: bytes.NewBuffer([]byte{}), + logChan: make(chan []byte, 5000), + closeChan: make(chan interface{}), + maxSize: 1024, + } + go ws.run() + return ws +} + +func (l *lumberjackWriteSyncer) run() { + ticker := time.NewTicker(1 * time.Second) + + for { + select { + case <-ticker.C: + if l.buf.Len() > 0 { + l.sync() + } + case bs := <-l.logChan: + _, err := l.buf.Write(bs) + if err != nil { + continue + } + if l.buf.Len() > l.maxSize { + l.sync() + } + case <-l.closeChan: + l.sync() + return + } + } +} + +func (l *lumberjackWriteSyncer) Stop() { + close(l.closeChan) +} + +func (l *lumberjackWriteSyncer) Write(bs []byte) (int, error) { + b := make([]byte, len(bs)) + for i, c := range bs { + b[i] = c + } + l.logChan <- b + return 0, nil +} + +func (l *lumberjackWriteSyncer) Sync() error { + return nil +} + +func (l *lumberjackWriteSyncer) sync() error { + defer l.buf.Reset() + _, err := l.Logger.Write(l.buf.Bytes()) + if err != nil { + return err + } + return nil +} diff --git a/app/utils/logx/sugar.go b/app/utils/logx/sugar.go new file mode 100644 index 0000000..ab380fc --- /dev/null +++ b/app/utils/logx/sugar.go @@ -0,0 +1,192 @@ +package logx + +import ( + "errors" + "fmt" + "strconv" + + "go.uber.org/zap" +) + +type LogX struct { + logger *zap.Logger + atomLevel *zap.AtomicLevel +} + +type Field = zap.Field +type FieldMap map[string]interface{} + +// 判断其他类型--start +func getFields(msg string, format bool, args ...interface{}) (string, []Field) { + var str []interface{} + var fields []zap.Field + if len(args) > 0 { + for _, v := range args { + if f, ok := v.(Field); ok { + fields = append(fields, f) + } else if f, ok := v.(FieldMap); ok { + fields = append(fields, formatFieldMap(f)...) + } else { + str = append(str, AnyToString(v)) + } + } + if format { + return fmt.Sprintf(msg, str...), fields + } + str = append([]interface{}{msg}, str...) + return fmt.Sprintln(str...), fields + } + return msg, []Field{} +} + +func (l *LogX) Debug(s interface{}, args ...interface{}) error { + es, e := checkErr(s) + if es != "" { + msg, field := getFields(es, false, args...) + l.logger.Debug(msg, field...) + } + return e +} +func (l *LogX) Info(s interface{}, args ...interface{}) error { + es, e := checkErr(s) + if es != "" { + msg, field := getFields(es, false, args...) + l.logger.Info(msg, field...) + } + return e +} +func (l *LogX) Warn(s interface{}, args ...interface{}) error { + es, e := checkErr(s) + if es != "" { + msg, field := getFields(es, false, args...) + l.logger.Warn(msg, field...) + } + return e +} +func (l *LogX) Error(s interface{}, args ...interface{}) error { + es, e := checkErr(s) + if es != "" { + msg, field := getFields(es, false, args...) + l.logger.Error(msg, field...) + } + return e +} +func (l *LogX) DPanic(s interface{}, args ...interface{}) error { + es, e := checkErr(s) + if es != "" { + msg, field := getFields(es, false, args...) + l.logger.DPanic(msg, field...) + } + return e +} +func (l *LogX) Panic(s interface{}, args ...interface{}) error { + es, e := checkErr(s) + if es != "" { + msg, field := getFields(es, false, args...) + l.logger.Panic(msg, field...) + } + return e +} +func (l *LogX) Fatal(s interface{}, args ...interface{}) error { + es, e := checkErr(s) + if es != "" { + msg, field := getFields(es, false, args...) + l.logger.Fatal(msg, field...) + } + return e +} + +func checkErr(s interface{}) (string, error) { + switch e := s.(type) { + case error: + return e.Error(), e + case string: + return e, errors.New(e) + case []byte: + return string(e), nil + default: + return "", nil + } +} + +func (l *LogX) LogError(err error) error { + return l.Error(err.Error()) +} + +func (l *LogX) Debugf(msg string, args ...interface{}) error { + s, f := getFields(msg, true, args...) + l.logger.Debug(s, f...) + return errors.New(s) +} + +func (l *LogX) Infof(msg string, args ...interface{}) error { + s, f := getFields(msg, true, args...) + l.logger.Info(s, f...) + return errors.New(s) +} + +func (l *LogX) Warnf(msg string, args ...interface{}) error { + s, f := getFields(msg, true, args...) + l.logger.Warn(s, f...) + return errors.New(s) +} + +func (l *LogX) Errorf(msg string, args ...interface{}) error { + s, f := getFields(msg, true, args...) + l.logger.Error(s, f...) + return errors.New(s) +} + +func (l *LogX) DPanicf(msg string, args ...interface{}) error { + s, f := getFields(msg, true, args...) + l.logger.DPanic(s, f...) + return errors.New(s) +} + +func (l *LogX) Panicf(msg string, args ...interface{}) error { + s, f := getFields(msg, true, args...) + l.logger.Panic(s, f...) + return errors.New(s) +} + +func (l *LogX) Fatalf(msg string, args ...interface{}) error { + s, f := getFields(msg, true, args...) + l.logger.Fatal(s, f...) + return errors.New(s) +} + +func AnyToString(raw interface{}) string { + switch i := raw.(type) { + case []byte: + return string(i) + case int: + return strconv.FormatInt(int64(i), 10) + case int64: + return strconv.FormatInt(i, 10) + case float32: + return strconv.FormatFloat(float64(i), 'f', 2, 64) + case float64: + return strconv.FormatFloat(i, 'f', 2, 64) + case uint: + return strconv.FormatInt(int64(i), 10) + case uint8: + return strconv.FormatInt(int64(i), 10) + case uint16: + return strconv.FormatInt(int64(i), 10) + case uint32: + return strconv.FormatInt(int64(i), 10) + case uint64: + return strconv.FormatInt(int64(i), 10) + case int8: + return strconv.FormatInt(int64(i), 10) + case int16: + return strconv.FormatInt(int64(i), 10) + case int32: + return strconv.FormatInt(int64(i), 10) + case string: + return i + case error: + return i.Error() + } + return fmt.Sprintf("%#v", raw) +} diff --git a/app/utils/map.go b/app/utils/map.go new file mode 100644 index 0000000..d9f3b7a --- /dev/null +++ b/app/utils/map.go @@ -0,0 +1,9 @@ +package utils + +// GetOneKeyOfMapString 取出Map的一个key +func GetOneKeyOfMapString(collection map[string]string) string { + for k := range collection { + return k + } + return "" +} diff --git a/app/utils/map_and_struct.go b/app/utils/map_and_struct.go new file mode 100644 index 0000000..34904ce --- /dev/null +++ b/app/utils/map_and_struct.go @@ -0,0 +1,341 @@ +package utils + +import ( + "encoding/json" + "fmt" + "reflect" + "strconv" + "strings" +) + +func Map2Struct(vals map[string]interface{}, dst interface{}) (err error) { + return Map2StructByTag(vals, dst, "json") +} + +func Map2StructByTag(vals map[string]interface{}, dst interface{}, structTag string) (err error) { + defer func() { + e := recover() + if e != nil { + if v, ok := e.(error); ok { + err = fmt.Errorf("Panic: %v", v.Error()) + } else { + err = fmt.Errorf("Panic: %v", e) + } + } + }() + + pt := reflect.TypeOf(dst) + pv := reflect.ValueOf(dst) + + if pv.Kind() != reflect.Ptr || pv.Elem().Kind() != reflect.Struct { + return fmt.Errorf("not a pointer of struct") + } + + var f reflect.StructField + var ft reflect.Type + var fv reflect.Value + + for i := 0; i < pt.Elem().NumField(); i++ { + f = pt.Elem().Field(i) + fv = pv.Elem().Field(i) + ft = f.Type + + if f.Anonymous || !fv.CanSet() { + continue + } + + tag := f.Tag.Get(structTag) + + name, option := parseTag(tag) + + if name == "-" { + continue + } + + if name == "" { + name = strings.ToLower(f.Name) + } + val, ok := vals[name] + + if !ok { + if option == "required" { + return fmt.Errorf("'%v' not found", name) + } + if len(option) != 0 { + val = option // default value + } else { + //fv.Set(reflect.Zero(ft)) // TODO set zero value or just ignore it? + continue + } + } + + // convert or set value to field + vv := reflect.ValueOf(val) + vt := reflect.TypeOf(val) + + if vt.Kind() != reflect.String { + // try to assign and convert + if vt.AssignableTo(ft) { + fv.Set(vv) + continue + } + + if vt.ConvertibleTo(ft) { + fv.Set(vv.Convert(ft)) + continue + } + + return fmt.Errorf("value type not match: field=%v(%v) value=%v(%v)", f.Name, ft.Kind(), val, vt.Kind()) + } + s := strings.TrimSpace(vv.String()) + if len(s) == 0 && option == "required" { + return fmt.Errorf("value of required argument can't not be empty") + } + fk := ft.Kind() + + // convert string to value + if fk == reflect.Ptr && ft.Elem().Kind() == reflect.String { + fv.Set(reflect.ValueOf(&s)) + continue + } + if fk == reflect.Ptr || fk == reflect.Struct { + err = convertJsonValue(s, name, fv) + } else if fk == reflect.Slice { + err = convertSlice(s, f.Name, ft, fv) + } else { + err = convertValue(fk, s, f.Name, fv) + } + + if err != nil { + return err + } + continue + } + + return nil +} + +func Struct2Map(s interface{}) map[string]interface{} { + return Struct2MapByTag(s, "json") +} +func Struct2MapByTag(s interface{}, tagName string) map[string]interface{} { + t := reflect.TypeOf(s) + v := reflect.ValueOf(s) + + if v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct { + t = t.Elem() + v = v.Elem() + } + + if v.Kind() != reflect.Struct { + return nil + } + + m := make(map[string]interface{}) + + for i := 0; i < t.NumField(); i++ { + fv := v.Field(i) + ft := t.Field(i) + + if !fv.CanInterface() { + continue + } + + if ft.PkgPath != "" { // unexported + continue + } + + var name string + var option string + tag := ft.Tag.Get(tagName) + if tag != "" { + ts := strings.Split(tag, ",") + if len(ts) == 1 { + name = ts[0] + } else if len(ts) > 1 { + name = ts[0] + option = ts[1] + } + if name == "-" { + continue // skip this field + } + if name == "" { + name = strings.ToLower(ft.Name) + } + if option == "omitempty" { + if isEmpty(&fv) { + continue // skip empty field + } + } + } else { + name = strings.ToLower(ft.Name) + } + + if ft.Anonymous && fv.Kind() == reflect.Ptr && fv.IsNil() { + continue + } + if (ft.Anonymous && fv.Kind() == reflect.Struct) || + (ft.Anonymous && fv.Kind() == reflect.Ptr && fv.Elem().Kind() == reflect.Struct) { + + // embedded struct + embedded := Struct2MapByTag(fv.Interface(), tagName) + for embName, embValue := range embedded { + m[embName] = embValue + } + } else if option == "string" { + kind := fv.Kind() + if kind == reflect.Int || kind == reflect.Int8 || kind == reflect.Int16 || kind == reflect.Int32 || kind == reflect.Int64 { + m[name] = strconv.FormatInt(fv.Int(), 10) + } else if kind == reflect.Uint || kind == reflect.Uint8 || kind == reflect.Uint16 || kind == reflect.Uint32 || kind == reflect.Uint64 { + m[name] = strconv.FormatUint(fv.Uint(), 10) + } else if kind == reflect.Float32 || kind == reflect.Float64 { + m[name] = strconv.FormatFloat(fv.Float(), 'f', 2, 64) + } else { + m[name] = fv.Interface() + } + } else { + m[name] = fv.Interface() + } + } + + return m +} + +func isEmpty(v *reflect.Value) bool { + k := v.Kind() + if k == reflect.Bool { + return v.Bool() == false + } else if reflect.Int < k && k < reflect.Int64 { + return v.Int() == 0 + } else if reflect.Uint < k && k < reflect.Uintptr { + return v.Uint() == 0 + } else if k == reflect.Float32 || k == reflect.Float64 { + return v.Float() == 0 + } else if k == reflect.Array || k == reflect.Map || k == reflect.Slice || k == reflect.String { + return v.Len() == 0 + } else if k == reflect.Interface || k == reflect.Ptr { + return v.IsNil() + } + return false +} + +func convertSlice(s string, name string, ft reflect.Type, fv reflect.Value) error { + var err error + et := ft.Elem() + + if et.Kind() == reflect.Ptr || et.Kind() == reflect.Struct { + return convertJsonValue(s, name, fv) + } + + ss := strings.Split(s, ",") + + if len(s) == 0 || len(ss) == 0 { + return nil + } + + fs := reflect.MakeSlice(ft, 0, len(ss)) + + for _, si := range ss { + ev := reflect.New(et).Elem() + + err = convertValue(et.Kind(), si, name, ev) + if err != nil { + return err + } + fs = reflect.Append(fs, ev) + } + + fv.Set(fs) + + return nil +} + +func convertJsonValue(s string, name string, fv reflect.Value) error { + var err error + d := StringToSlice(s) + + if fv.Kind() == reflect.Ptr { + if fv.IsNil() { + fv.Set(reflect.New(fv.Type().Elem())) + } + } else { + fv = fv.Addr() + } + + err = json.Unmarshal(d, fv.Interface()) + + if err != nil { + return fmt.Errorf("invalid json '%v': %v, %v", name, err.Error(), s) + } + + return nil +} + +func convertValue(kind reflect.Kind, s string, name string, fv reflect.Value) error { + if !fv.CanAddr() { + return fmt.Errorf("can not addr: %v", name) + } + + if kind == reflect.String { + fv.SetString(s) + return nil + } + + if kind == reflect.Bool { + switch s { + case "true": + fv.SetBool(true) + case "false": + fv.SetBool(false) + case "1": + fv.SetBool(true) + case "0": + fv.SetBool(false) + default: + return fmt.Errorf("invalid bool: %v value=%v", name, s) + } + return nil + } + + if reflect.Int <= kind && kind <= reflect.Int64 { + i, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return fmt.Errorf("invalid int: %v value=%v", name, s) + } + fv.SetInt(i) + + } else if reflect.Uint <= kind && kind <= reflect.Uint64 { + i, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return fmt.Errorf("invalid int: %v value=%v", name, s) + } + fv.SetUint(i) + + } else if reflect.Float32 == kind || kind == reflect.Float64 { + i, err := strconv.ParseFloat(s, 64) + + if err != nil { + return fmt.Errorf("invalid float: %v value=%v", name, s) + } + + fv.SetFloat(i) + } else { + // not support or just ignore it? + // return fmt.Errorf("type not support: field=%v(%v) value=%v(%v)", name, ft.Kind(), val, vt.Kind()) + } + return nil +} + +func parseTag(tag string) (string, string) { + tags := strings.Split(tag, ",") + + if len(tags) <= 0 { + return "", "" + } + + if len(tags) == 1 { + return tags[0], "" + } + + return tags[0], tags[1] +} diff --git a/app/utils/md5.go b/app/utils/md5.go new file mode 100644 index 0000000..52c108d --- /dev/null +++ b/app/utils/md5.go @@ -0,0 +1,12 @@ +package utils + +import ( + "crypto/md5" + "encoding/hex" +) + +func Md5(str string) string { + h := md5.New() + h.Write([]byte(str)) + return hex.EncodeToString(h.Sum(nil)) +} diff --git a/app/utils/qrcode/decodeFile.go b/app/utils/qrcode/decodeFile.go new file mode 100644 index 0000000..f50fb28 --- /dev/null +++ b/app/utils/qrcode/decodeFile.go @@ -0,0 +1,33 @@ +package qrcode + +import ( + "image" + _ "image/jpeg" + _ "image/png" + "os" + + "github.com/makiuchi-d/gozxing" + "github.com/makiuchi-d/gozxing/qrcode" +) + +func DecodeFile(fi string) (string, error) { + file, err := os.Open(fi) + if err != nil { + return "", err + } + img, _, err := image.Decode(file) + if err != nil { + return "", err + } + // prepare BinaryBitmap + bmp, err := gozxing.NewBinaryBitmapFromImage(img) + if err != nil { + return "", err + } + // decode image + result, err := qrcode.NewQRCodeReader().Decode(bmp, nil) + if err != nil { + return "", err + } + return result.String(), nil +} diff --git a/app/utils/qrcode/getBase64.go b/app/utils/qrcode/getBase64.go new file mode 100644 index 0000000..11d149c --- /dev/null +++ b/app/utils/qrcode/getBase64.go @@ -0,0 +1,43 @@ +package qrcode + +// 生成登录二维码图片, 方便在网页上显示 + +import ( + "bytes" + "encoding/base64" + "image/jpeg" + "image/png" + + "github.com/boombuler/barcode" + "github.com/boombuler/barcode/qr" +) + +func GetJPGBase64(content string, edges ...int) string { + edgeLen := 300 + if len(edges) > 0 && edges[0] > 100 && edges[0] < 2000 { + edgeLen = edges[0] + } + img, _ := qr.Encode(content, qr.L, qr.Unicode) + img, _ = barcode.Scale(img, edgeLen, edgeLen) + + emptyBuff := bytes.NewBuffer(nil) // 开辟一个新的空buff缓冲区 + jpeg.Encode(emptyBuff, img, nil) + dist := make([]byte, 50000) // 开辟存储空间 + base64.StdEncoding.Encode(dist, emptyBuff.Bytes()) // buff转成base64 + return "data:image/png;base64," + string(dist) // 输出图片base64(type = []byte) +} + +func GetPNGBase64(content string, edges ...int) string { + edgeLen := 300 + if len(edges) > 0 && edges[0] > 100 && edges[0] < 2000 { + edgeLen = edges[0] + } + img, _ := qr.Encode(content, qr.L, qr.Unicode) + img, _ = barcode.Scale(img, edgeLen, edgeLen) + + emptyBuff := bytes.NewBuffer(nil) // 开辟一个新的空buff缓冲区 + png.Encode(emptyBuff, img) + dist := make([]byte, 50000) // 开辟存储空间 + base64.StdEncoding.Encode(dist, emptyBuff.Bytes()) // buff转成base64 + return string(dist) // 输出图片base64(type = []byte) +} diff --git a/app/utils/qrcode/saveFile.go b/app/utils/qrcode/saveFile.go new file mode 100644 index 0000000..4854783 --- /dev/null +++ b/app/utils/qrcode/saveFile.go @@ -0,0 +1,85 @@ +package qrcode + +// 生成登录二维码图片 + +import ( + "errors" + "image" + "image/jpeg" + "image/png" + "os" + "path/filepath" + "strings" + + "github.com/boombuler/barcode" + "github.com/boombuler/barcode/qr" +) + +func SaveJpegFile(filePath, content string, edges ...int) error { + edgeLen := 300 + if len(edges) > 0 && edges[0] > 100 && edges[0] < 2000 { + edgeLen = edges[0] + } + img, _ := qr.Encode(content, qr.L, qr.Unicode) + img, _ = barcode.Scale(img, edgeLen, edgeLen) + + return writeFile(filePath, img, "jpg") +} + +func SavePngFile(filePath, content string, edges ...int) error { + edgeLen := 300 + if len(edges) > 0 && edges[0] > 100 && edges[0] < 2000 { + edgeLen = edges[0] + } + img, _ := qr.Encode(content, qr.L, qr.Unicode) + img, _ = barcode.Scale(img, edgeLen, edgeLen) + + return writeFile(filePath, img, "png") +} + +func writeFile(filePath string, img image.Image, format string) error { + if err := createDir(filePath); err != nil { + return err + } + file, err := os.Create(filePath) + defer file.Close() + if err != nil { + return err + } + switch strings.ToLower(format) { + case "png": + err = png.Encode(file, img) + break + case "jpg": + err = jpeg.Encode(file, img, nil) + default: + return errors.New("format not accept") + } + if err != nil { + return err + } + return nil +} + +func createDir(filePath string) error { + var err error + // filePath, _ = filepath.Abs(filePath) + dirPath := filepath.Dir(filePath) + dirInfo, err := os.Stat(dirPath) + if err != nil { + if !os.IsExist(err) { + err = os.MkdirAll(dirPath, 0777) + if err != nil { + return err + } + } else { + return err + } + } else { + if dirInfo.IsDir() { + return nil + } + return errors.New("directory is a file") + } + return nil +} diff --git a/app/utils/qrcode/writeWeb.go b/app/utils/qrcode/writeWeb.go new file mode 100644 index 0000000..57e1e92 --- /dev/null +++ b/app/utils/qrcode/writeWeb.go @@ -0,0 +1,39 @@ +package qrcode + +import ( + "bytes" + "image/jpeg" + "image/png" + "net/http" + + "github.com/boombuler/barcode" + "github.com/boombuler/barcode/qr" +) + +func WritePng(w http.ResponseWriter, content string, edges ...int) error { + edgeLen := 300 + if len(edges) > 0 && edges[0] > 100 && edges[0] < 2000 { + edgeLen = edges[0] + } + img, _ := qr.Encode(content, qr.L, qr.Unicode) + img, _ = barcode.Scale(img, edgeLen, edgeLen) + buff := bytes.NewBuffer(nil) + png.Encode(buff, img) + w.Header().Set("Content-Type", "image/png") + _, err := w.Write(buff.Bytes()) + return err +} + +func WriteJpg(w http.ResponseWriter, content string, edges ...int) error { + edgeLen := 300 + if len(edges) > 0 && edges[0] > 100 && edges[0] < 2000 { + edgeLen = edges[0] + } + img, _ := qr.Encode(content, qr.L, qr.Unicode) + img, _ = barcode.Scale(img, edgeLen, edgeLen) + buff := bytes.NewBuffer(nil) + jpeg.Encode(buff, img, nil) + w.Header().Set("Content-Type", "image/jpg") + _, err := w.Write(buff.Bytes()) + return err +} diff --git a/app/utils/rand.go b/app/utils/rand.go new file mode 100644 index 0000000..0024fd0 --- /dev/null +++ b/app/utils/rand.go @@ -0,0 +1,31 @@ +package utils + +import ( + crand "crypto/rand" + "fmt" + "math/big" + "math/rand" + "time" +) + +func RandString(l int, c ...string) string { + var ( + chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + str string + num *big.Int + ) + if len(c) > 0 { + chars = c[0] + } + chrLen := int64(len(chars)) + for len(str) < l { + num, _ = crand.Int(crand.Reader, big.NewInt(chrLen)) + str += string(chars[num.Int64()]) + } + return str +} + +func RandNum() string { + seed := time.Now().UnixNano() + rand.Int63() + return fmt.Sprintf("%05v", rand.New(rand.NewSource(seed)).Int31n(1000000)) +} diff --git a/app/utils/rsa.go b/app/utils/rsa.go new file mode 100644 index 0000000..fb8274a --- /dev/null +++ b/app/utils/rsa.go @@ -0,0 +1,170 @@ +package utils + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "io/ioutil" + "log" + "os" +) + +// 生成私钥文件 TODO 未指定路径 +func RsaKeyGen(bits int) error { + privateKey, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return err + } + derStream := x509.MarshalPKCS1PrivateKey(privateKey) + block := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: derStream, + } + priFile, err := os.Create("private.pem") + if err != nil { + return err + } + err = pem.Encode(priFile, block) + priFile.Close() + if err != nil { + return err + } + // 生成公钥文件 + publicKey := &privateKey.PublicKey + derPkix, err := x509.MarshalPKIXPublicKey(publicKey) + if err != nil { + return err + } + block = &pem.Block{ + Type: "PUBLIC KEY", + Bytes: derPkix, + } + pubFile, err := os.Create("public.pem") + if err != nil { + return err + } + err = pem.Encode(pubFile, block) + pubFile.Close() + if err != nil { + return err + } + return nil +} + +// 生成私钥文件, 返回 privateKey , publicKey, error +func RsaKeyGenText(bits int) (string, string, error) { // bits 字节位 1024/2048 + privateKey, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return "", "", err + } + derStream := x509.MarshalPKCS1PrivateKey(privateKey) + block := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: derStream, + } + priBuff := bytes.NewBuffer(nil) + err = pem.Encode(priBuff, block) + if err != nil { + return "", "", err + } + // 生成公钥文件 + publicKey := &privateKey.PublicKey + derPkix, err := x509.MarshalPKIXPublicKey(publicKey) + if err != nil { + return "", "", err + } + block = &pem.Block{ + Type: "PUBLIC KEY", + Bytes: derPkix, + } + pubBuff := bytes.NewBuffer(nil) + err = pem.Encode(pubBuff, block) + if err != nil { + return "", "", err + } + return priBuff.String(), pubBuff.String(), nil +} + +// 加密 +func RsaEncrypt(rawData, publicKey []byte) ([]byte, error) { + block, _ := pem.Decode(publicKey) + if block == nil { + return nil, errors.New("public key error") + } + pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, err + } + pub := pubInterface.(*rsa.PublicKey) + return rsa.EncryptPKCS1v15(rand.Reader, pub, rawData) +} + +// 公钥加密 +func RsaEncrypts(data, keyBytes []byte) []byte { + //解密pem格式的公钥 + block, _ := pem.Decode(keyBytes) + if block == nil { + panic(errors.New("public key error")) + } + // 解析公钥 + pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + panic(err) + } + // 类型断言 + pub := pubInterface.(*rsa.PublicKey) + //加密 + ciphertext, err := rsa.EncryptPKCS1v15(rand.Reader, pub, data) + if err != nil { + panic(err) + } + return ciphertext +} + +// 解密 +func RsaDecrypt(cipherText, privateKey []byte) ([]byte, error) { + block, _ := pem.Decode(privateKey) + if block == nil { + return nil, errors.New("private key error") + } + priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + return rsa.DecryptPKCS1v15(rand.Reader, priv, cipherText) +} + +// 从证书获取公钥 +func OpensslPemGetPublic(pathOrString string) (interface{}, error) { + var certPem []byte + var err error + if IsFile(pathOrString) && Exists(pathOrString) { + certPem, err = ioutil.ReadFile(pathOrString) + if err != nil { + return nil, err + } + if string(certPem) == "" { + return nil, errors.New("empty pem file") + } + } else { + if pathOrString == "" { + return nil, errors.New("empty pem string") + } + certPem = StringToSlice(pathOrString) + } + block, rest := pem.Decode(certPem) + if block == nil || block.Type != "PUBLIC KEY" { + //log.Fatal("failed to decode PEM block containing public key") + return nil, errors.New("failed to decode PEM block containing public key") + } + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Got a %T, with remaining data: %q", pub, rest) + return pub, nil +} diff --git a/app/utils/serialize.go b/app/utils/serialize.go new file mode 100644 index 0000000..1ac4d80 --- /dev/null +++ b/app/utils/serialize.go @@ -0,0 +1,23 @@ +package utils + +import ( + "encoding/json" +) + +func Serialize(data interface{}) []byte { + res, err := json.Marshal(data) + if err != nil { + return []byte{} + } + return res +} + +func Unserialize(b []byte, dst interface{}) { + if err := json.Unmarshal(b, dst); err != nil { + dst = nil + } +} + +func SerializeStr(data interface{}, arg ...interface{}) string { + return string(Serialize(data)) +} diff --git a/app/utils/shuffle.go b/app/utils/shuffle.go new file mode 100644 index 0000000..2c845a8 --- /dev/null +++ b/app/utils/shuffle.go @@ -0,0 +1,48 @@ +package utils + +import ( + "math/rand" + "time" +) + +// 打乱随机字符串 +func ShuffleString(s *string) { + if len(*s) > 1 { + b := []byte(*s) + rand.Seed(time.Now().UnixNano()) + rand.Shuffle(len(b), func(x, y int) { + b[x], b[y] = b[y], b[x] + }) + *s = string(b) + } +} + +// 打乱随机slice +func ShuffleSliceBytes(b []byte) { + if len(b) > 1 { + rand.Seed(time.Now().UnixNano()) + rand.Shuffle(len(b), func(x, y int) { + b[x], b[y] = b[y], b[x] + }) + } +} + +// 打乱slice int +func ShuffleSliceInt(i []int) { + if len(i) > 1 { + rand.Seed(time.Now().UnixNano()) + rand.Shuffle(len(i), func(x, y int) { + i[x], i[y] = i[y], i[x] + }) + } +} + +// 打乱slice interface +func ShuffleSliceInterface(i []interface{}) { + if len(i) > 1 { + rand.Seed(time.Now().UnixNano()) + rand.Shuffle(len(i), func(x, y int) { + i[x], i[y] = i[y], i[x] + }) + } +} diff --git a/app/utils/sign_check.go b/app/utils/sign_check.go new file mode 100644 index 0000000..798f63d --- /dev/null +++ b/app/utils/sign_check.go @@ -0,0 +1,125 @@ +package utils + +import ( + "applet/app/utils/logx" + "fmt" + "github.com/forgoer/openssl" + "github.com/gin-gonic/gin" + "github.com/syyongx/php2go" + "strings" +) + +var publicKey = []byte(`-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCFQD7RL2tDNuwdg0jTfV0zjAzh +WoCWfGrcNiucy2XUHZZU2oGhHv1N10qu3XayTDD4pu4sJ73biKwqR6ZN7IS4Sfon +vrzaXGvrTG4kmdo3XrbrkzmyBHDLTsJvv6pyS2HPl9QPSvKDN0iJ66+KN8QjBpw1 +FNIGe7xbDaJPY733/QIDAQAB +-----END PUBLIC KEY-----`) + +var privateKey = []byte(`-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQCFQD7RL2tDNuwdg0jTfV0zjAzhWoCWfGrcNiucy2XUHZZU2oGh +Hv1N10qu3XayTDD4pu4sJ73biKwqR6ZN7IS4SfonvrzaXGvrTG4kmdo3Xrbrkzmy +BHDLTsJvv6pyS2HPl9QPSvKDN0iJ66+KN8QjBpw1FNIGe7xbDaJPY733/QIDAQAB +AoGADi14wY8XDY7Bbp5yWDZFfV+QW0Xi2qAgSo/k8gjeK8R+I0cgdcEzWF3oz1Q2 +9d+PclVokAAmfj47e0AmXLImqMCSEzi1jDBUFIRoJk9WE1YstE94mrCgV0FW+N/u ++L6OgZcjmF+9dHKprnpaUGQuUV5fF8j0qp8S2Jfs3Sw+dOECQQCQnHALzFjmXXIR +Ez3VSK4ZoYgDIrrpzNst5Hh6AMDNZcG3CrCxlQrgqjgTzBSr3ZSavvkfYRj42STk +TqyX1tQFAkEA6+O6UENoUTk2lG7iO/ta7cdIULnkTGwQqvkgLIUjk6w8E3sBTIfw +rerTEmquw5F42HHE+FMrRat06ZN57lENmQJAYgUHlZevcoZIePZ35Qfcqpbo4Gc8 +Fpm6vwKr/tZf2Vlt0qo2VkhWFS6L0C92m4AX6EQmDHT+Pj7BWNdS+aCuGQJBAOkq +NKPZvWdr8jNOV3mKvxqB/U0uMigIOYGGtvLKt5vkh42J7ILFbHW8w95UbWMKjDUG +X/hF3WQEUo//Imsa2yECQHSZIpJxiTRueoDiyRt0LH+jdbYFUu/6D0UIYXhFvP/p +EZX+hfCfUnNYX59UVpRjSZ66g0CbCjuBPOhmOD+hDeQ= +-----END RSA PRIVATE KEY-----`) + +func GetApiVersion(c *gin.Context) int { + var apiVersion = c.GetHeader("apiVersion") + if StrToInt(apiVersion) == 0 { //没有版本号先不校验 + apiVersion = c.GetHeader("Apiversion") + } + if StrToInt(apiVersion) == 0 { //没有版本号先不校验 + apiVersion = c.GetHeader("api_version") + } + return StrToInt(apiVersion) +} + +//签名校验 +func SignCheck(c *gin.Context) bool { + var apiVersion = GetApiVersion(c) + if apiVersion == 0 { //没有版本号先不校验 + return true + } + //1.通过rsa 解析出 aes + var key = c.GetHeader("key") + + //拼接对应参数 + var uri = c.Request.RequestURI + var query = GetQueryParam(uri) + fmt.Println(query) + query["timestamp"] = c.GetHeader("timestamp") + query["nonce"] = c.GetHeader("nonce") + query["key"] = key + token := c.GetHeader("Authorization") + if token != "" { + // 按空格分割 + parts := strings.SplitN(token, " ", 2) + if len(parts) == 2 && parts[0] == "Bearer" { + token = parts[1] + } + } + query["token"] = token + //2.query参数按照 ASCII 码从小到大排序 + str := JoinStringsInASCII(query, "&", false, false, "") + //3.拼上密钥 + secret := "" + if InArr(c.GetHeader("platform"), []string{"android", "ios"}) { + secret = c.GetString("app_api_secret_key") + } else if c.GetHeader("platform") == "wap" { + secret = c.GetString("h5_api_secret_key") + } else { + secret = c.GetString("applet_api_secret_key") + } + str = fmt.Sprintf("%s&secret=%s", str, secret) + fmt.Println(str) + //4.md5加密 转小写 + sign := strings.ToLower(Md5(str)) + //5.判断跟前端传来的sign是否一致 + if sign != c.GetHeader("sign") { + return false + } + return true +} + +func ResultAes(c *gin.Context, raw []byte) string { + var key = c.GetHeader("key") + base, _ := php2go.Base64Decode(key) + aes, err := RsaDecrypt([]byte(base), privateKey) + if err != nil { + logx.Info(err) + return "" + } + + str, _ := openssl.AesECBEncrypt(raw, aes, openssl.PKCS7_PADDING) + value := php2go.Base64Encode(string(str)) + fmt.Println(value) + + return value +} + +func ResultAesDecrypt(c *gin.Context, raw string) string { + var key = c.GetHeader("key") + base, _ := php2go.Base64Decode(key) + aes, err := RsaDecrypt([]byte(base), privateKey) + if err != nil { + logx.Info(err) + return "" + } + fmt.Println(raw) + value1, _ := php2go.Base64Decode(raw) + if value1 == "" { + return "" + } + str1, _ := openssl.AesECBDecrypt([]byte(value1), aes, openssl.PKCS7_PADDING) + + return string(str1) +} diff --git a/app/utils/slice.go b/app/utils/slice.go new file mode 100644 index 0000000..30bd4ee --- /dev/null +++ b/app/utils/slice.go @@ -0,0 +1,26 @@ +package utils + +// ContainsString is 字符串是否包含在字符串切片里 +func ContainsString(array []string, val string) (index int) { + index = -1 + for i := 0; i < len(array); i++ { + if array[i] == val { + index = i + return + } + } + return +} + +func PaginateSliceInt64(x []int64, skip int, size int) []int64 { + if skip > len(x) { + skip = len(x) + } + + end := skip + size + if end > len(x) { + end = len(x) + } + + return x[skip:end] +} diff --git a/app/utils/slice_and_string.go b/app/utils/slice_and_string.go new file mode 100644 index 0000000..3ae6946 --- /dev/null +++ b/app/utils/slice_and_string.go @@ -0,0 +1,47 @@ +package utils + +import ( + "fmt" + "reflect" + "strings" + "unsafe" +) + +// string与slice互转,零copy省内存 + +// zero copy to change slice to string +func Slice2String(b []byte) (s string) { + pBytes := (*reflect.SliceHeader)(unsafe.Pointer(&b)) + pString := (*reflect.StringHeader)(unsafe.Pointer(&s)) + pString.Data = pBytes.Data + pString.Len = pBytes.Len + return +} + +// no copy to change string to slice +func StringToSlice(s string) (b []byte) { + pBytes := (*reflect.SliceHeader)(unsafe.Pointer(&b)) + pString := (*reflect.StringHeader)(unsafe.Pointer(&s)) + pBytes.Data = pString.Data + pBytes.Len = pString.Len + pBytes.Cap = pString.Len + return +} + +// 任意slice合并 +func SliceJoin(sep string, elems ...interface{}) string { + l := len(elems) + if l == 0 { + return "" + } + if l == 1 { + s := fmt.Sprint(elems[0]) + sLen := len(s) - 1 + if s[0] == '[' && s[sLen] == ']' { + return strings.Replace(s[1:sLen], " ", sep, -1) + } + return s + } + sep = strings.Replace(fmt.Sprint(elems), " ", sep, -1) + return sep[1 : len(sep)-1] +} diff --git a/app/utils/string.go b/app/utils/string.go new file mode 100644 index 0000000..e7142ef --- /dev/null +++ b/app/utils/string.go @@ -0,0 +1,155 @@ +package utils + +import ( + "fmt" + "github.com/syyongx/php2go" + "reflect" + "sort" + "strings" +) + +func Implode(glue string, args ...interface{}) string { + data := make([]string, len(args)) + for i, s := range args { + data[i] = fmt.Sprint(s) + } + return strings.Join(data, glue) +} + +//字符串是否在数组里 +func InArr(target string, str_array []string) bool { + for _, element := range str_array { + if target == element { + return true + } + } + return false +} + +//把数组的值放到key里 +func ArrayColumn(array interface{}, key string) (result map[string]interface{}, err error) { + result = make(map[string]interface{}) + t := reflect.TypeOf(array) + v := reflect.ValueOf(array) + if t.Kind() != reflect.Slice { + return nil, nil + } + if v.Len() == 0 { + return nil, nil + } + for i := 0; i < v.Len(); i++ { + indexv := v.Index(i) + if indexv.Type().Kind() != reflect.Struct { + return nil, nil + } + mapKeyInterface := indexv.FieldByName(key) + if mapKeyInterface.Kind() == reflect.Invalid { + return nil, nil + } + mapKeyString, err := InterfaceToString(mapKeyInterface.Interface()) + if err != nil { + return nil, err + } + result[mapKeyString] = indexv.Interface() + } + return result, err +} + +//转string +func InterfaceToString(v interface{}) (result string, err error) { + switch reflect.TypeOf(v).Kind() { + case reflect.Int64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32: + result = fmt.Sprintf("%v", v) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + result = fmt.Sprintf("%v", v) + case reflect.String: + result = v.(string) + default: + err = nil + } + return result, err +} + +func HideTrueName(name string) string { + res := "**" + if name != "" { + runs := []rune(name) + leng := len(runs) + if leng <= 3 { + res = string(runs[0:1]) + res + } else if leng < 5 { + res = string(runs[0:2]) + res + } else if leng < 10 { + res = string(runs[0:2]) + "***" + string(runs[leng-2:leng]) + } else if leng < 16 { + res = string(runs[0:3]) + "****" + string(runs[leng-3:leng]) + } else { + res = string(runs[0:4]) + "*****" + string(runs[leng-4:leng]) + } + } + return res +} +func GetQueryParam(uri string) map[string]string { + //根据问号分割路由还是query参数 + uriList := strings.Split(uri, "?") + var query = make(map[string]string, 0) + //有参数才处理 + if len(uriList) == 2 { + //分割query参数 + var queryList = strings.Split(uriList[1], "&") + if len(queryList) > 0 { + //key value 分别赋值 + for _, v := range queryList { + var valueList = strings.Split(v, "=") + if len(valueList) == 2 { + value, _ := php2go.URLDecode(valueList[1]) + if value == "" { + value = valueList[1] + } + query[valueList[0]] = value + } + } + } + } + return query +} + +//JoinStringsInASCII 按照规则,参数名ASCII码从小到大排序后拼接 +//data 待拼接的数据 +//sep 连接符 +//onlyValues 是否只包含参数值,true则不包含参数名,否则参数名和参数值均有 +//includeEmpty 是否包含空值,true则包含空值,否则不包含,注意此参数不影响参数名的存在 +//exceptKeys 被排除的参数名,不参与排序及拼接 +func JoinStringsInASCII(data map[string]string, sep string, onlyValues, includeEmpty bool, exceptKeys ...string) string { + var list []string + var keyList []string + m := make(map[string]int) + if len(exceptKeys) > 0 { + for _, except := range exceptKeys { + m[except] = 1 + } + } + for k := range data { + if _, ok := m[k]; ok { + continue + } + value := data[k] + if !includeEmpty && value == "" { + continue + } + if onlyValues { + keyList = append(keyList, k) + } else { + list = append(list, fmt.Sprintf("%s=%s", k, value)) + } + } + if onlyValues { + sort.Strings(keyList) + for _, v := range keyList { + list = append(list, AnyToString(data[v])) + } + } else { + sort.Strings(list) + } + return strings.Join(list, sep) +} diff --git a/app/utils/time.go b/app/utils/time.go new file mode 100644 index 0000000..6860a57 --- /dev/null +++ b/app/utils/time.go @@ -0,0 +1,226 @@ +package utils + +import ( + "errors" + "fmt" + "strconv" + "strings" + "time" +) + +func StrToTime(s string) (int64, error) { + // delete all not int characters + if s == "" { + return time.Now().Unix(), nil + } + r := make([]rune, 14) + l := 0 + // 过滤除数字以外的字符 + for _, v := range s { + if '0' <= v && v <= '9' { + r[l] = v + l++ + if l == 14 { + break + } + } + } + for l < 14 { + r[l] = '0' // 补0 + l++ + } + t, err := time.Parse("20060102150405", string(r)) + if err != nil { + return 0, err + } + return t.Unix(), nil +} + +func TimeToStr(unixSecTime interface{}, layout ...string) string { + i := AnyToInt64(unixSecTime) + if i == 0 { + return "" + } + f := "2006-01-02 15:04:05" + if len(layout) > 0 { + f = layout[0] + } + return time.Unix(i, 0).Format(f) +} +func Time2String(date time.Time, format string) string { + if format == "" { + format = "2006-01-02 15:04:05" + } + timeS := date.Format(format) + if timeS == "0001-01-01 00:00:00" { + return "" + } + return timeS +} + +func FormatNanoUnix() string { + return strings.Replace(time.Now().Format("20060102150405.0000000"), ".", "", 1) +} + +func TimeParse(format, src string) (time.Time, error) { + return time.ParseInLocation(format, src, time.Local) +} + +func TimeParseStd(src string) time.Time { + t, _ := TimeParse("2006-01-02 15:04:05", src) + return t +} + +func TimeStdParseUnix(src string) int64 { + t, err := TimeParse("2006-01-02 15:04:05", src) + if err != nil { + return 0 + } + return t.Unix() +} + +// 获取一个当前时间 时间间隔 时间戳 +func GetTimeInterval(unit string, amount int) (startTime, endTime int64) { + t := time.Now() + nowTime := t.Unix() + tmpTime := int64(0) + switch unit { + case "years": + tmpTime = time.Date(t.Year()+amount, t.Month(), t.Day(), t.Hour(), 0, 0, 0, t.Location()).Unix() + case "months": + tmpTime = time.Date(t.Year(), t.Month()+time.Month(amount), t.Day(), t.Hour(), 0, 0, 0, t.Location()).Unix() + case "days": + tmpTime = time.Date(t.Year(), t.Month(), t.Day()+amount, t.Hour(), 0, 0, 0, t.Location()).Unix() + case "hours": + tmpTime = time.Date(t.Year(), t.Month(), t.Day(), t.Hour()+amount, 0, 0, 0, t.Location()).Unix() + } + if amount > 0 { + startTime = nowTime + endTime = tmpTime + } else { + startTime = tmpTime + endTime = nowTime + } + return +} + +// 几天前 +func TimeInterval(newTime int) string { + now := time.Now().Unix() + newTime64 := AnyToInt64(newTime) + if newTime64 >= now { + return "刚刚" + } + interval := now - newTime64 + switch { + case interval < 60: + return AnyToString(interval) + "秒前" + case interval < 60*60: + return AnyToString(interval/60) + "分前" + case interval < 60*60*24: + return AnyToString(interval/60/60) + "小时前" + case interval < 60*60*24*30: + return AnyToString(interval/60/60/24) + "天前" + case interval < 60*60*24*30*12: + return AnyToString(interval/60/60/24/30) + "月前" + default: + return AnyToString(interval/60/60/24/30/12) + "年前" + } +} + +// 时分秒字符串转时间戳,传入示例:8:40 or 8:40:10 +func HmsToUnix(str string) (int64, error) { + t := time.Now() + arr := strings.Split(str, ":") + if len(arr) < 2 { + return 0, errors.New("Time format error") + } + h, _ := strconv.Atoi(arr[0]) + m, _ := strconv.Atoi(arr[1]) + s := 0 + if len(arr) == 3 { + s, _ = strconv.Atoi(arr[3]) + } + formatted1 := fmt.Sprintf("%d%02d%02d%02d%02d%02d", t.Year(), t.Month(), t.Day(), h, m, s) + res, err := time.ParseInLocation("20060102150405", formatted1, time.Local) + if err != nil { + return 0, err + } else { + return res.Unix(), nil + } +} + +// 获取特定时间范围 +func GetTimeRange(s string) map[string]int64 { + t := time.Now() + var stime, etime time.Time + + switch s { + case "today": + stime = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) + etime = time.Date(t.Year(), t.Month(), t.Day()+1, 0, 0, 0, 0, t.Location()) + case "yesterday": + stime = time.Date(t.Year(), t.Month(), t.Day()-1, 0, 0, 0, 0, t.Location()) + etime = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) + case "within_seven_days": + // 前6天0点 + stime = time.Date(t.Year(), t.Month(), t.Day()-6, 0, 0, 0, 0, t.Location()) + // 明天 0点 + etime = time.Date(t.Year(), t.Month(), t.Day()+1, 0, 0, 0, 0, t.Location()) + case "current_month": + stime = time.Date(t.Year(), t.Month(), 0, 0, 0, 0, 0, t.Location()) + etime = time.Date(t.Year(), t.Month()+1, 0, 0, 0, 0, 0, t.Location()) + case "last_month": + stime = time.Date(t.Year(), t.Month()-1, 0, 0, 0, 0, 0, t.Location()) + etime = time.Date(t.Year(), t.Month(), 0, 0, 0, 0, 0, t.Location()) + } + + return map[string]int64{ + "start": stime.Unix(), + "end": etime.Unix(), + } +} + +// 获取特定时间范围 +func GetDateTimeRangeStr(s string) (string, string) { + t := time.Now() + var stime, etime time.Time + + switch s { + case "today": + stime = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) + etime = time.Date(t.Year(), t.Month(), t.Day()+1, 0, 0, 0, 0, t.Location()) + case "yesterday": + stime = time.Date(t.Year(), t.Month(), t.Day()-1, 0, 0, 0, 0, t.Location()) + etime = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) + case "within_seven_days": + // 前6天0点 + stime = time.Date(t.Year(), t.Month(), t.Day()-6, 0, 0, 0, 0, t.Location()) + // 明天 0点 + etime = time.Date(t.Year(), t.Month(), t.Day()+1, 0, 0, 0, 0, t.Location()) + case "current_month": + stime = time.Date(t.Year(), t.Month(), 0, 0, 0, 0, 0, t.Location()) + etime = time.Date(t.Year(), t.Month()+1, 0, 0, 0, 0, 0, t.Location()) + case "last_month": + stime = time.Date(t.Year(), t.Month()-1, 0, 0, 0, 0, 0, t.Location()) + etime = time.Date(t.Year(), t.Month(), 0, 0, 0, 0, 0, t.Location()) + } + + return stime.Format("2006-01-02 15:04:05"), etime.Format("2006-01-02 15:04:05") +} + +//获取传入的时间所在月份的第一天,即某月第一天的0点。如传入time.Now(), 返回当前月份的第一天0点时间。 +func GetFirstDateOfMonth(d time.Time) time.Time { + d = d.AddDate(0, 0, -d.Day()+1) + return GetZeroTime(d) +} + +//获取传入的时间所在月份的最后一天,即某月最后一天的0点。如传入time.Now(), 返回当前月份的最后一天0点时间。 +func GetLastDateOfMonth(d time.Time) time.Time { + return GetFirstDateOfMonth(d).AddDate(0, 1, -1) +} + +//获取某一天的0点时间 +func GetZeroTime(d time.Time) time.Time { + return time.Date(d.Year(), d.Month(), d.Day(), 0, 0, 0, 0, d.Location()) +} diff --git a/app/utils/url.go b/app/utils/url.go new file mode 100644 index 0000000..f0c7d6b --- /dev/null +++ b/app/utils/url.go @@ -0,0 +1,16 @@ +package utils + +import ( + "net/url" + "strings" +) + +func UriFilterExcludeQueryString(uri string) string { + URL, _ := url.Parse(uri) + + clearUri := strings.ReplaceAll(uri, URL.RawQuery, "") + + clearUri = strings.TrimRight(clearUri, "?") + + return strings.TrimRight(clearUri, "/") +} diff --git a/app/utils/uuid.go b/app/utils/uuid.go new file mode 100644 index 0000000..da7018b --- /dev/null +++ b/app/utils/uuid.go @@ -0,0 +1,76 @@ +package utils + +import ( + "github.com/sony/sonyflake" + + "applet/app/utils/logx" + "fmt" + "math/rand" + "time" +) + +const ( + KC_RAND_KIND_NUM = 0 // 纯数字 + KC_RAND_KIND_LOWER = 1 // 小写字母 + KC_RAND_KIND_UPPER = 2 // 大写字母 + KC_RAND_KIND_ALL = 3 // 数字、大小写字母 +) + +func newUUID() *[16]byte { + u := &[16]byte{} + rand.Read(u[:16]) + u[8] = (u[8] | 0x80) & 0xBf + u[6] = (u[6] | 0x40) & 0x4f + return u +} + +func UUIDString() string { + u := newUUID() + return fmt.Sprintf("%x-%x-%x-%x-%x", u[:4], u[4:6], u[6:8], u[8:10], u[10:]) +} + +func UUIDHexString() string { + u := newUUID() + return fmt.Sprintf("%x%x%x%x%x", u[:4], u[4:6], u[6:8], u[8:10], u[10:]) +} +func UUIDBinString() string { + u := newUUID() + return fmt.Sprintf("%s", [16]byte(*u)) +} + +func Krand(size int, kind int) []byte { + ikind, kinds, result := kind, [][]int{[]int{10, 48}, []int{26, 97}, []int{26, 65}}, make([]byte, size) + isAll := kind > 2 || kind < 0 + rand.Seed(time.Now().UnixNano()) + for i := 0; i < size; i++ { + if isAll { // random ikind + ikind = rand.Intn(3) + } + scope, base := kinds[ikind][0], kinds[ikind][1] + result[i] = uint8(base + rand.Intn(scope)) + } + return result +} + +// OrderUUID is only num for uuid +func OrderUUID(uid int) string { + ustr := IntToStr(uid) + tstr := Int64ToStr(time.Now().Unix()) + ulen := len(ustr) + tlen := len(tstr) + rlen := 18 - ulen - tlen + krb := Krand(rlen, KC_RAND_KIND_NUM) + return ustr + tstr + string(krb) +} + +var flake *sonyflake.Sonyflake + +func GenId() int64 { + + id, err := flake.NextID() + if err != nil { + _ = logx.Errorf("flake.NextID() failed with %s\n", err) + panic(err) + } + return int64(id) +} diff --git a/app/utils/validator_err_trans.go b/app/utils/validator_err_trans.go new file mode 100644 index 0000000..29d97bf --- /dev/null +++ b/app/utils/validator_err_trans.go @@ -0,0 +1,55 @@ +package utils + +import ( + "fmt" + "github.com/gin-gonic/gin/binding" + "github.com/go-playground/locales/en" + "github.com/go-playground/locales/zh" + ut "github.com/go-playground/universal-translator" + "github.com/go-playground/validator/v10" + enTranslations "github.com/go-playground/validator/v10/translations/en" + chTranslations "github.com/go-playground/validator/v10/translations/zh" + "reflect" +) + +var ValidatorTrans ut.Translator + +// ValidatorTransInit 验证器错误信息翻译初始化 +// local 通常取决于 http 请求头的 'Accept-Language' +func ValidatorTransInit(local string) (err error) { + if v, ok := binding.Validator.Engine().(*validator.Validate); ok { + zhT := zh.New() //chinese + enT := en.New() //english + uni := ut.New(enT, zhT, enT) + + var o bool + ValidatorTrans, o = uni.GetTranslator(local) + if !o { + return fmt.Errorf("uni.GetTranslator(%s) failed", local) + } + // 注册一个方法,从自定义标签label中获取值(用在把字段名映射为中文) + v.RegisterTagNameFunc(func(field reflect.StructField) string { + label := field.Tag.Get("label") + if label == "" { + return field.Name + } + return label + }) + // 注册翻译器 + switch local { + case "en": + err = enTranslations.RegisterDefaultTranslations(v, ValidatorTrans) + case "zh": + err = chTranslations.RegisterDefaultTranslations(v, ValidatorTrans) + default: + err = enTranslations.RegisterDefaultTranslations(v, ValidatorTrans) + } + return + } + return +} + +// ValidatorTransInitZh 验证器错误信息翻译为中文初始化 +func ValidatorTransInitZh() (err error) { + return ValidatorTransInit("zh") +} diff --git a/bin/.gitignore b/bin/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/bin/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..adb5edb --- /dev/null +++ b/build.sh @@ -0,0 +1,18 @@ +echo "update -> repo" +git fetch +git reset --hard origin/master +echo "update repo -> Success" + +id="git rev-parse --short HEAD" +export ZYOS_APP_COMMIT_ID=`eval $id` +echo "GET the Commit ID for git -> $ZYOS_APP_COMMIT_ID" + +echo "Start build image " + +image_name=registry-vpc.cn-shenzhen.aliyuncs.com/fnuoos-prd/zyos-mall:${ZYOS_APP_COMMIT_ID} +#final_image_name=registry.cn-shenzhen.aliyuncs.com/fnuoos-prd/zyos:${ZYOS_APP_COMMIT_ID} +docker build -t ${image_name} . + +docker push ${image_name} +echo "Push image -> $image_name Success" +export ZYOS_APP_LATEST_VERSION=${image_name} \ No newline at end of file diff --git a/cmd/task/main.go b/cmd/task/main.go new file mode 100644 index 0000000..ec3c112 --- /dev/null +++ b/cmd/task/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" + + "applet/app/cfg" + "applet/app/db" + "applet/app/task" + "applet/app/utils" + "applet/app/utils/logx" +) + +func init() { + // 加载任务配置 + cfg.InitTaskCfg() + // 日志配置 + cfg.InitLog() + // 初始化redis + cfg.InitCache() + baseDb := *cfg.DB + baseDb.Path = fmt.Sprintf(cfg.DB.Path, cfg.DB.Name) + if err := db.InitDB(&baseDb); err != nil { + panic(err) + } + utils.CurlDebug = true + //cfg.InitMemCache() +} + +func main() { + go func() { + // 初始化jobs方法列表、添加reload方法定时更新任务 + task.Init() + task.Run() + }() + // graceful shutdown + quit := make(chan os.Signal) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + _ = logx.Info("Server exiting...") +} diff --git a/cmd_dao.bat b/cmd_dao.bat new file mode 100644 index 0000000..24a07d5 --- /dev/null +++ b/cmd_dao.bat @@ -0,0 +1,23 @@ +@echo off +setlocal + +set "BasePath=./" + +REM 假设已经提供了文件名作为参数 +set "FileName=%~1" + +REM 将参数设置最终文件名 +set "FinalFile=%BasePath%app\db\dao\%FileName%_dao.go" + +REM 将文件名转换成大驼峰格式并设置成最终接口名 +for /f "delims=" %%i in ('powershell -File "%BasePath%etc\ps\ConvertToUpperCase.ps1" -inputString "%FileName%"') do set "InterfaceName=%%i" + +REM 使用 PowerShell 替换接口名称,并指定 UTF-8 编码 +powershell -Command "(Get-Content '%BasePath%etc\template\template_interface.tpl') -replace 'DemoInterface', '%InterfaceName%' | Out-File -FilePath '%BasePath%temp_interface.go' -Encoding UTF8" + +REM 如果需要,将临时文件重命名为最终文件(取决于move Y?N) +move /Y "%BasePath%temp_interface.go" "%FinalFile%" + +echo Interface file %FileName%_dao.go generated successfully. + +endlocal diff --git a/cmd_db.bat b/cmd_db.bat new file mode 100644 index 0000000..9f052f0 --- /dev/null +++ b/cmd_db.bat @@ -0,0 +1,25 @@ +@echo off + +set Table=* +set TName="" +set one=%1 + +if "%one%" NEQ "" ( + set Table=%one% + set TName="^%one%$" +) + +set BasePath="./" +set DBUSER="root" +set DBPSW="Fnuo123com@" +set DBNAME="advertisement" +set DBHOST="119.23.182.117" +set DBPORT="3306" + +del "app\db\model\%Table%.go" + +echo start reverse table %Table% + +xorm reverse mysql "%DBUSER%:%DBPSW%@tcp(%DBHOST%:%DBPORT%)/%DBNAME%?charset=utf8" %BasePath%/etc/db_tpl %BasePath%/app/db/model/ %TName% + +echo end \ No newline at end of file diff --git a/cmd_db.sh b/cmd_db.sh new file mode 100644 index 0000000..ab769e2 --- /dev/null +++ b/cmd_db.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# 使用方法, 直接执行该脚本更新所有表, cmd_db.sh 表名, 如 ./cmd_db.sh tableName + +Table=* +TName="" +if [ "$1" ] ;then + Table=$1 + TName="^$1$" +fi + +BasePath="./" +DBUSER="root" +DBPSW="Fnuo123com@" +DBNAME="fnuoos_test1" +DBHOST="119.23.182.117" +DBPORT="3306" + +rm -rf $BasePath/app/db/model/$Table.go && \ + +xorm reverse mysql "$DBUSER:$DBPSW@tcp($DBHOST:$DBPORT)/$DBNAME?charset=utf8" $BasePath/etc/db_tpl $BasePath/app/db/model/ $TName \ No newline at end of file diff --git a/cmd_implement.bat b/cmd_implement.bat new file mode 100644 index 0000000..65688b0 --- /dev/null +++ b/cmd_implement.bat @@ -0,0 +1,23 @@ +@echo off +setlocal + +set "BasePath=./" + +REM 假设已经提供了文件名作为参数 +set "FileName=%~1" + +REM 将参数设置最终文件名 +set "FinalFile=%BasePath%app\db\implement\%FileName%_dao.go" + +REM 将文件名转换成大驼峰格式并设置成最终实现类名 +for /f "delims=" %%i in ('powershell -File "%BasePath%etc\ps\ConvertToUpperCase.ps1" -inputString "%FileName%"') do set "ImplementName=%%i" + +REM 使用 PowerShell 替换接口名称,并指定 UTF-8 编码 +powershell -Command "(Get-Content '%BasePath%etc\template\template_implement.tpl') -replace 'DemoImplement', '%ImplementName%' | Out-File -FilePath '%BasePath%temp_implement.go' -Encoding UTF8" + +REM 如果需要,将临时文件重命名为最终文件(取决于move Y?N) +move /Y "%BasePath%temp_implement.go" "%FinalFile%" + +echo Implement file %FileName%_implement.go generated successfully. + +endlocal diff --git a/cmd_run.bat b/cmd_run.bat new file mode 100644 index 0000000..51d7b81 --- /dev/null +++ b/cmd_run.bat @@ -0,0 +1,12 @@ +@echo off + +set BasePath=%~dp0 +set APP=applet.exe +set CfgPath=%BasePath%\etc\cfg.yml + +del %BasePath%\bin\%APP% + +go build -o %BasePath%\bin\%APP% %BasePath%\cmd\main.go && %BasePath%\bin\%APP% -c=%CfgPath% + + +pause diff --git a/cmd_run.sh b/cmd_run.sh new file mode 100644 index 0000000..6758f1b --- /dev/null +++ b/cmd_run.sh @@ -0,0 +1,8 @@ +#!/bin/bash +APP=applet +BasePath=$(dirname $(readlink -f $0)) +CfgPath=$BasePath/etc/cfg.yml +cd $BasePath +rm -rf $BasePath/bin/$APP +go build -o $BasePath/bin/$APP $BasePath/main.go \ +&& $BasePath/bin/$APP -c=$CfgPath \ No newline at end of file diff --git a/cmd_task.bat b/cmd_task.bat new file mode 100644 index 0000000..f70eabc --- /dev/null +++ b/cmd_task.bat @@ -0,0 +1,13 @@ +@echo off + +set Name=task +set BasePath=%~dp0 +set APP=%Name%.exe +set CfgPath=%BasePath%etc\%Name%.yml + +del %BasePath%\bin\%APP% + +go build -o %BasePath%\bin\%APP% %BasePath%\cmd\%Name%\main.go && %BasePath%\bin\%APP% -c=%CfgPath% + + +pause diff --git a/cmd_task.sh b/cmd_task.sh new file mode 100644 index 0000000..8f7da10 --- /dev/null +++ b/cmd_task.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +APP=task +BasePath=$(dirname $(readlink -f $0)) +CfgPath=$BasePath/etc/task.yml +cd $BasePath +rm -rf $BasePath/bin/$APP +go build -o $BasePath/bin/$APP $BasePath/cmd/$APP/main.go \ +&& $BasePath/bin/$APP -c=$CfgPath \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..3e7d945 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,1708 @@ +// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT +// This file was generated by swaggo/swag + +package docs + +import ( + "bytes" + "encoding/json" + "strings" + + "github.com/alecthomas/template" + "github.com/swaggo/swag" +) + +var doc = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{.Description}}", + "title": "{{.Title}}", + "termsOfService": "智莺生活后端组", + "contact": { + "name": "sherlockwhite" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/v1/acq/fix": { + "get": { + "description": "拉新活动--fix", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "拉新活动" + ], + "summary": "拉新活动--fix", + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/acquisition/home": { + "post": { + "description": "本期榜单/上期榜单/我的邀请人数和奖励/任务列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "拉新活动" + ], + "summary": "拉新--首页数据", + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/acquisition/qrcode": { + "get": { + "description": "二维码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "拉新活动" + ], + "summary": "拉新--邀请二维码", + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/acquisition/reward/detail": { + "post": { + "description": "拉新活动--我的奖励明细", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "拉新活动" + ], + "summary": "拉新活动--我的奖励明细", + "parameters": [ + { + "description": "1为以发放,2为待发放,3为失效", + "name": "state", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/acquisition/reward_receive": { + "post": { + "description": "拉新活动--领取奖励", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "拉新活动" + ], + "summary": "拉新活动--领取奖励", + "parameters": [ + { + "description": "任务ID", + "name": "job_id", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/credit/card/config": { + "get": { + "description": "获取信用卡配置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "信用卡" + ], + "summary": "获取信用卡配置", + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/duomai/mall/detail": { + "post": { + "description": "多麦商城--商城详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "多麦商城" + ], + "summary": "多麦商城--商城详情", + "parameters": [ + { + "description": "商城id", + "name": "brand_id", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/duomai/mall/home": { + "get": { + "description": "多麦商城--首页数据", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "多麦商城" + ], + "summary": "多麦商城--首页数据", + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/duomai/mall/search": { + "post": { + "description": "多麦商城--搜索", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "多麦商城" + ], + "summary": "多麦商城--搜索", + "parameters": [ + { + "description": "搜索关键词", + "name": "key", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/duomai/mall/update": { + "get": { + "description": "多麦商城--更新数据", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "多麦商城" + ], + "summary": "多麦商城--更新数据", + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/logistic/query": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "物流" + ], + "summary": "快递100物流查询", + "parameters": [ + { + "description": "logisticQueryReq", + "name": "req", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/hdl.logisticQueryReq" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + }, + "400": { + "description": "{\"code\":400000,\"data\":[],\"msg\":\"该快递公司不支持查询\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/mod/pub.flutter.duomai.mall.detail.page": { + "get": { + "description": "多麦商城详情页样式", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "多麦商城" + ], + "summary": "多麦商城详情页样式", + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/mod/pub.flutter.duomai.mall.home.page": { + "get": { + "description": "多麦商城首页样式", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "多麦商城" + ], + "summary": "多麦商城首页样式", + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/mod/pub.flutter.duomai.mall.search.page": { + "get": { + "description": "多麦商城搜索页样式", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "多麦商城" + ], + "summary": "多麦商城搜索页样式", + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/privilege/card/activation": { + "post": { + "description": "权益卡激活", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "权益卡" + ], + "summary": "权益卡激活", + "parameters": [ + { + "description": "json", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/hdl.privilegeOpenCardCheckReq" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/privilege/open_card/check": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "权益卡" + ], + "summary": "权益卡卡号卡密检测", + "parameters": [ + { + "description": "json", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/hdl.privilegeOpenCardCheckReq" + } + } + ], + "responses": { + "200": { + "description": "0:不存在 1:已经被使用 2:可用", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/privilege/open_card/order_query": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "权益卡" + ], + "summary": "权益卡开卡订单查询页面", + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/hdl.PrivilegeOpenCardOrdQueryPageResp" + } + }, + "400": { + "description": "{\"code\":400000,\"data\":[],\"msg\":\"验证码错误\"}", + "schema": { + "type": "string" + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "权益卡" + ], + "summary": "权益卡开卡订单查询", + "parameters": [ + { + "description": "json", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/hdl.PrivilegeOpenCardOrdQueryReq" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/model.PrivilegeOpenCardOrd" + } + }, + "400": { + "description": "{\"code\":400000,\"data\":[],\"msg\":\"验证码错误\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/privilege/open_card/order_suc": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "权益卡" + ], + "summary": "订单支付成功页面", + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/model.PrivilegeOpenCardOrd" + } + } + } + } + }, + "/api/v1/privilege/open_card/pay_page": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "权益卡" + ], + "summary": "权益卡开卡支付页面", + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/hdl.privilegeOpenCardPayPageResp" + } + }, + "400": { + "description": "{\"code\":400000,\"data\":[],\"msg\":\"权益卡配置缺失\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/shake_ticket/:goods_id/:type": { + "get": { + "description": "收藏/领券买/分享赚", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "抖券" + ], + "summary": "抖券商品收藏/领券买/分享赚", + "parameters": [ + { + "type": "string", + "description": "商品id", + "name": "goods_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "类型(0取消收藏,1收藏,2点击 领券买,3点击分享赚)", + "name": "type", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/shake_ticket_list": { + "get": { + "description": "定向计划/高佣专场/精选低价包邮/偏远地区包邮/抖货商品/各大榜单商品/今日值得买", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "抖券" + ], + "summary": "抖券商品推荐列表", + "parameters": [ + { + "type": "string", + "description": "页码", + "name": "page", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "页数", + "name": "page_size", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "分类id(3定向计划/4高佣专场/5精选低价包邮/6偏远地区包邮/7抖货商品/8各大榜单商品/9今日值得买)", + "name": "category_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "渠道", + "name": "pvd", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/md.IndexRecommendList" + } + } + } + } + }, + "/api/v1/sign/fast/in": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "登录、注册" + ], + "summary": "用户手机快速登录", + "parameters": [ + { + "description": "json", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/md.FastLoginRequestBody" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/md.LoginResponse" + } + }, + "400": { + "description": "{\"code\":400001,\"data\":[],\"msg\":\"请求参数错误\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/sub_region_list": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "地址" + ], + "summary": "获取区域id下的区域", + "parameters": [ + { + "type": "string", + "description": "上级地区类型:root(查询省级列表)、province(省级ID下的城市)、city(市级id下的区域)", + "name": "parent", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "上级地区id", + "name": "id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/hdl.resultItem" + } + } + }, + "400": { + "description": "{\"code\":400001,\"data\":[],\"msg\":\"请求参数错误\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/user/address/:id": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "地址" + ], + "summary": "用户地址详情", + "parameters": [ + { + "type": "string", + "description": "地址id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/model.UserAddress" + } + }, + "400": { + "description": "{\"code\":400000,\"data\":[],\"msg\":\"地址不存在\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/user/address/delete/:id": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "地址" + ], + "summary": "用户地址删除", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + }, + "400": { + "description": "{\"code\":400000,\"data\":[],\"msg\":\"地址不存在\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/user/address/update": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "地址" + ], + "summary": "用户地址新增、编辑", + "parameters": [ + { + "description": "json参数,Id不传为新增", + "name": "\"\"", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/hdl.updateAddressReq" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + }, + "400": { + "description": "{\"code\":400000,\"data\":[],\"msg\":\"地址不存在\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/user/addresses": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "地址" + ], + "summary": "用户地址列表", + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/model.UserAddress" + } + } + }, + "500": { + "description": "{\"code\":500000,\"data\":[],\"msg\":\"数据库操作失败\"}", + "schema": { + "type": "string" + } + } + } + } + } + }, + "definitions": { + "hdl.PrivilegeOpenCardOrdQueryPageResp": { + "type": "object", + "properties": { + "btn_bg_color_1": { + "type": "string" + }, + "btn_bg_color_2": { + "type": "string" + }, + "btn_text": { + "type": "string" + }, + "btn_text_color": { + "type": "string" + }, + "header_img": { + "type": "string" + }, + "logistic_company": { + "type": "array", + "items": { + "$ref": "#/definitions/model.LogisticCompany" + } + } + } + }, + "hdl.PrivilegeOpenCardOrdQueryReq": { + "type": "object", + "required": [ + "com", + "num" + ], + "properties": { + "com": { + "description": "快递公司名称", + "type": "string" + }, + "num": { + "description": "快递单号", + "type": "string" + } + } + }, + "hdl.logisticQueryReq": { + "type": "object", + "required": [ + "com", + "num" + ], + "properties": { + "com": { + "description": "快递公司名称", + "type": "string" + }, + "num": { + "description": "快递单号", + "type": "string" + } + } + }, + "hdl.privilegeOpenCardCheckReq": { + "type": "object", + "required": [ + "key", + "num" + ], + "properties": { + "key": { + "description": "卡密", + "type": "string" + }, + "num": { + "description": "卡号", + "type": "string" + } + } + }, + "hdl.privilegeOpenCardPayPageResp": { + "type": "object", + "properties": { + "amount": { + "description": "付费金额", + "type": "string" + }, + "card_type": { + "description": "卡的类型:\"1\"实体卡 \"2\"虚拟卡", + "type": "string" + }, + "date_type": { + "description": "日期类型:month:月 season:季 year:年 forever:永久", + "type": "string" + }, + "page_style": { + "description": "页面样式", + "$ref": "#/definitions/hdl.privilegeOpenCardPayStyle" + } + } + }, + "hdl.privilegeOpenCardPayStyle": { + "type": "object", + "properties": { + "exclusive_privilege": { + "description": "专属特权", + "type": "array", + "items": { + "type": "object", + "properties": { + "brand_id": { + "type": "string" + }, + "brand_img": { + "type": "string" + }, + "brand_img_url": { + "type": "string" + }, + "brand_name": { + "type": "string" + }, + "is_show": { + "type": "string" + }, + "sub_title": { + "type": "string" + } + } + } + }, + "payment_btn": { + "description": "底部支付按钮", + "type": "array", + "items": { + "type": "object", + "properties": { + "bg_img": { + "type": "string" + }, + "bg_img_url": { + "type": "string" + }, + "func": { + "type": "string" + }, + "name": { + "type": "string" + }, + "text": { + "type": "string" + }, + "text_color": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + }, + "payment_choose_icon": { + "description": "支付方式选中、未选中图标", + "type": "array", + "items": { + "type": "object", + "properties": { + "icon": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + }, + "payment_style": { + "description": "支付方式", + "type": "array", + "items": { + "type": "object", + "properties": { + "icon": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + }, + "style": { + "description": "头部样式", + "type": "object", + "properties": { + "header_bg_img": { + "type": "string" + }, + "header_bg_img_url": { + "type": "string" + }, + "special_deals_img": { + "type": "string" + }, + "special_deals_img_url": { + "type": "string" + }, + "special_deals_text": { + "type": "string" + } + } + } + } + }, + "hdl.resultItem": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "440100000000" + }, + "name": { + "type": "string", + "example": "city" + } + } + }, + "hdl.updateAddressReq": { + "type": "object", + "required": [ + "city_id", + "county_id", + "detail", + "phone", + "province_id", + "receiver" + ], + "properties": { + "city_id": { + "type": "string" + }, + "county_id": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "phone": { + "type": "string" + }, + "post_code": { + "type": "string" + }, + "province_id": { + "type": "string" + }, + "receiver": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "md.Apple": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, + "md.FastLoginRequestBody": { + "type": "object", + "properties": { + "apple": { + "$ref": "#/definitions/md.Apple" + }, + "captcha": { + "type": "string" + }, + "is_not_create": { + "type": "string" + }, + "mobile": { + "type": "string" + }, + "parent_uid": { + "type": "string" + }, + "qq": { + "$ref": "#/definitions/md.QQLogin" + }, + "return_user_msg": { + "type": "string" + }, + "taobao": { + "$ref": "#/definitions/md.TaobaoLogin" + }, + "wechat": { + "$ref": "#/definitions/md.WeChat" + }, + "wechat_mini": { + "$ref": "#/definitions/md.WeChatMiniApp" + }, + "zone": { + "type": "string" + } + } + }, + "md.IndexRecommendList": { + "type": "object", + "properties": { + "good": { + "type": "array", + "items": { + "$ref": "#/definitions/md.RecommendGood" + } + }, + "provider": { + "description": "BarTitleList []BarTitle ` + "`" + `json:\"bar_title_list\"` + "`" + `", + "type": "string" + } + } + }, + "md.LoginResponse": { + "type": "object", + "properties": { + "bind_phone_enable": { + "type": "string" + }, + "is_pid": { + "type": "string" + }, + "perms": { + "type": "array", + "items": { + "type": "string" + } + }, + "phone": { + "type": "string" + }, + "register_invite_code_enable": { + "type": "string" + }, + "register_popup_condition": { + "description": "弹出类型设置", + "$ref": "#/definitions/md.RegisterPopupCondition" + }, + "token": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "wechat_applet_open_id": { + "type": "string" + }, + "wechat_union_id": { + "type": "string" + } + } + }, + "md.Marquee": { + "type": "object", + "properties": { + "avatar_url": { + "type": "string" + }, + "content": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "md.ProductDetailResponse": { + "type": "object", + "properties": { + "commission": { + "type": "string" + }, + "coupon_price": { + "type": "string" + }, + "good_id": { + "type": "string" + }, + "mod_list": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "provider": { + "type": "string" + }, + "provider_name": { + "type": "string" + }, + "shop_avatar": { + "type": "string" + } + } + }, + "md.QQLogin": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "avatar_url": { + "type": "string" + }, + "city": { + "type": "string" + }, + "expires_in": { + "type": "string" + }, + "gender": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "open_id": { + "type": "string" + }, + "pay_token": { + "type": "string" + }, + "pf": { + "type": "string" + }, + "pf_key": { + "type": "string" + }, + "province": { + "type": "string" + }, + "ret": { + "type": "string" + }, + "unionid": { + "type": "string" + } + } + }, + "md.RecommendGood": { + "type": "object", + "properties": { + "commission": { + "type": "string" + }, + "coupon": { + "type": "string" + }, + "current_price": { + "type": "string" + }, + "detail_data": { + "$ref": "#/definitions/md.ProductDetailResponse" + }, + "good_id": { + "type": "string" + }, + "good_image": { + "type": "string" + }, + "good_title": { + "type": "string" + }, + "inorder_count": { + "type": "string" + }, + "is_collect": { + "type": "integer" + }, + "is_coupons": { + "type": "integer" + }, + "is_share": { + "type": "integer" + }, + "market_price": { + "type": "string" + }, + "marquee_list": { + "type": "array", + "items": { + "$ref": "#/definitions/md.Marquee" + } + }, + "provider": { + "type": "string" + }, + "provider_name": { + "type": "string" + }, + "pvd": { + "type": "string" + }, + "shop_avatar": { + "type": "string" + }, + "shop_name": { + "type": "string" + }, + "video": { + "type": "string" + } + } + }, + "md.RegisterPopupCondition": { + "type": "object", + "properties": { + "invite_code": { + "description": "邀请码设置:弹出类型是激活码的时候起作用", + "type": "object", + "properties": { + "popup": { + "description": "是否弹出 “0”否 “1”是", + "type": "string" + }, + "should_input": { + "description": "是否必填 “0”否 “1”是", + "type": "string" + } + } + }, + "popup_type": { + "description": "弹出类型:“0”关闭 ”1”激活码 “2”邀请码", + "type": "string" + }, + "should_input": { + "description": "是否必填 “0”否 “1”是", + "type": "string" + } + } + }, + "md.TaobaoLogin": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "auth_code": { + "type": "string" + }, + "avatar_url": { + "type": "string" + }, + "nick_name": { + "type": "string" + }, + "open_id": { + "type": "string" + }, + "sid": { + "type": "string" + } + } + }, + "md.WeChat": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "avatar_url": { + "type": "string" + }, + "city": { + "type": "string" + }, + "expires_in": { + "type": "string" + }, + "gender": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "open_id": { + "type": "string" + }, + "pay_token": { + "type": "string" + }, + "pf": { + "type": "string" + }, + "pf_key": { + "type": "string" + }, + "province": { + "type": "string" + }, + "ret": { + "type": "string" + }, + "unionid": { + "type": "string" + } + } + }, + "md.WeChatMiniApp": { + "type": "object", + "properties": { + "avatar": { + "type": "string" + }, + "code": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "open_id": { + "type": "string" + }, + "unionid": { + "type": "string" + } + } + }, + "model.LogisticCompany": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "model.PrivilegeOpenCardOrd": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "after_sale_id": { + "type": "integer" + }, + "card_key": { + "type": "string" + }, + "card_num": { + "type": "string" + }, + "card_type": { + "type": "integer" + }, + "cost_price": { + "type": "string" + }, + "create_time": { + "type": "string" + }, + "date_type": { + "type": "integer" + }, + "given_data": { + "type": "string" + }, + "logistic_company": { + "type": "string" + }, + "logistic_num": { + "type": "string" + }, + "ord_id": { + "type": "integer" + }, + "pay_channel": { + "type": "integer" + }, + "pay_time": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "receiver": { + "type": "string" + }, + "settle_at": { + "type": "integer" + }, + "state": { + "type": "integer" + }, + "uid": { + "type": "integer" + }, + "update_time": { + "type": "string" + } + } + }, + "model.UserAddress": { + "type": "object", + "properties": { + "city_id": { + "type": "string" + }, + "city_name": { + "type": "string" + }, + "county_id": { + "type": "string" + }, + "county_name": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "phone": { + "type": "string" + }, + "post_code": { + "type": "string" + }, + "province_id": { + "type": "string" + }, + "province_name": { + "type": "string" + }, + "receiver": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "uid": { + "type": "integer" + } + } + } + }, + "securityDefinitions": { + "MasterID": { + "type": "apiKey", + "name": "MasterID", + "in": "header" + } + } +}` + +type swaggerInfo struct { + Version string + Host string + BasePath string + Schemes []string + Title string + Description string +} + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = swaggerInfo{ + Version: "1.0", + Host: "localhost:5000", + BasePath: "/", + Schemes: []string{}, + Title: "智莺生活移动端接口", + Description: "移动端接口", +} + +type s struct{} + +func (s *s) ReadDoc() string { + sInfo := SwaggerInfo + sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1) + + t, err := template.New("swagger_info").Funcs(template.FuncMap{ + "marshal": func(v interface{}) string { + a, _ := json.Marshal(v) + return string(a) + }, + }).Parse(doc) + if err != nil { + return doc + } + + var tpl bytes.Buffer + if err := t.Execute(&tpl, sInfo); err != nil { + return doc + } + + return tpl.String() +} + +func init() { + swag.Register(swag.Name, &s{}) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..527c14e --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,1646 @@ +{ + "swagger": "2.0", + "info": { + "description": "移动端接口", + "title": "智莺生活移动端接口", + "termsOfService": "智莺生活后端组", + "contact": { + "name": "sherlockwhite" + }, + "version": "1.0" + }, + "host": "localhost:5000", + "basePath": "/", + "paths": { + "/api/v1/acq/fix": { + "get": { + "description": "拉新活动--fix", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "拉新活动" + ], + "summary": "拉新活动--fix", + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/acquisition/home": { + "post": { + "description": "本期榜单/上期榜单/我的邀请人数和奖励/任务列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "拉新活动" + ], + "summary": "拉新--首页数据", + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/acquisition/qrcode": { + "get": { + "description": "二维码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "拉新活动" + ], + "summary": "拉新--邀请二维码", + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/acquisition/reward/detail": { + "post": { + "description": "拉新活动--我的奖励明细", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "拉新活动" + ], + "summary": "拉新活动--我的奖励明细", + "parameters": [ + { + "description": "1为以发放,2为待发放,3为失效", + "name": "state", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/acquisition/reward_receive": { + "post": { + "description": "拉新活动--领取奖励", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "拉新活动" + ], + "summary": "拉新活动--领取奖励", + "parameters": [ + { + "description": "任务ID", + "name": "job_id", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/credit/card/config": { + "get": { + "description": "获取信用卡配置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "信用卡" + ], + "summary": "获取信用卡配置", + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/duomai/mall/detail": { + "post": { + "description": "多麦商城--商城详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "多麦商城" + ], + "summary": "多麦商城--商城详情", + "parameters": [ + { + "description": "商城id", + "name": "brand_id", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/duomai/mall/home": { + "get": { + "description": "多麦商城--首页数据", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "多麦商城" + ], + "summary": "多麦商城--首页数据", + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/duomai/mall/search": { + "post": { + "description": "多麦商城--搜索", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "多麦商城" + ], + "summary": "多麦商城--搜索", + "parameters": [ + { + "description": "搜索关键词", + "name": "key", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/duomai/mall/update": { + "get": { + "description": "多麦商城--更新数据", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "多麦商城" + ], + "summary": "多麦商城--更新数据", + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/logistic/query": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "物流" + ], + "summary": "快递100物流查询", + "parameters": [ + { + "description": "logisticQueryReq", + "name": "req", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/hdl.logisticQueryReq" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + }, + "400": { + "description": "{\"code\":400000,\"data\":[],\"msg\":\"该快递公司不支持查询\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/mod/pub.flutter.duomai.mall.detail.page": { + "get": { + "description": "多麦商城详情页样式", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "多麦商城" + ], + "summary": "多麦商城详情页样式", + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/mod/pub.flutter.duomai.mall.home.page": { + "get": { + "description": "多麦商城首页样式", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "多麦商城" + ], + "summary": "多麦商城首页样式", + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/mod/pub.flutter.duomai.mall.search.page": { + "get": { + "description": "多麦商城搜索页样式", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "多麦商城" + ], + "summary": "多麦商城搜索页样式", + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/privilege/card/activation": { + "post": { + "description": "权益卡激活", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "权益卡" + ], + "summary": "权益卡激活", + "parameters": [ + { + "description": "json", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/hdl.privilegeOpenCardCheckReq" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/privilege/open_card/check": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "权益卡" + ], + "summary": "权益卡卡号卡密检测", + "parameters": [ + { + "description": "json", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/hdl.privilegeOpenCardCheckReq" + } + } + ], + "responses": { + "200": { + "description": "0:不存在 1:已经被使用 2:可用", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/privilege/open_card/order_query": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "权益卡" + ], + "summary": "权益卡开卡订单查询页面", + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/hdl.PrivilegeOpenCardOrdQueryPageResp" + } + }, + "400": { + "description": "{\"code\":400000,\"data\":[],\"msg\":\"验证码错误\"}", + "schema": { + "type": "string" + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "权益卡" + ], + "summary": "权益卡开卡订单查询", + "parameters": [ + { + "description": "json", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/hdl.PrivilegeOpenCardOrdQueryReq" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/model.PrivilegeOpenCardOrd" + } + }, + "400": { + "description": "{\"code\":400000,\"data\":[],\"msg\":\"验证码错误\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/privilege/open_card/order_suc": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "权益卡" + ], + "summary": "订单支付成功页面", + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/model.PrivilegeOpenCardOrd" + } + } + } + } + }, + "/api/v1/privilege/open_card/pay_page": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "权益卡" + ], + "summary": "权益卡开卡支付页面", + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/hdl.privilegeOpenCardPayPageResp" + } + }, + "400": { + "description": "{\"code\":400000,\"data\":[],\"msg\":\"权益卡配置缺失\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/shake_ticket/:goods_id/:type": { + "get": { + "description": "收藏/领券买/分享赚", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "抖券" + ], + "summary": "抖券商品收藏/领券买/分享赚", + "parameters": [ + { + "type": "string", + "description": "商品id", + "name": "goods_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "类型(0取消收藏,1收藏,2点击 领券买,3点击分享赚)", + "name": "type", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/shake_ticket_list": { + "get": { + "description": "定向计划/高佣专场/精选低价包邮/偏远地区包邮/抖货商品/各大榜单商品/今日值得买", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "抖券" + ], + "summary": "抖券商品推荐列表", + "parameters": [ + { + "type": "string", + "description": "页码", + "name": "page", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "页数", + "name": "page_size", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "分类id(3定向计划/4高佣专场/5精选低价包邮/6偏远地区包邮/7抖货商品/8各大榜单商品/9今日值得买)", + "name": "category_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "渠道", + "name": "pvd", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/md.IndexRecommendList" + } + } + } + } + }, + "/api/v1/sign/fast/in": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "登录、注册" + ], + "summary": "用户手机快速登录", + "parameters": [ + { + "description": "json", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/md.FastLoginRequestBody" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/md.LoginResponse" + } + }, + "400": { + "description": "{\"code\":400001,\"data\":[],\"msg\":\"请求参数错误\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/sub_region_list": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "地址" + ], + "summary": "获取区域id下的区域", + "parameters": [ + { + "type": "string", + "description": "上级地区类型:root(查询省级列表)、province(省级ID下的城市)、city(市级id下的区域)", + "name": "parent", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "上级地区id", + "name": "id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/hdl.resultItem" + } + } + }, + "400": { + "description": "{\"code\":400001,\"data\":[],\"msg\":\"请求参数错误\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/user/address/:id": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "地址" + ], + "summary": "用户地址详情", + "parameters": [ + { + "type": "string", + "description": "地址id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/model.UserAddress" + } + }, + "400": { + "description": "{\"code\":400000,\"data\":[],\"msg\":\"地址不存在\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/user/address/delete/:id": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "地址" + ], + "summary": "用户地址删除", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + }, + "400": { + "description": "{\"code\":400000,\"data\":[],\"msg\":\"地址不存在\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/user/address/update": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "地址" + ], + "summary": "用户地址新增、编辑", + "parameters": [ + { + "description": "json参数,Id不传为新增", + "name": "\"\"", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/hdl.updateAddressReq" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + }, + "400": { + "description": "{\"code\":400000,\"data\":[],\"msg\":\"地址不存在\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/user/addresses": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "地址" + ], + "summary": "用户地址列表", + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/model.UserAddress" + } + } + }, + "500": { + "description": "{\"code\":500000,\"data\":[],\"msg\":\"数据库操作失败\"}", + "schema": { + "type": "string" + } + } + } + } + } + }, + "definitions": { + "hdl.PrivilegeOpenCardOrdQueryPageResp": { + "type": "object", + "properties": { + "btn_bg_color_1": { + "type": "string" + }, + "btn_bg_color_2": { + "type": "string" + }, + "btn_text": { + "type": "string" + }, + "btn_text_color": { + "type": "string" + }, + "header_img": { + "type": "string" + }, + "logistic_company": { + "type": "array", + "items": { + "$ref": "#/definitions/model.LogisticCompany" + } + } + } + }, + "hdl.PrivilegeOpenCardOrdQueryReq": { + "type": "object", + "required": [ + "com", + "num" + ], + "properties": { + "com": { + "description": "快递公司名称", + "type": "string" + }, + "num": { + "description": "快递单号", + "type": "string" + } + } + }, + "hdl.logisticQueryReq": { + "type": "object", + "required": [ + "com", + "num" + ], + "properties": { + "com": { + "description": "快递公司名称", + "type": "string" + }, + "num": { + "description": "快递单号", + "type": "string" + } + } + }, + "hdl.privilegeOpenCardCheckReq": { + "type": "object", + "required": [ + "key", + "num" + ], + "properties": { + "key": { + "description": "卡密", + "type": "string" + }, + "num": { + "description": "卡号", + "type": "string" + } + } + }, + "hdl.privilegeOpenCardPayPageResp": { + "type": "object", + "properties": { + "amount": { + "description": "付费金额", + "type": "string" + }, + "card_type": { + "description": "卡的类型:\"1\"实体卡 \"2\"虚拟卡", + "type": "string" + }, + "date_type": { + "description": "日期类型:month:月 season:季 year:年 forever:永久", + "type": "string" + }, + "page_style": { + "description": "页面样式", + "$ref": "#/definitions/hdl.privilegeOpenCardPayStyle" + } + } + }, + "hdl.privilegeOpenCardPayStyle": { + "type": "object", + "properties": { + "exclusive_privilege": { + "description": "专属特权", + "type": "array", + "items": { + "type": "object", + "properties": { + "brand_id": { + "type": "string" + }, + "brand_img": { + "type": "string" + }, + "brand_img_url": { + "type": "string" + }, + "brand_name": { + "type": "string" + }, + "is_show": { + "type": "string" + }, + "sub_title": { + "type": "string" + } + } + } + }, + "payment_btn": { + "description": "底部支付按钮", + "type": "array", + "items": { + "type": "object", + "properties": { + "bg_img": { + "type": "string" + }, + "bg_img_url": { + "type": "string" + }, + "func": { + "type": "string" + }, + "name": { + "type": "string" + }, + "text": { + "type": "string" + }, + "text_color": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + }, + "payment_choose_icon": { + "description": "支付方式选中、未选中图标", + "type": "array", + "items": { + "type": "object", + "properties": { + "icon": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + }, + "payment_style": { + "description": "支付方式", + "type": "array", + "items": { + "type": "object", + "properties": { + "icon": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + }, + "style": { + "description": "头部样式", + "type": "object", + "properties": { + "header_bg_img": { + "type": "string" + }, + "header_bg_img_url": { + "type": "string" + }, + "special_deals_img": { + "type": "string" + }, + "special_deals_img_url": { + "type": "string" + }, + "special_deals_text": { + "type": "string" + } + } + } + } + }, + "hdl.resultItem": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "440100000000" + }, + "name": { + "type": "string", + "example": "city" + } + } + }, + "hdl.updateAddressReq": { + "type": "object", + "required": [ + "city_id", + "county_id", + "detail", + "phone", + "province_id", + "receiver" + ], + "properties": { + "city_id": { + "type": "string" + }, + "county_id": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "phone": { + "type": "string" + }, + "post_code": { + "type": "string" + }, + "province_id": { + "type": "string" + }, + "receiver": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "md.Apple": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, + "md.FastLoginRequestBody": { + "type": "object", + "properties": { + "apple": { + "$ref": "#/definitions/md.Apple" + }, + "captcha": { + "type": "string" + }, + "is_not_create": { + "type": "string" + }, + "mobile": { + "type": "string" + }, + "parent_uid": { + "type": "string" + }, + "qq": { + "$ref": "#/definitions/md.QQLogin" + }, + "return_user_msg": { + "type": "string" + }, + "taobao": { + "$ref": "#/definitions/md.TaobaoLogin" + }, + "wechat": { + "$ref": "#/definitions/md.WeChat" + }, + "wechat_mini": { + "$ref": "#/definitions/md.WeChatMiniApp" + }, + "zone": { + "type": "string" + } + } + }, + "md.IndexRecommendList": { + "type": "object", + "properties": { + "good": { + "type": "array", + "items": { + "$ref": "#/definitions/md.RecommendGood" + } + }, + "provider": { + "description": "BarTitleList []BarTitle `json:\"bar_title_list\"`", + "type": "string" + } + } + }, + "md.LoginResponse": { + "type": "object", + "properties": { + "bind_phone_enable": { + "type": "string" + }, + "is_pid": { + "type": "string" + }, + "perms": { + "type": "array", + "items": { + "type": "string" + } + }, + "phone": { + "type": "string" + }, + "register_invite_code_enable": { + "type": "string" + }, + "register_popup_condition": { + "description": "弹出类型设置", + "$ref": "#/definitions/md.RegisterPopupCondition" + }, + "token": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "wechat_applet_open_id": { + "type": "string" + }, + "wechat_union_id": { + "type": "string" + } + } + }, + "md.Marquee": { + "type": "object", + "properties": { + "avatar_url": { + "type": "string" + }, + "content": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "md.ProductDetailResponse": { + "type": "object", + "properties": { + "commission": { + "type": "string" + }, + "coupon_price": { + "type": "string" + }, + "good_id": { + "type": "string" + }, + "mod_list": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "provider": { + "type": "string" + }, + "provider_name": { + "type": "string" + }, + "shop_avatar": { + "type": "string" + } + } + }, + "md.QQLogin": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "avatar_url": { + "type": "string" + }, + "city": { + "type": "string" + }, + "expires_in": { + "type": "string" + }, + "gender": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "open_id": { + "type": "string" + }, + "pay_token": { + "type": "string" + }, + "pf": { + "type": "string" + }, + "pf_key": { + "type": "string" + }, + "province": { + "type": "string" + }, + "ret": { + "type": "string" + }, + "unionid": { + "type": "string" + } + } + }, + "md.RecommendGood": { + "type": "object", + "properties": { + "commission": { + "type": "string" + }, + "coupon": { + "type": "string" + }, + "current_price": { + "type": "string" + }, + "detail_data": { + "$ref": "#/definitions/md.ProductDetailResponse" + }, + "good_id": { + "type": "string" + }, + "good_image": { + "type": "string" + }, + "good_title": { + "type": "string" + }, + "inorder_count": { + "type": "string" + }, + "is_collect": { + "type": "integer" + }, + "is_coupons": { + "type": "integer" + }, + "is_share": { + "type": "integer" + }, + "market_price": { + "type": "string" + }, + "marquee_list": { + "type": "array", + "items": { + "$ref": "#/definitions/md.Marquee" + } + }, + "provider": { + "type": "string" + }, + "provider_name": { + "type": "string" + }, + "pvd": { + "type": "string" + }, + "shop_avatar": { + "type": "string" + }, + "shop_name": { + "type": "string" + }, + "video": { + "type": "string" + } + } + }, + "md.RegisterPopupCondition": { + "type": "object", + "properties": { + "invite_code": { + "description": "邀请码设置:弹出类型是激活码的时候起作用", + "type": "object", + "properties": { + "popup": { + "description": "是否弹出 “0”否 “1”是", + "type": "string" + }, + "should_input": { + "description": "是否必填 “0”否 “1”是", + "type": "string" + } + } + }, + "popup_type": { + "description": "弹出类型:“0”关闭 ”1”激活码 “2”邀请码", + "type": "string" + }, + "should_input": { + "description": "是否必填 “0”否 “1”是", + "type": "string" + } + } + }, + "md.TaobaoLogin": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "auth_code": { + "type": "string" + }, + "avatar_url": { + "type": "string" + }, + "nick_name": { + "type": "string" + }, + "open_id": { + "type": "string" + }, + "sid": { + "type": "string" + } + } + }, + "md.WeChat": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "avatar_url": { + "type": "string" + }, + "city": { + "type": "string" + }, + "expires_in": { + "type": "string" + }, + "gender": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "open_id": { + "type": "string" + }, + "pay_token": { + "type": "string" + }, + "pf": { + "type": "string" + }, + "pf_key": { + "type": "string" + }, + "province": { + "type": "string" + }, + "ret": { + "type": "string" + }, + "unionid": { + "type": "string" + } + } + }, + "md.WeChatMiniApp": { + "type": "object", + "properties": { + "avatar": { + "type": "string" + }, + "code": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "open_id": { + "type": "string" + }, + "unionid": { + "type": "string" + } + } + }, + "model.LogisticCompany": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "model.PrivilegeOpenCardOrd": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "after_sale_id": { + "type": "integer" + }, + "card_key": { + "type": "string" + }, + "card_num": { + "type": "string" + }, + "card_type": { + "type": "integer" + }, + "cost_price": { + "type": "string" + }, + "create_time": { + "type": "string" + }, + "date_type": { + "type": "integer" + }, + "given_data": { + "type": "string" + }, + "logistic_company": { + "type": "string" + }, + "logistic_num": { + "type": "string" + }, + "ord_id": { + "type": "integer" + }, + "pay_channel": { + "type": "integer" + }, + "pay_time": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "receiver": { + "type": "string" + }, + "settle_at": { + "type": "integer" + }, + "state": { + "type": "integer" + }, + "uid": { + "type": "integer" + }, + "update_time": { + "type": "string" + } + } + }, + "model.UserAddress": { + "type": "object", + "properties": { + "city_id": { + "type": "string" + }, + "city_name": { + "type": "string" + }, + "county_id": { + "type": "string" + }, + "county_name": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "phone": { + "type": "string" + }, + "post_code": { + "type": "string" + }, + "province_id": { + "type": "string" + }, + "province_name": { + "type": "string" + }, + "receiver": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "uid": { + "type": "integer" + } + } + } + }, + "securityDefinitions": { + "MasterID": { + "type": "apiKey", + "name": "MasterID", + "in": "header" + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..ca2a7a9 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,1084 @@ +basePath: / +definitions: + hdl.PrivilegeOpenCardOrdQueryPageResp: + properties: + btn_bg_color_1: + type: string + btn_bg_color_2: + type: string + btn_text: + type: string + btn_text_color: + type: string + header_img: + type: string + logistic_company: + items: + $ref: '#/definitions/model.LogisticCompany' + type: array + type: object + hdl.PrivilegeOpenCardOrdQueryReq: + properties: + com: + description: 快递公司名称 + type: string + num: + description: 快递单号 + type: string + required: + - com + - num + type: object + hdl.logisticQueryReq: + properties: + com: + description: 快递公司名称 + type: string + num: + description: 快递单号 + type: string + required: + - com + - num + type: object + hdl.privilegeOpenCardCheckReq: + properties: + key: + description: 卡密 + type: string + num: + description: 卡号 + type: string + required: + - key + - num + type: object + hdl.privilegeOpenCardPayPageResp: + properties: + amount: + description: 付费金额 + type: string + card_type: + description: 卡的类型:"1"实体卡 "2"虚拟卡 + type: string + date_type: + description: 日期类型:month:月 season:季 year:年 forever:永久 + type: string + page_style: + $ref: '#/definitions/hdl.privilegeOpenCardPayStyle' + description: 页面样式 + type: object + hdl.privilegeOpenCardPayStyle: + properties: + exclusive_privilege: + description: 专属特权 + items: + properties: + brand_id: + type: string + brand_img: + type: string + brand_img_url: + type: string + brand_name: + type: string + is_show: + type: string + sub_title: + type: string + type: object + type: array + payment_btn: + description: 底部支付按钮 + items: + properties: + bg_img: + type: string + bg_img_url: + type: string + func: + type: string + name: + type: string + text: + type: string + text_color: + type: string + type: + type: string + type: object + type: array + payment_choose_icon: + description: 支付方式选中、未选中图标 + items: + properties: + icon: + type: string + icon_url: + type: string + type: + type: string + type: object + type: array + payment_style: + description: 支付方式 + items: + properties: + icon: + type: string + icon_url: + type: string + type: + type: string + type: object + type: array + style: + description: 头部样式 + properties: + header_bg_img: + type: string + header_bg_img_url: + type: string + special_deals_img: + type: string + special_deals_img_url: + type: string + special_deals_text: + type: string + type: object + type: object + hdl.resultItem: + properties: + id: + example: "440100000000" + type: string + name: + example: city + type: string + type: object + hdl.updateAddressReq: + properties: + city_id: + type: string + county_id: + type: string + detail: + type: string + id: + type: integer + phone: + type: string + post_code: + type: string + province_id: + type: string + receiver: + type: string + tag: + type: string + required: + - city_id + - county_id + - detail + - phone + - province_id + - receiver + type: object + md.Apple: + properties: + token: + type: string + type: object + md.FastLoginRequestBody: + properties: + apple: + $ref: '#/definitions/md.Apple' + captcha: + type: string + is_not_create: + type: string + mobile: + type: string + parent_uid: + type: string + qq: + $ref: '#/definitions/md.QQLogin' + return_user_msg: + type: string + taobao: + $ref: '#/definitions/md.TaobaoLogin' + wechat: + $ref: '#/definitions/md.WeChat' + wechat_mini: + $ref: '#/definitions/md.WeChatMiniApp' + zone: + type: string + type: object + md.IndexRecommendList: + properties: + good: + items: + $ref: '#/definitions/md.RecommendGood' + type: array + provider: + description: BarTitleList []BarTitle `json:"bar_title_list"` + type: string + type: object + md.LoginResponse: + properties: + bind_phone_enable: + type: string + is_pid: + type: string + perms: + items: + type: string + type: array + phone: + type: string + register_invite_code_enable: + type: string + register_popup_condition: + $ref: '#/definitions/md.RegisterPopupCondition' + description: 弹出类型设置 + token: + type: string + user_id: + type: string + username: + type: string + wechat_applet_open_id: + type: string + wechat_union_id: + type: string + type: object + md.Marquee: + properties: + avatar_url: + type: string + content: + type: string + name: + type: string + type: object + md.ProductDetailResponse: + properties: + commission: + type: string + coupon_price: + type: string + good_id: + type: string + mod_list: + items: + additionalProperties: true + type: object + type: array + provider: + type: string + provider_name: + type: string + shop_avatar: + type: string + type: object + md.QQLogin: + properties: + access_token: + type: string + avatar_url: + type: string + city: + type: string + expires_in: + type: string + gender: + type: string + nickname: + type: string + open_id: + type: string + pay_token: + type: string + pf: + type: string + pf_key: + type: string + province: + type: string + ret: + type: string + unionid: + type: string + type: object + md.RecommendGood: + properties: + commission: + type: string + coupon: + type: string + current_price: + type: string + detail_data: + $ref: '#/definitions/md.ProductDetailResponse' + good_id: + type: string + good_image: + type: string + good_title: + type: string + inorder_count: + type: string + is_collect: + type: integer + is_coupons: + type: integer + is_share: + type: integer + market_price: + type: string + marquee_list: + items: + $ref: '#/definitions/md.Marquee' + type: array + provider: + type: string + provider_name: + type: string + pvd: + type: string + shop_avatar: + type: string + shop_name: + type: string + video: + type: string + type: object + md.RegisterPopupCondition: + properties: + invite_code: + description: 邀请码设置:弹出类型是激活码的时候起作用 + properties: + popup: + description: 是否弹出 “0”否 “1”是 + type: string + should_input: + description: 是否必填 “0”否 “1”是 + type: string + type: object + popup_type: + description: 弹出类型:“0”关闭 ”1”激活码 “2”邀请码 + type: string + should_input: + description: 是否必填 “0”否 “1”是 + type: string + type: object + md.TaobaoLogin: + properties: + access_token: + type: string + auth_code: + type: string + avatar_url: + type: string + nick_name: + type: string + open_id: + type: string + sid: + type: string + type: object + md.WeChat: + properties: + access_token: + type: string + avatar_url: + type: string + city: + type: string + expires_in: + type: string + gender: + type: string + nickname: + type: string + open_id: + type: string + pay_token: + type: string + pf: + type: string + pf_key: + type: string + province: + type: string + ret: + type: string + unionid: + type: string + type: object + md.WeChatMiniApp: + properties: + avatar: + type: string + code: + type: string + nickname: + type: string + open_id: + type: string + unionid: + type: string + type: object + model.LogisticCompany: + properties: + code: + type: string + name: + type: string + type: object + model.PrivilegeOpenCardOrd: + properties: + address: + type: string + after_sale_id: + type: integer + card_key: + type: string + card_num: + type: string + card_type: + type: integer + cost_price: + type: string + create_time: + type: string + date_type: + type: integer + given_data: + type: string + logistic_company: + type: string + logistic_num: + type: string + ord_id: + type: integer + pay_channel: + type: integer + pay_time: + type: string + phone: + type: string + receiver: + type: string + settle_at: + type: integer + state: + type: integer + uid: + type: integer + update_time: + type: string + type: object + model.UserAddress: + properties: + city_id: + type: string + city_name: + type: string + county_id: + type: string + county_name: + type: string + detail: + type: string + id: + type: integer + phone: + type: string + post_code: + type: string + province_id: + type: string + province_name: + type: string + receiver: + type: string + tag: + type: string + uid: + type: integer + type: object +host: localhost:5000 +info: + contact: + name: sherlockwhite + description: 移动端接口 + termsOfService: 智莺生活后端组 + title: 智莺生活移动端接口 + version: "1.0" +paths: + /api/v1/acq/fix: + get: + consumes: + - application/json + description: 拉新活动--fix + produces: + - application/json + responses: + "200": + description: ok + schema: + type: string + summary: 拉新活动--fix + tags: + - 拉新活动 + /api/v1/acquisition/home: + post: + consumes: + - application/json + description: 本期榜单/上期榜单/我的邀请人数和奖励/任务列表 + produces: + - application/json + responses: + "200": + description: ok + schema: + type: string + summary: 拉新--首页数据 + tags: + - 拉新活动 + /api/v1/acquisition/qrcode: + get: + consumes: + - application/json + description: 二维码 + produces: + - application/json + responses: + "200": + description: ok + schema: + type: string + summary: 拉新--邀请二维码 + tags: + - 拉新活动 + /api/v1/acquisition/reward/detail: + post: + consumes: + - application/json + description: 拉新活动--我的奖励明细 + parameters: + - description: 1为以发放,2为待发放,3为失效 + in: body + name: state + required: true + schema: + type: string + produces: + - application/json + responses: + "200": + description: ok + schema: + type: string + summary: 拉新活动--我的奖励明细 + tags: + - 拉新活动 + /api/v1/acquisition/reward_receive: + post: + consumes: + - application/json + description: 拉新活动--领取奖励 + parameters: + - description: 任务ID + in: body + name: job_id + required: true + schema: + type: string + produces: + - application/json + responses: + "200": + description: ok + schema: + type: string + summary: 拉新活动--领取奖励 + tags: + - 拉新活动 + /api/v1/credit/card/config: + get: + consumes: + - application/json + description: 获取信用卡配置 + produces: + - application/json + responses: + "200": + description: ok + schema: + type: string + summary: 获取信用卡配置 + tags: + - 信用卡 + /api/v1/duomai/mall/detail: + post: + consumes: + - application/json + description: 多麦商城--商城详情 + parameters: + - description: 商城id + in: body + name: brand_id + required: true + schema: + type: string + produces: + - application/json + responses: + "200": + description: ok + schema: + type: string + summary: 多麦商城--商城详情 + tags: + - 多麦商城 + /api/v1/duomai/mall/home: + get: + consumes: + - application/json + description: 多麦商城--首页数据 + produces: + - application/json + responses: + "200": + description: ok + schema: + type: string + summary: 多麦商城--首页数据 + tags: + - 多麦商城 + /api/v1/duomai/mall/search: + post: + consumes: + - application/json + description: 多麦商城--搜索 + parameters: + - description: 搜索关键词 + in: body + name: key + required: true + schema: + type: string + produces: + - application/json + responses: + "200": + description: ok + schema: + type: string + summary: 多麦商城--搜索 + tags: + - 多麦商城 + /api/v1/duomai/mall/update: + get: + consumes: + - application/json + description: 多麦商城--更新数据 + produces: + - application/json + responses: + "200": + description: ok + schema: + type: string + summary: 多麦商城--更新数据 + tags: + - 多麦商城 + /api/v1/logistic/query: + post: + consumes: + - application/json + parameters: + - description: logisticQueryReq + in: body + name: req + required: true + schema: + $ref: '#/definitions/hdl.logisticQueryReq' + produces: + - application/json + responses: + "200": + description: ok + schema: + type: string + "400": + description: '{"code":400000,"data":[],"msg":"该快递公司不支持查询"}' + schema: + type: string + summary: 快递100物流查询 + tags: + - 物流 + /api/v1/mod/pub.flutter.duomai.mall.detail.page: + get: + consumes: + - application/json + description: 多麦商城详情页样式 + produces: + - application/json + responses: + "200": + description: ok + schema: + type: string + summary: 多麦商城详情页样式 + tags: + - 多麦商城 + /api/v1/mod/pub.flutter.duomai.mall.home.page: + get: + consumes: + - application/json + description: 多麦商城首页样式 + produces: + - application/json + responses: + "200": + description: ok + schema: + type: string + summary: 多麦商城首页样式 + tags: + - 多麦商城 + /api/v1/mod/pub.flutter.duomai.mall.search.page: + get: + consumes: + - application/json + description: 多麦商城搜索页样式 + produces: + - application/json + responses: + "200": + description: ok + schema: + type: string + summary: 多麦商城搜索页样式 + tags: + - 多麦商城 + /api/v1/privilege/card/activation: + post: + consumes: + - application/json + description: 权益卡激活 + parameters: + - description: json + in: body + name: body + required: true + schema: + $ref: '#/definitions/hdl.privilegeOpenCardCheckReq' + produces: + - application/json + responses: + "200": + description: ok + schema: + type: string + summary: 权益卡激活 + tags: + - 权益卡 + /api/v1/privilege/open_card/check: + get: + consumes: + - application/json + parameters: + - description: json + in: body + name: body + required: true + schema: + $ref: '#/definitions/hdl.privilegeOpenCardCheckReq' + produces: + - application/json + responses: + "200": + description: 0:不存在 1:已经被使用 2:可用 + schema: + type: string + summary: 权益卡卡号卡密检测 + tags: + - 权益卡 + /api/v1/privilege/open_card/order_query: + get: + produces: + - application/json + responses: + "200": + description: ok + schema: + $ref: '#/definitions/hdl.PrivilegeOpenCardOrdQueryPageResp' + "400": + description: '{"code":400000,"data":[],"msg":"验证码错误"}' + schema: + type: string + summary: 权益卡开卡订单查询页面 + tags: + - 权益卡 + post: + parameters: + - description: json + in: body + name: body + required: true + schema: + $ref: '#/definitions/hdl.PrivilegeOpenCardOrdQueryReq' + produces: + - application/json + responses: + "200": + description: ok + schema: + $ref: '#/definitions/model.PrivilegeOpenCardOrd' + "400": + description: '{"code":400000,"data":[],"msg":"验证码错误"}' + schema: + type: string + summary: 权益卡开卡订单查询 + tags: + - 权益卡 + /api/v1/privilege/open_card/order_suc: + get: + produces: + - application/json + responses: + "200": + description: ok + schema: + $ref: '#/definitions/model.PrivilegeOpenCardOrd' + summary: 订单支付成功页面 + tags: + - 权益卡 + /api/v1/privilege/open_card/pay_page: + get: + produces: + - application/json + responses: + "200": + description: ok + schema: + $ref: '#/definitions/hdl.privilegeOpenCardPayPageResp' + "400": + description: '{"code":400000,"data":[],"msg":"权益卡配置缺失"}' + schema: + type: string + summary: 权益卡开卡支付页面 + tags: + - 权益卡 + /api/v1/shake_ticket/:goods_id/:type: + get: + consumes: + - application/json + description: 收藏/领券买/分享赚 + parameters: + - description: 商品id + in: path + name: goods_id + required: true + type: string + - description: 类型(0取消收藏,1收藏,2点击 领券买,3点击分享赚) + in: path + name: type + required: true + type: string + produces: + - application/json + responses: + "200": + description: ok + schema: + type: string + summary: 抖券商品收藏/领券买/分享赚 + tags: + - 抖券 + /api/v1/shake_ticket_list: + get: + consumes: + - application/json + description: 定向计划/高佣专场/精选低价包邮/偏远地区包邮/抖货商品/各大榜单商品/今日值得买 + parameters: + - description: 页码 + in: query + name: page + required: true + type: string + - description: 页数 + in: query + name: page_size + required: true + type: string + - description: 分类id(3定向计划/4高佣专场/5精选低价包邮/6偏远地区包邮/7抖货商品/8各大榜单商品/9今日值得买) + in: query + name: category_id + required: true + type: string + - description: 渠道 + in: query + name: pvd + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/md.IndexRecommendList' + summary: 抖券商品推荐列表 + tags: + - 抖券 + /api/v1/sign/fast/in: + post: + parameters: + - description: json + in: body + name: body + required: true + schema: + $ref: '#/definitions/md.FastLoginRequestBody' + produces: + - application/json + responses: + "200": + description: ok + schema: + $ref: '#/definitions/md.LoginResponse' + "400": + description: '{"code":400001,"data":[],"msg":"请求参数错误"}' + schema: + type: string + summary: 用户手机快速登录 + tags: + - 登录、注册 + /api/v1/sub_region_list: + get: + parameters: + - description: 上级地区类型:root(查询省级列表)、province(省级ID下的城市)、city(市级id下的区域) + in: query + name: parent + required: true + type: string + - description: 上级地区id + in: query + name: id + type: string + produces: + - application/json + responses: + "200": + description: ok + schema: + items: + $ref: '#/definitions/hdl.resultItem' + type: array + "400": + description: '{"code":400001,"data":[],"msg":"请求参数错误"}' + schema: + type: string + summary: 获取区域id下的区域 + tags: + - 地址 + /api/v1/user/address/:id: + get: + parameters: + - description: 地址id + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: ok + schema: + $ref: '#/definitions/model.UserAddress' + "400": + description: '{"code":400000,"data":[],"msg":"地址不存在"}' + schema: + type: string + summary: 用户地址详情 + tags: + - 地址 + /api/v1/user/address/delete/:id: + post: + consumes: + - application/json + parameters: + - description: ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: ok + schema: + type: string + "400": + description: '{"code":400000,"data":[],"msg":"地址不存在"}' + schema: + type: string + summary: 用户地址删除 + tags: + - 地址 + /api/v1/user/address/update: + post: + consumes: + - application/json + parameters: + - description: json参数,Id不传为新增 + in: body + name: '""' + required: true + schema: + $ref: '#/definitions/hdl.updateAddressReq' + produces: + - application/json + responses: + "200": + description: ok + schema: + type: string + "400": + description: '{"code":400000,"data":[],"msg":"地址不存在"}' + schema: + type: string + summary: 用户地址新增、编辑 + tags: + - 地址 + /api/v1/user/addresses: + get: + produces: + - application/json + responses: + "200": + description: ok + schema: + items: + $ref: '#/definitions/model.UserAddress' + type: array + "500": + description: '{"code":500000,"data":[],"msg":"数据库操作失败"}' + schema: + type: string + summary: 用户地址列表 + tags: + - 地址 +securityDefinitions: + MasterID: + in: header + name: MasterID + type: apiKey +swagger: "2.0" diff --git a/etc/db_tpl/config b/etc/db_tpl/config new file mode 100644 index 0000000..34c75ee --- /dev/null +++ b/etc/db_tpl/config @@ -0,0 +1,7 @@ +lang=go +genJson=1 +prefix=cos_ +ignoreColumnsJSON= +created= +updated= +deleted= \ No newline at end of file diff --git a/etc/db_tpl/struct.go.tpl b/etc/db_tpl/struct.go.tpl new file mode 100644 index 0000000..74b2896 --- /dev/null +++ b/etc/db_tpl/struct.go.tpl @@ -0,0 +1,17 @@ +package {{.Models}} + +{{$ilen := len .Imports}} +{{if gt $ilen 0}} +import ( + {{range .Imports}}"{{.}}"{{end}} +) +{{end}} + +{{range .Tables}} +type {{Mapper .Name}} struct { +{{$table := .}} +{{range .ColumnsSeq}}{{$col := $table.GetColumn .}} {{Mapper $col.Name}} {{Type $col}} {{Tag $table $col}} +{{end}} +} +{{end}} + diff --git a/etc/ps/ConvertToUpperCase.ps1 b/etc/ps/ConvertToUpperCase.ps1 new file mode 100644 index 0000000..9a61727 --- /dev/null +++ b/etc/ps/ConvertToUpperCase.ps1 @@ -0,0 +1,9 @@ +param($inputString) +$words = $inputString.Split('_') +$outputString = "" +foreach ($word in $words) { + $outputString += $word.Substring(0,1).ToUpper() + $word.Substring(1).ToLower() + "" +} +$outputString = $outputString.TrimEnd() +$outputString -replace ' ', '' # 如果想要没有空格的字符串,取消注释这行代码 +$outputString \ No newline at end of file diff --git a/etc/task.yml b/etc/task.yml new file mode 100644 index 0000000..6642724 --- /dev/null +++ b/etc/task.yml @@ -0,0 +1,37 @@ +# debug release test +debug: true +prd: false +local: true +# 缓存 +redis_addr: '120.24.28.6:32572' + +app_comm: + url: http://127.0.0.1:5003 + + +admin: + api_aes_key: e{&[^Ft(.~g]1eR-]VO + api_aes_iv: ZV`7<5X]/2brS@sz +# 数据库 +db: + host: '119.23.182.117:3306' + name: 'zyos_website' + user: 'root' + psw: 'Fnuo123com@' + show_log: true + max_lifetime: 30 + max_open_conns: 100 + max_idle_conns: 100 + path: 'tmp/task_sql_%v.log' + +# 日志 +log: + level: 'debug' # 普通日志级别 #debug, info, warn, fatal, panic + is_stdout: true + time_format: 'standard' # sec, second, milli, nano, standard, iso + encoding: 'console' + is_file_out: true + file_dir: './tmp/' + file_max_size: 256 + file_max_age: 1 + file_name: 'task.log' diff --git a/etc/template/template_implement.tpl b/etc/template/template_implement.tpl new file mode 100644 index 0000000..ecc280f --- /dev/null +++ b/etc/template/template_implement.tpl @@ -0,0 +1,14 @@ +package implement + +import ( + "applet/app/db/dao" + "xorm.io/xorm" +) + +func NewDemoImplementDb(engine *xorm.Engine) dao.DemoImplementDao { + return &DemoImplementDb{Db: engine} +} + +type DemoImplementDb struct { + Db *xorm.Engine +} \ No newline at end of file diff --git a/etc/template/template_interface.tpl b/etc/template/template_interface.tpl new file mode 100644 index 0000000..2a05d90 --- /dev/null +++ b/etc/template/template_interface.tpl @@ -0,0 +1,5 @@ +package dao + +type DemoInterfaceDao interface { + //TODO:: You can add specific method definitions here +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ae20d24 --- /dev/null +++ b/go.mod @@ -0,0 +1,90 @@ +module applet + +go 1.18 + +require ( + github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 + github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 + github.com/boombuler/barcode v1.0.1 + github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/forgoer/openssl v0.0.0-20201023062029-c3112b0c8700 + github.com/gin-contrib/sessions v0.0.3 + github.com/gin-gonic/gin v1.6.3 + github.com/go-playground/locales v0.13.0 + github.com/go-playground/universal-translator v0.17.0 + github.com/go-playground/validator/v10 v10.4.2 + github.com/go-redis/redis v6.15.9+incompatible + github.com/go-sql-driver/mysql v1.6.0 + github.com/gocolly/colly v1.2.0 + github.com/gomodule/redigo v2.0.0+incompatible + github.com/iGoogle-ink/gopay v1.5.36 + github.com/makiuchi-d/gozxing v0.0.0-20210324052758-57132e828831 + github.com/qiniu/api.v7/v7 v7.8.2 + github.com/robfig/cron/v3 v3.0.1 + github.com/sony/sonyflake v1.0.0 + github.com/swaggo/swag v1.7.0 + github.com/syyongx/php2go v0.9.4 + github.com/tidwall/gjson v1.7.4 + go.uber.org/zap v1.16.0 + gopkg.in/natefinch/lumberjack.v2 v2.0.0 + gopkg.in/yaml.v2 v2.4.0 + xorm.io/xorm v1.0.7 +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/goquery v1.5.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/andybalholm/cascadia v1.1.0 // indirect + github.com/antchfx/htmlquery v1.3.2 // indirect + github.com/antchfx/xmlquery v1.4.1 // indirect + github.com/antchfx/xpath v1.3.1 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.5 // indirect + github.com/go-openapi/spec v0.20.3 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/snappy v0.0.3 // indirect + github.com/gookit/color v1.3.8 // indirect + github.com/gorilla/context v1.1.1 // indirect + github.com/gorilla/securecookie v1.1.1 // indirect + github.com/gorilla/sessions v1.2.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.10 // indirect + github.com/kennygrant/sanitize v1.2.4 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/onsi/ginkgo v1.15.0 // indirect + github.com/onsi/gomega v1.10.5 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect + github.com/smartystreets/goconvey v1.6.4 // indirect + github.com/stretchr/testify v1.7.0 // indirect + github.com/syndtr/goleveldb v1.0.0 // indirect + github.com/temoto/robotstxt v1.1.2 // indirect + github.com/tidwall/match v1.0.3 // indirect + github.com/tidwall/pretty v1.1.0 // indirect + github.com/ugorji/go/codec v1.2.5 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect + golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect + golang.org/x/net v0.7.0 // indirect + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect + golang.org/x/tools v0.1.12 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + google.golang.org/appengine v1.6.1 // indirect + google.golang.org/protobuf v1.26.0 // indirect + honnef.co/go/tools v0.0.1-2020.1.4 // indirect + xorm.io/builder v0.3.9 // indirect +) diff --git a/k8s/mall-task-prd.yaml b/k8s/mall-task-prd.yaml new file mode 100644 index 0000000..1e592eb --- /dev/null +++ b/k8s/mall-task-prd.yaml @@ -0,0 +1,56 @@ +kind: Deployment +apiVersion: apps/v1 +metadata: + name: mall-task + namespace: zhios + labels: + app: mall-task + annotations: + kubesphere.io/creator: wuhanqin + kubesphere.io/description: 自营商城go定时任务 +spec: + replicas: 1 + selector: + matchLabels: + app: mall-task + template: + metadata: + labels: + app: mall-task + spec: + volumes: + - name: host-time + hostPath: + path: /etc/localtime + type: '' + - name: mall-task-cfg1 + configMap: + name: zhios-mall-task + items: + - key: task.yml + path: task.yml + defaultMode: 420 + containers: + - name: container-mall-task + image: 'registry.cn-shenzhen.aliyuncs.com/fnuoos-prd/zyos-mall-task:0.3' + resources: + limits: + cpu: '1' + memory: 1000Mi + requests: + cpu: 200m + memory: 1000Mi + volumeMounts: + - name: host-time + readOnly: true + mountPath: /etc/localtime + - name: mall-task-cfg1 + readOnly: true + mountPath: /var/zyos/task.yml + subPath: task.yml + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + imagePullPolicy: Always + restartPolicy: Always + terminationGracePeriodSeconds: 30 + dnsPolicy: ClusterFirst diff --git a/k8s/mall-task.yaml b/k8s/mall-task.yaml new file mode 100644 index 0000000..534392a --- /dev/null +++ b/k8s/mall-task.yaml @@ -0,0 +1,56 @@ +kind: Deployment +apiVersion: apps/v1 +metadata: + name: mall-task + namespace: dev + labels: + app: mall-task + annotations: + kubesphere.io/creator: wuhanqin + kubesphere.io/description: 自营商城go定时任务 +spec: + replicas: 1 + selector: + matchLabels: + app: mall-task + template: + metadata: + labels: + app: mall-task + spec: + volumes: + - name: host-time + hostPath: + path: /etc/localtime + type: '' + - name: mall-task-cfg1 + configMap: + name: mall-task-cfg + items: + - key: task.yml + path: task.yml + defaultMode: 420 + containers: + - name: container-mall-task + image: 'registry.cn-shenzhen.aliyuncs.com/fnuoos-prd/zyos-mall-task:0.1' + resources: + limits: + cpu: '1' + memory: 1000Mi + requests: + cpu: 200m + memory: 1000Mi + volumeMounts: + - name: host-time + readOnly: true + mountPath: /etc/localtime + - name: mall-task-cfg1 + readOnly: true + mountPath: /var/zyos/task.yml + subPath: task.yml + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + imagePullPolicy: Always + restartPolicy: Always + terminationGracePeriodSeconds: 30 + dnsPolicy: ClusterFirst diff --git a/k8s/zyos-mall-deployment_prd.yaml b/k8s/zyos-mall-deployment_prd.yaml new file mode 100644 index 0000000..e2dc9da --- /dev/null +++ b/k8s/zyos-mall-deployment_prd.yaml @@ -0,0 +1,49 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + namespace: zhios + name: zhios-mall + labels: + app: zhios-mall +spec: + replicas: 1 + template: + metadata: + name: zhios-mall + labels: + app: zhios-mall + spec: + containers: + - name: zhios-mall-container + image: registry-vpc.cn-shenzhen.aliyuncs.com/fnuoos-prd/zyos-mall:0.1 + ports: + - containerPort: 5002 + name: 5002tcp + protocol: TCP + resources: + limits: + cpu: "1" + memory: 256Mi + requests: + cpu: 200m + memory: 128Mi + imagePullPolicy: IfNotPresent + restartPolicy: Always + volumes: + - name: host-time + hostPath: + path: /etc/localtime + type: '' + - name: mall-cfg + configMap: + name: zhios-mall-cfg + defaultMode: 420 + selector: + matchLabels: + app: zhios-mall + + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 25% + maxSurge: 25% diff --git a/main.go b/main.go new file mode 100644 index 0000000..0974be1 --- /dev/null +++ b/main.go @@ -0,0 +1,88 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "applet/app/cfg" + "applet/app/db" + "applet/app/router" + "applet/app/utils" +) + +//系统初始化 +func init() { + cfg.InitCfg() //配置初始化 + cfg.InitLog() //日志初始化 + cfg.InitCache() //缓存初始化 + if cfg.Debug { //判断是否是debug + if err := db.InitDB(cfg.DB); err != nil { //主数据库初始化 + panic(err) + } + channel := make(chan int, 0) //开辟管道,缓冲为 + go db.InitDBs(channel) + <-channel + } + fmt.Println("init success") + +} + +// @title 智莺生活移动端接口 +// @version 1.0 +// @description 移动端接口 +// @termsOfService 智莺生活后端组 +// @contact.name sherlockwhite +// @host localhost:5000 +// @securityDefinitions.apikey MasterID +// @in header +// @name MasterID +// @BasePath / +func main() { + // 启动获取所有品牌 + //go taoke.GetAllBrand() + r := router.Init() //创建路由 + // arkid.Init() + srv := &http.Server{ //设置http服务参数 + Addr: cfg.SrvAddr, //指定ip和端口 + Handler: r, //指定路由 + } + // 读取默认站长的推广位 并写进redis + // master, err := db.UserProfileFindByID(,"1") + + // if err != nil { + // panic(err) + // } + if cfg.CurlDebug { + utils.CurlDebug = true + } + // + // if has := cache.SetJson(svc.SysCfgGet(nil, "app_name")+"_default_pid_user", master, 0); has != true { + // panic(errors.New("设置默认pid缓存失败")) + // } + // Initializing the server in a goroutine so that it won't block the graceful shutdown handling below + go func() { //协程启动监听http服务 + fmt.Println("Listening and serving HTTP on " + cfg.SrvAddr) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("listen: %s\n", err) + } + }() + // graceful shutdown + //退出go守护进程 + quit := make(chan os.Signal) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + log.Println("Shutting down server...") + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + log.Fatal("Server forced to shutdown:", err) + } + log.Println("Server exiting") + +} diff --git a/static/html/LandingPage.html b/static/html/LandingPage.html new file mode 100644 index 0000000..313f321 --- /dev/null +++ b/static/html/LandingPage.html @@ -0,0 +1,179 @@ + + + + + + Landing Page + + + + + + + +
+
+ PullNew +
+ 恭喜您获得 +
+
+ 现金红包 +
+ ImgBtn + +
+
+ + + + \ No newline at end of file diff --git a/static/html/article.html b/static/html/article.html new file mode 100644 index 0000000..ad76ce5 --- /dev/null +++ b/static/html/article.html @@ -0,0 +1,95 @@ + + + + + + + + {{.title}} + + + + + + +
+
+

{{.title}}

+
+
+

{{.author}}

+ {{.time}} +
+ +
+ {{.studyCount}}人已学习 +
+
+
+
+
+ + + + diff --git a/static/html/kz_goods_share.html b/static/html/kz_goods_share.html new file mode 100644 index 0000000..348bddf --- /dev/null +++ b/static/html/kz_goods_share.html @@ -0,0 +1,664 @@ + + + + + + + + + + + + + + + + +
+ +
+ + + +
+ + + + 好货 + + + + 下载APP + + + + + +
+ +
+ + + +
+ + + + + + + +
+ +

{{.goodTitle}}

+ +
+ +

+ + + + 券后价 + 价格 + + + + + ¥{{.goodPrice}}¥{{.goodCostPrice}} + +

+ + + +
+ + + +

{{.goodCouponPrice}}元券

+ +
+ +
+ +
+ +
+ + + +
{{.tkl}}
+ + + +
+ + 一键复制 + +
+ + + +
+ + 长按复制上方口令,打开手机【】购买 + +
+ +
\ No newline at end of file diff --git a/test/aces_test.go b/test/aces_test.go new file mode 100644 index 0000000..bce9cd1 --- /dev/null +++ b/test/aces_test.go @@ -0,0 +1,175 @@ +package test + +import ( + "applet/app/db" + "applet/app/md" + "applet/app/svc" + "applet/app/utils" + "encoding/base64" + "encoding/json" + "fmt" + "testing" +) + +func TestAesCrypt_Encrypt(t *testing.T) { + var aesCrypt = utils.AesCrypt{ + Key: []byte("e{&[^Ft(.~g]1eR-]VO"), + Iv: []byte("ZV`7<5X]/2brS@sz"), + } + + var text = `{"uid":"82","applyOrder":"821607392542143106","db":{"db_host":"119.23.182.117","db_port":"3306","db_name":"fnuoos_template","db_username":"root","db_password":"Fnuo123com@"}}` + result, err := aesCrypt.Encrypt([]byte(text)) + if err != nil { + fmt.Println(err) + return + } + + pass64 := base64.StdEncoding.EncodeToString(result) + fmt.Println(pass64) +} + +func TestAesCrypt_Decrypt(t *testing.T) { + var aesCrypt = utils.AesCrypt{ + Key: []byte("e{&[^Ft(.~g]1eR-]VO"), + Iv: []byte("ZV`7<5X]/2brS@sz"), + } + + pass64 := "JD0RXX1YbZPWKeNiVKsq0jQ1Bfnbln3fIMcmJkovU5gUCf329y9ZdqECWe4OKpoOk25/hPNaBH9VwellhIQhpw==" + bytesPass, err := base64.StdEncoding.DecodeString(pass64) + if err != nil { + fmt.Println(err) + return + } + + plainText, err := aesCrypt.Decrypt(bytesPass) + if err != nil { + fmt.Println(err) + return + } + + fmt.Println(string(plainText)) +} + +func Test_Vi(t *testing.T) { + fmt.Println("123") + fmt.Println([]byte("ZV`7<5X]/2brS@sz")) +} + +func Test_CombineData(t *testing.T) { + data := + `{ + "Uid": 21699, + "Lv": 0, + "NewLv": 0, + "LevelWeight": -1, + "Profit": 7.13, + "SubsidyFee": 0, + "ProfitList": [ + { + "cid": "0", + "val": 7.13 + }, + { + "cid": "1", + "val": 10 + }, + { + "cid": "19", + "val": 120 + }, + { + "cid": "20", + "val": 0 + }, + { + "cid": "21", + "val": 0 + } + ], + "SubsidyFeeList": null, + "OwnbuyReturnType": 0, + "Diff": 0, + "ParentUser": { + "Uid": 553, + "Lv": 8, + "NewLv": 0, + "LevelWeight": 2, + "Profit": 0, + "SubsidyFee": 0, + "ProfitList": [ + { + "cid": "0", + "val": 0 + } + ], + "SubsidyFeeList": null, + "OwnbuyReturnType": 0, + "Diff": 1, + "ParentUser": { + "Uid": 21699, + "Lv": 0, + "NewLv": 0, + "LevelWeight": -1, + "Profit": 7.13, + "SubsidyFee": 0, + "ProfitList": [ + { + "cid": "0", + "val": 7.13 + }, + { + "cid": "1", + "val": 10 + }, + { + "cid": "19", + "val": 120 + }, + { + "cid": "20", + "val": 0 + }, + { + "cid": "21", + "val": 0 + } + ], + "SubsidyFeeList": null, + "OwnbuyReturnType": 0, + "Diff": 0, + "ParentUser": { + "Uid": 553, + "Lv": 8, + "NewLv": 0, + "LevelWeight": 2, + "Profit": 0, + "SubsidyFee": 0, + "ProfitList": [ + { + "cid": "0", + "val": 0 + } + ], + "SubsidyFeeList": null, + "OwnbuyReturnType": 0, + "Diff": 1, + "ParentUser": null + } + } + } + }` + bytes := []byte(data) + var lvUser md.LvUser + if err := json.Unmarshal(bytes, &lvUser); err != nil { + fmt.Println(err) + return + } + dataSlice := svc.CombineVirtualCoinRelateData(&lvUser, 249534162504132595, "mall_goods", 0) + for _, item := range dataSlice { + fmt.Printf("%#v\n", item) + } + err := db.DbInsertBatch(db.DBs["123456"], dataSlice) + if err != nil { + fmt.Println(err) + } +} diff --git a/test/toke_test.go b/test/toke_test.go new file mode 100644 index 0000000..f37d853 --- /dev/null +++ b/test/toke_test.go @@ -0,0 +1,35 @@ +package test + +import ( + "applet/app/db" + "applet/app/lib/taoke" + "applet/app/svc" + "fmt" + "testing" +) + +func TestBrandList(t *testing.T) { + tkBrandStruct, err := taoke.BrandList("1", "20") + if err != nil { + t.Error(err) + } + for _, brandStruct := range *tkBrandStruct { + fmt.Println(brandStruct) + } + +} + +func TestSuperCategory(t *testing.T) { + category, err := taoke.SuperCategory() + if err != nil { + t.Error(err) + } + for _, brandStruct := range *category { + fmt.Println(brandStruct) + } +} + +func TestAddBrandDB(t *testing.T) { + engine := db.DBs["123456"] + svc.AddBrandDB(engine) +} diff --git a/test/zhimeng_api.go b/test/zhimeng_api.go new file mode 100644 index 0000000..6ef1670 --- /dev/null +++ b/test/zhimeng_api.go @@ -0,0 +1,156 @@ +package test + +import ( + "fmt" + "github.com/gocolly/colly" + "github.com/gocolly/colly/extensions" + "github.com/tidwall/gjson" + "net/http" + "regexp" + "strings" +) + +/* +目前可用接口 +[商品查询]https://www.showdoc.com.cn/59349170678610?page_id=339616554551473 +[商品详情]https://www.showdoc.com.cn/59349170678610?page_id=339687047645094 + +*/ + +// Response is SDK Response +type Response struct { + Msg string `json:"msg"` + Success int `json:"success"` + Data interface{} `json:"data"` +} + +func main() { + // // JD + // postData := map[string]string{"keyword": "联想", "p": "1", "size": "10"} + // fmt.Println(postData["time"]) + // res, _ := zhimeng.Send("jd", "getgoods", postData) + // fmt.Println(string(res)) + // p := Response{} + // json.Unmarshal(res, &p) + // fmt.Println(p) + // // VIP + // postData = map[string]string{"keyword": "联想", "p": "1", "size": "10", "order": "0"} + // fmt.Println(postData["time"]) + // res, _ = zhimeng.Send("wph", "seach_goods", postData) + // fmt.Println(string(res)) + // p = Response{} + // json.Unmarshal(res, &p) + // fmt.Println(p) + // // PDD + // postData = map[string]string{"keyword": "联想", "p": "1", "size": "10", "sort": "goods_price asc"} + // res, _ = zhimeng.Send("pdd", "getgoods", postData) + // fmt.Println(string(res)) + // p = Response{} + // json.Unmarshal(res, &p) + // fmt.Println(p) + for i := 0; i < 1000; i++ { + fmt.Println(i) + scrapPDD() + } +} + +func scrapJD() { + c := colly.NewCollector(func(collector *colly.Collector) { + extensions.RandomUserAgent(collector) + }) + c.OnResponse(func(r *colly.Response) { + re, _ := regexp.Compile(`[(]//[^\s]*[)]`) + body := r.Body + fmt.Println(string(body)) + urls := re.FindAllString(string(body), -1) + fmt.Println(urls) + for _, url := range urls { + url = strip(url, "()") + url = "https:" + url + fmt.Println(url) + } + }) + c.Visit("https://wqsitem.jd.com/detail/100008309360_d100008309360_normal.html") +} + +func scrapPDD() { + var cookies = []*http.Cookie{} + var mapcookies = make(map[string]string) + url := fmt.Sprintf("https://mobile.yangkeduo.com/goods.html?goods_id=%s", "156632692649") + cs := "api_uid=CiHUKl9DZKpL6QBVK4qWAg==; _nano_fp=Xpdbl0PyX5Pxn0TynT_DTGXbst0kz5cjzGAQDnBR; ua=Mozilla%2F5.0%20(Windows%20NT%2010.0%3B%20Win64%3B%20x64)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F84.0.4147.135%20Safari%2F537.36; webp=1; quick_entrance_click_record=20200824%2C1; PDDAccessToken=XRC6FNX7FRBL6AJRMRBRN4CDG2PZXO3YJZYHFUA4O2PLDAWVYXHA1125821; pdd_user_id=9622705741400; pdd_user_uin=F27EAZ4V5S7EGEVMCJI2P7RFLE_GEXDA; chat_config={'host_whitelist':['.yangkeduo.com','.pinduoduo.com','.10010.com/queen/tencent/pinduoduo-fill.html','.ha.10086.cn/pay/card-sale!toforward.action','wap.ha.10086.cn','m.10010.com']}; pdd_vds=gaLMNqmfGfyYEpyYiZGWopaCicNHbXGWtDNcOZnWLqiDNfLHOXnZaqtCLDiX" + csList := strings.Split(cs, ";") + for _, c := range csList { + s := strings.Trim(c, " ") + sList := strings.SplitN(s, "=", 2) + + mapcookies[sList[len(sList)-len(sList)]] = sList[(len(sList) - len(sList) + 1)] + + } + fmt.Println(mapcookies) + for key, value := range mapcookies { + if key == "ua" { + continue + } + cookies = append(cookies, &http.Cookie{Name: key, Value: value}) + } + c := colly.NewCollector( + colly.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36"), + ) + + c.OnResponse(func(r *colly.Response) { + re, _ := regexp.Compile(`window.rawData=.*}`) + body := r.Body + fmt.Println(string(body)) + result := re.FindString(string(body)) + // fmt.Println(result) + result = strings.SplitN(result, "=", 2)[1] + // fmt.Println(result) + value := gjson.Get(result, "store.initDataObj.goods.detailGallery") + // fmt.Println(value) + list := value.Array() + imageList := []string{} + for _, v := range list { + nv := gjson.Get(v.String(), "url") + imageList = append(imageList, nv.String()) + } + fmt.Println(imageList) + ck := c.Cookies("https://mobile.yangkeduo.com") + fmt.Println(ck) + cookies = ck + }) + + c.SetCookies("https://mobile.yangkeduo.com", cookies) + + c.Visit(url) +} + +func strip(ss string, charss string) string { + s, chars := []rune(ss), []rune(charss) + length := len(s) + max := len(s) - 1 + l, r := true, true //标记当左端或者右端找到正常字符后就停止继续寻找 + start, end := 0, max + tmpEnd := 0 + charset := make(map[rune]bool) //创建字符集,也就是唯一的字符,方便后面判断是否存在 + for i := 0; i < len(chars); i++ { + charset[chars[i]] = true + } + for i := 0; i < length; i++ { + if _, exist := charset[s[i]]; l && !exist { + start = i + l = false + } + tmpEnd = max - i + if _, exist := charset[s[tmpEnd]]; r && !exist { + end = tmpEnd + r = false + } + if !l && !r { + break + } + } + if l && r { // 如果左端和右端都没找到正常字符,那么表示该字符串没有正常字符 + return "" + } + return string(s[start : end+1]) +} diff --git a/wap.conf b/wap.conf new file mode 100644 index 0000000..8f0d455 --- /dev/null +++ b/wap.conf @@ -0,0 +1,35 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + #将DNS指向kubernetes集群内的DNS + resolver kube-dns.kube-system.svc.cluster.local valid=30s; + + set $oss_endpoint_service zyos-oss.dev.svc.cluster.local:5000; + set $endpoint_service zyos-app.dev.svc.cluster.local:5000; + + location /static { + root /usr/share/nginx/html; + index index.html index.htm; + } + location /api/qiniu { + proxy_pass http://$oss_endpoint_service; + proxy_set_header Connection ""; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + break; + } + #proxy server + location /api { + proxy_pass http://$endpoint_service; + + proxy_http_version 1.1; + + proxy_set_header Connection ""; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + break; + } +} \ No newline at end of file diff --git a/zyos-mall-deployment.yaml b/zyos-mall-deployment.yaml new file mode 100644 index 0000000..03e4c77 --- /dev/null +++ b/zyos-mall-deployment.yaml @@ -0,0 +1,49 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + namespace: dev + name: zyos-mall + labels: + app: zyos-mall +spec: + replicas: 1 + template: + metadata: + name: zyos-mall + labels: + app: zyos-mall + spec: + containers: + - name: zyos-mall-container + image: registry-vpc.cn-shenzhen.aliyuncs.com/fnuoos-prd/zyos-mall:0.1 + ports: + - containerPort: 5002 + name: 5002tcp + protocol: TCP + resources: + limits: + cpu: "1" + memory: 256Mi + requests: + cpu: 200m + memory: 128Mi + imagePullPolicy: IfNotPresent + restartPolicy: Always + volumes: + - name: host-time + hostPath: + path: /etc/localtime + type: '' + - name: mall-cfg + configMap: + name: zyos-mall-cfg + defaultMode: 420 + selector: + matchLabels: + app: zyos-mall + + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 25% + maxSurge: 25%