diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f86fdb4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+# Binaries for programs and plugins
+
diff --git a/.idea/community_team.iml b/.idea/community_team.iml
new file mode 100644
index 0000000..5e764c4
--- /dev/null
+++ b/.idea/community_team.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..de50cfd
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..28977ea
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,37 @@
+# 多重构建,减少镜像大小
+# 构建:使用golang:1.13版本
+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"
+ENV PHONE_DATA_DIR="./static/bat"
+# 设置工作区
+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="zengzhengrong"
+ENV TZ="Asia/Shanghai"
+ENV PHONE_DATA_DIR="./static/bat"
+
+COPY static/html static/html
+COPY static/bat static/bat
+# 时区纠正
+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-prd b/Dockerfile-prd
new file mode 100644
index 0000000..beb5959
--- /dev/null
+++ b/Dockerfile-prd
@@ -0,0 +1,39 @@
+# 多重构建,减少镜像大小
+# 构建:使用golang:1.18版本(直接使用自己从香港下载回来的镜像版本,提高打包速度)
+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"
+ENV PHONE_DATA_DIR="./static/bat"
+# 设置工作区
+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="zengzhengrong"
+ENV TZ="Asia/Shanghai"
+ENV PHONE_DATA_DIR="./static/bat"
+
+COPY static/html static/html
+COPY static/bat static/bat
+# 时区纠正
+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"]
+CMD ["bash","-c","sysctl -w net.ipv4.tcp_tw_reuse=1 && sysctl -w net.ipv4.tcp_fin_timeout=10 && sysctl -w net.ipv4.ip_local_port_range='1024 65535' && sysctl -p && ./zyos -c /var/zyos/cfg.yml"]
+
+
diff --git a/Dockerfile-task b/Dockerfile-task
new file mode 100644
index 0000000..6e86a95
--- /dev/null
+++ b/Dockerfile-task
@@ -0,0 +1,38 @@
+# 多重构建,减少镜像大小
+# 构建:使用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"
+ENV PHONE_DATA_DIR="./static/bat"
+# 设置工作区
+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_order_task cmd/task/main.go
+
+FROM ubuntu:xenial as prod
+LABEL maintainer="zengzhengrong"
+ENV TZ="Asia/Shanghai"
+ENV PHONE_DATA_DIR="./static/bat"
+
+COPY static/html static/html
+COPY static/bat static/bat
+
+# 时区纠正
+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_order_task ./zyos_order_task
+
+COPY --from=build /go/release/etc/task.yml /var/zyos/task.yml
+
+# 启动服务
+CMD ["./zyos_order_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..de4d11e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,49 @@
+# applet
+
+## 要看 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..3caf9f6
--- /dev/null
+++ b/app/cfg/cfg_app.go
@@ -0,0 +1,122 @@
+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"`
+ RedisAddrSecond RedisAddrSeconds `yaml:"redis_addr_second"`
+ DB DBCfg `yaml:"db"`
+ MQ MQCfg `yaml:"mq"`
+ ES ESCfg `yaml:"es"`
+ Log LogCfg `yaml:"log"`
+ ArkID ArkIDCfg `yaml:"arkid"`
+ Admin AdminCfg `yaml:"admin"`
+ Official OfficialCfg `yaml:"official"`
+ WebsiteBackend WebsiteBackendCfg `yaml:"website_backend"`
+ WxappletFilepath WxappletFilepathCfg `yaml:"wxapplet_filepath"`
+ H5Filepath H5FilepathCfg `yaml:"h5_filepath"`
+ ImBusinessRpc ImBusinessRpcCfg `yaml:"im_business_rpc"`
+ Local bool
+ AppComm AppCommCfg `yaml:"app_comm"`
+ Zhimeng ZhimengCfg `yaml:"zm"`
+ Supply SupplyCfg `yaml:"supply"`
+ ZhiosOpen ZhiosOpenCfg `yaml:"zhios_open"`
+ ZhimengDB DBCfg `yaml:"zhimeng_db"`
+}
+type RedisAddrSeconds struct {
+ Addr string `json:"addr"`
+ Pwd string `json:"pwd"`
+}
+type ZhiosOpenCfg struct {
+ URL string `yaml:"url"`
+}
+type ImBusinessRpcCfg struct {
+ URL string `yaml:"url"`
+ PORT string `yaml:"port"`
+}
+
+// 公共模块
+type AppCommCfg struct {
+ URL string `yaml:"url"`
+}
+type SupplyCfg struct {
+ URL string `yaml:"url"`
+}
+type ZhimengCfg struct {
+ URL string `yaml:"url"`
+}
+
+// OfficialCfg is 官网
+
+type OfficialCfg struct {
+ URL string `yaml:"url"`
+}
+type WxappletFilepathCfg struct {
+ URL string `yaml:"url"`
+}
+type H5FilepathCfg struct {
+ URL string `yaml:"url"`
+}
+type WebsiteBackendCfg struct {
+ URL string `yaml:"url"`
+}
+
+// AdminCfg is 后台接口调用需要
+type AdminCfg struct {
+ URL string `yaml:"url"`
+ IURL string `yaml:"iurl"`
+ AesKey string `yaml:"api_aes_key"`
+ AesIV string `yaml:"api_aes_iv"`
+}
+
+type ArkIDCfg struct {
+ Admin string `yaml:"admin"`
+ AdminPassword string `yaml:"admin_password"`
+ Url string `yaml:"url`
+}
+
+//数据库配置结构体
+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 MQCfg struct {
+ Host string `yaml:"host"`
+ Port string `yaml:"port"`
+ User string `yaml:"user"`
+ Pwd string `yaml:"pwd"`
+}
+type ESCfg struct {
+ Url string `yaml:"url"`
+ User string `yaml:"user"`
+ Pwd string `yaml:"pwd"`
+}
+
+//日志配置结构体
+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/cfg_task.go b/app/cfg/cfg_task.go
new file mode 100644
index 0000000..cbaa432
--- /dev/null
+++ b/app/cfg/cfg_task.go
@@ -0,0 +1,4 @@
+package cfg
+
+type TaskConfig struct {
+}
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_cache_second.go b/app/cfg/init_cache_second.go
new file mode 100644
index 0000000..6b3e6a2
--- /dev/null
+++ b/app/cfg/init_cache_second.go
@@ -0,0 +1,9 @@
+package cfg
+
+import (
+ "applet/app/utils/cachesecond"
+)
+
+func InitCacheSecond() {
+ cachesecond.NewRedis(RedisAddrSecond.Addr, RedisAddrSecond.Pwd)
+}
diff --git a/app/cfg/init_cfg.go b/app/cfg/init_cfg.go
new file mode 100644
index 0000000..7ffae1d
--- /dev/null
+++ b/app/cfg/init_cfg.go
@@ -0,0 +1,81 @@
+package cfg
+
+import (
+ "flag"
+ "io/ioutil"
+
+ "gopkg.in/yaml.v2"
+)
+
+//配置文件数据,全局变量
+var (
+ Debug bool
+ Prd bool
+ CurlDebug bool
+ SrvAddr string
+ RedisAddr string
+ RedisAddrSecond *RedisAddrSeconds
+ DB *DBCfg
+ MQ *MQCfg
+ ES *ESCfg
+ Log *LogCfg
+ ArkID *ArkIDCfg
+ Admin *AdminCfg
+ Official *OfficialCfg
+ WxappletFilepath *WxappletFilepathCfg
+ H5Filepath *H5FilepathCfg
+ Local bool
+ AppComm *AppCommCfg
+ Zhimeng *ZhimengCfg
+ WebsiteBackend *WebsiteBackendCfg
+ Supply *SupplyCfg
+ ImBusinessRpc *ImBusinessRpcCfg
+ ZhiosOpen *ZhiosOpenCfg
+ ZhimengDB *DBCfg
+)
+
+//初始化配置文件,将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
+ MQ = &conf.MQ
+ Log = &conf.Log
+ ArkID = &conf.ArkID
+ RedisAddr = conf.RedisAddr
+ RedisAddrSecond = &conf.RedisAddrSecond
+ SrvAddr = conf.SrvAddr
+ Admin = &conf.Admin
+ Official = &conf.Official
+ WxappletFilepath = &conf.WxappletFilepath
+ AppComm = &conf.AppComm
+ Zhimeng = &conf.Zhimeng
+ Supply = &conf.Supply
+ H5Filepath = &conf.H5Filepath
+ WebsiteBackend = &conf.WebsiteBackend
+ ImBusinessRpc = &conf.ImBusinessRpc
+ ES = &conf.ES
+ ZhiosOpen = &conf.ZhiosOpen
+ ZhimengDB = &conf.ZhimengDB
+
+}
diff --git a/app/cfg/init_es.go b/app/cfg/init_es.go
new file mode 100644
index 0000000..ab5595b
--- /dev/null
+++ b/app/cfg/init_es.go
@@ -0,0 +1,12 @@
+package cfg
+
+import (
+ "code.fnuoos.com/go_rely_warehouse/zyos_go_es.git/es"
+ "encoding/json"
+)
+
+func InitEs() {
+ data, _ := json.Marshal(ES)
+ filePutContents("init_es", string(data))
+ es.Init(ES.Url, ES.User, ES.Pwd)
+}
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_rabbitmq.go b/app/cfg/init_rabbitmq.go
new file mode 100644
index 0000000..f04cd5e
--- /dev/null
+++ b/app/cfg/init_rabbitmq.go
@@ -0,0 +1,28 @@
+package cfg
+
+import (
+ "code.fnuoos.com/go_rely_warehouse/zyos_go_mq.git/rabbit"
+ "encoding/json"
+ "os"
+ "strings"
+ "time"
+)
+
+func InitMq() {
+ data, _ := json.Marshal(MQ)
+ filePutContents("init_rabbit_mq", string(data))
+ err := rabbit.Init(MQ.Host, MQ.Port, MQ.User, MQ.Pwd)
+ if err != nil {
+ filePutContents("init_rabbit_mq", err.Error())
+ return
+ }
+}
+
+func filePutContents(fileName string, content string) {
+ fd, _ := os.OpenFile("./tmp/"+fileName+".log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
+ fd_time := time.Now().Format("2006-01-02 15:04:05")
+ fd_content := strings.Join([]string{"[", fd_time, "] ", content, "\n"}, "")
+ buf := []byte(fd_content)
+ fd.Write(buf)
+ fd.Close()
+}
diff --git a/app/cfg/init_task.go b/app/cfg/init_task.go
new file mode 100644
index 0000000..e2f4a22
--- /dev/null
+++ b/app/cfg/init_task.go
@@ -0,0 +1,53 @@
+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
+ MQ = &conf.MQ
+ ES = &conf.ES
+ Log = &conf.Log
+ Admin = &conf.Admin
+ RedisAddr = conf.RedisAddr
+ RedisAddrSecond = &conf.RedisAddrSecond
+ Local = conf.Local
+ AppComm = &conf.AppComm
+ Zhimeng = &conf.Zhimeng
+ Supply = &conf.Supply
+ ZhiosOpen = &conf.ZhiosOpen
+ ZhimengDB = &conf.ZhimengDB
+
+}
+
+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/db.go b/app/db/db.go
new file mode 100644
index 0000000..d2aa2f6
--- /dev/null
+++ b/app/db/db.go
@@ -0,0 +1,105 @@
+package db
+
+import (
+ "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
+}
+func QueryNativeStringWithSess(sess *xorm.Session, sql string, args ...interface{}) ([]map[string]string, error) {
+ results, err := sess.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
+}
+
+// InsertCommWithSession common insert
+func InsertCommWithSession(session *xorm.Session, model interface{}) (int64, error) {
+ row, err := session.InsertOne(model)
+ return row, err
+}
+
+// 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
+}
diff --git a/app/db/db_cate.go b/app/db/db_cate.go
new file mode 100644
index 0000000..f355a86
--- /dev/null
+++ b/app/db/db_cate.go
@@ -0,0 +1,15 @@
+package db
+
+import (
+ "applet/app/db/model"
+ "xorm.io/xorm"
+)
+
+func GetCate(eg *xorm.Engine, storeId string) *[]model.CommunityTeamCate {
+ var data []model.CommunityTeamCate
+ err := eg.Where("is_show=1 and uid=?", storeId).OrderBy("sort desc,id desc").Find(&data)
+ if err != nil {
+ return nil
+ }
+ return &data
+}
diff --git a/app/db/db_cloud_bundle.go b/app/db/db_cloud_bundle.go
new file mode 100644
index 0000000..27a3f60
--- /dev/null
+++ b/app/db/db_cloud_bundle.go
@@ -0,0 +1,111 @@
+package db
+
+import (
+ "applet/app/db/model"
+ "applet/app/md"
+ "errors"
+ "fmt"
+ "github.com/gin-gonic/gin"
+
+ "xorm.io/xorm"
+)
+
+// GetCloudBundleByVersion is 根据版本 获取打包记录
+func GetCloudBundleByVersion(Db *xorm.Engine, appverison string, os, ep int) (*model.CloudBundle, error) {
+ m := new(model.CloudBundle)
+ has, err := Db.Where("version = ? and os = ? and template_during_audit<>'' and ep=?", appverison, os, ep).Get(m)
+ if err != nil {
+ return nil, err
+ }
+ if !has {
+ return nil, errors.New("not Found")
+ }
+ return m, nil
+}
+
+// GetCloudBundleByVersionPlatform is 根据版本\os 获取打包记录
+func GetCloudBundleByVersionPlatform(Db *xorm.Engine, appverison string, platform string, appType string) (*model.CloudBundle, error) {
+ m := new(model.CloudBundle)
+ var tag int
+ var ep = 0
+ if platform == "ios" {
+ tag = 2
+ } else {
+ tag = 1
+ }
+ if appType == "daogou" || appType == "" {
+ ep = 0
+ } else {
+ ep = 1
+ }
+ has, err := Db.Where("version = ? and os=? and ep=?", appverison, tag, ep).Desc("build_number").Get(m)
+ if err != nil {
+ return nil, err
+ }
+ if !has {
+ return nil, errors.New("not Found")
+ }
+ return m, nil
+}
+
+// GetCloudBundleByVersionPlatformWithAudit is 根据版本\os 获取打包记录
+func GetCloudBundleByVersionPlatformWithAudit(Db *xorm.Engine, appverison string, platform string, appType string) (*model.CloudBundle, error) {
+ m := new(model.CloudBundle)
+ var tag int
+ var ep = 0
+ if platform == "ios" {
+ tag = 2
+ } else {
+ tag = 1
+ }
+ if appType == "daogou" || appType == "" {
+ ep = 0
+ } else {
+ ep = 1
+ }
+ has, err := Db.Where("version = ? and os=? and ep=? and template_during_audit<>''", appverison, tag, ep).Desc("build_number").Get(m)
+ if err != nil {
+ return nil, err
+ }
+ if !has {
+ return nil, errors.New("not Found")
+ }
+ return m, nil
+}
+func GetCloudBuild(c *gin.Context, platform string) string {
+ appVersion := c.GetHeader("app_version_name")
+ ep := 0
+ if c.GetString("app_type") != "" && c.GetString("app_type") != "daogou" {
+ ep = 1
+ }
+ if platform == "" {
+ platform = c.GetHeader("platform")
+ }
+
+ version := ""
+ os := 0
+ switch platform {
+ case md.PLATFORM_ANDROID:
+ if ep == 1 {
+ version = SysCfgGet(c, "biz_android_audit_version")
+ } else {
+ version = SysCfgGet(c, "android_audit_version")
+ }
+ os = 1
+ case md.PLATFORM_IOS:
+ if ep == 1 {
+ version = SysCfgGet(c, "biz_ios_audit_version")
+ } else {
+ version = SysCfgGet(c, "ios_audit_version")
+ }
+ os = 2
+ }
+ var data model.CloudBundle
+ get, err := DBs[c.GetString("mid")].Where("is_auditing=1 and os=? and ep=? and version=?", os, ep, appVersion).Get(&data)
+ fmt.Println(get)
+ fmt.Println(err)
+ if data.Version != "" {
+ version = data.Version
+ }
+ return version
+}
diff --git a/app/db/db_coupon.go b/app/db/db_coupon.go
new file mode 100644
index 0000000..3a49c63
--- /dev/null
+++ b/app/db/db_coupon.go
@@ -0,0 +1 @@
+package db
diff --git a/app/db/db_goods.go b/app/db/db_goods.go
new file mode 100644
index 0000000..42e151f
--- /dev/null
+++ b/app/db/db_goods.go
@@ -0,0 +1,31 @@
+package db
+
+import (
+ "applet/app/db/model"
+ "applet/app/utils"
+ "xorm.io/xorm"
+)
+
+func GetGoods(eg *xorm.Engine, arg map[string]string) *[]model.CommunityTeamGoods {
+ var data []model.CommunityTeamGoods
+ sess := eg.Where("store_type=0 and state=0")
+ if arg["cid"] != "" {
+ sess.And("cid=?", arg["cid"])
+ }
+ limit := utils.StrToInt(arg["size"])
+ start := (utils.StrToInt(arg["p"]) - 1) * limit
+ err := sess.OrderBy("sale_count desc,id desc").Limit(limit, start).Find(&data)
+ if err != nil {
+ return nil
+ }
+ return &data
+}
+
+func GetGoodsSess(sess *xorm.Session, id int) *model.CommunityTeamGoods {
+ var data model.CommunityTeamGoods
+ get, err := sess.Where("id=?", id).Get(&data)
+ if get == false || err != nil {
+ return nil
+ }
+ return &data
+}
diff --git a/app/db/db_goods_sku.go b/app/db/db_goods_sku.go
new file mode 100644
index 0000000..f386b19
--- /dev/null
+++ b/app/db/db_goods_sku.go
@@ -0,0 +1,15 @@
+package db
+
+import (
+ "applet/app/db/model"
+ "xorm.io/xorm"
+)
+
+func GetGoodsSku(eg *xorm.Engine, goodsId string) *[]model.CommunityTeamSku {
+ var data []model.CommunityTeamSku
+ err := eg.Where("goods_id=?", goodsId).Find(&data)
+ if err != nil {
+ return nil
+ }
+ return &data
+}
diff --git a/app/db/db_order.go b/app/db/db_order.go
new file mode 100644
index 0000000..29d4879
--- /dev/null
+++ b/app/db/db_order.go
@@ -0,0 +1,72 @@
+package db
+
+import (
+ "applet/app/db/model"
+ "applet/app/utils"
+ "xorm.io/xorm"
+)
+
+func GetOrderEg(eg *xorm.Engine, oid string) *model.CommunityTeamOrder {
+ var data model.CommunityTeamOrder
+ get, err := eg.Where("oid=?", oid).Get(&data)
+ if get == false || err != nil {
+ return nil
+ }
+ return &data
+}
+func GetOrder(sess *xorm.Session, oid string) *model.CommunityTeamOrder {
+ var data model.CommunityTeamOrder
+ get, err := sess.Where("oid=?", oid).Get(&data)
+ if get == false || err != nil {
+ return nil
+ }
+ return &data
+}
+func GetOrderInfo(sess *xorm.Session, oid string) *[]model.CommunityTeamOrderInfo {
+ var data []model.CommunityTeamOrderInfo
+ err := sess.Where("oid=?", oid).Find(&data)
+ if err != nil {
+ return nil
+ }
+ return &data
+}
+func GetOrderInfoAllEg(eg *xorm.Engine, oid string) *[]model.CommunityTeamOrderInfo {
+ var data []model.CommunityTeamOrderInfo
+ err := eg.Where("oid=?", oid).Find(&data)
+ if err != nil {
+ return nil
+ }
+ return &data
+}
+func GetOrderInfoEg(eg *xorm.Engine, oid string) *model.CommunityTeamOrderInfo {
+ var data model.CommunityTeamOrderInfo
+ get, err := eg.Where("oid=?", oid).Asc("id").Get(&data)
+ if get == false || err != nil {
+ return nil
+ }
+ return &data
+}
+
+func GetOrderList(eg *xorm.Engine, arg map[string]string) *[]model.CommunityTeamOrder {
+ var data []model.CommunityTeamOrder
+ sess := eg.Where("1=1")
+ if arg["uid"] != "" {
+ sess.And("uid=?", arg["uid"])
+ }
+ if arg["state"] != "" {
+ sess.And("state=?", arg["state"])
+ }
+ if arg["store_uid"] != "" {
+ sess.And("store_uid=?", arg["store_uid"])
+ }
+ if arg["code"] != "" {
+ sess.And("code=?", arg["code"])
+ }
+ limit := utils.StrToInt(arg["size"])
+ start := (utils.StrToInt(arg["p"]) - 1) * limit
+ err := sess.OrderBy("id desc").Limit(limit, start).Find(&data)
+ if err != nil {
+ return nil
+ }
+ return &data
+}
diff --git a/app/db/db_store.go b/app/db/db_store.go
new file mode 100644
index 0000000..7600c85
--- /dev/null
+++ b/app/db/db_store.go
@@ -0,0 +1,86 @@
+package db
+
+import (
+ "applet/app/db/model"
+ "applet/app/utils"
+ "fmt"
+ "xorm.io/xorm"
+)
+
+func GetStore(eg *xorm.Engine, arg map[string]string) []map[string]string {
+ lng := utils.StrToFloat64(arg["lng"])
+ lat := utils.StrToFloat64(arg["lat"])
+ sel := ` *,sqrt( ( (( %f - lng)*PI()*12656*cos((( %f +lat)/2)*PI()/180)/180) * (( %f - lng)*PI()*12656*cos (((%f+lat)/2)*PI()/180)/180) ) + ( ((%f-lat)*PI()*12656/180) * ((%f-lat)*PI()*12656/180) ) ) AS km`
+ sel = fmt.Sprintf(sel, lng, lat, lng, lat, lat, lat)
+ sql := `select %s from community_team_store where %s %s`
+ where := "1=1"
+ if arg["parent_uid"] != "" {
+ where += " and store_type=2 and parent_uid=" + arg["parent_uid"]
+ } else if arg["uid"] != "" {
+ where += " and store_type=1 and uid=" + arg["parent_uid"]
+ } else {
+ where += " and store_type=" + arg["store_type"]
+ }
+ if arg["city"] != "" {
+ where += " and city='" + arg["city"] + "'"
+ }
+ if arg["name"] != "" {
+ where += " and name like '%" + arg["name"] + "'"
+ }
+ start := (utils.StrToInt(arg["p"]) - 1) * utils.StrToInt(arg["size"])
+ group := " order by %s limit " + utils.IntToStr(start) + "," + arg["size"]
+ groupStr := "fan desc,km asc,id asc"
+ if arg["cid"] == "1" {
+ groupStr = "km asc,id asc"
+ }
+ group = fmt.Sprintf(group, groupStr)
+ sql = fmt.Sprintf(sql, sel, where, group)
+ fmt.Println(sql)
+ nativeString, _ := QueryNativeString(eg, sql)
+ return nativeString
+}
+func GetStoreLike(eg *xorm.Engine, arg map[string]string) []map[string]string {
+ lng := utils.StrToFloat64(arg["lng"])
+ lat := utils.StrToFloat64(arg["lat"])
+ sel := ` cts.*,sqrt( ( (( %f - cts.lng)*PI()*12656*cos((( %f +cts.lat)/2)*PI()/180)/180) * (( %f - cts.lng)*PI()*12656*cos (((%f+cts.lat)/2)*PI()/180)/180) ) + ( ((%f-cts.lat)*PI()*12656/180) * ((%f-cts.lat)*PI()*12656/180) ) ) AS km`
+ sel = fmt.Sprintf(sel, lng, lat, lng, lat, lat, lat)
+ sql := `select %s from community_team_store_like ctsl
+ left join community_team_store cts on ctsl.store_id=cts.id
+where %s %s`
+ where := "cts.state=1"
+ if arg["parent_uid"] != "" {
+ where += " and cts.store_type=2 and cts.parent_uid=" + arg["parent_uid"]
+ } else if arg["uid"] != "" {
+ where += " and cts.store_type=1 and cts.uid=" + arg["parent_uid"]
+ } else {
+ where += " and cts.store_type=" + arg["store_type"]
+ }
+ if arg["city"] != "" {
+ where += " and cts.city='" + arg["city"] + "'"
+ }
+ if arg["name"] != "" {
+ where += " and cts.name like '%" + arg["name"] + "'"
+ }
+ start := (utils.StrToInt(arg["p"]) - 1) * utils.StrToInt(arg["size"])
+ group := " order by km asc,cts.id asc limit " + utils.IntToStr(start) + "," + arg["size"]
+ sql = fmt.Sprintf(sql, sel, where, group)
+ fmt.Println(sql)
+ nativeString, _ := QueryNativeString(eg, sql)
+ return nativeString
+}
+func GetStoreId(sess *xorm.Session, id string) *model.CommunityTeamStore {
+ var data model.CommunityTeamStore
+ get, err := sess.Where("id=?", id).Get(&data)
+ if get == false || err != nil {
+ return nil
+ }
+ return &data
+}
+func GetStoreIdEg(eg *xorm.Engine, id string) *model.CommunityTeamStore {
+ var data model.CommunityTeamStore
+ get, err := eg.Where("id=?", id).Get(&data)
+ if get == false || err != nil {
+ return nil
+ }
+ return &data
+}
diff --git a/app/db/db_sys_cfg.go b/app/db/db_sys_cfg.go
new file mode 100644
index 0000000..8eeb247
--- /dev/null
+++ b/app/db/db_sys_cfg.go
@@ -0,0 +1,120 @@
+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
+}
+func SysCfgGetOneData(Db *xorm.Engine, key string) string {
+ var cfgList model.SysCfg
+ if has, err := Db.Where("`key`=?", key).Get(&cfgList); err != nil || has == false {
+ return ""
+ }
+ return cfgList.Val
+}
+
+// 返回最后插入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
+}
+
+// 单条记录获取DB
+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
+}
+func SysCfgGetWithStr(eg *xorm.Engine, masterId string, HKey string) string {
+
+ cfg, err := SysCfgGetOne(eg, HKey)
+ if err != nil || cfg == nil {
+ _ = logx.Error(err)
+ return ""
+ }
+ return cfg.Val
+}
+
+// 多条记录获取DB
+func SysCfgFindWithDb(eg *xorm.Engine, masterId string, keys ...string) map[string]string {
+ res := map[string]string{}
+ //TODO::判断keys长度(大于10个直接查数据库)
+ if len(keys) > 10 {
+ cfgList, _ := SysCfgGetAll(eg)
+ if cfgList == nil {
+ return nil
+ }
+ for _, v := range *cfgList {
+ res[v.Key] = v.Val
+ }
+ } else {
+ for _, key := range keys {
+ res[key] = SysCfgGetWithDb(eg, masterId, key)
+ }
+ }
+ return res
+}
diff --git a/app/db/db_sys_mod.go b/app/db/db_sys_mod.go
new file mode 100644
index 0000000..da57360
--- /dev/null
+++ b/app/db/db_sys_mod.go
@@ -0,0 +1,688 @@
+package db
+
+import (
+ "applet/app/db/model"
+ "applet/app/e"
+ "applet/app/utils"
+ "applet/app/utils/logx"
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+ "github.com/tidwall/gjson"
+ "xorm.io/xorm"
+)
+
+// 返回所有, 不管是否显示
+func SysModFindAll(Db *xorm.Engine) (*[]model.SysModule, error) {
+ var m []model.SysModule
+ if err := Db.Find(&m); err != nil {
+ return nil, err
+ }
+ return &m, nil
+}
+
+// 查找主模块数据
+func SysModFindMain(Db *xorm.Engine) (*[]model.SysModule, error) {
+ var m []model.SysModule
+ if err := Db.Where("mod_pid = 0 AND state = 1 AND position = 'base'").
+ Asc("sort").
+ Find(&m); err != nil {
+ return nil, err
+ }
+ return &m, nil
+}
+
+// 用父ID查找子模块数据
+func SysModFindByPId(c *gin.Context, Db *xorm.Engine, id int) (*[]model.SysModule, error) {
+ var m []model.SysModule
+ if err := Db.Where("state = 1").Where("mod_pid = ?", id).
+ Asc("sort").
+ Find(&m); err != nil {
+ return nil, err
+ }
+ mm, err := sysModFormat(c, &m)
+ if err != nil {
+ return nil, err
+ }
+ var ms []model.SysModule
+ modname_list := []string{"product", "search_result_taobao_item", "hot_rank_tab_view"}
+ for _, item := range *mm.(*[]model.SysModule) {
+ if item.ModName == "product_detail_title" {
+ if strings.Contains(item.Data, "tmall") == false {
+ item.Data = strings.Replace(item.Data, "\"platform_css\":[", "\"platform_css\":[{\"name\":\"天猫\",\"type\":\"tmall\",\"text_color\":\"#FFFFFF\",\"bg_color\":\"#FF4242\"},", 1)
+ }
+ if strings.Contains(item.Data, "kuaishou") == false {
+ item.Data = strings.Replace(item.Data, "\"platform_css\":[", "\"platform_css\":[{\"name\":\"快手\",\"type\":\"kuaishou\",\"text_color\":\"#FFFFFF\",\"bg_color\":\"#FF4242\"},", 1)
+ }
+ if strings.Contains(item.Data, "tikTok") == false {
+ item.Data = strings.Replace(item.Data, "\"platform_css\":[", "\"platform_css\":[{\"name\":\"抖音\",\"type\":\"tikTok\",\"text_color\":\"#FFFFFF\",\"bg_color\":\"#FF4242\"},", 1)
+ }
+ }
+
+ if strings.Contains(item.Data, "tmall") == false && utils.InArr(item.ModName, modname_list) {
+ item.Data = strings.Replace(item.Data, "{\"index\":\"6\",\"type\":\"kaola\",\"platform_name\":\"考拉\",\"provider_name_color\":\"#FFFFFF\",\"provider_bg_color\":\"#FF4242\"}", "{\"index\":\"6\",\"type\":\"kaola\",\"platform_name\":\"考拉\",\"provider_name_color\":\"#FFFFFF\",\"provider_bg_color\":\"#FF4242\"},{\"index\":\"7\",\"type\":\"tmall\",\"platform_name\":\"天猫\",\"provider_name_color\":\"#FFFFFF\",\"provider_bg_color\":\"#FF4242\"}", 1)
+ item.Data = strings.Replace(item.Data, "{\"type\":\"kaola\",\"provider_name_color\":\"#FFFFFF\",\"provider_bg_color\":\"#FF4242\"}", "{\"type\":\"kaola\",\"provider_name_color\":\"#FFFFFF\",\"provider_bg_color\":\"#FF4242\"},{\"type\":\"tmall\",\"provider_name_color\":\"#FFFFFF\",\"provider_bg_color\":\"#FF4242\"}", 1)
+ }
+ if strings.Contains(item.Data, "优惠卷") {
+ item.Data = strings.Replace(item.Data, "优惠卷", "优惠券", -1)
+ }
+ item.Data = strings.ReplaceAll(item.Data, "\\/", "/")
+ item.Data = strings.ReplaceAll(item.Data, "\\u0026", "&")
+
+ ms = append(ms, item)
+ }
+ return &ms, nil
+}
+
+// 用父ID查找子模块数据
+func SysModFindByPIds(c *gin.Context, Db *xorm.Engine, ids ...int) (*[]model.SysModule, error) {
+ var m []model.SysModule
+ if err := Db.In("mod_pid", ids).Where("state = 1").
+ Asc("sort").
+ Find(&m); err != nil {
+ return nil, err
+ }
+ mm, err := sysModFormat(c, &m)
+ if err != nil {
+ return nil, err
+ }
+ var ms []model.SysModule
+ for _, item := range *mm.(*[]model.SysModule) {
+ //数据里面
+ if strings.Contains(item.Data, "tmall") == false && item.ModName == "product" {
+ item.Data = strings.Replace(item.Data, "{\"index\":\"6\",\"type\":\"kaola\",\"platform_name\":\"考拉\",\"provider_name_color\":\"#FFFFFF\",\"provider_bg_color\":\"#FF4242\"}", "{\"index\":\"6\",\"type\":\"kaola\",\"platform_name\":\"考拉\",\"provider_name_color\":\"#FFFFFF\",\"provider_bg_color\":\"#FF4242\"},{\"index\":\"7\",\"type\":\"tmall\",\"platform_name\":\"天猫\",\"provider_name_color\":\"#FFFFFF\",\"provider_bg_color\":\"#FF4242\"}", 1)
+ }
+ item = SysModDataByReplace(c, item)
+ ms = append(ms, item)
+ }
+
+ return &ms, nil
+}
+
+// 用IDS找对应模块数据
+func SysModFindByIds(Db *xorm.Engine, ids ...int) (*[]model.SysModule, error) {
+ var m []model.SysModule
+ if err := Db.In("mod_id", ids).Where("state = 1").
+ Cols("mod_id,mod_pid,mod_name,position,skip_identifier,title,subtitle,url,margin,aspect_ratio,icon,img,font_color,bg_img,bg_color,bg_color_t,badge,path,data,sort").
+ Asc("sort").Find(&m); err != nil {
+ return nil, err
+ }
+
+ return &m, nil
+}
+
+// ID查找对应模块
+func SysModFindById(c *gin.Context, Db *xorm.Engine, id string) (*model.SysModule, error) {
+ var m model.SysModule
+ if has, err := Db.Where("state = 1 AND mod_id = ?", id).
+ Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ mm, err := sysModFormat(c, &m)
+ if err != nil {
+ return nil, err
+ }
+
+ return mm.(*model.SysModule), nil
+}
+
+// SysModFindByTmpId is 根据模板
+func SysModFindByTmpId(c *gin.Context, Db *xorm.Engine, id string) (*model.SysModule, error) {
+ var m model.SysModule
+ if has, err := Db.Where("state = 1 AND template_id = ?", id).
+ Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ mm, err := sysModFormat(c, &m)
+ if err != nil {
+ return nil, err
+ }
+ return mm.(*model.SysModule), nil
+}
+
+// Name查找对应模块
+func SysModFindByName(c *gin.Context, Db *xorm.Engine, name string) (*model.SysModule, error) {
+ var m model.SysModule
+ if has, err := Db.Where("state = 1 AND mod_name = ?", name).
+ Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ mm, err := sysModFormat(c, &m)
+ if err != nil {
+ return nil, err
+ }
+ return mm.(*model.SysModule), nil
+}
+
+// SysModFindByName is Name查找对应模块
+func SysModFindByNames(names ...string) (*[]model.SysModule, error) {
+ var m []model.SysModule
+ if err := Db.In("mod_name", names).Where("state = 1").
+ Cols("mod_id,mod_pid,mod_name,position,skip_identifier,title,subtitle,url,margin,aspect_ratio,icon,img,font_color,bg_img,bg_color,bg_color_t,badge,path,data,sort").
+ Find(&m); err != nil {
+ return nil, err
+ }
+ return &m, nil
+}
+
+// SysModFindByPosition is 根据位置查找对应模块
+func SysModFindByPosition(Db *xorm.Engine, positions ...string) (*[]model.SysModule, error) {
+ var m []model.SysModule
+ if err := Db.In("position", positions).Where("state = 1").Find(&m); err != nil {
+ return nil, err
+ }
+ return &m, nil
+}
+
+// 根据跳转标识 查找对应模块
+func SysModFindBySkipIdentifier(c *gin.Context, Db *xorm.Engine, name string) (*model.SysModule, error) {
+ var m model.SysModule
+ if has, err := Db.Where("state = 1 AND skip_identifier = ?", name).
+ Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ mm, err := sysModFormat(c, &m)
+ if err != nil {
+ return nil, err
+ }
+ return mm.(*model.SysModule), nil
+}
+
+func SysModFindBySkipIdentifierWithUid(c *gin.Context, Db *xorm.Engine, name string, uid int) (*model.SysModule, error) {
+ var m model.SysModule
+ if has, err := Db.Where("state = 1 AND skip_identifier = ? and uid=?", name, uid).
+ Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ mm, err := sysModFormat(c, &m)
+ if err != nil {
+ return nil, err
+ }
+ return mm.(*model.SysModule), nil
+}
+
+// 根据跳转标识和位置 查找对应模块list
+func SysModFindBySkipIdentifierAndPosition(c *gin.Context, Db *xorm.Engine, name string, position string) (*[]model.SysModule, error) {
+ var m []model.SysModule
+ if err := Db.Where("state = 1 AND skip_identifier = ? AND position = ?", name, position).
+ Cols("mod_id,mod_pid,mod_name,position,skip_identifier,title,subtitle,url,margin,aspect_ratio,icon,img,font_color,bg_img,bg_color,bg_color_t,badge,path,data,sort").
+ Asc("sort").Find(&m); err != nil {
+ return nil, err
+ }
+ mm, err := sysModFormat(c, &m)
+ if err != nil {
+ return nil, err
+ }
+ return mm.(*[]model.SysModule), nil
+}
+func SysModFindByTempId(Db *xorm.Engine, ids []int) (*[]model.SysModule, error) {
+ var m []model.SysModule
+ if err := Db.Where("state = 1 ").In("template_id", ids).Asc("sort").Find(&m); err != nil {
+ return nil, err
+ }
+ return &m, nil
+}
+
+// SysModFindByTemplateIDAndSkip is 根据模板id 查找对应模块
+func SysModFindByTemplateIDAndSkip(Db *xorm.Engine, id interface{}, skip string) (*model.SysModule, error) {
+ var m model.SysModule
+ if has, err := Db.Where("state = 1 AND template_id = ? AND skip_identifier = ?", id, skip).
+ Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// SysModFindByTemplateIDAndPID is 根据模板id 和pid =0 查找父模块
+func SysModFindByTemplateIDAndPID(Db *xorm.Engine, id interface{}, pid interface{}) (*model.SysModule, error) {
+ var m model.SysModule
+ if has, err := Db.Where("state = 1 AND template_id = ? AND mod_pid = ?", id, pid).
+ Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+
+ return &m, nil
+}
+
+// SysModFindByTemplateIDAndModName is 根据模板id 和mod name 查找模块
+func SysModFindByTemplateIDAndModName(Db *xorm.Engine, id interface{}, modName string) (*model.SysModule, error) {
+ var m model.SysModule
+ if has, err := Db.Where("state = 1 AND template_id = ? AND mod_name = ?", id, modName).
+ Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+func SysModFindByTemplateIDAndModNameWithIds(Db *xorm.Engine, ids []int, modName string) (*model.SysModule, error) {
+ var m model.SysModule
+ if has, err := Db.In("template_id", ids).And(" mod_name = ?", modName).
+ Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// SysModFindNavIsUsed 查找正在使用的底部导航栏模板
+func SysModFindNavIsUsedByPlatform(c *gin.Context, Db *xorm.Engine, platform string) (*model.SysModule, error) {
+ var (
+ tm model.SysTemplate
+ m model.SysModule
+ )
+
+ mid := c.GetString("mid")
+ fmt.Println("===================================app_type", c.GetString("app_type"))
+ if c.GetString("app_type") != "" && c.GetString("app_type") != "daogou" {
+ var (
+ tempType string
+ )
+ switch c.GetString("app_type") {
+ case "o2o":
+ tempType = "o2o_store_bottomNav"
+
+ }
+
+ switch platform {
+ case "ios":
+ if has, err := Db.Where("is_use = 1 AND type = ? AND platform = 2 ", tempType).
+ Cols("id,uid,name,is_use,is_system").
+ Get(&tm); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ appVersion := GetCloudBuild(c, platform)
+ if c.GetHeader("app_version_name") == appVersion && c.GetHeader("app_version_name") != "" {
+ m, err := GetCloudBundleByVersion(Db, appVersion, 2, 1)
+ if err != nil {
+ return nil, logx.Warn(err)
+ }
+ tm.Id = int(gjson.Get(m.TemplateDuringAudit, "bottom").Int())
+ }
+ case "android":
+ has, err := Db.Where("is_use = 1 AND type = ? AND platform = 2 ", tempType).Cols("id,uid,name,is_use,is_system").Get(&tm)
+ if err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ fmt.Println("===================================app_type", tm)
+
+ appVersion := GetCloudBuild(c, platform)
+ fmt.Println("===================================app_type", appVersion)
+
+ if appVersion != "" && c.GetHeader("app_version_name") == appVersion {
+ m, err := GetCloudBundleByVersion(Db, appVersion, 1, 1)
+ if err != nil {
+ return nil, logx.Warn(err)
+ }
+ fmt.Println("===================================app_type", m)
+
+ tm.Id = int(gjson.Get(m.TemplateDuringAudit, "bottom").Int())
+ }
+ case "wx_applet", "wap":
+ if has, err := Db.Where("is_use = 1 AND type = ? AND platform = 4 ", tempType).
+ Cols("id,uid,name,is_use,is_system").
+ Get(&tm); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ case "baidu_applet":
+ if has, err := Db.Where("is_use = 1 AND type = ? AND platform = 4 ", tempType).
+ Cols("id,uid,name,is_use,is_system").
+ Get(&tm); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ case "toutiao_applet":
+ if has, err := Db.Where("is_use = 1 AND type = ? AND platform = 4 ", tempType).
+ Cols("id,uid,name,is_use,is_system").
+ Get(&tm); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ case "alipay_applet":
+ if has, err := Db.Where("is_use = 1 AND type = ? AND platform = 4 ", tempType).
+ Cols("id,uid,name,is_use,is_system").
+ Get(&tm); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ default:
+ return &m, errors.New("Platform not support")
+ }
+ if has, err := Db.Where("state = 1 AND template_id = ?", tm.Id).
+ Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+
+ fmt.Println("===================================app_type", m)
+ if tempType == "o2o_store_bottomNav" {
+ bottomMap := make(map[string]interface{})
+ utils.Unserialize([]byte(m.Data), &bottomMap)
+ list, ok := bottomMap["list"]
+ if ok {
+ m.Data = string(utils.MarshalJSONCamelCase2JsonSnakeCase(utils.SerializeStr(list)))
+ }
+ }
+ mm, err := sysModFormat(c, &m)
+ if err != nil {
+ return nil, err
+ }
+ return mm.(*model.SysModule), nil
+ }
+ switch platform {
+ case "ios":
+ if has, err := Db.Where("is_use = 1 AND type = 'bottom' AND platform = 2 ").
+ Cols("id,uid,name,is_use,is_system").
+ Get(&tm); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ appVersion := GetCloudBuild(c, platform)
+ if c.GetHeader("app_version_name") == appVersion && c.GetHeader("app_version_name") != "" {
+ m, err := GetCloudBundleByVersion(Db, appVersion, 2, 0)
+ if err != nil {
+ return nil, logx.Warn(err)
+ }
+ tm.Id = int(gjson.Get(m.TemplateDuringAudit, "bottom").Int())
+ }
+
+ if has, err := Db.Where("state = 1 AND template_id = ? AND mod_name = 'bottom_nav'", tm.Id).
+ Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ mm, err := sysModFormat(c, &m)
+ if err != nil {
+ return nil, err
+ }
+ return mm.(*model.SysModule), nil
+ case "android":
+ has, err := Db.Where("is_use = 1 AND type = 'bottom' AND platform = 2 ").Cols("id,uid,name,is_use,is_system").Get(&tm)
+ if err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ appVersion := GetCloudBuild(c, platform)
+ if appVersion != "" && c.GetHeader("app_version_name") == appVersion {
+ m, err := GetCloudBundleByVersion(Db, appVersion, 1, 0)
+ if err != nil {
+ return nil, logx.Warn(err)
+ }
+ tm.Id = int(gjson.Get(m.TemplateDuringAudit, "bottom").Int())
+ }
+ if has, err := Db.Where("state = 1 AND template_id = ? AND mod_name = 'bottom_nav'", tm.Id).
+ Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ mm, err := sysModFormat(c, &m)
+ if err != nil {
+ return nil, err
+ }
+ return mm.(*model.SysModule), nil
+ case "wx_applet", "wap":
+ wxAppletCfg := GetAppletKey(c, Db)
+ id := utils.StrToInt(wxAppletCfg["bottom_nav_css_id"])
+ if id == 0 {
+ return nil, e.NewErr(400, "找不到模板配置")
+ }
+ if has, err := Db.Where("state = 1 AND template_id = ? AND mod_name = 'bottom_nav'", id).
+ Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ mm, err := sysModFormat(c, &m)
+ if err != nil {
+ return nil, err
+ }
+ return mm.(*model.SysModule), nil
+ case "baidu_applet":
+ if has, err := Db.Where("is_use = 1 AND type = 'bottom' AND platform = 4 ").
+ Cols("id,uid,name,is_use,is_system").
+ Get(&tm); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ appVersion := SysCfgGetWithDb(Db, mid, "baidu_audit_version")
+
+ if appVersion != "" && c.GetHeader("app_version_name") == appVersion {
+ m := SysCfgGetWithDb(Db, mid, "baidu_audit_template")
+ if m == "" {
+ return nil, e.NewErr(400, "找不到模板配置")
+ }
+ tm.Id = int(gjson.Get(m, "bottom").Int())
+ }
+ if has, err := Db.Where("state = 1 AND template_id = ? AND mod_name = 'bottom_nav'", tm.Id).
+ Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ mm, err := sysModFormat(c, &m)
+ if err != nil {
+ return nil, err
+ }
+ return mm.(*model.SysModule), nil
+ case "toutiao_applet":
+ if has, err := Db.Where("is_use = 1 AND type = 'bottom' AND platform = 4 ").
+ Cols("id,uid,name,is_use,is_system").
+ Get(&tm); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ appVersion := SysCfgGetWithDb(Db, mid, "tt_audit_version")
+ if appVersion != "" && c.GetHeader("app_version_name") == appVersion {
+ m := SysCfgGetWithDb(Db, mid, "tt_audit_template")
+ if m == "" {
+ return nil, errors.New("找不到模板配置")
+ }
+ tm.Id = int(gjson.Get(m, "bottom").Int())
+ }
+ if has, err := Db.Where("state = 1 AND template_id = ? AND mod_name = 'bottom_nav'", tm.Id).
+ Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ mm, err := sysModFormat(c, &m)
+ if err != nil {
+ return nil, err
+ }
+ return mm.(*model.SysModule), nil
+ case "alipay_applet":
+ if has, err := Db.Where("is_use = 1 AND type = 'bottom' AND platform = 4 ").
+ Cols("id,uid,name,is_use,is_system").
+ Get(&tm); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ appVersion := SysCfgGetWithDb(Db, mid, "zfb_audit_version")
+ if appVersion != "" && c.GetHeader("app_version_name") == appVersion {
+ m := SysCfgGetWithDb(Db, mid, "zfb_audit_template")
+ if m == "" {
+ return nil, errors.New("找不到模板配置")
+ }
+ tm.Id = int(gjson.Get(m, "bottom").Int())
+ }
+ if has, err := Db.Where("state = 1 AND template_id = ? AND mod_name = 'bottom_nav'", tm.Id).
+ Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ mm, err := sysModFormat(c, &m)
+ if err != nil {
+ return nil, err
+ }
+ return mm.(*model.SysModule), nil
+ default:
+ return &m, errors.New("Platform not support")
+ }
+
+}
+
+// SysModFindBySkipIdentifierAndModName is 根据mod_name和位置 查找对应模块
+func SysModFindBySkipIdentifierAndModName(c *gin.Context, Db *xorm.Engine, name string, modName string) (*model.SysModule, error) {
+ var m model.SysModule
+ if has, err := Db.Where("state = 1 AND skip_identifier = ? AND mod_name = ?", name, modName).Get(&m); err != nil || !has {
+ return nil, logx.Warn(err)
+ }
+ mm, err := sysModFormat(c, &m)
+ if err != nil {
+ return nil, err
+ }
+ return mm.(*model.SysModule), nil
+}
+
+// SysModFindByModName is 根据mod_name和位置 查找对应模块
+func SysModFindByModName(c *gin.Context, Db *xorm.Engine, modName string) (*model.SysModule, error) {
+ var m model.SysModule
+ if has, err := Db.Where("state = 1 AND mod_name = ?", modName).Get(&m); err != nil || !has {
+ return nil, logx.Warn(err)
+ }
+ mm, err := sysModFormat(c, &m)
+ if err != nil {
+ return nil, err
+ }
+ return mm.(*model.SysModule), nil
+}
+
+// 根据跳转标识和平台类型查找
+func SysModFindBySkipIdentifierAndPlatform(c *gin.Context, Db *xorm.Engine, name string, platform int) (*model.SysModule, error) {
+ var m model.SysModule
+ if has, err := Db.Where("state = 1 AND skip_identifier = ? AND platform = ?", name, platform).Get(&m); err != nil || !has {
+ return nil, logx.Warn(err)
+ }
+ mm, err := sysModFormat(c, &m)
+ if err != nil {
+ return nil, err
+ }
+ return mm.(*model.SysModule), nil
+}
+
+// 公共处理modData的链接
+func SysModDataByReplace(c *gin.Context, mod model.SysModule) model.SysModule {
+ //替换链接的一些参数
+ if strings.Contains(mod.Data, "[replace_APP_URL]") {
+ mod.Data = strings.Replace(mod.Data, "[replace_APP_URL]", c.GetString("domain_wap_base"), -1)
+ }
+ if strings.Contains(mod.Data, "[replace_masterId]") {
+ mod.Data = strings.Replace(mod.Data, "[replace_masterId]", c.GetString("mid"), -1)
+ }
+ if strings.Contains(mod.Data, "[replace_platform]") {
+ mod.Data = strings.Replace(mod.Data, "[replace_platform]", c.GetHeader("Platform"), -1)
+ }
+ if strings.Contains(mod.Data, "优惠卷") {
+ mod.Data = strings.Replace(mod.Data, "优惠卷", "优惠券", -1)
+ }
+ if strings.Contains(mod.Data, "[replace_uid]") {
+ token := c.GetHeader("Authorization")
+ // 按空格分割
+ parts := strings.SplitN(token, " ", 2)
+ if len(parts) == 2 && parts[0] == "Bearer" {
+ // parts[1]是获取到的tokenString,我们使用之前定义好的解析JWT的函数来解析它
+ mc, _ := utils.ParseToken(parts[1])
+ mod.Data = strings.Replace(mod.Data, "[replace_uid]", strconv.Itoa(mc.UID), -1)
+ }
+ }
+ //if strings.Contains(mod.Data, "\"child_category_id") && strings.Contains(mod.Data, "\"category_id") {
+ // //如果存在这两个字段,要换一下
+ // mod.Data = strings.ReplaceAll(mod.Data, "\"category_id", "\"null_category_id")
+ // mod.Data = strings.ReplaceAll(mod.Data, "\"child_category_id", "\"category_id")
+ //}
+ return mod
+}
+
+// 公共处理modData的链接
+func StringByReplace(c *gin.Context, skip string) string {
+ //替换链接的一些参数
+ if strings.Contains(skip, "[replace_APP_URL]") {
+ skip = strings.Replace(skip, "[replace_APP_URL]", c.GetString("domain_wap_base"), -1)
+ }
+ if strings.Contains(skip, "[replace_masterId]") {
+ skip = strings.Replace(skip, "[replace_masterId]", c.GetString("mid"), -1)
+ }
+ if strings.Contains(skip, "[replace_platform]") {
+ skip = strings.Replace(skip, "[replace_platform]", c.GetHeader("Platform"), -1)
+ }
+ if strings.Contains(skip, "优惠卷") {
+ skip = strings.Replace(skip, "优惠卷", "优惠券", -1)
+ }
+ if strings.Contains(skip, "[replace_uid]") {
+ token := c.GetHeader("Authorization")
+ // 按空格分割
+ parts := strings.SplitN(token, " ", 2)
+ if len(parts) == 2 && parts[0] == "Bearer" {
+ // parts[1]是获取到的tokenString,我们使用之前定义好的解析JWT的函数来解析它
+ mc, _ := utils.ParseToken(parts[1])
+ skip = strings.Replace(skip, "[replace_uid]", strconv.Itoa(mc.UID), -1)
+ }
+ }
+
+ //if strings.Contains(mod.Data, "\"child_category_id") && strings.Contains(mod.Data, "\"category_id") {
+ // //如果存在这两个字段,要换一下
+ // mod.Data = strings.ReplaceAll(mod.Data, "\"category_id", "\"null_category_id")
+ // mod.Data = strings.ReplaceAll(mod.Data, "\"child_category_id", "\"category_id")
+ //}
+ return skip
+}
+
+// 公共处理modSkip的链接 首页弹窗
+func SysModSkipByReplace(c *gin.Context, mod *model.SysPopup) *model.SysPopup {
+ //替换链接的一些参数
+ if strings.Contains(mod.Skip, "[replace_APP_URL]") {
+ mod.Skip = strings.Replace(mod.Skip, "[replace_APP_URL]", c.GetString("domain_wap_base"), -1)
+ }
+ if strings.Contains(mod.Skip, "[replace_masterId]") {
+ mod.Skip = strings.Replace(mod.Skip, "[replace_masterId]", c.GetString("mid"), -1)
+ }
+ if strings.Contains(mod.Skip, "[replace_platform]") {
+ mod.Skip = strings.Replace(mod.Skip, "[replace_platform]", c.GetHeader("Platform"), -1)
+ }
+ if strings.Contains(mod.Skip, "优惠卷") {
+ mod.Skip = strings.Replace(mod.Skip, "优惠卷", "优惠券", -1)
+ }
+ if strings.Contains(mod.Skip, "[replace_uid]") {
+ token := c.GetHeader("Authorization")
+ // 按空格分割
+ parts := strings.SplitN(token, " ", 2)
+ if len(parts) == 2 && parts[0] == "Bearer" {
+ // parts[1]是获取到的tokenString,我们使用之前定义好的解析JWT的函数来解析它
+ mc, _ := utils.ParseToken(parts[1])
+ mod.Skip = strings.Replace(mod.Skip, "[replace_uid]", strconv.Itoa(mc.UID), -1)
+ }
+ }
+
+ //if strings.Contains(mod.Data, "\"child_category_id") && strings.Contains(mod.Data, "\"category_id") {
+ // //如果存在这两个字段,要换一下
+ // mod.Data = strings.ReplaceAll(mod.Data, "\"category_id", "\"null_category_id")
+ // mod.Data = strings.ReplaceAll(mod.Data, "\"child_category_id", "\"category_id")
+ //}
+ return mod
+}
+
+// 公共处理modData的链接
+func SysModDataByReplaceSecond(c *gin.Context, mod *model.SysModule) *model.SysModule {
+ //替换链接的一些参数
+ if strings.Contains(mod.Data, "[replace_APP_URL]") {
+ mod.Data = strings.Replace(mod.Data, "[replace_APP_URL]", c.GetString("domain_wap_base"), -1)
+ }
+ if strings.Contains(mod.Data, "[replace_masterId]") {
+ mod.Data = strings.Replace(mod.Data, "[replace_masterId]", c.GetString("mid"), -1)
+ }
+ if strings.Contains(mod.Data, "优惠卷") {
+ mod.Data = strings.Replace(mod.Data, "优惠卷", "优惠券", -1)
+ }
+ if strings.Contains(mod.Data, "[replace_uid]") {
+ token := c.GetHeader("Authorization")
+ // 按空格分割
+ parts := strings.SplitN(token, " ", 2)
+ if len(parts) == 2 && parts[0] == "Bearer" {
+ // parts[1]是获取到的tokenString,我们使用之前定义好的解析JWT的函数来解析它
+ mc, _ := utils.ParseToken(parts[1])
+ if mc == nil {
+ mod.Data = strings.Replace(mod.Data, "[replace_uid]", "0", -1)
+ } else {
+ mod.Data = strings.Replace(mod.Data, "[replace_uid]", strconv.Itoa(mc.UID), -1)
+ }
+ }
+ }
+
+ if mod.ModName == "product" && strings.Contains(mod.Data, "product_3") {
+ if strings.Contains(mod.Data, "second_kill_style") == false {
+ mod.Data = strings.ReplaceAll(mod.Data, "\"coupon_commission\"", "\"second_kill_style\":{\"btn_bg_img\":\"http://ossn.izhim.net/gift.png\",\"left_stock_text_color\":\"#D59E21\",\"buy_now_text_color\":\"#D59E21\",\"is_show\":\"0\"},\"coupon_commission\"")
+ }
+ }
+ mod.Data = strings.ReplaceAll(mod.Data, "\\/", "/")
+ mod.Data = strings.ReplaceAll(mod.Data, "\\u0026", "&")
+
+ //if strings.Contains(mod.Data, "\"child_category_id") && strings.Contains(mod.Data, "\"category_id") {
+ // //如果存在这两个字段,要换一下
+ // mod.Data = strings.ReplaceAll(mod.Data, "\"category_id", "\"null_category_id")
+ // mod.Data = strings.ReplaceAll(mod.Data, "\"child_category_id", "\"category_id")
+ //}
+ return mod
+}
diff --git a/app/db/db_sys_mod_format_img.go b/app/db/db_sys_mod_format_img.go
new file mode 100644
index 0000000..bfeceb3
--- /dev/null
+++ b/app/db/db_sys_mod_format_img.go
@@ -0,0 +1,194 @@
+package db
+
+import (
+ "applet/app/db/model"
+ "applet/app/utils"
+ "errors"
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "github.com/syyongx/php2go"
+ "regexp"
+ "strings"
+)
+
+func sysModFormat(c *gin.Context, m interface{}) (interface{}, error) {
+ var mods []model.SysModule
+ protocol := SysCfgGet(c, "file_bucket_scheme")
+ domain := SysCfgGet(c, "file_bucket_host")
+ //fmt.Println(protocol, domain)
+ if protocol == "" || domain == "" {
+ return nil, errors.New("System configuration error, object storage protocol and domain name not found")
+ }
+ modname_list := []string{"product", "search_result_taobao_item", "hot_rank_tab_view"}
+ switch m.(type) {
+ case *[]model.SysModule:
+ ms := m.(*[]model.SysModule)
+ for _, item := range *ms {
+ item.Data = ReformatStr(protocol, domain, item.Data, c)
+ if item.ModName == "product_detail_title" {
+ if strings.Contains(item.Data, "tmall") == false {
+ item.Data = strings.Replace(item.Data, "\"platform_css\":[", "\"platform_css\":[{\"name\":\"天猫\",\"type\":\"tmall\",\"text_color\":\"#FFFFFF\",\"bg_color\":\"#FF4242\"},", 1)
+ }
+ if strings.Contains(item.Data, "kuaishou") == false {
+ item.Data = strings.Replace(item.Data, "\"platform_css\":[", "\"platform_css\":[{\"name\":\"快手\",\"type\":\"kuaishou\",\"text_color\":\"#FFFFFF\",\"bg_color\":\"#FF4242\"},", 1)
+ }
+ if strings.Contains(item.Data, "tikTok") == false {
+ item.Data = strings.Replace(item.Data, "\"platform_css\":[", "\"platform_css\":[{\"name\":\"抖音\",\"type\":\"tikTok\",\"text_color\":\"#FFFFFF\",\"bg_color\":\"#FF4242\"},", 1)
+ }
+ }
+
+ if strings.Contains(item.Data, "tmall") == false && utils.InArr(item.ModName, modname_list) {
+ item.Data = strings.Replace(item.Data, "{\"index\":\"6\",\"type\":\"kaola\",\"platform_name\":\"考拉\",\"provider_name_color\":\"#FFFFFF\",\"provider_bg_color\":\"#FF4242\"}", "{\"index\":\"6\",\"type\":\"kaola\",\"platform_name\":\"考拉\",\"provider_name_color\":\"#FFFFFF\",\"provider_bg_color\":\"#FF4242\"},{\"index\":\"7\",\"type\":\"tmall\",\"platform_name\":\"天猫\",\"provider_name_color\":\"#FFFFFF\",\"provider_bg_color\":\"#FF4242\"}", 1)
+ item.Data = strings.Replace(item.Data, "{\"type\":\"kaola\",\"provider_name_color\":\"#FFFFFF\",\"provider_bg_color\":\"#FF4242\"}", "{\"type\":\"kaola\",\"provider_name_color\":\"#FFFFFF\",\"provider_bg_color\":\"#FF4242\"},{\"type\":\"tmall\",\"provider_name_color\":\"#FFFFFF\",\"provider_bg_color\":\"#FF4242\"}", 1)
+ }
+ if strings.Contains(item.Data, "\"virtual_coin_pre_fix_name\":") == false && item.ModName == "product" {
+ item.Data = strings.ReplaceAll(item.Data, "\"virtual_coin_name\":", "\"virtual_coin_pre_fix_name\":\"返\",\"virtual_coin_name\":")
+ }
+ item = SysModDataByReplace(c, item)
+
+ mods = append(mods, item)
+ }
+ return &mods, nil
+ case *model.SysModule:
+ m := m.(*model.SysModule)
+ m.Data = ReformatStr(protocol, domain, m.Data, c)
+ m = SysModDataByReplaceSecond(c, m)
+ return m, nil
+ case []*model.UserLevel:
+ ms := m.([]*model.UserLevel)
+ for _, item := range ms {
+ item.CssSet = ReformatStr(protocol, domain, item.CssSet, c)
+ }
+ return ms, nil
+ case []*model.SysPushUser:
+ ms := m.([]*model.SysPushUser)
+ for _, item := range ms {
+ item.SendData = ReformatStr(protocol, domain, item.SendData, c)
+ }
+ return ms, nil
+ case *model.SysPushUser:
+ m := m.(*model.SysPushUser)
+ m.SendData = ReformatStr(protocol, domain, m.SendData, c)
+ return m, nil
+ default:
+ return nil, nil
+ }
+}
+
+func ReformatComm(str, protocol, domain string) string {
+ // PNG
+ replaceList := reformatImg(str)
+ l := removeDuplicateElement(replaceList)
+ for _, s := range l {
+ if strings.Contains(s, "http") {
+ continue
+ }
+ ss := s
+ s = strings.ReplaceAll(s, `\`, "")
+ s = strings.ReplaceAll(s, `"`, "")
+ s = php2go.Rawurlencode(s)
+ new := fmt.Sprintf("%s://%s/%s", protocol, domain, s)
+ //if skipHTTPPng(new) {
+ // continue
+ //}
+ str = strings.Replace(str, ss, `"`+new+`"`, -1)
+ }
+ return str
+}
+func ReformatStr(protocol, domain, str string, c *gin.Context) string {
+ //protocol := SysCfgGet(c, "file_bucket_scheme")
+ //domain := SysCfgGet(c, "file_bucket_host")
+
+ // PNG
+ str = ReformatComm(str, protocol, domain)
+ return str
+}
+
+// 正则匹配
+func reformatPng(data string) []string {
+ re, _ := regexp.Compile(`"([^\"]*.png")`)
+ list := re.FindAllString(data, -1)
+ return list
+}
+func reformatJpeg(data string) []string {
+ re, _ := regexp.Compile(`"([^\"]*.jpeg")`)
+ list := re.FindAllString(data, -1)
+ return list
+}
+func reformatMp4(data string) []string {
+ re, _ := regexp.Compile(`"([^\"]*.mp4")`)
+ list := re.FindAllString(data, -1)
+ return list
+}
+
+func skipHTTPPng(data string) bool {
+ re, _ := regexp.Compile(`(http|https):\/\/([^\"]*.png)`)
+ return re.MatchString(data)
+}
+
+// 正则匹配
+func reformatJPG(data string) []string {
+ re, _ := regexp.Compile(`"([^\"]*.jpg")`)
+ list := re.FindAllString(data, -1)
+ return list
+}
+
+// 正则匹配
+func reformatGIF(data string) []string {
+ re, _ := regexp.Compile(`"([^\"]*.gif")`)
+ list := re.FindAllString(data, -1)
+ return list
+}
+func reformatImg(data string) []string {
+ re, _ := regexp.Compile(`"([^\"]*.(png|jpg|jpeg|gif|mp4)")`)
+ list := re.FindAllString(data, -1)
+ return list
+}
+
+func removeDuplicateElement(addrs []string) []string {
+ result := make([]string, 0, len(addrs))
+ temp := map[string]int{}
+ i := 1
+ for _, item := range addrs {
+ if _, ok := temp[item]; !ok {
+ temp[item] = i
+ result = append(result, item)
+ continue
+ }
+ temp[item] = temp[item] + 1
+ }
+ // fmt.Println(temp)
+ return result
+}
+
+// 单条记录获取DB
+func SysCfgGet(c *gin.Context, key string) string {
+ res := SysCfgFind(c, key)
+ //fmt.Println(res)
+ if _, ok := res[key]; !ok {
+ return ""
+ }
+ return res[key]
+}
+
+// 多条记录获取
+func SysCfgFind(c *gin.Context, keys ...string) map[string]string {
+ eg := DBs[c.GetString("mid")]
+ masterId := c.GetString("mid")
+ res := map[string]string{}
+ //TODO::判断keys长度(大于10个直接查数据库)
+ if len(keys) > 10 {
+ cfgList, _ := SysCfgGetAll(eg)
+ if cfgList == nil {
+ return nil
+ }
+ for _, v := range *cfgList {
+ res[v.Key] = v.Val
+ }
+ } else {
+ for _, key := range keys {
+ res[key] = SysCfgGetWithDb(eg, masterId, key)
+ }
+ }
+ return res
+}
diff --git a/app/db/db_user.go b/app/db/db_user.go
new file mode 100644
index 0000000..00dcf05
--- /dev/null
+++ b/app/db/db_user.go
@@ -0,0 +1,395 @@
+package db
+
+import (
+ "applet/app/db/model"
+ "applet/app/utils/logx"
+ "fmt"
+ "strings"
+ "xorm.io/xorm"
+)
+
+// UserisExistByUsernameAndPassword is usernameAndPassword exist
+func UserisExistByUsernameAndPassword(Db *xorm.Engine, username, password, zone string) (*model.User, error) {
+ var user model.User
+ sess := Db.Where("(username = ? or phone=?) ", username, username)
+ if zone != "" && zone != "86" {
+ sess = sess.And("zone=?", zone)
+ }
+ has, err := sess.Get(&user)
+ if err != nil || has == false {
+ return nil, err
+ }
+ return &user, nil
+}
+
+// UserisExistByMobile is exist
+func UserisExistByMobile(Db *xorm.Engine, n string) (bool, error) {
+ has, err := Db.Where("phone = ? and phone<>''", n).Exist(&model.User{})
+
+ if err != nil {
+ return false, err
+ }
+ return has, nil
+}
+func UserisFindByMobile(sess *xorm.Session, n string) *model.User {
+ var data model.User
+ has, err := sess.Where("phone = ? and phone<>''", n).Get(&data)
+
+ if err != nil || has == false {
+ return nil
+ }
+ return &data
+}
+
+// UserInByUIDByLevel is In查询 以及是否是有效用户
+func UserInByUIDByLevel(Db *xorm.Engine, ids []int, levelID interface{}) (*[]model.User, error) {
+ var m []model.User
+ if err := Db.In("uid", ids).Where("level = ?", levelID).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UserFindByMobile search user by mobile
+func UserFindByMobile(Db *xorm.Engine, mobile string) (*model.User, error) {
+ var m model.User
+ if has, err := Db.Where("(phone = ? OR uid = ?) AND delete_at = 0", mobile, mobile).
+ Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UserFindExistByMobile search user by mobile
+func UserFindExistByMobile(Db *xorm.Engine, mobile string) (*model.User, bool, error) {
+ var m model.User
+ has, err := Db.Where("(phone = ? OR uid = ?) AND delete_at = 0", mobile, mobile).Get(&m)
+ if err != nil {
+ logx.Infof("UserFindExistByMobile err")
+ return nil, false, logx.Warn(err)
+ }
+ return &m, has, nil
+}
+
+// UserFindByMobile search user by mobile
+func UserFindByMobileAll(Db *xorm.Engine, mobile string) (*model.User, error) {
+ var m model.User
+ if has, err := Db.Where("(phone = ? OR uid = ?)", mobile, mobile).
+ Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UserGetByMobileIgnoreDelete search user by mobile ignore delete
+func UserGetByMobileIgnoreDelete(Db *xorm.Engine, mobile, zone string) (*model.User, bool, error) {
+ m := new(model.User)
+ sess := Db.Where("phone = ?", mobile)
+ if zone != "" && zone != "86" {
+ sess = sess.And("zone=?", zone)
+ }
+ has, err := sess.Get(m)
+ if err != nil {
+ return nil, false, logx.Warn(err)
+ }
+ return m, has, nil
+}
+
+// UsersFindByMobileLike search users by mobile
+func UsersFindByMobileLike(Db *xorm.Engine, mobile string) (*[]model.User, error) {
+ var m []model.User
+ if err := Db.Where("phone like ?", "%"+mobile+"%").
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UsersFindByNickNameLike search users by nickname
+func UsersFindByNickNameLike(Db *xorm.Engine, nickname string) (*[]model.User, error) {
+ var m []model.User
+ if err := Db.Where("nickname like ?", "%"+nickname+"%").
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+func UsersFindByInviteCode(Db *xorm.Engine, nickname string) (*[]model.UserProfile, error) {
+ var m []model.UserProfile
+ if err := Db.Where("invite_code like ? or custom_invite_code like ?", "%"+strings.ToLower(nickname)+"%", "%"+strings.ToLower(nickname)+"%").
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+func UsersFindByInviteCodeMust(Db *xorm.Engine, nickname string) (*[]model.UserProfile, error) {
+ var m []model.UserProfile
+ if err := Db.Where("invite_code = ? or custom_invite_code = ?", strings.ToLower(nickname), strings.ToLower(nickname)).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UsersInByIds is 根据ids 查找users
+func UsersInByIds(Db *xorm.Engine, ids []int, limit, start int) (*[]model.User, error) {
+ var m []model.User
+ if limit == 0 && start == 0 {
+ if err := Db.In("uid", ids).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+ }
+ if err := Db.In("uid", ids).Limit(limit, start).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UsersInByIdsWhereLv is 根据ids和 lv会员等级 查找users
+func UsersInByIdsWhereLv(Db *xorm.Engine, ids []int, lv interface{}, limit, start int) (*[]model.User, error) {
+ var m []model.User
+ if limit == 0 && start == 0 {
+ if err := Db.Where("level = ?", lv).In("uid", ids).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+ }
+ if err := Db.Where("level = ?", lv).In("uid", ids).Limit(limit, start).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UsersInByIdsByAscWhereLv is 根据ids和 lv会员等级 查找users 升排序
+func UsersInByIdsByAscWhereLv(Db *xorm.Engine, ids []int, lv interface{}, limit, start int, c string) (*[]model.User, error) {
+ var m []model.User
+ if limit == 0 && start == 0 {
+ if err := Db.Where("level = ?", lv).In("uid", ids).Asc(c).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+ }
+ if err := Db.Where("level = ?", lv).In("uid", ids).Asc(c).Limit(limit, start).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UsersInByIdsByDescWhereLv is 根据ids和 lv会员等级 查找users 降排序
+func UsersInByIdsByDescWhereLv(Db *xorm.Engine, ids []int, lv interface{}, limit, start int, c string) (*[]model.User, error) {
+ var m []model.User
+ if limit == 0 && start == 0 {
+ if err := Db.Where("level = ?", lv).In("uid", ids).Desc(c).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+ }
+ if err := Db.Where("level = ?", lv).In("uid", ids).Desc(c).Limit(limit, start).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+func UserFindByPhoneOrUsername(Db *xorm.Engine, mobile string) (*model.User, error) {
+ var m model.User
+ if has, err := Db.Where("(phone = ? or username=?) AND delete_at = 0", mobile, mobile).
+ Get(&m); err != nil || has == false {
+ return nil, err
+ }
+ return &m, nil
+}
+
+// UserFindByArkidUserName search user by mobile
+func UserFindByArkidUserName(Db *xorm.Engine, name string) (*model.User, error) {
+ var m model.User
+ if has, err := Db.Where("username = ?", name).
+ Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UserFindByID is find user byid
+func UserFindByID(Db *xorm.Engine, id interface{}) (*model.User, error) {
+ var m model.User
+ if has, err := Db.Where("uid = ?", id).
+ Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+func UserFindByIDWithSession(sess *xorm.Session, id interface{}) (*model.User, error) {
+ var m model.User
+ if has, err := sess.Where("uid = ?", id).
+ Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+func UserFindByIDs(Db *xorm.Engine, uids []int) (*[]model.User, error) {
+ var m []model.User
+ if err := Db.In("uid", uids).Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+func UserFindByall(Db *xorm.Engine) (*[]model.User, error) {
+ var m []model.User
+ if err := Db.Where("uid>0").Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+func UserFindByIDsToStr(Db *xorm.Engine, uids []string) (*[]model.User, error) {
+ var m []model.User
+ if err := Db.In("uid", uids).Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+func UserFindByIsSet(Db *xorm.Engine, limit, start int) ([]model.UserProfile, error) {
+ var m []model.UserProfile
+ if err := Db.Where("is_set=? and parent_uid=0 and uid>0", 0).Limit(limit, start).Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return m, nil
+}
+func UserFindByParentUid(Db *xorm.Engine, parentUid int) ([]model.UserProfile, error) {
+ var m []model.UserProfile
+ if err := Db.Where("parent_uid=?", parentUid).Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return m, nil
+}
+
+// UsersInByIdsByDesc is 根据某列 降序
+func UsersInByIdsByDesc(Db *xorm.Engine, ids []int, limit, start int, c string) (*[]model.User, error) {
+ var m []model.User
+ if limit == 0 && start == 0 {
+ if err := Db.In("uid", ids).Desc(c).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+ }
+ if err := Db.In("uid", ids).Desc(c).Limit(limit, start).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UsersInByIdsByAsc is 根据某列 升序
+func UsersInByIdsByAsc(Db *xorm.Engine, ids []int, limit, start int, c string) (*[]model.User, error) {
+ var m []model.User
+ if limit == 0 && start == 0 {
+ if err := Db.In("uid", ids).Asc(c).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+ }
+ if err := Db.In("uid", ids).Asc(c).Limit(limit, start).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UserInsert is insert user
+func UserInsert(Db *xorm.Engine, user *model.User) (int64, error) {
+ affected, err := Db.Insert(user)
+ if err != nil {
+ return 0, err
+ }
+ return affected, nil
+}
+
+// UserIsExistByMobile is mobile exist
+func UserIsExistByMobile(Db *xorm.Engine, mobile string) (bool, error) {
+ //fmt.Println(mobile)
+ has, err := Db.Where("phone = ? OR uid = ?", mobile, mobile).Exist(&model.User{})
+ fmt.Println(has, mobile)
+ if err != nil {
+ return false, err
+ }
+ return has, nil
+}
+
+// UserIsExistByID is mobile exist by id
+func UserIsExistByID(Db *xorm.Engine, id string) (bool, error) {
+ has, err := Db.Where("uid = ?", id).Exist(&model.User{})
+ if err != nil {
+ return false, err
+ }
+ return has, nil
+}
+
+// UserUpdate is update user
+func UserUpdate(Db *xorm.Engine, uid interface{}, user *model.User, forceColums ...string) (int64, error) {
+ var (
+ affected int64
+ err error
+ )
+ if forceColums != nil {
+ affected, err = Db.Where("uid=?", uid).Cols(forceColums...).Update(user)
+ } else {
+ affected, err = Db.Where("uid=?", uid).Update(user)
+ }
+ if err != nil {
+ return 0, err
+ }
+ return affected, nil
+}
+
+func UserUpdateWithSession(Db *xorm.Session, uid interface{}, user *model.User, forceColums ...string) (int64, error) {
+ var (
+ affected int64
+ err error
+ )
+ if forceColums != nil {
+ affected, err = Db.Where("uid=?", uid).Cols(forceColums...).Update(user)
+ } else {
+ affected, err = Db.Where("uid=?", uid).Update(user)
+ }
+ if err != nil {
+ return 0, err
+ }
+ return affected, nil
+}
+
+func UpdateUserFinValid() {
+
+}
+
+// UserDelete is delete user
+func UserDelete(Db *xorm.Engine, uid interface{}) (int64, error) {
+ return Db.Where("uid = ?", uid).Delete(model.User{})
+}
+
+func UserDeleteWithSess(sess *xorm.Session, uid interface{}) (int64, error) {
+ return sess.Where("uid = ?", uid).Delete(model.User{})
+}
+
+func UserProfileCheckInviteCode(eg *xorm.Engine, uid int, inviteCode string) bool {
+ var data model.UserProfile
+ get, err := eg.Where("invite_code=? or custom_invite_code=?", inviteCode, inviteCode).Get(&data)
+ if get == false || err != nil {
+ return false
+ }
+ if uid == data.Uid {
+ return false
+ }
+ return true
+}
diff --git a/app/db/db_user_fin_flow.go b/app/db/db_user_fin_flow.go
new file mode 100644
index 0000000..253b061
--- /dev/null
+++ b/app/db/db_user_fin_flow.go
@@ -0,0 +1,103 @@
+package db
+
+import (
+ "applet/app/db/model"
+ "applet/app/utils/logx"
+
+ "xorm.io/xorm"
+)
+
+// GetFinUserFlowByID is 用户流水记录
+func GetFinUserFlowByID(Db *xorm.Engine, id interface{}) (*model.FinUserFlow, error) {
+ var m model.FinUserFlow
+ if has, err := Db.Where("id = ?", id).Get(&m); err != nil || !has {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// GetFinUserFlowByID is 用户流水记录
+func GetFinUserFlowByUIDANDOID(Db *xorm.Engine, types, uid, ordId string) (*model.FinUserFlow, error) {
+ var m model.FinUserFlow
+ if has, err := Db.Where("uid = ? and ord_id = ? and type = ?", uid, ordId, types).Get(&m); err != nil || !has {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+func GetFinUserFlowByUIDANDOIDTOORDTYPE(Db *xorm.Engine, types, uid, ordId string) (*model.FinUserFlow, error) {
+ var m model.FinUserFlow
+ if has, err := Db.Where("uid = ? and ord_id = ? and ord_type = ?", uid, ordId, types).Get(&m); err != nil || !has {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// GetFinUserFlowByID is 用户流水记录
+func GetFinUserFlowByOIDANDORDTYPE(Db *xorm.Engine, types, ordId, ordType string) (*model.FinUserFlow, error) {
+ var m model.FinUserFlow
+ if has, err := Db.Where("ord_id = ? and ord_action = ? and ord_type= ?", ordId, types, ordType).Get(&m); err != nil || !has {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+//FinUserFlowInsertOne is 插入一条流水记录
+func FinUserFlowInsertOne(Db *xorm.Engine, m *model.FinUserFlow) error {
+ _, err := Db.InsertOne(m)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+//FinUserFlowInsertOne is 插入一条流水记录
+func FinUserFlowWithSessionInsertOne(session *xorm.Session, m *model.FinUserFlow) error {
+ _, err := session.InsertOne(m)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// FinUserFlowByUID is 用户流水
+func FinUserFlowInputByUID(Db *xorm.Engine, uid interface{}, time string, limit, start int) ([]*model.FinUserFlow, error) {
+ var m []*model.FinUserFlow
+ if err := Db.Where("uid = ? AND create_at like ? and (amount> ? or ord_type=?)", uid, time+"%", "0", "fast_return").In("type", "0").Desc("create_at").Limit(limit, start).Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return m, nil
+}
+func FinUserFlowInputByUIDWithAmount(Db *xorm.Engine, uid interface{}, types, before_amount, after_amount string) (*model.FinUserFlow, error) {
+ var m model.FinUserFlow
+ if has, err := Db.Where("uid = ? and ord_type='withdraw' and ord_action = ? and before_amount= ? and after_amount = ?", uid, types, before_amount, after_amount).Get(&m); err != nil || !has {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// FinUserFlowByUIDByOrderAction is 用户流水 by OrderAction
+func FinUserFlowInputByUIDByOrderActionByTime(Db *xorm.Engine, uid, oa interface{}, time string, limit, start int) ([]*model.FinUserFlow, error) {
+ var m []*model.FinUserFlow
+ if err := Db.Where("uid = ? AND create_at like ? and amount>0", uid, time+"%").In("ord_action", oa).Desc("create_at").Limit(limit, start).Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return m, nil
+}
+
+// FinUserFlowByUIDByOrderAction is 用户流水 by OrderAction
+func FinUserFlowInputByUIDByTypeByTime(Db *xorm.Engine, uid int, time string, limit, start int) ([]*model.FinUserFlow, error) {
+ var m []*model.FinUserFlow
+ if err := Db.Where("uid = ? AND type = 1 AND create_at like ? and (amount>0 or ord_type=? or ord_action=?)", uid, time+"%", "fast_return", 101).Desc("create_at").Limit(limit, start).Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return m, nil
+}
+
+// 在事务中使用,插入一条流水记录
+func FinUserFlowInsertOneWithSession(session *xorm.Session, m *model.FinUserFlow) error {
+ _, err := session.InsertOne(m)
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/app/db/db_user_level.go b/app/db/db_user_level.go
new file mode 100644
index 0000000..5169251
--- /dev/null
+++ b/app/db/db_user_level.go
@@ -0,0 +1,226 @@
+package db
+
+import (
+ "applet/app/db/model"
+ "applet/app/e"
+ "applet/app/utils/logx"
+ "github.com/gin-gonic/gin"
+ "xorm.io/xorm"
+)
+
+//UserLevelByID is 根据用户id 获取对应的等级信息
+func UserLevelByID(Db *xorm.Engine, id interface{}) (*model.UserLevel, error) {
+ m := new(model.UserLevel)
+ has, err := Db.Where("id = ?", id).Get(m)
+ if err != nil {
+ return nil, logx.Warn(err)
+ }
+ if !has {
+ return nil, logx.Error("Not found")
+ }
+
+ return m, nil
+}
+func UserLevelByIDWithSession(sess *xorm.Session, id interface{}) (*model.UserLevel, error) {
+ m := new(model.UserLevel)
+ has, err := sess.Where("id = ?", id).Get(m)
+ if err != nil {
+ return nil, logx.Warn(err)
+ }
+ if !has {
+ return nil, logx.Error("Not found")
+ }
+
+ return m, nil
+}
+
+//UserLevelTop is 查询最高的等级
+func UserLevelTop(Db *xorm.Engine) (*model.UserLevel, error) {
+ m := new(model.UserLevel)
+ has, err := Db.OrderBy("level_weight DESC").Get(m)
+ if err != nil {
+ return nil, logx.Warn(err)
+ }
+ if !has {
+ return nil, logx.Error("Not found")
+ }
+
+ return m, nil
+}
+
+//UserLevelNext is 查询下一等级
+func UserLevelNext(Db *xorm.Engine, curLevelWeight int) (*model.UserLevel, error) {
+ m := new(model.UserLevel)
+ has, err := Db.Where("level_weight > ? and before_hide=?", curLevelWeight, 0).OrderBy("level_weight ASC").Get(m)
+ if err != nil {
+ return nil, logx.Warn(err)
+ }
+ if !has {
+ return nil, logx.Error("Not found")
+ }
+
+ return m, nil
+}
+
+// UserLevelByWeight is 根据权重获取对应的等级
+func UserLevelByWeight(Db *xorm.Engine, w interface{}) (*model.UserLevel, error) {
+ m := new(model.UserLevel)
+ has, err := Db.Where("level_weight = ?", w).Get(m)
+ if err != nil {
+ return nil, logx.Warn(err)
+ }
+ if !has {
+ return nil, logx.Warn("Not found")
+ }
+ return m, nil
+}
+
+//UserLevelInIDescByWeight is In 查询获取 权重最低 对应等级
+func UserLevelInIDescByWeightLow(Db *xorm.Engine) ([]*model.UserLevel, error) {
+ var ms []*model.UserLevel
+ if err := Db.Asc("level_weight").Limit(1).Find(&ms); err != nil {
+ return nil, err
+ }
+ return ms, nil
+
+}
+func UserLevelInIDescByWeightLowWithOne(Db *xorm.Engine) (*model.UserLevel, error) {
+ var ms model.UserLevel
+ has, err := Db.Asc("level_weight").Get(&ms)
+ if err != nil {
+ return nil, err
+ }
+ if has == false {
+ return nil, e.NewErr(400, "等级不存在")
+ }
+ return &ms, nil
+
+}
+func UserLevelInIDescByWeightDescWithOne(Db *xorm.Engine) (*model.UserLevel, error) {
+ var ms model.UserLevel
+ has, err := Db.Desc("level_weight").Get(&ms)
+ if err != nil {
+ return nil, err
+ }
+ if has == false {
+ return nil, e.NewErr(400, "等级不存在")
+ }
+ return &ms, nil
+
+}
+func UserLevelByWeightNext(Db *xorm.Engine, levelWeight int) (*model.UserLevel, error) {
+ var ms model.UserLevel
+ if has, err := Db.Where("level_weight>? and is_use=? and before_hide=?", levelWeight, 1, 0).Asc("level_weight").Get(&ms); err != nil || has == false {
+ return nil, err
+ }
+ return &ms, nil
+
+}
+func UserLevelByWeightMax(Db *xorm.Engine) (*model.UserLevel, error) {
+ var ms model.UserLevel
+ if has, err := Db.Where("is_use=? and before_hide=?", 1, 0).Desc("level_weight").Get(&ms); err != nil || has == false {
+ return nil, err
+ }
+ return &ms, nil
+
+}
+
+//UserLevelInIDescByWeight is In 查询获取对应等级 根据权重排序
+func UserLevelInIDescByWeight(Db *xorm.Engine, ids []int) ([]*model.UserLevel, error) {
+ var ms []*model.UserLevel
+ if err := Db.In("id", ids).Desc("level_weight").Find(&ms); err != nil {
+ return nil, err
+ }
+ return ms, nil
+
+}
+func UserLevelIDescByWeight(Db *xorm.Engine, id int) (*model.UserLevel, error) {
+ var ms model.UserLevel
+ if has, err := Db.Where("id=?", id).Get(&ms); err != nil || has == false {
+ return nil, err
+ }
+ return &ms, nil
+
+}
+
+// UserLevlAll is 获取所有开启等级并且升序返回
+func UserLevlAll(c *gin.Context, Db *xorm.Engine) ([]*model.UserLevel, error) {
+ var m []*model.UserLevel
+ err := Db.Where("is_use = ?", 1).Asc("level_weight").Find(&m)
+ if err != nil {
+ return nil, logx.Warn(err)
+ }
+
+ mm, err := sysModFormat(c, m)
+ if err != nil {
+ return nil, err
+ }
+ return mm.([]*model.UserLevel), nil
+}
+func UserLevlAllNew(c *gin.Context, Db *xorm.Engine) ([]*model.UserLevel, error) {
+ var m []*model.UserLevel
+ err := Db.Where("is_use = ? and before_hide=?", 1, 0).Asc("level_weight").Find(&m)
+ if err != nil {
+ return nil, logx.Warn(err)
+ }
+
+ mm, err := sysModFormat(c, m)
+ if err != nil {
+ return nil, err
+ }
+ return mm.([]*model.UserLevel), nil
+}
+
+// UserLevlEgAll is 获取所有开启等级并且升序返回
+func UserLevlEgAll(Db *xorm.Engine) ([]*model.UserLevel, error) {
+ var m []*model.UserLevel
+ err := Db.Where("is_use = ?", 1).Asc("level_weight").Find(&m)
+ if err != nil {
+ return nil, logx.Warn(err)
+ }
+ return m, nil
+}
+func UserFindByLevel(eg *xorm.Engine, level int) []model.User {
+ var data []model.User
+ eg.Where("level=?", level).Find(&data)
+ return data
+}
+
+// UserLevlAllByWeight is 获取所有等级并且权重升序返回
+func UserLevlAllByWeight(c *gin.Context, Db *xorm.Engine) ([]*model.UserLevel, error) {
+ var m []*model.UserLevel
+ err := Db.Asc("level_weight").Find(&m)
+ if err != nil {
+ return nil, logx.Warn(err)
+ }
+
+ mm, err := sysModFormat(c, m)
+ if err != nil {
+ return nil, err
+ }
+ return mm.([]*model.UserLevel), nil
+}
+func UserLevelByAllMap(Db *xorm.Engine) map[int]*model.UserLevel {
+ var maps = make(map[int]*model.UserLevel, 0)
+ var m []*model.UserLevel
+ err := Db.Where("is_use = ?", 1).Asc("level_weight").Find(&m)
+ if err != nil {
+ return maps
+ }
+ for _, v := range m {
+ maps[v.Id] = v
+ }
+ return maps
+}
+func UserLevelByNotHideAllMap(Db *xorm.Engine) map[int]*model.UserLevel {
+ var maps = make(map[int]*model.UserLevel, 0)
+ var m []*model.UserLevel
+ err := Db.Where("is_use = ? and before_hide=?", 1, 0).Asc("level_weight").Find(&m)
+ if err != nil {
+ return maps
+ }
+ for _, v := range m {
+ maps[v.Id] = v
+ }
+ return maps
+}
diff --git a/app/db/db_user_profile.go b/app/db/db_user_profile.go
new file mode 100644
index 0000000..7f1c3c5
--- /dev/null
+++ b/app/db/db_user_profile.go
@@ -0,0 +1,578 @@
+package db
+
+import (
+ "applet/app/db/model"
+ "applet/app/md"
+ "applet/app/utils"
+ "applet/app/utils/logx"
+ "errors"
+ "xorm.io/xorm"
+)
+
+// UserProfileFindByArkID is get userprofile by arkid
+func UserProfileFindByArkID(Db *xorm.Engine, id interface{}) (*model.UserProfile, error) {
+ var m model.UserProfile
+ if has, err := Db.Where("arkid_uid = ?", id).Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+func UserProfileFindByArkToken(Db *xorm.Engine, id interface{}) (*model.UserProfile, error) {
+ var m model.UserProfile
+ if has, err := Db.Where("arkid_token<>'' and arkid_token = ?", id).Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UserProfileFindByInviteCode is get userprofile by InviteCode
+func UserProfileFindByInviteCode(Db *xorm.Engine, code string) (*model.UserProfile, error) {
+ var m model.UserProfile
+ if has, err := Db.Where("invite_code = ?", code).Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UserProfileFindByInviteCode is get userprofile by InviteCode
+func UserProfileFindByCustomInviteCode(Db *xorm.Engine, code string) (*model.UserProfile, error) {
+ var m model.UserProfile
+ if has, err := Db.Where("custom_invite_code = ?", code).Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UserProfileFindByInviteCodes is get userprofile by InviteCode
+func UserProfileFindByInviteCodes(Db *xorm.Engine, codes ...string) (*[]model.UserProfile, error) {
+ var m []model.UserProfile
+ if err := Db.In("invite_code", codes).Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+func UserProfileFindByAll(Db *xorm.Engine) (*[]model.UserProfile, error) {
+ var m []model.UserProfile
+ if err := Db.Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UserProfileFindByCustomInviteCodes is get userprofile by CustomInviteCode
+func UserProfileFindByCustomInviteCodes(Db *xorm.Engine, codes ...string) (*[]model.UserProfile, error) {
+ var m []model.UserProfile
+ if err := Db.In("custom_invite_code", codes).Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UserProfileFindByID search user_profile by userid
+func UserProfileFindByID(Db *xorm.Engine, id interface{}) (*model.UserProfile, error) {
+ var m model.UserProfile
+ if has, err := Db.Where("uid = ?", id).Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+func UserProfileFindAll(Db *xorm.Engine) (*[]model.UserProfile, error) {
+ var m []model.UserProfile
+ if err := Db.Where("invite_code='' and uid>=0").Asc("uid").Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+func UserProfileFindByIDsToStr(Db *xorm.Engine, uids []string) (*[]model.UserProfile, error) {
+ var m []model.UserProfile
+ if err := Db.In("uid", uids).Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+func UserProfileFindByIDSess(sess *xorm.Session, id interface{}) (*model.UserProfile, error) {
+ var m model.UserProfile
+ if has, err := sess.Where("uid = ?", id).Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+func UserProfileFindByPID(Db *xorm.Engine, id interface{}) (*model.UserProfile, error) {
+ var m model.UserProfile
+ if has, err := Db.Where("parent_uid = ?", id).Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UserProfileOrderByNew 找最新的记录
+func UserProfileOrderByNew(Db *xorm.Engine) (*model.UserProfile, error) {
+ var m model.UserProfile
+ if has, err := Db.Where("invite_code != ''").OrderBy("uid desc").Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UserProfileFindByTaobaoOpenID search user_profile ByTaobaoOpenID
+func UserProfileFindByTaobaoOpenID(Db *xorm.Engine, openid interface{}) (*model.UserProfile, error) {
+ var m model.UserProfile
+ if has, err := Db.Where("third_party_taobao_oid = ?", openid).Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UserProfileFindByQQOpenID search user_profile ByTaobaoOpenID
+func UserProfileFindByQQOpenID(Db *xorm.Engine, openid interface{}) (*model.UserProfile, error) {
+ var m model.UserProfile
+ if has, err := Db.Where("third_party_qq_openid = ?", openid).Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UserProfileFindByAppleToken search user_profile AppleToken
+func UserProfileFindByAppleToken(Db *xorm.Engine, token interface{}) (*model.UserProfile, error) {
+ var m model.UserProfile
+ if has, err := Db.Where("third_party_apple_token = ?", token).Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UserProfileFindByWeChatOpenID search user_profile By 微信openid
+func UserProfileFindByWeChatOpenID(Db *xorm.Engine, openid interface{}) (*model.UserProfile, error) {
+ var m model.UserProfile
+ if has, err := Db.Where("third_party_wechat_openid = ?", openid).Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UserProfileFindByWeChatMiniOpenID search user_profile By 小程序openid
+func UserProfileFindByWeChatMiniOpenID(Db *xorm.Engine, openid interface{}) (*model.UserProfile, error) {
+ var m model.UserProfile
+ if has, err := Db.Where("third_party_wechat_mini_openid = ?", openid).Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UserProfileFindByWeChatUnionID search user_profile By 微信唯一id
+func UserProfileFindByWeChatUnionID(Db *xorm.Engine, openid interface{}) (*model.UserProfile, error) {
+ if openid == "" {
+ return nil, errors.New("不存在")
+ }
+ var m model.UserProfile
+ if has, err := Db.Where("third_party_wechat_unionid = ? ", openid).Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+func UserProfileFindByAccAlipay(Db *xorm.Engine, accAlipay string, uid int) (bool, error) {
+ if has, err := Db.Where("acc_alipay = ? and uid <>?", accAlipay, uid).Exist(&model.UserProfile{}); err != nil || has == false {
+ return false, logx.Warn(err)
+ }
+ return true, nil
+}
+
+// UserProfileisExistByTaobaoOpenID is exist by Taobao
+func UserProfileisExistByTaobaoOpenID(Db *xorm.Engine, openid string) (bool, error) {
+ has, err := Db.Where("third_party_taobao_oid = ?", openid).Exist(&model.UserProfile{})
+
+ if err != nil {
+ return false, err
+ }
+ return has, nil
+}
+func UserProfileisThirdPartyWechatH5Openid(Db *xorm.Engine, openid string) (*model.UserProfile, error) {
+ var user model.UserProfile
+ has, err := Db.Where("third_party_wechat_h5_openid = ? and third_party_wechat_h5_openid<>''", openid).Get(&user)
+
+ if err != nil || has == false {
+ return nil, err
+ }
+ return &user, nil
+}
+
+// UserProfileisExistByQQOpenID is exist by QQ openid
+func UserProfileisExistByQQOpenID(Db *xorm.Engine, openid string) (bool, error) {
+ has, err := Db.Where("third_party_qq_openid = ?", openid).Exist(&model.UserProfile{})
+
+ if err != nil {
+ return false, err
+ }
+ return has, nil
+}
+
+// UserProfileisExistByAppleToken is exist by apple token
+func UserProfileisExistByAppleToken(Db *xorm.Engine, token string) (bool, error) {
+ has, err := Db.Where("third_party_apple_token = ?", token).Exist(&model.UserProfile{})
+
+ if err != nil {
+ return false, err
+ }
+ return has, nil
+}
+
+// UserProfileisExistByWeChatOpenID is exist by Wecaht openid
+func UserProfileisExistByWeChatOpenID(Db *xorm.Engine, openid string) (bool, error) {
+ has, err := Db.Where("third_party_wechat_openid = ?", openid).Exist(&model.UserProfile{})
+
+ if err != nil {
+ return false, err
+ }
+ return has, nil
+}
+
+// UserProfileisExistByWeChatMiniOpenID is exist by Wecaht openid
+func UserProfileisExistByWeChatMiniOpenID(Db *xorm.Engine, openid string) (bool, error) {
+ has, err := Db.Where("third_party_wechat_mini_openid = ?", openid).Exist(&model.UserProfile{})
+
+ if err != nil {
+ return false, err
+ }
+ return has, nil
+}
+
+// UserProfileisExistByWeChatUnionID is exist by Wecaht openid
+func UserProfileisExistByWeChatUnionID(Db *xorm.Engine, openid string) (bool, error) {
+ if openid == "" {
+ return false, errors.New("不存在")
+ }
+ has, err := Db.Where("third_party_wechat_unionid = ? ", openid).Exist(&model.UserProfile{})
+
+ if err != nil {
+ return false, err
+ }
+ return has, nil
+}
+
+// UserProfileisExistByRelationIDAndSpecialID is exist by RelationIdAndSpecialId
+func UserProfileisExistByRelationIDAndSpecialID(Db *xorm.Engine, SpecialID, RelationID int64) (bool, error) {
+ has, err := Db.Where("acc_taobao_self_id = ? AND acc_taobao_share_id = ?", SpecialID, RelationID).Exist(&model.UserProfile{})
+
+ if err != nil {
+ return false, err
+ }
+ return has, nil
+}
+
+// UserProfileisExistBySpecialID is exist by SpecialId
+func UserProfileisExistBySpecialID(Db *xorm.Engine, SpecialID string) (bool, error) {
+ has, err := Db.Where("acc_taobao_self_id = ? ", SpecialID).Exist(&model.UserProfile{})
+
+ if err != nil {
+ return false, err
+ }
+ return has, nil
+}
+
+// UserProfileCountByRelationID 统计relationID数量
+func UserProfileCountByRelationID(Db *xorm.Engine) (total int64, err error) {
+ relate := new(model.UserProfile)
+ total, err = Db.Where("acc_taobao_share_id > 0").Count(relate)
+ return
+}
+
+// UserProfileCountByPUID 统计直推下级数量
+func UserProfileCountByPUID(Db *xorm.Engine, puid int) (total int64, err error) {
+ relate := new(model.UserProfile)
+ total, err = Db.Where("parent_uid = ?", puid).Count(relate)
+ return
+}
+
+// UserProfileisExistByRelationID is exist by RelationID
+func UserProfileisExistByRelationID(Db *xorm.Engine, RelationID string) (bool, error) {
+ has, err := Db.Where("acc_taobao_share_id = ? ", RelationID).Exist(&model.UserProfile{})
+
+ if err != nil {
+ return false, err
+ }
+ return has, nil
+}
+
+// UserProfileFindByIDs is in sql by ids
+func UserProfileFindByIDs(Db *xorm.Engine, uids ...int) (*[]model.UserProfile, error) {
+ var m []model.UserProfile
+ if err := Db.In("uid", uids).Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+func UserProfileFindByIDsStr(Db *xorm.Engine, uids []string) (*[]model.UserProfile, error) {
+ var m []model.UserProfile
+ if err := Db.In("uid", uids).Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UserProfileByPuid search user_profile by parent_uid
+func UserProfileByPuid(Db *xorm.Engine, puid interface{}) (*[]model.UserProfile, error) {
+ var m []model.UserProfile
+ if err := Db.Where("parent_uid = ?", puid).Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+func UserProfileByPuidWithSess(sess *xorm.Session, puid interface{}) (*[]model.UserProfile, error) {
+ var m []model.UserProfile
+ if err := sess.Where("parent_uid = ?", puid).Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UsersProfileInByIds is profiles by ids
+func UsersProfileInByIds(Db *xorm.Engine, ids []int, limit, start int) (*[]model.UserProfile, error) {
+ var m []model.UserProfile
+ if limit == 0 && start == 0 {
+ if err := Db.In("uid", ids).Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+ }
+ if err := Db.In("uid", ids).Limit(limit, start).Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UsersProfileInByUIDByisVerify is In查询 以及是否是有效用户
+func UsersProfileInByUIDByisVerify(Db *xorm.Engine, ids []int, isVerify interface{}) (*[]model.UserProfile, error) {
+ var m []model.UserProfile
+ if err := Db.In("uid", ids).Where("is_verify = ?", isVerify).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UsersProfileInByIdsByDesc is 根据某列 降序
+func UsersProfileInByIdsByDesc(Db *xorm.Engine, ids []int, limit, start int, c string) (*[]model.UserProfile, error) {
+ var m []model.UserProfile
+ if limit == 0 && start == 0 {
+ if err := Db.In("uid", ids).Desc(c).Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+ }
+ if err := Db.In("uid", ids).Desc(c).Limit(limit, start).Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UsersProfileInByIdsByAsc is 根据某列 升序
+func UsersProfileInByIdsByAsc(Db *xorm.Engine, ids []int, limit, start int, c string) (*[]model.UserProfile, error) {
+ var m []model.UserProfile
+ if limit == 0 && start == 0 {
+ if err := Db.In("uid", ids).Asc(c).Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+ }
+ if err := Db.In("uid", ids).Asc(c).Limit(limit, start).Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UsersProfileByAll is 查询所有分享id大于0的数据
+func UsersProfileByTaobaoShateIdNotNull(Db *xorm.Engine, limit, start int) (*[]model.UserProfile, error) {
+ var m []model.UserProfile
+ if err := Db.Where("acc_taobao_share_id > 0").Limit(limit, start).Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UserProfileIsExistByUserID is mobile exist
+func UserProfileIsExistByUserID(Db *xorm.Engine, id int) (bool, error) {
+ has, err := Db.Where("uid = ?", id).Exist(&model.UserProfile{})
+ if err != nil {
+ return false, err
+ }
+ return has, nil
+}
+
+// UserProfileIsExistByInviteCode is exist ?
+func UserProfileIsExistByInviteCode(Db *xorm.Engine, code string) (bool, error) {
+ has, err := Db.Where("invite_code = ?", code).Exist(&model.UserProfile{})
+ if err != nil {
+ return false, err
+ }
+ return has, nil
+}
+
+// UserProfileIsExistByCustomInviteCode is exist ?
+func UserProfileIsExistByCustomInviteCode(Db *xorm.Engine, code string) (bool, error) {
+ has, err := Db.Where("custom_invite_code = ?", code).Exist(&model.UserProfile{})
+ if err != nil {
+ return false, err
+ }
+ return has, nil
+}
+
+// UserProfileInsert is insert user
+func UserProfileInsert(Db *xorm.Engine, userProfile *model.UserProfile) (int64, error) {
+ affected, err := Db.Insert(userProfile)
+ if err != nil {
+ return 0, err
+ }
+ return affected, nil
+}
+
+// UserProfileUpdate is update userprofile
+func UserProfileUpdate(Db *xorm.Engine, uid interface{}, userProfile *model.UserProfile, forceCols ...string) (int64, error) {
+ var (
+ affected int64
+ err error
+ )
+ if forceCols != nil {
+ affected, err = Db.Where("uid=?", uid).Cols(forceCols...).Update(userProfile)
+ } else {
+ affected, err = Db.Where("uid=?", uid).AllCols().Omit("fin_valid,parent_uid").Update(userProfile)
+ }
+
+ if err != nil {
+ return 0, logx.Warn(err)
+ }
+ return affected, nil
+}
+func UserProfileUpdateWithSess(sess *xorm.Session, uid interface{}, userProfile *model.UserProfile, forceCols ...string) (int64, error) {
+ var (
+ affected int64
+ err error
+ )
+ if forceCols != nil {
+ affected, err = sess.Where("uid=?", uid).Cols(forceCols...).Update(userProfile)
+ } else {
+ affected, err = sess.Where("uid=?", uid).AllCols().Omit("fin_valid").Update(userProfile)
+ }
+
+ if err != nil {
+ return 0, logx.Warn(err)
+ }
+ return affected, nil
+}
+
+// UserProfileUpdateByArkID is update userprofile
+func UserProfileUpdateByArkID(Db *xorm.Engine, arkid interface{}, userProfile *model.UserProfile, forceCols ...string) (int64, error) {
+ var (
+ affected int64
+ err error
+ )
+ if forceCols != nil {
+ affected, err = Db.Where("arkid_uid=?", arkid).Cols(forceCols...).Update(userProfile)
+ } else {
+ affected, err = Db.Where("arkid_uid=?", arkid).Update(userProfile)
+ }
+ if err != nil {
+ return 0, logx.Warn(err)
+ }
+ return affected, nil
+}
+
+// UserProfileDelete is delete user profile
+func UserProfileDelete(Db *xorm.Engine, uid interface{}) (int64, error) {
+ return Db.Where("uid = ?", uid).Delete(model.UserProfile{})
+}
+func UserProfileDeleteWithSess(sess *xorm.Session, uid interface{}) (int64, error) {
+ return sess.Where("uid = ?", uid).Delete(model.UserProfile{})
+}
+func UserProfileFindByIdWithSession(session *xorm.Session, uid int) (*model.UserProfile, error) {
+ var m model.UserProfile
+ if has, err := session.Where("uid = ?", uid).Get(&m); err != nil || has == false {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// 在事务中更新用户信息
+func UserProfileUpdateWithSession(session *xorm.Session, uid interface{}, userProfile *model.UserProfile, forceCols ...string) (int64, error) {
+ var (
+ affected int64
+ err error
+ )
+ if forceCols != nil {
+ affected, err = session.Where("uid=?", uid).Cols(forceCols...).Update(userProfile)
+ } else {
+ affected, err = session.Where("uid=?", uid).Omit("fin_valid").Update(userProfile)
+ }
+ if err != nil {
+ return 0, logx.Warn(err)
+ }
+ return affected, nil
+}
+
+// 根据uid获取md.user
+func UserAllInfoByUid(Db *xorm.Engine, uid interface{}) (*md.User, error) {
+ u, err := UserFindByID(Db, uid)
+ if err != nil {
+ return nil, err
+ }
+ if u == nil {
+ return nil, errors.New("user is nil")
+ }
+ up, err := UserProfileFindByID(Db, uid)
+ if err != nil {
+ return nil, err
+ }
+ if utils.AnyToInt64(uid) == 0 {
+ userLevel, err := UserLevelInIDescByWeightLowWithOne(Db)
+ if err != nil {
+ return nil, err
+ }
+ if userLevel != nil {
+ u.Level = userLevel.Id
+ }
+ }
+ // 获取user 等级
+ ul, err := UserLevelByID(Db, u.Level)
+ if u.Uid == 0 {
+ one, err := UserLevelInIDescByWeightLowWithOne(Db)
+ if err != nil {
+ return nil, err
+ }
+ ul = one
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ user := &md.User{
+ Info: u,
+ Profile: up,
+ Level: ul,
+ }
+ return user, nil
+}
+
+// UpdateUserProfileFinValid 更新用户余额
+func UpdateUserProfileFinValid(Db *xorm.Engine, uid interface{}, newAmount string) error {
+ update, err := Db.Where("uid=?", uid).Update(&model.UserProfile{FinValid: newAmount})
+ if err != nil {
+ return err
+ }
+ if update != 1 {
+ return errors.New("更新失败")
+ }
+
+ return nil
+}
+
+// UpdateUserProfileFinValidWithSess 事务更新用户余额
+func UpdateUserProfileFinValidWithSess(sess *xorm.Session, uid interface{}, newAmount string) error {
+ update, err := sess.Where("uid=?", uid).Update(&model.UserProfile{FinValid: newAmount})
+ if err != nil {
+ return err
+ }
+ if update != 1 {
+ return errors.New("更新失败")
+ }
+
+ return nil
+}
diff --git a/app/db/db_user_relate.go b/app/db/db_user_relate.go
new file mode 100644
index 0000000..380b80d
--- /dev/null
+++ b/app/db/db_user_relate.go
@@ -0,0 +1,249 @@
+package db
+
+import (
+ "applet/app/db/model"
+ "applet/app/utils/logx"
+
+ "xorm.io/xorm"
+)
+
+// UserRelateInsert is 插入一条数据到用户关系表
+func UserRelateInsert(Db *xorm.Engine, userRelate *model.UserRelate) (int64, error) {
+ affected, err := Db.Insert(userRelate)
+ if err != nil {
+ return 0, err
+ }
+ return affected, nil
+}
+func UserRelateInsertWithSess(sess *xorm.Session, userRelate *model.UserRelate) (int64, error) {
+ affected, err := sess.Insert(userRelate)
+ if err != nil {
+ return 0, err
+ }
+ return affected, nil
+}
+func UserRelateUpdate(Db *xorm.Engine, userRelate *model.UserRelate) (int64, error) {
+ affected, err := Db.Where("parent_uid=? and uid=?", userRelate.ParentUid, userRelate.Uid).Cols("level,invite_time").Update(userRelate)
+ if err != nil {
+ return 0, err
+ }
+ return affected, nil
+}
+
+//UserRelateByPuid is 获取用户关系列表 by puid
+func UserRelatesByPuid(Db *xorm.Engine, puid interface{}, limit, start int) (*[]model.UserRelate, error) {
+ var m []model.UserRelate
+ if limit == 0 && start == 0 {
+ if err := Db.Where("parent_uid = ?", puid).
+ Cols(`id,parent_uid,uid,level,invite_time`).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+ }
+ if err := Db.Where("parent_uid = ?", puid).
+ Cols(`id,parent_uid,uid,level,invite_time`).Limit(limit, start).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+
+ return &m, nil
+}
+
+//UserRelatesByPuidByLv is 获取用户关系列表 by puid 和lv
+func UserRelatesByPuidByLv(Db *xorm.Engine, puid, lv interface{}, limit, start int) (*[]model.UserRelate, error) {
+ var m []model.UserRelate
+ if limit == 0 && start == 0 {
+ if err := Db.Where("parent_uid = ? AND level = ?", puid, lv).
+ Cols(`id,parent_uid,uid,level,invite_time`).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+ }
+ if err := Db.Where("parent_uid = ? AND level = ?", puid, lv).
+ Cols(`id,parent_uid,uid,level,invite_time`).Limit(limit, start).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+
+ return &m, nil
+}
+
+//UserRelatesByPuidByLvByTime is 获取直属 level =1用户关系列表 by puid 和lv by time
+func UserRelatesByPuidByLvByTime(Db *xorm.Engine, puid, lv, stime, etime interface{}, limit, start int) (*[]model.UserRelate, error) {
+ var m []model.UserRelate
+ if limit == 0 && start == 0 {
+ if err := Db.Where("parent_uid = ? AND level = ? AND invite_time > ? AND invite_time < ?", puid, lv, stime, etime).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+ }
+ if err := Db.Where("parent_uid = ? AND level = ? AND invite_time > ? AND invite_time < ?", puid, lv, stime, etime).
+ Limit(limit, start).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+
+ return &m, nil
+}
+
+//UserRelatesByPuidByTime is 获取户关系列表 by puid 和lv by time
+func UserRelatesByPuidByTime(Db *xorm.Engine, puid, stime, etime interface{}, limit, start int) (*[]model.UserRelate, error) {
+ var m []model.UserRelate
+ if limit == 0 && start == 0 {
+ if err := Db.Where("parent_uid = ? AND invite_time > ? AND invite_time < ?", puid, stime, etime).
+ Cols(`id,parent_uid,uid,level,invite_time`).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+ }
+ if err := Db.Where("parent_uid = ? AND invite_time > ? AND invite_time < ?", puid, stime, etime).
+ Cols(`id,parent_uid,uid,level,invite_time`).Limit(limit, start).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+
+ return &m, nil
+}
+
+//UserRelatesByPuidExceptLv is 获取用户关系列表 by puid 和非 lv
+func UserRelatesByPuidExceptLv(Db *xorm.Engine, puid, lv interface{}, limit, start int) (*[]model.UserRelate, error) {
+ var m []model.UserRelate
+ if limit == 0 && start == 0 {
+ if err := Db.Where("parent_uid = ? AND level != ?", puid, lv).
+ Cols(`id,parent_uid,uid,level,invite_time`).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+ }
+ if err := Db.Where("parent_uid = ? AND level != ?", puid, lv).
+ Cols(`id,parent_uid,uid,level,invite_time`).Limit(limit, start).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+
+ return &m, nil
+}
+
+//UserRelateByUID is 获取用户关系表 by uid
+func UserRelateByUID(Db *xorm.Engine, uid interface{}) (*model.UserRelate, bool, error) {
+ r := new(model.UserRelate)
+ has, err := Db.Where("uid=?", uid).Get(r)
+ if err != nil {
+ return nil, false, err
+ }
+ return r, has, nil
+}
+
+//UserRelateByUIDByLv is 获取用户关系表 by uid
+func UserRelateByUIDByLv(Db *xorm.Engine, uid, lv interface{}) (*model.UserRelate, bool, error) {
+ r := new(model.UserRelate)
+ has, err := Db.Where("uid=? AND level=?", uid, lv).Get(r)
+ if err != nil {
+ return nil, false, err
+ }
+ return r, has, nil
+}
+
+//UserRelateByUIDAndPUID 根据 Puid 和uid 查找 ,用于确认关联
+func UserRelateByUIDAndPUID(Db *xorm.Engine, uid, puid interface{}) (*model.UserRelate, bool, error) {
+ r := new(model.UserRelate)
+ has, err := Db.Where("uid=? AND parent_uid=?", uid, puid).Get(r)
+ if err != nil {
+ return nil, false, err
+ }
+ return r, has, nil
+}
+
+//UserRelatesByPuIDAndLv is 查询用户关系表 获取指定等级和puid的关系
+func UserRelatesByPuIDAndLv(Db *xorm.Engine, puid, lv interface{}) (*[]model.UserRelate, error) {
+ var m []model.UserRelate
+ if err := Db.Where("parent_uid = ? AND level = ?", puid, lv).
+ Cols(`id,parent_uid,uid,level,invite_time`).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UserRelateInByUID is In查询
+func UserRelateInByUID(Db *xorm.Engine, ids []int) (*[]model.UserRelate, error) {
+ var m []model.UserRelate
+ if err := Db.In("uid", ids).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+// UserRelatesByUIDDescLv is Where 查询 根据level 降序
+func UserRelatesByUIDDescLv(Db *xorm.Engine, id interface{}) (*[]model.UserRelate, error) {
+ var m []model.UserRelate
+ if err := Db.Where("uid = ?", id).Desc("level").
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+func UserRelatesByInvite(Db *xorm.Engine, times interface{}) (*[]model.UserRelate, error) {
+ var m []model.UserRelate
+ if err := Db.Where("invite_time >= ?", times).
+ Find(&m); err != nil {
+ return nil, logx.Warn(err)
+ }
+ return &m, nil
+}
+
+//UserRelateCountByPUID is 根据puid 计数
+func UserRelateCountByPUID(Db *xorm.Engine, pid interface{}) (int64, error) {
+
+ count, err := Db.Where("parent_uid = ?", pid).Count(model.UserRelate{})
+ if err != nil {
+ return 0, nil
+ }
+ return count, nil
+}
+
+//UserRelateDelete is 删除关联他上级的关系记录,以及删除他下级的关联记录
+func UserRelateDelete(Db *xorm.Engine, uid interface{}) (int64, error) {
+ // 删除与之上级的记录
+ _, err := Db.Where("uid = ?", uid).Delete(model.UserRelate{})
+ if err != nil {
+ return 0, err
+ }
+ // 删除与之下级的记录
+ _, err = Db.Where("parent_uid = ?", uid).Delete(model.UserRelate{})
+ if err != nil {
+ return 0, err
+ }
+
+ return 1, nil
+}
+func UserRelateDeleteWithSession(sess *xorm.Session, uid interface{}) (int64, error) {
+ // 删除与之上级的记录
+ _, err := sess.Where("uid = ?", uid).Delete(model.UserRelate{})
+ if err != nil {
+ return 0, err
+ }
+ // 删除与之下级的记录
+ _, err = sess.Where("parent_uid = ?", uid).Delete(model.UserRelate{})
+ if err != nil {
+ return 0, err
+ }
+
+ return 1, nil
+}
+
+//UserRelateDelete is 删除关联他上级的关系记录
+func UserRelateExtendDelete(Db *xorm.Engine, uid interface{}) (int64, error) {
+ // 删除与之上级的记录
+ _, err := Db.Where("uid = ?", uid).Delete(model.UserRelate{})
+ if err != nil {
+ return 0, err
+ }
+ return 1, nil
+}
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..9043ca0
--- /dev/null
+++ b/app/db/dbs_map.go
@@ -0,0 +1,232 @@
+package db
+
+import (
+ model2 "applet/app/db/offical/model"
+ "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
+ 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)
+ //err = Db.Where(" db_master_id=?", 36274003).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 '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_wap_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 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 masterId + "." + domainBase.V, nil
+}
+func GetOfficialDomainInfoByTypeToAgent(Db *xorm.Engine, masterId, uuid, key string) (string, error) {
+ type SysCfg struct {
+ K string
+ V string
+ Memo string
+ }
+ var domainBase model2.MasterListCfg
+ has, err := Db.Where("uid=? and k=?", uuid, "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 masterId + ".h5." + 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 masterId + "." + domainBase.V, nil
+}
diff --git a/app/db/dbs_sys_cfg.go b/app/db/dbs_sys_cfg.go
new file mode 100644
index 0000000..0225144
--- /dev/null
+++ b/app/db/dbs_sys_cfg.go
@@ -0,0 +1,55 @@
+package db
+
+import (
+ "xorm.io/xorm"
+
+ "applet/app/db/model"
+ "applet/app/utils/logx"
+)
+
+// 系统配置get
+func DbsSysCfgGetAll(eg *xorm.Engine) (*[]model.SysCfg, error) {
+ var cfgList []model.SysCfg
+ if err := eg.Cols("`key`,`val`").Find(&cfgList); cfgList == nil || err != nil {
+ return nil, logx.Error(err)
+ }
+ return &cfgList, nil
+}
+
+// 获取一条记录
+func DbsSysCfgGet(eg *xorm.Engine, key string) (*model.SysCfg, error) {
+ var cfgList model.SysCfg
+ if has, err := eg.Where("`key`=?", key).Get(&cfgList); err != nil || has == false {
+ return nil, logx.Error(err)
+ }
+ return &cfgList, nil
+}
+
+func DbsSysCfgInsert(eg *xorm.Engine, key, val string) bool {
+ cfg := model.SysCfg{Key: key, Val: val}
+ _, err := eg.Where("`key`=?", key).Cols("val,memo").Update(&cfg)
+ if err != nil {
+ logx.Error(err)
+ return false
+ }
+ return true
+}
+func DbsSysCfgInserts(eg *xorm.Engine, key, val string) bool {
+ cfg := model.SysCfg{Key: key, Val: val}
+ _, err := eg.InsertOne(&cfg)
+ if err != nil {
+ logx.Error(err)
+ return false
+ }
+ return true
+}
+
+func DbsSysCfgUpdate(eg *xorm.Engine, key, val string) bool {
+ cfg := model.SysCfg{Key: key, Val: val}
+ _, err := eg.Where("`key`=?", key).Cols("val").Update(&cfg)
+ if err != nil {
+ logx.Error(err)
+ return false
+ }
+ return true
+}
diff --git a/app/db/model/cloud_bundle.go b/app/db/model/cloud_bundle.go
new file mode 100644
index 0000000..31c7524
--- /dev/null
+++ b/app/db/model/cloud_bundle.go
@@ -0,0 +1,19 @@
+package model
+
+type CloudBundle struct {
+ Id int `json:"id" xorm:"not null pk autoincr INT(10)"`
+ Os int `json:"os" xorm:"not null default 1 comment('系统类型:1.Android; 2.IOS') TINYINT(1)"`
+ Ep int `json:"ep" xorm:" default 0 comment('') TINYINT(1)"`
+ Version string `json:"version" xorm:"not null default '' comment('版本号') VARCHAR(255)"`
+ Modules string `json:"modules" xorm:"not null default '' comment('包含的模块') VARCHAR(255)"`
+ ApplyAt int `json:"apply_at" xorm:"comment('申请时间') INT(11)"`
+ FinishAt int `json:"finish_at" xorm:"comment('完成时间') INT(11)"`
+ IsAuditing int `json:"is_auditing" xorm:"comment('完成时间') INT(11)"`
+ State int `json:"state" xorm:"not null default 1 comment('状态:正在排队0,正在同步代码1,正在更新配置2,正在混淆3,正在打包4,正在上传5,打包成功999,异常-1') SMALLINT(5)"`
+ Memo string `json:"memo" xorm:"comment('备注') TEXT"`
+ ErrorMsg string `json:"error_msg" xorm:"comment('错误信息') TEXT"`
+ Src string `json:"src" xorm:"comment('包源地址') VARCHAR(255)"`
+ BuildId string `json:"build_id" xorm:"comment('build版本ID') VARCHAR(255)"`
+ BuildNumber string `json:"build_number" xorm:"default '' VARCHAR(255)"`
+ TemplateDuringAudit string `json:"template_during_audit" xorm:"not null default '' VARCHAR(255)"`
+}
diff --git a/app/db/model/community_team_cate.go b/app/db/model/community_team_cate.go
new file mode 100644
index 0000000..462304c
--- /dev/null
+++ b/app/db/model/community_team_cate.go
@@ -0,0 +1,10 @@
+package model
+
+type CommunityTeamCate struct {
+ Id int `json:"id" xorm:"not null pk autoincr INT(11)"`
+ Uid int `json:"uid" xorm:"default 0 comment('0是官方') INT(11)"`
+ StoreType int `json:"store_type" xorm:"default 0 comment('0官方自营店 1加盟店 2连锁店') INT(11)"`
+ Title string `json:"title" xorm:"VARCHAR(255)"`
+ Sort int `json:"sort" xorm:"default 0 INT(11)"`
+ IsShow int `json:"is_show" xorm:"default 0 INT(1)"`
+}
diff --git a/app/db/model/community_team_coupon.go b/app/db/model/community_team_coupon.go
new file mode 100644
index 0000000..aeb2775
--- /dev/null
+++ b/app/db/model/community_team_coupon.go
@@ -0,0 +1,27 @@
+package model
+
+import (
+ "time"
+)
+
+type CommunityTeamCoupon struct {
+ Id int `json:"id" xorm:"not null pk autoincr INT(11)"`
+ Name string `json:"name" xorm:"comment('方案名称') VARCHAR(255)"`
+ Comment string `json:"comment" xorm:"comment('备注') VARCHAR(2048)"`
+ PublishTime time.Time `json:"publish_time" xorm:"comment('立即发布时间') DATETIME"`
+ Kind int `json:"kind" xorm:"not null comment('优惠券类型,1:立减,2:满减,3折扣') TINYINT(1)"`
+ IsReach int `json:"is_reach" xorm:"comment('是否有门槛,1:有,2:否;优惠券类型为3折扣时') TINYINT(1)"`
+ Cal string `json:"cal" xorm:"comment('满减及折扣有门槛时算法,{"reach": "20.12", "reduce": "2.2"}, reach:满X元,减/折reduce') VARCHAR(255)"`
+ State int `json:"state" xorm:"not null default 1 comment('状态,是否使用(1:使用;2:不使用)') TINYINT(1)"`
+ ActivityTimeStart time.Time `json:"activity_time_start" xorm:"not null comment('活动起止时间,开始时间') DATETIME"`
+ ActivityTimeEnd time.Time `json:"activity_time_end" xorm:"not null comment('活动起止时间,结束时间') DATETIME"`
+ ActivityStatement string `json:"activity_statement" xorm:"not null default '0' comment('活动规则说明') VARCHAR(5000)"`
+ Sort int `json:"sort" xorm:"comment('排序') INT(11)"`
+ Ext string `json:"ext" xorm:"comment('拓展字段') TEXT"`
+ IsPublishNow int `json:"is_publish_now" xorm:"not null default 0 comment('是否立即发布,0:否,1:是') TINYINT(1)"`
+ CreatedTime time.Time `json:"created_time" xorm:"not null default CURRENT_TIMESTAMP comment('创建时间') DATETIME"`
+ UpdatedTime time.Time `json:"updated_time" xorm:"not null default CURRENT_TIMESTAMP comment('更新时间') DATETIME"`
+ DeletedTime time.Time `json:"deleted_time" xorm:"comment('删除时间') DATETIME"`
+ StoreId int `json:"store_id" xorm:"default 0 INT(11)"`
+ StoreType int `json:"store_type" xorm:"default 0 comment('0官方自营店 1加盟店 2连锁店') INT(11)"`
+}
diff --git a/app/db/model/community_team_coupon_user.go b/app/db/model/community_team_coupon_user.go
new file mode 100644
index 0000000..a98b4d4
--- /dev/null
+++ b/app/db/model/community_team_coupon_user.go
@@ -0,0 +1,25 @@
+package model
+
+import (
+ "time"
+)
+
+type CommunityTeamCouponUser struct {
+ Id int64 `json:"id" xorm:"pk autoincr BIGINT(20)"`
+ Uid int `json:"uid" xorm:"comment('用户id') index INT(11)"`
+ MerchantSchemeId int `json:"merchant_scheme_id" xorm:"comment('优惠券方案的id') INT(11)"`
+ IsUse int `json:"is_use" xorm:"comment('是否已使用:0否1是2失效') TINYINT(1)"`
+ Kind int `json:"kind" xorm:"comment('优惠券类型:1立减2满减3折扣') TINYINT(1)"`
+ Cal string `json:"cal" xorm:"not null default '0.00' comment('折扣算法json:形式:{"reach":"10.00", "reduce":"1.00"},无门槛reach为0') VARCHAR(255)"`
+ ValidTimeStart time.Time `json:"valid_time_start" xorm:"comment('有效日期开始') DATETIME"`
+ ValidTimeEnd time.Time `json:"valid_time_end" xorm:"comment('有效日期结束') DATETIME"`
+ UseRule int `json:"use_rule" xorm:"comment('1全部商品可用2指定商品3指定活动类型') TINYINT(1)"`
+ UseActivityType int `json:"use_activity_type" xorm:"comment('可用的活动类型: 1拼团活动 2秒杀活动 3砍价活动') TINYINT(1)"`
+ CreateTime time.Time `json:"create_time" xorm:"default CURRENT_TIMESTAMP comment('创建时间') DATETIME"`
+ UpdateTime time.Time `json:"update_time" xorm:"default CURRENT_TIMESTAMP comment('更新时间') DATETIME"`
+ StoreId int `json:"store_id" xorm:"default 0 comment('0是官方') INT(11)"`
+ StoreType int `json:"store_type" xorm:"default 0 comment('0官方自营店 1加盟店 2连锁店') INT(11)"`
+ Name string `json:"name" xorm:"comment('方案名称') VARCHAR(255)"`
+ ActivityStatement string `json:"activity_statement" xorm:"not null default '0' comment('活动规则说明') VARCHAR(5000)"`
+ Img string `json:"img" xorm:"VARCHAR(255)"`
+}
diff --git a/app/db/model/community_team_goods.go b/app/db/model/community_team_goods.go
new file mode 100644
index 0000000..36f5338
--- /dev/null
+++ b/app/db/model/community_team_goods.go
@@ -0,0 +1,29 @@
+package model
+
+import (
+ "time"
+)
+
+type CommunityTeamGoods struct {
+ Id int `json:"id" xorm:"not null pk autoincr INT(11)"`
+ Uid int `json:"uid" xorm:"default 0 comment('0是官方') INT(11)"`
+ StoreType int `json:"store_type" xorm:"default 0 comment('0官方自营店 1加盟店 2连锁店') INT(11)"`
+ Title string `json:"title" xorm:"VARCHAR(255)"`
+ Img string `json:"img" xorm:"VARCHAR(255)"`
+ Info string `json:"info" xorm:"comment('描述') VARCHAR(255)"`
+ Cid int `json:"cid" xorm:"default 0 INT(11)"`
+ State int `json:"state" xorm:"default 0 comment('0上架 1下架') INT(11)"`
+ CreateAt time.Time `json:"create_at" xorm:"DATETIME"`
+ UpdateAt time.Time `json:"update_at" xorm:"DATETIME"`
+ Price string `json:"price" xorm:"DECIMAL(10,2)"`
+ Stock int `json:"stock" xorm:"default 0 INT(11)"`
+ Spe string `json:"spe" xorm:"not null default '' comment('所有规格属性json') VARCHAR(5012)"`
+ LinePrice string `json:"line_price" xorm:"not null default 0.00 comment('划线价') DECIMAL(12,2)"`
+ SpeImages string `json:"spe_images" xorm:"not null comment('第一组规格值对应的图片json') TEXT"`
+ IsSingleSku int `json:"is_single_sku" xorm:"not null default 1 comment('是否单规格,0:否,1是') TINYINT(1)"`
+ Sort int `json:"sort" xorm:"default 0 INT(11)"`
+ IsSpeImageOn int `json:"is_spe_image_on" xorm:"not null default 0 comment('是否开启规格图片:0否 1是') TINYINT(1)"`
+ Commission string `json:"commission" xorm:"default 0.00 DECIMAL(20,2)"`
+ ImageList string `json:"image_list" xorm:"comment('商品图json') TEXT"`
+ SaleCount int `json:"sale_count" xorm:"default 0 INT(11)"`
+}
diff --git a/app/db/model/community_team_order.go b/app/db/model/community_team_order.go
new file mode 100644
index 0000000..24e0df3
--- /dev/null
+++ b/app/db/model/community_team_order.go
@@ -0,0 +1,37 @@
+package model
+
+import (
+ "time"
+)
+
+type CommunityTeamOrder struct {
+ Id int `json:"id" xorm:"not null pk autoincr INT(11)"`
+ Uid int `json:"uid" xorm:"default 0 INT(11)"`
+ StoreType int `json:"store_type" xorm:"default 0 comment('0官方自营店 1加盟店 2连锁店') INT(11)"`
+ StoreUid int `json:"store_uid" xorm:"default 0 comment('门店用户id') INT(11)"`
+ ParentUid int `json:"parent_uid" xorm:"default 0 comment('上级代理') INT(11)"`
+ Num int `json:"num" xorm:"default 0 comment('') INT(11)"`
+ CouponId int `json:"coupon_id" xorm:"default 0 comment('') INT(11)"`
+ Address string `json:"address" xorm:"comment('详细地址') VARCHAR(255)"`
+ Commission string `json:"commission" xorm:"default 0.00 comment('分佣(元)') DECIMAL(20,2)"`
+ CreateAt time.Time `json:"create_at" xorm:"DATETIME"`
+ UpdateAt time.Time `json:"update_at" xorm:"DATETIME"`
+ BuyPhone string `json:"buy_phone" xorm:"VARCHAR(255)"`
+ Phone string `json:"phone" xorm:"VARCHAR(255)"`
+ BuyName string `json:"buy_name" xorm:"VARCHAR(255)"`
+ State int `json:"state" xorm:"default 0 comment('0待付款 1已支付 2已提货') INT(11)"`
+ PayAt time.Time `json:"pay_at" xorm:"comment('付款时间') DATETIME"`
+ ConfirmAt time.Time `json:"confirm_at" xorm:"comment('提货时间') DATETIME"`
+ Oid int64 `json:"oid" xorm:"default 0 comment('主单号') BIGINT(20)"`
+ Code string `json:"code" xorm:"comment('提货码') VARCHAR(255)"`
+ Type int `json:"type" xorm:"default 0 comment('0自提 1外卖') INT(1)"`
+ PayMethod int `json:"pay_method" xorm:"default 0 comment('1余额 2支付宝 3微信') INT(11)"`
+ PayId string `json:"pay_id" xorm:"comment('第三方的支付id') VARCHAR(255)"`
+ Amount string `json:"amount" xorm:"default 0.00 comment('总金额') DECIMAL(20,2)"`
+ Memo string `json:"memo" xorm:"comment('备注') VARCHAR(255)"`
+ TakeTime time.Time `json:"take_time" xorm:"comment('预计提货时间') DATETIME"`
+ MealNum int `json:"meal_num" xorm:"default 0 comment('餐具数量') INT(11)"`
+ Coupon string `json:"coupon" xorm:"default 0.00 DECIMAL(10,2)"`
+ Timer string `json:"timer" xorm:"comment('预计提货时间') VARCHAR(255)"`
+ IsNow int `json:"is_now" xorm:"default 0 comment('是否立即提货') INT(1)"`
+}
diff --git a/app/db/model/community_team_order_info.go b/app/db/model/community_team_order_info.go
new file mode 100644
index 0000000..20eddb5
--- /dev/null
+++ b/app/db/model/community_team_order_info.go
@@ -0,0 +1,13 @@
+package model
+
+type CommunityTeamOrderInfo struct {
+ Id int `json:"id" xorm:"not null pk autoincr INT(11)"`
+ Oid int64 `json:"oid" xorm:"BIGINT(20)"`
+ Title string `json:"title" xorm:"comment('标题') VARCHAR(255)"`
+ Img string `json:"img" xorm:"comment('图片') VARCHAR(255)"`
+ Price string `json:"price" xorm:"default 0.00 comment('单价') DECIMAL(10,2)"`
+ Num int `json:"num" xorm:"default 0 comment('数量') INT(11)"`
+ SkuInfo string `json:"sku_info" xorm:"comment('sku信息') VARCHAR(255)"`
+ GoodsId int `json:"goods_id" xorm:"default 0 INT(11)"`
+ SkuId int `json:"sku_id" xorm:"default 0 INT(11)"`
+}
diff --git a/app/db/model/community_team_sku.go b/app/db/model/community_team_sku.go
new file mode 100644
index 0000000..3342e7f
--- /dev/null
+++ b/app/db/model/community_team_sku.go
@@ -0,0 +1,20 @@
+package model
+
+import (
+ "time"
+)
+
+type CommunityTeamSku struct {
+ SkuId int64 `json:"sku_id" xorm:"not null pk autoincr BIGINT(20)"`
+ GoodsId int `json:"goods_id" xorm:"not null comment('商品id') INT(11)"`
+ Price string `json:"price" xorm:"not null default 0.00 comment('价格') DECIMAL(12,2)"`
+ CreateTime time.Time `json:"create_time" xorm:"default CURRENT_TIMESTAMP comment('创建时间') DATETIME"`
+ UpdateTime time.Time `json:"update_time" xorm:"default CURRENT_TIMESTAMP comment('更新时间') DATETIME"`
+ Stock int `json:"stock" xorm:"not null default 0 comment('库存') INT(11)"`
+ Indexes string `json:"indexes" xorm:"not null default '' comment('规格值组合的下标') VARCHAR(100)"`
+ Sku string `json:"sku" xorm:"not null comment('规格组合json') VARCHAR(2048)"`
+ SaleCount int `json:"sale_count" xorm:"not null default 0 comment('销量') INT(11)"`
+ Sort int `json:"sort" xorm:"not null default 0 comment('排序') INT(11)"`
+ Discount string `json:"discount" xorm:"default 10.00 comment('折扣,为10无折扣') DECIMAL(6,2)"`
+ SkuCode string `json:"sku_code" xorm:"default '' comment('sku编码') VARCHAR(255)"`
+}
diff --git a/app/db/model/community_team_store.go b/app/db/model/community_team_store.go
new file mode 100644
index 0000000..60cc0f0
--- /dev/null
+++ b/app/db/model/community_team_store.go
@@ -0,0 +1,24 @@
+package model
+
+import (
+ "time"
+)
+
+type CommunityTeamStore struct {
+ Id int `json:"id" xorm:"not null pk autoincr INT(11)"`
+ Uid int `json:"uid" xorm:"default 0 INT(11)"`
+ StoreType int `json:"store_type" xorm:"default 0 comment('0官方自营店 1加盟店 2连锁店') INT(11)"`
+ ParentUid int `json:"parent_uid" xorm:"default 0 comment('上级代理') INT(11)"`
+ Lat string `json:"lat" xorm:"default 0.000000 comment('纬度') DECIMAL(30,6)"`
+ Lng string `json:"lng" xorm:"default 0.000000 comment('经度') DECIMAL(30,6)"`
+ Address string `json:"address" xorm:"comment('详细地址') VARCHAR(255)"`
+ Commission string `json:"commission" xorm:"default 0.00 comment('分佣比例%') DECIMAL(20,2)"`
+ CreateAt time.Time `json:"create_at" xorm:"DATETIME"`
+ UpdateAt time.Time `json:"update_at" xorm:"DATETIME"`
+ State int `json:"state" xorm:"default 0 comment('0非店长 1店长') INT(1)"`
+ WorkState int `json:"work_state" xorm:"default 0 comment('0营业中 1休息中') INT(1)"`
+ Name string `json:"name" xorm:"VARCHAR(255)"`
+ Province string `json:"province" xorm:"comment('省级的名称') VARCHAR(255)"`
+ City string `json:"city" xorm:"comment('市级的名称') VARCHAR(255)"`
+ District string `json:"district" xorm:"comment('县,区名称') VARCHAR(255)"`
+}
diff --git a/app/db/model/community_team_store_like.go b/app/db/model/community_team_store_like.go
new file mode 100644
index 0000000..586346f
--- /dev/null
+++ b/app/db/model/community_team_store_like.go
@@ -0,0 +1,12 @@
+package model
+
+import (
+ "time"
+)
+
+type CommunityTeamStoreLike struct {
+ Id int `json:"id" xorm:"not null pk autoincr INT(11)"`
+ Uid int `json:"uid" xorm:"default 0 INT(11)"`
+ StoreId int `json:"store_id" xorm:"default 0 INT(11)"`
+ Time time.Time `json:"time" xorm:"DATETIME"`
+}
diff --git a/app/db/model/db_mapping.go b/app/db/model/db_mapping.go
new file mode 100644
index 0000000..f2f5d06
--- /dev/null
+++ b/app/db/model/db_mapping.go
@@ -0,0 +1,18 @@
+package model
+
+import (
+ "time"
+)
+
+type DbMapping struct {
+ DbMasterId string `json:"db_master_id" xorm:"not pk null comment('站长id') VARCHAR(32)"`
+ 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 time.Time `json:"created_at" xorm:"not null default CURRENT_TIMESTAMP comment('创建时间') TIMESTAMP"`
+ UpdatedAt time.Time `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/fin_user_flow.go b/app/db/model/fin_user_flow.go
new file mode 100644
index 0000000..785da48
--- /dev/null
+++ b/app/db/model/fin_user_flow.go
@@ -0,0 +1,30 @@
+package model
+
+import (
+ "time"
+)
+
+type FinUserFlow struct {
+ Id int64 `json:"id" xorm:"pk autoincr comment('流水编号') BIGINT(20)"`
+ Uid int `json:"uid" xorm:"not null default 0 comment('用户id') INT(11)"`
+ Type int `json:"type" xorm:"not null default 0 comment('0收入,1支出') TINYINT(1)"`
+ Amount string `json:"amount" xorm:"not null default 0.0000 comment('变动金额') DECIMAL(11,4)"`
+ BeforeAmount string `json:"before_amount" xorm:"not null default 0.0000 comment('变动前金额') DECIMAL(11,4)"`
+ AfterAmount string `json:"after_amount" xorm:"not null default 0.0000 comment('变动后金额') DECIMAL(11,4)"`
+ SysFee string `json:"sys_fee" xorm:"not null default 0.0000 comment('手续费') DECIMAL(11,4)"`
+ PaymentType int `json:"payment_type" xorm:"not null default 1 comment('1支付宝,2微信.3手动转账') TINYINT(1)"`
+ OrdType string `json:"ord_type" xorm:"not null default '' comment('订单类型taobao,jd,pdd,vip,suning,kaola,own自营,withdraw提现') VARCHAR(20)"`
+ OrdId string `json:"ord_id" xorm:"not null default '' comment('对应订单编号') VARCHAR(50)"`
+ OrdTitle string `json:"ord_title" xorm:"not null default '' comment('订单标题') VARCHAR(50)"`
+ OrdAction int `json:"ord_action" xorm:"not null default 0 comment('10自购,11推广,12团队,20提现,21消费') TINYINT(2)"`
+ OrdTime int `json:"ord_time" xorm:"not null default 0 comment('下单时间or提现时间') INT(11)"`
+ OrdDetail string `json:"ord_detail" xorm:"not null default '' comment('记录商品ID或提现账号') VARCHAR(50)"`
+ ExpectedTime string `json:"expected_time" xorm:"not null default '0' comment('预期到账时间,字符串用于直接显示,结算后清除内容') VARCHAR(30)"`
+ State int `json:"state" xorm:"not null default 1 comment('1未到账,2已到账') TINYINT(1)"`
+ Memo string `json:"memo" xorm:"not null default '' comment('备注') VARCHAR(2000)"`
+ OtherId int64 `json:"other_id" xorm:"not null default 0 comment('其他关联订单,具体根据订单类型判断') BIGINT(20)"`
+ AliOrdId string `json:"ali_ord_id" xorm:"default '' comment('支付宝订单号') VARCHAR(128)"`
+ ExtendType int `json:"extend_type" xorm:"not null default 1 comment('') TINYINT(1)"`
+ CreateAt time.Time `json:"create_at" xorm:"created not null default CURRENT_TIMESTAMP comment('创建时间') TIMESTAMP"`
+ UpdateAt time.Time `json:"update_at" xorm:"updated not null default CURRENT_TIMESTAMP comment('更新时间') TIMESTAMP"`
+}
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/model/sys_module.go b/app/db/model/sys_module.go
new file mode 100644
index 0000000..9a1b1d8
--- /dev/null
+++ b/app/db/model/sys_module.go
@@ -0,0 +1,35 @@
+package model
+
+import (
+ "time"
+)
+
+type SysModule struct {
+ ModId int `json:"mod_id" xorm:"not null pk autoincr INT(10)"`
+ ModPid int `json:"mod_pid" xorm:"not null default 0 comment('父级模块ID') INT(10)"`
+ TemplateId int `json:"template_id" xorm:"not null default 0 comment('模板ID') INT(11)"`
+ ModName string `json:"mod_name" xorm:"not null default '' comment('模块名称') VARCHAR(250)"`
+ Position string `json:"position" xorm:"not null default '' comment('位置') VARCHAR(250)"`
+ SkipIdentifier string `json:"skip_identifier" xorm:"not null default '' comment('跳转标识') VARCHAR(250)"`
+ Title string `json:"title" xorm:"not null default '' comment('标题') VARCHAR(128)"`
+ Subtitle string `json:"subtitle" xorm:"not null default '' comment('副标题') VARCHAR(255)"`
+ Url string `json:"url" xorm:"not null default '' comment('跳转链接') VARCHAR(512)"`
+ Margin string `json:"margin" xorm:"not null default '0,0,0,0' comment('边距,上右下左') VARCHAR(64)"`
+ AspectRatio string `json:"aspect_ratio" xorm:"not null default 0.00 comment('宽高比,宽/高保留两位小数') DECIMAL(4,2)"`
+ Icon string `json:"icon" xorm:"not null default '' comment('图标') VARCHAR(512)"`
+ Img string `json:"img" xorm:"not null default '' comment('图片') VARCHAR(512)"`
+ FontColor string `json:"font_color" xorm:"not null default '' comment('文字颜色') VARCHAR(128)"`
+ BgImg string `json:"bg_img" xorm:"not null default '' comment('背景图片') VARCHAR(512)"`
+ BgColor string `json:"bg_color" xorm:"not null default '' comment('背景颜色') VARCHAR(512)"`
+ BgColorT string `json:"bg_color_t" xorm:"not null default '' comment('背景颜色过度') VARCHAR(255)"`
+ Badge string `json:"badge" xorm:"not null default '' comment('badge图片') VARCHAR(512)"`
+ Path string `json:"path" xorm:"not null default '' comment('跳转路径') VARCHAR(255)"`
+ Data string `json:"data" xorm:"comment('内容') TEXT"`
+ Sort int `json:"sort" xorm:"not null default 0 comment('排序') INT(11)"`
+ Uid int `json:"uid" xorm:"not null default 0 comment('排序') INT(11)"`
+ State int `json:"state" xorm:"not null default 1 comment('0不显示,1显示') TINYINT(1)"`
+ IsGlobal int `json:"is_global" xorm:"not null default 0 comment('是否全局显示') TINYINT(1)"`
+ Platform int `json:"platform" xorm:"not null default 1 comment('平台;1:全平台;2:App应用(ios和android);3:H5(wap);4:微信小程序;5:抖音小程序;6:百度小程序') TINYINT(1)"`
+ CreateAt time.Time `json:"create_at" xorm:"not null default CURRENT_TIMESTAMP comment('创建时间') TIMESTAMP"`
+ UpdateAt time.Time `json:"update_at" xorm:"not null default CURRENT_TIMESTAMP comment('更新时间') TIMESTAMP"`
+}
diff --git a/app/db/model/sys_popup.go b/app/db/model/sys_popup.go
new file mode 100644
index 0000000..6ff4e5d
--- /dev/null
+++ b/app/db/model/sys_popup.go
@@ -0,0 +1,28 @@
+package model
+
+import (
+ "time"
+)
+
+type SysPopup struct {
+ Id int `json:"id" xorm:"not null pk autoincr INT(11)"`
+ Uid int `json:"uid" xorm:"not null default 0 INT(11)"`
+ Name string `json:"name" xorm:"not null default '' VARCHAR(32)"`
+ ConditionType string `json:"condition_type" xorm:"not null default 'all' comment('展示人群类型;all:所有人;new_user:新用户;level:指定等级;tag:指定标签;no_order_user:未出单用户') VARCHAR(32)"`
+ Condition string `json:"condition" xorm:"not null comment('弹窗条件,json') TEXT"`
+ Position string `json:"position" xorm:"not null default 'index' comment('展示位置;index:首页') VARCHAR(64)"`
+ Image string `json:"image" xorm:"not null default '' comment('弹窗图片') VARCHAR(128)"`
+ Platform string `json:"platform" xorm:"not null default '' comment('弹窗图片') VARCHAR(255)"`
+ Interval int `json:"interval" xorm:"not null default 0 comment('弹窗时间间隔;单位:分钟') INT(11)"`
+ Skip string `json:"skip" xorm:"not null default '' comment('跳转标识') VARCHAR(255)"`
+ Type int `json:"type" xorm:"not null default 1 comment('弹窗时间类型;1:固定时间;2:每天定时') TINYINT(1)"`
+ PopupTime string `json:"popup_time" xorm:"comment('弹窗时间,json') TEXT"`
+ State int `json:"state" xorm:"not null default 0 comment('状态;0:不启用;1:启用') TINYINT(1)"`
+ Sort int `json:"sort" xorm:"not null default 0 comment('排序') INT(11)"`
+ PopupType int `json:"popup_type" xorm:"not null default 0 comment('弹窗类型 0活动弹窗 1任务弹窗') INT(11)"`
+ TaskType int `json:"task_type" xorm:"not null default 0 comment('任务类型') INT(11)"`
+ TaskFinishType int `json:"task_finish_type" xorm:"not null default 0 comment('任务完成状态') INT(11)"`
+ CreateAt time.Time `json:"create_at" xorm:"default 'CURRENT_TIMESTAMP' TIMESTAMP"`
+ UpdateAt time.Time `json:"update_at" xorm:"default 'CURRENT_TIMESTAMP' TIMESTAMP"`
+ IsNotClose int `json:"is_not_close" xorm:"not null default 0 comment('不能关闭 0能关 1不能关') INT(1)"`
+}
diff --git a/app/db/model/sys_push_app.go b/app/db/model/sys_push_app.go
new file mode 100644
index 0000000..5d0ee70
--- /dev/null
+++ b/app/db/model/sys_push_app.go
@@ -0,0 +1,22 @@
+package model
+
+import (
+ "time"
+)
+
+type SysPushApp struct {
+ Id int64 `json:"id" xorm:"pk autoincr BIGINT(20)"`
+ Title string `json:"title" xorm:"not null default '' comment('标题') VARCHAR(32)"`
+ Content string `json:"content" xorm:"not null comment('内容') TEXT"`
+ Image string `json:"image" xorm:"not null default '' comment('图片(只有官方才有图片)') VARCHAR(255)"`
+ Provider string `json:"provider" xorm:"not null default 'mob' comment('平台供应商,如:mob,official:官方推送') VARCHAR(16)"`
+ Type string `json:"type" xorm:"not null default '' comment('模板类型 | 推送类型;public;:普通推送;activity:活动通知;order_self:新订单提醒(导购自购新订单),order_team:新订单提醒(团队新订单),order_share:新订单提醒(导购分享新订单),member_register:团队成员注册成功,level_upgrade:团队成员等级升级成功,withdraw_fail:提现失败提醒,withdraw_success:提现成功提醒,comission_settle_success:佣金结算提醒(平台结算)') VARCHAR(50)"`
+ SendAt int `json:"send_at" xorm:"not null default 0 comment('指定发送时间0为马上执行') INT(11)"`
+ State int `json:"state" xorm:"not null default 1 comment('1发送中,2成功,3失败,4部分成功') TINYINT(1)"`
+ DeviceProvider int `json:"device_provider" xorm:"default 1 comment('推送设备平台。1:全平台;2:安卓;3:ios') TINYINT(1)"`
+ Target int `json:"target" xorm:"not null default 1 comment('推送目标;1:全部会员;2:指定会员;3:指定等级;4:指定标签') TINYINT(1)"`
+ TargetCondition string `json:"target_condition" xorm:"comment('推送目标条件。json格式;') TEXT"`
+ Skip string `json:"skip" xorm:"not null default '' comment('跳转功能') VARCHAR(255)"`
+ CreateAt time.Time `json:"create_at" xorm:"not null default CURRENT_TIMESTAMP comment('创建时间') TIMESTAMP"`
+ UpdateAt time.Time `json:"update_at" xorm:"not null default CURRENT_TIMESTAMP comment('更新时间') TIMESTAMP"`
+}
diff --git a/app/db/model/sys_push_sms.go b/app/db/model/sys_push_sms.go
new file mode 100644
index 0000000..ef63a2b
--- /dev/null
+++ b/app/db/model/sys_push_sms.go
@@ -0,0 +1,19 @@
+package model
+
+import (
+ "time"
+)
+
+type SysPushSms struct {
+ Id int64 `json:"id" xorm:"pk autoincr BIGINT(20)"`
+ Content string `json:"content" xorm:"not null comment('内容') TEXT"`
+ Provider string `json:"provider" xorm:"not null default '' comment('短信供应平台,暂时没有') VARCHAR(20)"`
+ SendAt int `json:"send_at" xorm:"not null default 0 comment('指定发送时间0为马上执行') INT(10)"`
+ State int `json:"state" xorm:"not null default 0 comment('0发送中,1成功,2失败,3部分成功') TINYINT(1)"`
+ Target int `json:"target" xorm:"not null default 1 comment('推送目标;1:全部会员;2:指定会员;3:指定等级;4:指定标签') TINYINT(1)"`
+ TargetCondition string `json:"target_condition" xorm:"comment('推送目标条件。json格式;') TEXT"`
+ Skip string `json:"skip" xorm:"not null default '' comment('跳转功能') VARCHAR(255)"`
+ CreateAt time.Time `json:"create_at" xorm:"not null default CURRENT_TIMESTAMP comment('创建时间') TIMESTAMP"`
+ UpdateAt time.Time `json:"update_at" xorm:"not null default CURRENT_TIMESTAMP comment('更新时间') TIMESTAMP"`
+ Type string `json:"type" xorm:"not null default '' comment('模板类型 | 推送类型;public;:普通推送;activity:活动通知;order_self:新订单提醒(导购自购新订单),order_team:新订单提醒(团队新订单),order_share:新订单提醒(导购分享新订单),member_register:团队成员注册成功,level_upgrade:团队成员等级升级成功,withdraw_fail:提现失败提醒,withdraw_success:提现成功提醒,comission_settle_success:佣金结算提醒(平台结算)') VARCHAR(50)"`
+}
diff --git a/app/db/model/sys_push_template.go b/app/db/model/sys_push_template.go
new file mode 100644
index 0000000..ec82030
--- /dev/null
+++ b/app/db/model/sys_push_template.go
@@ -0,0 +1,17 @@
+package model
+
+import (
+ "time"
+)
+
+type SysPushTemplate struct {
+ Id int `json:"id" xorm:"not null pk autoincr INT(11)"`
+ IsAppPush int `json:"is_app_push" xorm:"not null default 1 comment('是否app推送') TINYINT(1)"`
+ IsSmsPush int `json:"is_sms_push" xorm:"not null default 0 comment('是否短信推送') TINYINT(1)"`
+ Type string `json:"type" xorm:"not null default '' comment('模板类型') VARCHAR(50)"`
+ Title string `json:"title" xorm:"not null default '' comment('标题') VARCHAR(128)"`
+ Content string `json:"content" xorm:"not null comment('内容') TEXT"`
+ Skip string `json:"skip" xorm:"not null default '' comment('跳转功能') VARCHAR(255)"`
+ CreateAt time.Time `json:"create_at" xorm:"default CURRENT_TIMESTAMP TIMESTAMP"`
+ UpdateAt time.Time `json:"update_at" xorm:"default CURRENT_TIMESTAMP TIMESTAMP"`
+}
diff --git a/app/db/model/sys_push_user.go b/app/db/model/sys_push_user.go
new file mode 100644
index 0000000..d4a2157
--- /dev/null
+++ b/app/db/model/sys_push_user.go
@@ -0,0 +1,18 @@
+package model
+
+import (
+ "time"
+)
+
+type SysPushUser struct {
+ Id int `json:"id" xorm:"not null pk autoincr INT(11)"`
+ PushId int `json:"push_id" xorm:"not null default 0 comment('sys_push_app表ID') index INT(11)"`
+ Uid int `json:"uid" xorm:"not null default 0 INT(11)"`
+ State int `json:"state" xorm:"not null default 0 comment('发送状态;0:失败;1:成功') TINYINT(1)"`
+ Time time.Time `json:"time" xorm:"default CURRENT_TIMESTAMP comment('发送时间') TIMESTAMP"`
+ SendData string `json:"send_data" xorm:"comment('发送内容,json格式') TEXT"`
+ Provider string `json:"provider" xorm:"not null default 'mob' comment('平台供应商,如:mob,official:官方推送') VARCHAR(16)"`
+ Type string `json:"type" xorm:"not null default '' comment('模板类型 | 推送类型;public;:普通推送;activity:活动通知;order_self:新订单提醒(导购自购新订单),order_team:新订单提醒(团队新订单),order_share:新订单提醒(导购分享新订单),member_register:团队成员注册成功,level_upgrade:团队成员等级升级成功,withdraw_fail:提现失败提醒,withdraw_success:提现成功提醒,comission_settle_success:佣金结算提醒(平台结算)') VARCHAR(50)"`
+ SendAt int `json:"send_at" xorm:"not null default 0 comment('官方活动显示时间(大于当前时间戳才显示);0为即可显示') INT(11)"`
+ IsRead int `json:"is_read" xorm:"not null default 0 comment('是否已读;0:未读;1:已读') TINYINT(1)"`
+}
diff --git a/app/db/model/sys_template.go b/app/db/model/sys_template.go
new file mode 100644
index 0000000..c9814ad
--- /dev/null
+++ b/app/db/model/sys_template.go
@@ -0,0 +1,20 @@
+package model
+
+import (
+ "time"
+)
+
+type SysTemplate struct {
+ Id int `json:"id" xorm:"not null pk autoincr INT(11)"`
+ Uid int `json:"uid" xorm:"not null default 0 INT(11)"`
+ Name string `json:"name" xorm:"not null default '' comment('模板名称') VARCHAR(32)"`
+ Title string `json:"title" xorm:"not null default '' comment('页面title字段') VARCHAR(32)"`
+ Type string `json:"type" xorm:"not null default 'index' comment('模板类型;index:首页;bottom:底部导航栏;member:会员中心;custom:自定义模板;share_goods_image:商品图文分享;share_goods_link:商品链接分享;share_goods_platform_xx:商品分享平台(xx对应平台类型)') VARCHAR(64)"`
+ Image string `json:"image" xorm:"not null default '' VARCHAR(128)"`
+ IsUse int `json:"is_use" xorm:"default 0 comment('是否使用;1:使用;0未使用') TINYINT(1)"`
+ Remark string `json:"remark" xorm:"not null default '' comment('备注') VARCHAR(128)"`
+ LimitData string `json:"limit_data" xorm:"not null comment('') TEXT"`
+ IsSystem int `json:"is_system" xorm:"not null default 0 comment('是否系统模板;0:否;1:是') TINYINT(1)"`
+ CreateAt time.Time `json:"create_at" xorm:"default CURRENT_TIMESTAMP TIMESTAMP"`
+ UpdateAt time.Time `json:"update_at" xorm:"default CURRENT_TIMESTAMP TIMESTAMP"`
+}
diff --git a/app/db/model/user.go b/app/db/model/user.go
new file mode 100644
index 0000000..0c5c5dc
--- /dev/null
+++ b/app/db/model/user.go
@@ -0,0 +1,37 @@
+package model
+
+import (
+ "time"
+)
+
+type User struct {
+ Uid int `json:"uid" xorm:"not null pk autoincr comment('主键ID') INT(10)"`
+ Username string `json:"username" xorm:"not null default '' comment('用户名') index VARCHAR(50)"`
+ Password string `json:"password" xorm:"not null default '' comment('密码') CHAR(32)"`
+ Passcode string `json:"passcode" xorm:"not null default '' comment('支付密码') CHAR(32)"`
+ Email string `json:"email" xorm:"not null default '' comment('邮箱') VARCHAR(128)"`
+ Phone string `json:"phone" xorm:"not null default '' comment('联系电话') index VARCHAR(20)"`
+ Nickname string `json:"nickname" xorm:"not null default '' comment('昵称') VARCHAR(20)"`
+ Level int `json:"level" xorm:"not null default 0 comment('用户等级id') INT(11)"`
+ IsStore int `json:"is_store" xorm:"not null default 0 comment('') INT(11)"`
+ InviteTotal int `json:"invite_total" xorm:"not null default 0 comment('直推邀请总人数') INT(11)"`
+ LevelArriveAt time.Time `json:"level_arrive_at" xorm:"not null default 'CURRENT_TIMESTAMP' comment('到达该等级的时间') TIMESTAMP"`
+ LevelExpireAt time.Time `json:"level_expire_at" xorm:"not null default '0000-00-00 00:00:00' comment('该等级过期时间') TIMESTAMP"`
+ CreateAt time.Time `json:"create_at" xorm:"not null default 'CURRENT_TIMESTAMP' comment('创建时间') TIMESTAMP"`
+ UpdateAt time.Time `json:"update_at" xorm:"default 'CURRENT_TIMESTAMP' comment('最后修改资料时间') TIMESTAMP"`
+ LastLoginAt time.Time `json:"last_login_at" xorm:"default 'CURRENT_TIMESTAMP' comment('最近登录时间') TIMESTAMP"`
+ DeleteAt int `json:"delete_at" xorm:"not null default 0 comment('是否删除;0未删除;1已删除') TINYINT(1)"`
+ State int `json:"state" xorm:"not null default 1 comment('0未激活,1正常,2冻结') TINYINT(1)"`
+ LastLoginIp string `json:"last_login_ip" xorm:"not null default '' comment('最后登录IP') VARCHAR(64)"`
+ RegisterIp string `json:"register_ip" xorm:"not null default '' comment('注册IP') VARCHAR(64)"`
+ IsFake int `json:"is_fake" xorm:"not null default 0 comment('0真实 1虚拟') TINYINT(1)"`
+ IsMarketer int `json:"is_marketer" xorm:"not null default 0 comment('是否市商 0否 1是') TINYINT(1)"`
+ CanChangeLv int `json:"can_change_lv" xorm:"not null default 0 comment('是否市商 0否 1是') TINYINT(1)"`
+ IsPop int `json:"is_pop" xorm:"not null default 0 comment('是否市商 0否 1是') TINYINT(1)"`
+ IsNotAddMoments int `json:"is_not_add_moments" xorm:"not null default 0 comment('是否市商 0否 1是') TINYINT(1)"`
+ Zone string `json:"zone" xorm:"not null default '86' comment('区号') VARCHAR(100)"`
+ SalePhone string `json:"sale_phone" xorm:"not null default '' comment('') VARCHAR(100)"`
+ Platform string `json:"platform" xorm:"not null default '' comment('') VARCHAR(100)"`
+ ImportFinTotal string `json:"import_fin_total" xorm:"not null default 0.000000 comment('累计总收益') DECIMAL(30,4)"`
+ FinTotal string `json:"fin_total" xorm:"not null default 0.000000 comment('累计总收益') DECIMAL(30,4)"`
+}
diff --git a/app/db/model/user_app_domain.go b/app/db/model/user_app_domain.go
new file mode 100644
index 0000000..4522cef
--- /dev/null
+++ b/app/db/model/user_app_domain.go
@@ -0,0 +1,8 @@
+package model
+
+type UserAppDomain struct {
+ Domain string `json:"domain" xorm:"not null pk comment('绑定域名') VARCHAR(100)"`
+ Uuid int `json:"uuid" xorm:"not null comment('对应APP ID编号') index unique(IDX_UUID_TYPE) INT(10)"`
+ Type string `json:"type" xorm:"not null comment('api接口域名,wap.h5域名,admin管理后台') unique(IDX_UUID_TYPE) ENUM('admin','api','wap')"`
+ IsSsl int `json:"is_ssl" xorm:"not null default 0 comment('是否开启ssl:0否;1是') TINYINT(255)"`
+}
diff --git a/app/db/model/user_app_list.go b/app/db/model/user_app_list.go
new file mode 100644
index 0000000..58b6cb5
--- /dev/null
+++ b/app/db/model/user_app_list.go
@@ -0,0 +1,8 @@
+package model
+
+type UserAppList struct {
+ Id int `json:"id" xorm:"int(11) NOT NULL "`
+ Uuid int64 `json:"uuid" xorm:"int(10) NOT NULL "`
+ AppId int64 `json:"app_id" xorm:"int(10) NOT NULL "`
+ SmsPlatform string `json:"sms_platform" xorm:"varchar(255) DEFAULT 'mob' "`
+}
diff --git a/app/db/model/user_level.go b/app/db/model/user_level.go
new file mode 100644
index 0000000..94726aa
--- /dev/null
+++ b/app/db/model/user_level.go
@@ -0,0 +1,23 @@
+package model
+
+import (
+ "time"
+)
+
+type UserLevel struct {
+ Id int `json:"id" xorm:"not null pk autoincr comment('等级id') INT(11)"`
+ BenefitIds string `json:"benefit_ids" xorm:"comment('该等级拥有的权益id【json】') TEXT"`
+ LevelName string `json:"level_name" xorm:"not null default '' comment('等级名称') VARCHAR(255)"`
+ LevelWeight int `json:"level_weight" xorm:"not null default 0 comment('等级权重') INT(11)"`
+ LevelUpdateCondition int `json:"level_update_condition" xorm:"not null default 2 comment('2是条件升级,1是无条件升级') TINYINT(1)"`
+ AutoAudit int `json:"auto_audit" xorm:"not null default 0 comment('(自动审核)0关闭,1开启') TINYINT(1)"`
+ AutoUpdate int `json:"auto_update" xorm:"not null default 0 comment('(自动升级)0关闭,1开启') TINYINT(1)"`
+ LevelDate int `json:"level_date" xorm:"default 0 comment('会员有效期(0永久有效,单位月)') INT(11)"`
+ IsUse int `json:"is_use" xorm:"not null default 1 comment('是否开启(0否,1是)') TINYINT(1)"`
+ ChoosableNum int `json:"choosable_num" xorm:"default 0 comment('可选任务数量(当is_must_task为0时生效)') INT(6)"`
+ BeforeHide int `json:"before_hide" xorm:"default 0 comment('可选任务数量(当is_must_task为0时生效)') INT(6)"`
+ Label int `json:"label" xorm:"default 0 comment('') INT(6)"`
+ Memo string `json:"memo" xorm:"default '' comment('备注') VARCHAR(255)"`
+ CssSet string `json:"css_set" xorm:"TEXT"`
+ CreateAt time.Time `json:"create_at" xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP"`
+}
diff --git a/app/db/model/user_profile.go b/app/db/model/user_profile.go
new file mode 100644
index 0000000..ca6f3b6
--- /dev/null
+++ b/app/db/model/user_profile.go
@@ -0,0 +1,102 @@
+package model
+
+import (
+ "time"
+)
+
+type UserProfile struct {
+ Uid int `json:"uid" xorm:"not null pk comment('关联userID') INT(20)"`
+ ArkidUid int `json:"arkid_uid" xorm:"not null default 0 comment('Arkid 用户ID') INT(20)"`
+ ParentUid int `json:"parent_uid" xorm:"not null default 0 comment('上级ID') INT(20)"`
+ ArkidToken string `json:"arkid_token" xorm:"not null default '' comment('token') VARCHAR(2000)"`
+ AvatarUrl string `json:"avatar_url" xorm:"not null default '' comment('头像URL') VARCHAR(2000)"`
+ CustomInviteCode string `json:"custom_invite_code" xorm:"not null default '' comment('邀请码(自定义)') VARCHAR(16)"`
+ InviteCode string `json:"invite_code" xorm:"not null default '' comment('邀请码(系统)') VARCHAR(16)"`
+ Gender int `json:"gender" xorm:"not null default 2 comment('性别0女,1男,2未知') TINYINT(1)"`
+ Birthday int `json:"birthday" xorm:"not null default 0 comment('出生日期') INT(10)"`
+ AccWxId string `json:"acc_wx_id" xorm:"not null default '' comment('账户_微信id') VARCHAR(50)"`
+ AccWxOpenid string `json:"acc_wx_openid" xorm:"not null default '' comment('账户_微信openid') VARCHAR(80)"`
+ AccTaobaoNickname string `json:"acc_taobao_nickname" xorm:"not null default '' comment('淘宝昵称') VARCHAR(50)"`
+ AccTaobaoAuthTime int64 `json:"acc_taobao_auth_time" xorm:"not null default 0 comment('淘宝授权备案时间') BIGINT(11)"`
+ AccTaobaoShareId int64 `json:"acc_taobao_share_id" xorm:"not null default 0 comment('淘宝分享relationId,') index BIGINT(12)"`
+ AccTaobaoSelfId int64 `json:"acc_taobao_self_id" xorm:"not null default 0 comment('淘宝自购specialId') index BIGINT(12)"`
+ AccJdSelfId string `json:"acc_jd_self_id" xorm:"not null default '' comment('京东自购ID') index VARCHAR(50)"`
+ AccJdShareId string `json:"acc_jd_share_id" xorm:"not null default '' comment('京东分享ID') index VARCHAR(50)"`
+ AccJdFreeId string `json:"acc_jd_free_id" xorm:"not null default '' comment('京东新人免单ID') VARCHAR(50)"`
+ AccJdCloudId string `json:"acc_jd_cloud_id" xorm:"not null default '' comment('京东新人免单ID') VARCHAR(50)"`
+ AccSuningSelfId string `json:"acc_suning_self_id" xorm:"not null default '' comment('苏宁自购ID') index VARCHAR(50)"`
+ AccSuningShareId string `json:"acc_suning_share_id" xorm:"not null default '' comment('苏宁分享ID') index VARCHAR(50)"`
+ AccSuningFreeId string `json:"acc_suning_free_id" xorm:"not null default '' comment('苏宁新人免单ID') VARCHAR(50)"`
+ AccPddSelfId string `json:"acc_pdd_self_id" xorm:"not null default '' comment('拼多多自购ID') index VARCHAR(50)"`
+ AccPddCloudId string `json:"acc_pdd_cloud_id" xorm:"not null default '' comment('拼多多自购ID') index VARCHAR(50)"`
+ AccPddShareId string `json:"acc_pdd_share_id" xorm:"not null default '' comment('拼多多分享ID') index VARCHAR(50)"`
+ AccPddFreeId string `json:"acc_pdd_free_id" xorm:"not null default '' comment('拼多多新人免单ID') VARCHAR(50)"`
+ AccPddBind int `json:"acc_pdd_bind" xorm:"not null default 0 comment('拼多多是否授权绑定') TINYINT(1)"`
+ AccVipSelfId string `json:"acc_vip_self_id" xorm:"not null default '' comment('唯品会自购ID') index VARCHAR(50)"`
+ AccVipShareId string `json:"acc_vip_share_id" xorm:"not null default '' comment('唯品会分享ID') index VARCHAR(50)"`
+ AccVipFreeId string `json:"acc_vip_free_id" xorm:"not null default '' comment('唯品会新人免单ID') VARCHAR(50)"`
+ AccKaolaSelfId string `json:"acc_kaola_self_id" xorm:"not null default '' comment('考拉自购ID') index VARCHAR(50)"`
+ AccKaolaShareId string `json:"acc_kaola_share_id" xorm:"not null default '' comment('考拉分享ID') index VARCHAR(50)"`
+ AccKaolaFreeId string `json:"acc_kaola_free_id" xorm:"not null default '' comment('考拉新人免单ID') VARCHAR(50)"`
+ AccDuomaiShareId int64 `json:"acc_duomai_share_id" xorm:"not null pk default 0 comment('多麦联盟分享ID') BIGINT(12)"`
+ AccAlipay string `json:"acc_alipay" xorm:"not null default '' comment('支付宝账号') VARCHAR(50)"`
+ AccAlipayRealName string `json:"acc_alipay_real_name" xorm:"not null default '' comment('支付宝账号真实姓名') VARCHAR(50)"`
+ CertTime int `json:"cert_time" xorm:"not null default 0 comment('认证时间') INT(10)"`
+ CertName string `json:"cert_name" xorm:"not null default '' comment('证件上名字,也是真实姓名') VARCHAR(50)"`
+ CertNum string `json:"cert_num" xorm:"not null default '' comment('证件号码') VARCHAR(50)"`
+ CertState int `json:"cert_state" xorm:"not null default 0 comment('认证状态(0为未认证,1为认证中,2为已认证,3为认证失败)') TINYINT(1)"`
+ FinCommission string `json:"fin_commission" xorm:"not null default 0.0000 comment('累计佣金') DECIMAL(10,4)"`
+ FinValid string `json:"fin_valid" xorm:"not null default 0.0000 comment('可用余额,fin=>finance财务') DECIMAL(10,4)"`
+ FinInvalid string `json:"fin_invalid" xorm:"not null default 0.0000 comment('不可用余额,冻结余额') DECIMAL(10,4)"`
+ FinSelfOrderCount int `json:"fin_self_order_count" xorm:"not null default 0 comment('自购订单数,包括未完成') INT(11)"`
+ FinSelfOrderCountDone int `json:"fin_self_order_count_done" xorm:"not null default 0 comment('自购已完成订单') INT(11)"`
+ FinSelfRebate float32 `json:"fin_self_rebate" xorm:"not null default 0.000000 comment('累积自购获得返利金额') FLOAT(14,6)"`
+ FinTotal float32 `json:"fin_total" xorm:"not null default 0.000000 comment('累计总收益') FLOAT(14,6)"`
+ Lat float32 `json:"lat" xorm:"not null default 0.000000 comment('纬度') FLOAT(15,6)"`
+ Lng float32 `json:"lng" xorm:"not null default 0.000000 comment('经度') FLOAT(15,6)"`
+ Memo string `json:"memo" xorm:"not null default '' comment('用户简述备注') VARCHAR(2048)"`
+ Qq string `json:"qq" xorm:"not null default '' comment('') VARCHAR(255)"`
+ IsNew int `json:"is_new" xorm:"not null default 1 comment('是否是新用户') TINYINT(1)"`
+ IsVerify int `json:"is_verify" xorm:"not null default 0 comment('是否有效会员') TINYINT(1)"`
+ IsOrdered int `json:"is_ordered" xorm:"not null default 0 comment('是否已完成首单(0否,1是)') TINYINT(1)"`
+ FromWay string `json:"from_way" xorm:"not null default '' comment('注册来源:
+no_captcha_phone:免验证码手机号注册;
+manual_phone:手动手机验证码注册;
+wx:微信授权;
+wx_mp:小程序授权;
+wx_pub:公众号授权;
+wx_bind_phone:微信注册绑定手机号;
+admin:管理员添加;taobao_bind_phone:淘宝注册绑定手机号,apple_bind_phone:苹果注册绑定手机号') VARCHAR(16)"`
+ HidOrder int `json:"hid_order" xorm:"not null default 0 comment('隐藏订单') TINYINT(3)"`
+ HidContact int `json:"hid_contact" xorm:"not null default 0 comment('隐藏联系方式') TINYINT(4)"`
+ NewMsgNotice int `json:"new_msg_notice" xorm:"not null default 1 comment('新消息通知') TINYINT(1)"`
+ WxAccount string `json:"wx_account" xorm:"not null default '' comment('微信号') VARCHAR(100)"`
+ WxQrcode string `json:"wx_qrcode" xorm:"not null default '' comment('微信二维码') VARCHAR(100)"`
+ ThirdPartyTaobaoOid string `json:"third_party_taobao_oid" xorm:"not null default '' comment('淘宝第三方登录openID') VARCHAR(100)"`
+ ThirdPartyTaobaoSid string `json:"third_party_taobao_sid" xorm:"not null default '' comment('淘宝第三方登录sID') VARCHAR(255)"`
+ ThirdPartyTaobaoAcctoken string `json:"third_party_taobao_acctoken" xorm:"not null default '' comment('淘宝第三方登录topaccesstoken') VARCHAR(100)"`
+ ThirdPartyTaobaoAuthcode string `json:"third_party_taobao_authcode" xorm:"not null default '' comment('淘宝第三方登录topAuthCode') VARCHAR(100)"`
+ ThirdPartyAppleToken string `json:"third_party_apple_token" xorm:"not null default '' comment('苹果第三方登录token') VARCHAR(1024)"`
+ ThirdPartyQqAccessToken string `json:"third_party_qq_access_token" xorm:"not null default '' comment('QQ第三方登录access_token') VARCHAR(255)"`
+ ThirdPartyQqExpiresIn string `json:"third_party_qq_expires_in" xorm:"not null default '' comment('QQ第三方登录expires_in(剩余时长)') VARCHAR(255)"`
+ ThirdPartyQqOpenid string `json:"third_party_qq_openid" xorm:"not null default '' comment('QQ第三方登陆openid(不变,用于认证)') VARCHAR(255)"`
+ ThirdPartyQqUnionid string `json:"third_party_qq_unionid" xorm:"not null default '' comment('QQ第三方登陆unionid') VARCHAR(255)"`
+ ThirdPartyWechatExpiresIn string `json:"third_party_wechat_expires_in" xorm:"not null default '' comment('微信第三方登录expires_in(剩余时长)') VARCHAR(255)"`
+ ThirdPartyWechatOpenid string `json:"third_party_wechat_openid" xorm:"not null default '' comment('微信第三方登陆openid(不变,用于认证)') VARCHAR(255)"`
+ ThirdPartyWechatUnionid string `json:"third_party_wechat_unionid" xorm:"not null default '' comment('微信第三方登陆unionid') VARCHAR(255)"`
+ ThirdPartyWechatMiniOpenid string `json:"third_party_wechat_mini_openid" xorm:"not null default '' comment('微信小程序登录open_id') VARCHAR(255)"`
+ ThirdPartyWechatH5Openid string `json:"third_party_wechat_h5_openid" xorm:"not null default '' comment('微信H5登录open_id') VARCHAR(255)"`
+ FreeRemainTime int `json:"free_remain_time" xorm:"not null default 0 comment('免单剩余次数') INT(11)"`
+ FreeCumulativeTime int `json:"free_cumulative_time" xorm:"not null default 0 comment('免单累计次数') INT(11)"`
+ SecondFreeRemainTime int `json:"second_free_remain_time" xorm:"not null default 0 comment('免单剩余次数') INT(11)"`
+ SecondFreeCumulativeTime int `json:"second_free_cumulative_time" xorm:"not null default 0 comment('免单累计次数') INT(11)"`
+ ThirdFreeRemainTime int `json:"third_free_remain_time" xorm:"not null default 0 comment('免单剩余次数') INT(11)"`
+ ThirdFreeCumulativeTime int `json:"third_free_cumulative_time" xorm:"not null default 0 comment('免单累计次数') INT(11)"`
+ IsDelete int `json:"is_delete" xorm:"not null default 0 comment('是否已删除') TINYINT(1)"`
+ UpdateAt time.Time `json:"update_at" xorm:"updated not null default CURRENT_TIMESTAMP comment('更新时间') TIMESTAMP"`
+ IsSet int `json:"is_set" xorm:"not null default 0 comment('用于一个客户生成关系链匹配的') INT(1)"`
+ IsOld int `json:"is_old" xorm:"not null default 0 comment('') INT(1)"`
+ RunVerifyTime int `json:"run_verify_time" xorm:"not null default 0 comment('') INT(1)"`
+ TaskId int `json:"task_id" xorm:"not null default 0 comment('') INT(1)"`
+ TaskType string `json:"task_type" xorm:"not null default '' comment('') VARCHAR(255)"`
+}
diff --git a/app/db/model/user_relate.go b/app/db/model/user_relate.go
new file mode 100644
index 0000000..375562f
--- /dev/null
+++ b/app/db/model/user_relate.go
@@ -0,0 +1,13 @@
+package model
+
+import (
+ "time"
+)
+
+type UserRelate struct {
+ Id int64 `json:"id" xorm:"pk autoincr comment('主键') BIGINT(10)"`
+ ParentUid int `json:"parent_uid" xorm:"not null default 0 comment('上级会员ID') unique(idx_union_u_p_id) INT(20)"`
+ Uid int `json:"uid" xorm:"not null default 0 comment('关联UserID') unique(idx_union_u_p_id) INT(20)"`
+ Level int `json:"level" xorm:"not null default 1 comment('推广等级(1直属,大于1非直属)') INT(10)"`
+ InviteTime time.Time `json:"invite_time" xorm:"not null default CURRENT_TIMESTAMP comment('邀请时间') TIMESTAMP"`
+}
diff --git a/app/db/model/virtual_coin.go b/app/db/model/virtual_coin.go
new file mode 100644
index 0000000..3baaa55
--- /dev/null
+++ b/app/db/model/virtual_coin.go
@@ -0,0 +1,17 @@
+package model
+
+type VirtualCoin struct {
+ Id int `json:"id" xorm:"not null pk autoincr INT(11)"`
+ Name string `json:"name" xorm:"not null default '' comment('名称') VARCHAR(255)"`
+ ExchangeRatio string `json:"exchange_ratio" xorm:"not null comment('兑换比例(与金额)') DECIMAL(5,2)"`
+ IsUse int `json:"is_use" xorm:"comment('是否开启:0否 1是') TINYINT(1)"`
+ CanExchange string `json:"can_exchange" xorm:"comment('能兑换的虚拟币id和手续费列表json') VARCHAR(255)"`
+ CanExchangeMoney int `json:"can_exchange_money" xorm:"not null default 0 comment('现金能否兑换:0否 1是') TINYINT(1)"`
+ IsBlock int `json:"is_block" xorm:"not null default 0 comment('是否区块币:0否 1是') TINYINT(1)"`
+ FunctionType string `json:"function_type" xorm:"comment('功能类型') VARCHAR(255)"`
+ CanCny int `json:"can_cny" xorm:"not null default 0 comment('是否能兑换余额:0否 1是') TINYINT(1)"`
+ CanTransfer int `json:"can_transfer" xorm:"not null default 0 comment('是否能支持转账:0否 1是') TINYINT(1)"`
+ CanBackout int `json:"can_backout" xorm:"not null default 0 comment('是否能支持转账撤回:0否 1是') TINYINT(1)"`
+ LimitLevelTransfer string `json:"limit_level_transfer" xorm:"default '' comment('能支持转账的用户等级') VARCHAR(600)"`
+ LimitLevelBackout string `json:"limit_level_backout" xorm:"comment('能支持撤回的用户等级') VARCHAR(600)"`
+}
diff --git a/app/db/model/virtual_coin_relate.go b/app/db/model/virtual_coin_relate.go
new file mode 100644
index 0000000..7987b99
--- /dev/null
+++ b/app/db/model/virtual_coin_relate.go
@@ -0,0 +1,17 @@
+package model
+
+type VirtualCoinRelate struct {
+ Id int64 `json:"id" xorm:"pk autoincr BIGINT(20)"`
+ Oid int64 `json:"oid" xorm:"not null default 0 comment('订单号') index unique(IDX_ORD) BIGINT(20)"`
+ Uid int `json:"uid" xorm:"not null default 0 comment('用户ID') unique(IDX_ORD) index INT(10)"`
+ CoinId int `json:"coin_id" xorm:"comment('虚拟币id') unique(IDX_ORD) INT(11)"`
+ Amount string `json:"amount" xorm:"not null default 0.000000 comment('数量') DECIMAL(16,6)"`
+ Pvd string `json:"pvd" xorm:"not null default '' comment('供应商taobao,jd,pdd,vip,suning,kaola,mall_goods,group_buy') index VARCHAR(255)"`
+ CreateAt int `json:"create_at" xorm:"not null default 0 comment('订单创建时间') index INT(10)"`
+ Level int `json:"level" xorm:"not null default 0 comment('0自购 1直推 大于1:间推') INT(10)"`
+ Mode string `json:"mode" xorm:"default '' comment('分佣方案类型') VARCHAR(255)"`
+ AdditionalSubsidy string `json:"additional_subsidy" xorm:"default 0.000000 comment('额外补贴 酒庄模式才有效') DECIMAL(16,6)"`
+ AdditionalSubsidyBili string `json:"additional_subsidy_bili" xorm:"default 0.000000 comment('额外补贴比例 酒庄模式才有效') DECIMAL(16,6)"`
+ TeamFreeze int `json:"team_freeze" xorm:"comment('定制') INT(1)"`
+ ExtendType int `json:"extend_type" xorm:"default 0 comment('0普通 1超级推荐人 2团长 3团长上级超级推荐人 4团长担保用户') unique(IDX_ORD) INT(11)"`
+}
diff --git a/app/db/model/wx_applet_list.go b/app/db/model/wx_applet_list.go
new file mode 100644
index 0000000..5632d75
--- /dev/null
+++ b/app/db/model/wx_applet_list.go
@@ -0,0 +1,44 @@
+package model
+
+import (
+ "time"
+)
+
+type WxAppletList struct {
+ Id int `json:"id" xorm:"not null pk autoincr INT(10)"`
+ Memo string `json:"memo" xorm:"not null default '' comment('备注') unique VARCHAR(255)"`
+ CompanyName string `json:"company_name" xorm:"not null default '' comment('企业名(需与工商部门登记信息一致);如果是“无主体名称个体工商户”则填“个体户+法人姓名”,例如“个体户张三”') VARCHAR(255)"`
+ Code string `json:"code" xorm:"not null default '' comment('企业代码') VARCHAR(255)"`
+ CodeType int `json:"code_type" xorm:"not null default 3 comment('企业代码类型 1:统一社会信用代码(18 位) 2:组织机构代码(9 位 xxxxxxxx-x) 3:营业执照注册号(15 位)') TINYINT(3)"`
+ LegalPersonaWechat string `json:"legal_persona_wechat" xorm:"not null default '' comment('法人微信号') VARCHAR(255)"`
+ LegalPersonaName string `json:"legal_persona_name" xorm:"not null default '' comment('法人姓名(绑定银行卡)') VARCHAR(255)"`
+ State int `json:"state" xorm:"not null default 0 comment('创建状态(0:创建中 1:创建成功 2:创建失败)') TINYINT(3)"`
+ Ext string `json:"ext" xorm:"comment('拓展字段') TEXT"`
+ UniqueIdentifier string `json:"unique_identifier" xorm:"not null default '' comment('唯一标识符(企业名称、企业代码、法人微信、法人姓名四个字段作为每次任务的唯一标示,来区别每次任务。)') VARCHAR(255)"`
+ AppId string `json:"app_id" xorm:"default '' comment('小程序appId') VARCHAR(255)"`
+ OriginalAppId string `json:"original_app_id" xorm:"default '' comment('原始ID') VARCHAR(255)"`
+ AuthorizerToken string `json:"authorizer_token" xorm:"default '' comment('授权token') VARCHAR(255)"`
+ AuthorizerRefreshToken string `json:"authorizer_refresh_token" xorm:"default '' comment('授权更新token') VARCHAR(255)"`
+ AuditVersionState int `json:"audit_version_state" xorm:"not null default 0 comment('线上版本号') TINYINT(3)"`
+ PublishVersionNum string `json:"publish_version_num" xorm:"comment('审核状态(0:暂无审核;1:审核中;2:审核通过;3:审核驳回)') VARCHAR(255)"`
+ AppletName string `json:"applet_name" xorm:"default '' comment('小程序名字') VARCHAR(255)"`
+ AppletSignature string `json:"applet_signature" xorm:"default '' comment('小程序简介') VARCHAR(255)"`
+ AppletLogo string `json:"applet_logo" xorm:"default '' comment('小程序logo') VARCHAR(255)"`
+ SetAppletNameInfo string `json:"set_applet_name_info" xorm:"default '' comment('小程序改名证件url') VARCHAR(255)"`
+ SetAppletNameInfoType int `json:"set_applet_name_info_type" xorm:"default 1 comment('小程序改名证件类型(1:个人号 2:组织号)') TINYINT(3)"`
+ SetAppletNameState int `json:"set_applet_name_state" xorm:"default 0 comment('小程序改名状态(0:未进行 1:进行中 2:改名成功 3:改名失败)') TINYINT(3)"`
+ SetAppletNameAuditId string `json:"set_applet_name_audit_id" xorm:"default '' comment('小程序改名的审核单id') VARCHAR(255)"`
+ CreateAt time.Time `json:"create_at" xorm:"not null default 'CURRENT_TIMESTAMP' TIMESTAMP"`
+ UpdateAt time.Time `json:"update_at" xorm:"not null default 'CURRENT_TIMESTAMP' TIMESTAMP"`
+ IsFilterTaobao int `json:"is_filter_taobao" xorm:"default 0 comment('是否过滤淘宝商品 0否 1是') INT(1)"`
+ CateId string `json:"cate_id" xorm:"default '' comment('主营类目id') VARCHAR(50)"`
+ BottomNavCssId int `json:"bottom_nav_css_id" xorm:"default 0 comment('底部导航样式id') INT(11)"`
+ AuthType string `json:"auth_type" xorm:"default 'reg' comment('授权方式 直接授权old_auth 注册授权reg') VARCHAR(100)"`
+ MpAuditVersion string `json:"mp_audit_version" xorm:"default '' comment('审核版本') VARCHAR(100)"`
+ MpAuditCssId int `json:"mp_audit_css_id" xorm:"default 0 comment('审核版本底部样式ID') INT(11)"`
+ MpAuditGoodsDetailCssId int `json:"mp_audit_goods_detail_css_id" xorm:"default 0 comment('审核版本底部样式ID') INT(11)"`
+ AppletBgColor string `json:"applet_bg_color" xorm:"default '' comment('导航栏/状态栏背景色') VARCHAR(100)"`
+ AppletNavColor string `json:"applet_nav_color" xorm:"default '' comment('导航栏/状态栏字体色') VARCHAR(100)"`
+ ShareUse int `json:"share_use" xorm:"default 0 comment('分享使用') INT(11)"`
+ OrderCssId int `json:"order_css_id" xorm:"default 0 comment('') INT(11)"`
+}
diff --git a/app/db/offical/db_sys_cfg.go b/app/db/offical/db_sys_cfg.go
new file mode 100644
index 0000000..50fc038
--- /dev/null
+++ b/app/db/offical/db_sys_cfg.go
@@ -0,0 +1,23 @@
+package offical
+
+import (
+ "applet/app/db"
+ officialModel "applet/app/db/offical/model"
+)
+
+func SysCfgByKey(key string) *officialModel.SysCfg {
+ var data officialModel.SysCfg
+ get, err := db.Db.Where("k=?", key).Get(&data)
+ if get == false || err != nil {
+ return nil
+ }
+ return &data
+}
+func SysCfgByKeyStr(key string) string {
+ var data officialModel.SysCfg
+ get, err := db.Db.Where("k=?", key).Get(&data)
+ if get == false || err != nil {
+ return ""
+ }
+ return data.V
+}
diff --git a/app/db/offical/db_user_app_list.go b/app/db/offical/db_user_app_list.go
new file mode 100644
index 0000000..d85b936
--- /dev/null
+++ b/app/db/offical/db_user_app_list.go
@@ -0,0 +1,15 @@
+package offical
+
+import (
+ "applet/app/db"
+ "applet/app/db/offical/model"
+)
+
+func GetUserAppList(uid string) *model.UserAppList {
+ var data model.UserAppList
+ get, err := db.Db.Where("uuid=?", uid).Get(&data)
+ if get == false || err != nil {
+ return nil
+ }
+ return &data
+}
diff --git a/app/db/offical/model/master_list_cfg.go b/app/db/offical/model/master_list_cfg.go
new file mode 100644
index 0000000..3963c50
--- /dev/null
+++ b/app/db/offical/model/master_list_cfg.go
@@ -0,0 +1,9 @@
+package model
+
+type MasterListCfg struct {
+ K string `json:"k" xorm:"not null VARCHAR(255)"`
+ V string `json:"v" xorm:"TEXT"`
+ Memo string `json:"memo" xorm:"VARCHAR(255)"`
+ Uid string `json:"uid" xorm:"comment('0是官方') VARCHAR(255)"`
+ Id int `json:"id" xorm:"not null pk autoincr INT(11)"`
+}
diff --git a/app/db/offical/model/sys_cfg.go b/app/db/offical/model/sys_cfg.go
new file mode 100644
index 0000000..5508c9a
--- /dev/null
+++ b/app/db/offical/model/sys_cfg.go
@@ -0,0 +1,7 @@
+package model
+
+type SysCfg struct {
+ K string `json:"k" xorm:"not null pk comment('键') VARCHAR(127)"`
+ V string `json:"v" xorm:"comment('值') TEXT"`
+ Memo string `json:"memo" xorm:"not null default '' comment('备注') VARCHAR(255)"`
+}
diff --git a/app/db/offical/model/user_app_list.go b/app/db/offical/model/user_app_list.go
new file mode 100644
index 0000000..9720400
--- /dev/null
+++ b/app/db/offical/model/user_app_list.go
@@ -0,0 +1,33 @@
+package model
+
+type UserAppList struct {
+ Id int `json:"id" xorm:"not null pk autoincr INT(11)"`
+ Uuid int `json:"uuid" xorm:"not null comment('masterId') INT(10)"`
+ Uid int `json:"uid" xorm:"not null comment('用户ID') INT(10)"`
+ AppId int `json:"app_id" xorm:"not null comment('应用ID') INT(10)"`
+ PlanId string `json:"plan_id" xorm:"not null default '' comment('套餐ID') VARCHAR(100)"`
+ Expire int `json:"expire" xorm:"not null default 0 comment('过期时间') INT(10)"`
+ Name string `json:"name" xorm:"not null default '' comment('应用主名称') VARCHAR(32)"`
+ Icon string `json:"icon" xorm:"not null default '' comment('应用主图标') VARCHAR(250)"`
+ CreateTime int `json:"create_time" xorm:"not null default 0 comment('初次激活时间') INT(10)"`
+ RenewTime int `json:"renew_time" xorm:"not null default 0 comment('上次续费时间') INT(10)"`
+ Domain string `json:"domain" xorm:"not null default '' comment('域名') index VARCHAR(110)"`
+ DomainAlias string `json:"domain_alias" xorm:"not null default '' comment('域名别名') index VARCHAR(110)"`
+ Platform string `json:"platform" xorm:"not null default '' comment('平台信息 ios,android,applet') VARCHAR(100)"`
+ Info string `json:"info" xorm:"comment('平台名称如ios.name.#ddd;') TEXT"`
+ PayMode int `json:"pay_mode" xorm:"not null default 1 comment('付费模式,0授信,1付款') TINYINT(1)"`
+ Price float32 `json:"price" xorm:"not null default 0.00 comment('应用价格') FLOAT(10,2)"`
+ PricePay float32 `json:"price_pay" xorm:"not null default 0.00 comment('实际付款价格') FLOAT(10,2)"`
+ OfficialPrice float32 `json:"official_price" xorm:"not null default 0.00 comment('应用价格') FLOAT(10,2)"`
+ OfficialPricePay float32 `json:"official_price_pay" xorm:"not null default 0.00 comment('实际付款价格') FLOAT(10,2)"`
+ State int `json:"state" xorm:"not null default 0 comment('0未创建,1正常,2停用,3过期') TINYINT(1)"`
+ DeleteAt int `json:"delete_at" xorm:"not null default 0 TINYINT(1)"`
+ CustomAndroidCount int `json:"custom_android_count" xorm:"default 0 comment('客户端安卓包名重置次数') INT(11)"`
+ CustomIosCount int `json:"custom_ios_count" xorm:"default 0 comment('客户端ios包名重置次数') INT(11)"`
+ StoreAndroidCount int `json:"store_android_count" xorm:"default 0 comment('商家端安卓包名重置次数') INT(11)"`
+ StoreIosCount int `json:"store_ios_count" xorm:"default 0 comment('商家端ios包名重置次数') INT(11)"`
+ SmsPlatform string `json:"sms_platform" xorm:"default 'mob' comment('mob ljioe联江') VARCHAR(255)"`
+ IsClose int `json:"is_close" xorm:"default 0 comment('是否关闭') INT(1)"`
+ Puid int `json:"puid" xorm:"default 0 comment('') INT(11)"`
+ StoreRateInfo string `json:"store_rate_info" xorm:"comment('付呗商品进件费率') TEXT"`
+}
diff --git a/app/db/wechat_applet_info.go b/app/db/wechat_applet_info.go
new file mode 100644
index 0000000..61c972e
--- /dev/null
+++ b/app/db/wechat_applet_info.go
@@ -0,0 +1,166 @@
+package db
+
+import (
+ "applet/app/cfg"
+ "applet/app/db/model"
+ "applet/app/utils"
+ "applet/app/utils/cache"
+ "crypto/tls"
+ "encoding/json"
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "github.com/tidwall/gjson"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "strings"
+ "time"
+ "xorm.io/xorm"
+)
+
+func GetAppletKey(c *gin.Context, eg *xorm.Engine) map[string]string {
+ appId := c.GetHeader("appid")
+ isFilterTaobao := "0"
+ appletName := ""
+ appletLogo := ""
+ originalAppId := ""
+ bottomNavCssId := ""
+ mpAuditVersion := ""
+ MpAuditGoodsDetailCssId := ""
+ orderCssId := ""
+ if appId == "" {
+ wxAppletCfg := SysCfgGetWithDb(eg, c.GetString("mid"), "wx_applet_key")
+ isFilterTaobao = gjson.Get(wxAppletCfg, "taobaoGoodsOnOff").String()
+ appletName = gjson.Get(wxAppletCfg, "appletName").String()
+ originalAppId = gjson.Get(wxAppletCfg, "originalAppId").String()
+ appletLogo = gjson.Get(wxAppletCfg, "appletIcon").String()
+ mpAuditVersion = SysCfgGetWithDb(eg, c.GetString("mid"), "mp_audit_version")
+ var tm model.SysTemplate
+ if has, err := eg.Where("is_use = 1 AND type = 'bottom' AND platform = 4 ").
+ Cols("id,uid,name,is_use,is_system").
+ Get(&tm); err != nil || has == false {
+ bottomNavCssId = ""
+ } else {
+ bottomNavCssId = utils.IntToStr(tm.Id)
+ }
+ if c.GetHeader("AppVersionName") == mpAuditVersion && c.GetHeader("AppVersionName") != "" {
+ m := SysCfgGetWithDb(eg, c.GetString("mid"), "mp_audit_template")
+ if m != "" {
+ bottomNavCssId = utils.Int64ToStr(gjson.Get(m, "bottom").Int())
+ }
+ m1 := SysCfgGet(c, "mp_audit_template")
+ if m1 != "" {
+ MpAuditGoodsDetailCssId = utils.Int64ToStr(gjson.Get(m1, "product_detail").Int())
+ }
+ }
+ appId = gjson.Get(wxAppletCfg, "appId").String()
+ } else {
+ var wxApplet model.WxAppletList
+ has, err2 := eg.Where("app_id=?", appId).Get(&wxApplet)
+ if has && err2 == nil {
+ isFilterTaobao = utils.IntToStr(wxApplet.IsFilterTaobao)
+ appletName = wxApplet.AppletName
+ appletLogo = wxApplet.AppletLogo
+ originalAppId = wxApplet.OriginalAppId
+ orderCssId = utils.IntToStr(wxApplet.OrderCssId)
+ mpAuditVersion = wxApplet.MpAuditVersion
+ bottomNavCssId = utils.IntToStr(wxApplet.BottomNavCssId)
+ MpAuditGoodsDetailCssId = ""
+ if c.GetHeader("AppVersionName") == mpAuditVersion && c.GetHeader("AppVersionName") != "" {
+ bottomNavCssId = utils.IntToStr(wxApplet.MpAuditCssId)
+ MpAuditGoodsDetailCssId = utils.IntToStr(wxApplet.MpAuditGoodsDetailCssId)
+ }
+ }
+ }
+ r := map[string]string{
+ "order_css_id": orderCssId,
+ "app_id": appId,
+ "is_filter_taobao": isFilterTaobao,
+ "applet_name": appletName,
+ "applet_logo": appletLogo,
+ "original_app_id": originalAppId,
+ "bottom_nav_css_id": bottomNavCssId,
+ "mp_audit_version": mpAuditVersion,
+ "mp_audit_goods_detail_css_id": MpAuditGoodsDetailCssId,
+ }
+ return r
+
+}
+func GetShareUse(c *gin.Context, eg *xorm.Engine) map[string]string {
+ var wxApplet model.WxAppletList
+ has, err2 := eg.Where("share_use=?", 1).Asc("id").Get(&wxApplet)
+ wxAppletCfg := SysCfgGetWithDb(eg, c.GetString("mid"), "wx_applet_key")
+ originalAppId := gjson.Get(wxAppletCfg, "originalAppId").String()
+ appId := gjson.Get(wxAppletCfg, "appId").String()
+ if has && err2 == nil {
+ originalAppId = wxApplet.OriginalAppId
+ appId = wxApplet.AppId
+ }
+ r := map[string]string{
+ "app_id": appId,
+ "original_app_id": originalAppId,
+ }
+ return r
+}
+func GetAppletToken(c *gin.Context, appId string, isMore, isReset string) (accessToken string) {
+ key := fmt.Sprintf("%s:%s:%s", c.GetString("mid"), "wx_applet_access_token2", appId)
+ token, err := cache.GetString(key)
+ if err == nil && token != "" && strings.Contains(token, "{") == false && isReset == "0" {
+ // 有缓存
+ accessToken = token
+ } else {
+ ExpiresIn := 1800
+ accessTokenStr := ApiToSuperAdminWx(c, appId, isMore)
+ if accessTokenStr == "" {
+ return ""
+ }
+ ExpiresIn = int(gjson.Get(accessTokenStr, "expires_in").Int()) - 60*60*1 //TODO::暂时只用1个小时
+ accessToken = gjson.Get(accessTokenStr, "authorizer_access_token").String()
+ if ExpiresIn > 0 {
+ //fmt.Printf("返回结果: %#v", res)
+ _, err = cache.SetEx(key, accessToken, ExpiresIn)
+ if err != nil {
+ fmt.Println("微信授权错误", err)
+ return ""
+ }
+ }
+
+ }
+ return accessToken
+}
+
+// 总后台微信token isMore多小程序登录总后台要判断读另一个表
+func ApiToSuperAdminWx(c *gin.Context, appid string, isMore string) string {
+ var req = make(map[string]string, 0)
+ var host string
+ host = cfg.WebsiteBackend.URL + "/Wx/getAuthorizerResult?appid=" + appid + "&is_more=" + isMore
+ fmt.Println(host)
+ tr := &http.Transport{
+ MaxIdleConnsPerHost: 200,
+ MaxIdleConns: 200,
+ MaxConnsPerHost: 200,
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+ }
+ client := &http.Client{
+ Timeout: 5 * time.Second,
+ Transport: tr,
+ }
+ byte1, _ := json.Marshal(req)
+ req1, _ := http.NewRequest("POST", host, strings.NewReader(string(byte1)))
+ req1.Header.Set("Content-Type", "application/json")
+ resp, err := (client).Do(req1)
+ if err != nil || resp == nil {
+ if resp != nil {
+ _, _ = io.Copy(ioutil.Discard, resp.Body)
+ }
+ return ""
+ }
+ defer resp.Body.Close()
+ respByte, _ := ioutil.ReadAll(resp.Body)
+
+ if len(respByte) == 0 {
+ return ""
+ }
+ return string(respByte)
+
+}
diff --git a/app/db/zhimeng_db.go b/app/db/zhimeng_db.go
new file mode 100644
index 0000000..b173da1
--- /dev/null
+++ b/app/db/zhimeng_db.go
@@ -0,0 +1,42 @@
+package db
+
+import (
+ "fmt"
+ "os"
+
+ _ "github.com/go-sql-driver/mysql"
+ "xorm.io/xorm"
+ "xorm.io/xorm/log"
+
+ "applet/app/cfg"
+)
+
+var ZhimengDb *xorm.Engine
+
+func InitZhimengDB(c *cfg.DBCfg) error {
+ var err error
+ if ZhimengDb, 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
+ }
+ ZhimengDb.SetConnMaxLifetime(c.MaxLifetime)
+ ZhimengDb.SetMaxOpenConns(c.MaxOpenConns)
+ ZhimengDb.SetMaxIdleConns(c.MaxIdleConns)
+ if err = Db.Ping(); err != nil {
+ return err
+ }
+ if c.ShowLog {
+ ZhimengDb.ShowSQL(true)
+ ZhimengDb.Logger().SetLevel(0)
+ 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 err
+ }
+ }
+ logger := log.NewSimpleLogger(f)
+ logger.ShowSQL(true)
+ ZhimengDb.SetLogger(logger)
+ }
+ return nil
+}
diff --git a/app/e/code.go b/app/e/code.go
new file mode 100644
index 0000000..6bebf2b
--- /dev/null
+++ b/app/e/code.go
@@ -0,0 +1,244 @@
+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
+ // 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
+ ERR_IS_BIND_PDD_ERR = 403058
+ ERR_MOB_SMS_NO_SAME = 403059
+ ERR_MOB_SMS_NO_EXISTS = 403060
+ // 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 = 404012
+ 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
+ ERR_IS_BIND_THIRDOTHERWECHAT = 500015
+ ERR_INIT_RABBITMQ = 500016
+)
+
+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失败",
+ // 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_MOB_SMS_NO_SAME: "验证码不一致",
+ ERR_MOB_SMS_NO_EXISTS: "验证码已失效",
+ 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_IS_BIND_THIRDOTHERWECHAT: "该账号已经绑定了其他微信账号",
+ 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: "该等级已经付过款",
+ ERR_IS_BIND_PDD_ERR: "获取PDD绑定信息失败",
+ // 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: "校验失败",
+ ERR_INIT_RABBITMQ: "连接mq失败",
+}
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..f8450a5
--- /dev/null
+++ b/app/e/msg.go
@@ -0,0 +1,212 @@
+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
+ }
+ }
+ jsonData, _ := json.Marshal(res)
+ strs := string(jsonData)
+ if c.GetString("translate_open") != "" {
+ strs = utils.ReadReverse(c, string(jsonData))
+ }
+ if utils.GetApiVersion(c) > 0 && utils.CheckUri(c) > 0 { //加了签名校验只返回加密的字符串
+ strs = utils.ResultAes(c, []byte(strs))
+ }
+ c.Writer.WriteString(strs)
+}
+
+func OutSucByLianlian(c *gin.Context, code string, msg string, data interface{}) {
+ res := gin.H{
+ "code": code,
+ "msg": msg,
+ "data": data,
+ }
+ jsonData, _ := json.Marshal(res)
+ strs := string(jsonData)
+ c.Writer.WriteString(strs)
+
+}
+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
+ }
+ }
+ jsonData, _ := json.Marshal(res)
+ strs := string(jsonData)
+ if c.GetString("translate_open") != "" {
+ strs = utils.ReadReverse(c, string(jsonData))
+ }
+ if utils.GetApiVersion(c) > 0 && utils.CheckUri(c) > 0 { //加了签名校验只返回加密的字符串
+ strs = utils.ResultAes(c, []byte(strs))
+ }
+ c.Writer.WriteString(strs)
+
+}
+
+// 错误输出
+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 c.GetString("translate_open") != "" {
+ msg = utils.ReadReverse(c, msg)
+ }
+ if utils.GetApiVersion(c) > 0 && utils.CheckUri(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 OutErrSecond(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 c.GetString("translate_open") != "" {
+ msg = utils.ReadReverse(c, msg)
+ }
+ if utils.GetApiVersion(c) > 0 && utils.CheckUri(c) > 0 { //加了签名校验只返回加密的字符串
+ jsonData, _ := json.Marshal(gin.H{
+ "code": code,
+ "msg": msg,
+ "data": "",
+ })
+ 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": "",
+ })
+ }
+
+}
+
+// 重定向
+func OutRedirect(c *gin.Context, code int, loc string) {
+ if code < 301 || code > 308 {
+ code = 303
+ }
+ c.Redirect(code, loc)
+ c.Abort()
+}
+func DouYouReturnMsg(c *gin.Context, statusCode, msg string) {
+ res := gin.H{
+ "status_code": statusCode,
+ }
+ if msg != "" {
+ res["message"] = msg
+ }
+ jsonData, _ := json.Marshal(res)
+ strs := string(jsonData)
+ c.Writer.WriteString(strs)
+}
+func MeituanLmReturnMsg(c *gin.Context, statusCode, msg string) {
+ res := gin.H{
+ "errcode": statusCode,
+ }
+ if msg != "" {
+ res["errmsg"] = msg
+ }
+ jsonData, _ := json.Marshal(res)
+ strs := string(jsonData)
+ c.Writer.WriteString(strs)
+}
+func TaskBoxReturnMsg(c *gin.Context, statusCode int, msg string) {
+ res := gin.H{
+ "code": statusCode,
+ }
+ if msg != "" {
+ res["message"] = msg
+ }
+ jsonData, _ := json.Marshal(res)
+ strs := string(jsonData)
+ c.Writer.WriteString(strs)
+}
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_merchant_coupon_scheme.go b/app/enum/enum_merchant_coupon_scheme.go
new file mode 100644
index 0000000..4d8e986
--- /dev/null
+++ b/app/enum/enum_merchant_coupon_scheme.go
@@ -0,0 +1,190 @@
+package enum
+
+// ActCouponType 优惠券类型
+type ActCouponType int
+
+const (
+ ActCouponTypeImmediate ActCouponType = iota + 1 // 立减
+ ActCouponTypeReachReduce // 满减
+ ActCouponTypeReachDiscount // 满折
+)
+
+func (em ActCouponType) String() string {
+ switch em {
+ case ActCouponTypeImmediate:
+ return "立减券"
+ case ActCouponTypeReachReduce:
+ return "满减券"
+ case ActCouponTypeReachDiscount:
+ return "折扣券"
+ default:
+ return "未知类型"
+ }
+}
+
+// ActCouponSendWay 发放形式
+type ActCouponSendWay int
+
+const (
+ ActCouponSendWayPositive ActCouponSendWay = iota + 1
+ ActCouponSendWayManual
+)
+
+func (em ActCouponSendWay) String() string {
+ switch em {
+ case ActCouponSendWayPositive:
+ return "主动发放"
+ case ActCouponSendWayManual:
+ return "手动领取"
+ default:
+ return "未知类型"
+ }
+}
+
+// ActCouponSendTimeType 发放时间类型
+type ActCouponSendTimeType int
+
+const (
+ ActCouponSendTimeTypeImmediate ActCouponSendTimeType = iota + 1
+ ActCouponSendTimeTypeTiming
+)
+
+func (em ActCouponSendTimeType) String() string {
+ switch em {
+ case ActCouponSendTimeTypeImmediate:
+ return "立即发放"
+ case ActCouponSendTimeTypeTiming:
+ return "定时"
+ default:
+ return "未知类型"
+ }
+}
+
+// ActCouponSendUserType 发放用户
+type ActCouponSendUserType int
+
+const (
+ ActCouponSendUserTypeLevel ActCouponSendUserType = iota + 1
+ ActCouponSendUserTypeAll
+ ActCouponSendUserTypeSpecify
+)
+
+func (em ActCouponSendUserType) String() string {
+ switch em {
+ case ActCouponSendUserTypeLevel:
+ return "指定会员等级"
+ case ActCouponSendUserTypeAll:
+ return "所有人可领"
+ case ActCouponSendUserTypeSpecify:
+ return "指定用户"
+ default:
+ return "未知类型"
+ }
+}
+
+// ActCouponValidTimeType 有效时间
+type ActCouponValidTimeType int
+
+const (
+ ActCouponValidTimeTypeFix ActCouponValidTimeType = iota + 1 // 固定日期
+ ActCouponValidTimeTypeXDay // 领取当日开始计算有效期
+ ActCouponValidTimeTypeXNextDay // 领取次日开始计算有效期
+)
+
+func (em ActCouponValidTimeType) String() string {
+ switch em {
+ case ActCouponValidTimeTypeFix:
+ return "固定日期"
+ case ActCouponValidTimeTypeXDay:
+ return "自领取当日起,%d天内有效"
+ case ActCouponValidTimeTypeXNextDay:
+ return "自领取次日起,%d天内有效"
+ default:
+ return "未知类型"
+ }
+}
+
+// ActCouponUseRule 使用规则
+type ActCouponUseRule int
+
+const (
+ ActCouponUseRuleAll ActCouponUseRule = iota + 1 // 全部商品可用
+ ActCouponUseRuleSpecifyGoods // 仅可用于指定商品
+ ActCouponUseRuleSpecifyActivity // 可用于活动类型
+)
+
+func (em ActCouponUseRule) String() string {
+ switch em {
+ case ActCouponUseRuleAll:
+ return "全部商品可用"
+ case ActCouponUseRuleSpecifyGoods:
+ return "仅可用于指定商品"
+ case ActCouponUseRuleSpecifyActivity:
+ return "可用于活动类型"
+ default:
+ return "未知类型"
+ }
+}
+
+type ActCouponUseActivityType int
+
+const (
+ ActCouponUseActivityTypeForGroup ActCouponUseActivityType = iota + 1
+ ActCouponUseActivityTypeForSecondKill
+ ActCouponUseActivityTypeForBargain
+)
+
+func (em ActCouponUseActivityType) String() string {
+ switch em {
+ case ActCouponUseActivityTypeForGroup:
+ return "拼团活动"
+ case ActCouponUseActivityTypeForSecondKill:
+ return "秒杀活动"
+ case ActCouponUseActivityTypeForBargain:
+ return "砍价活动"
+ default:
+ return "未知类型"
+ }
+}
+
+type ActCouponSendState int
+
+const (
+ ActCouponSendStateUnProvide ActCouponSendState = iota + 1 // 未发放
+ ActCouponSendStateProvide // 已发放
+ ActCouponSendStateProviding // 发放中
+)
+
+func (em ActCouponSendState) String() string {
+ switch em {
+ case ActCouponSendStateUnProvide:
+ return "未发放"
+ case ActCouponSendStateProvide:
+ return "已发放"
+ case ActCouponSendStateProviding:
+ return "发放中"
+ default:
+ return "未知类型"
+ }
+}
+
+type ActUserCouponUseState int
+
+const (
+ ActUserCouponUseStateUnUse ActUserCouponUseState = iota // 未使用
+ ActUserCouponUseStateUsed // 已使用
+ ActUserCouponUseStateUnValid // 失效
+)
+
+func (em ActUserCouponUseState) String() string {
+ switch em {
+ case ActUserCouponUseStateUnUse:
+ return "未使用"
+ case ActUserCouponUseStateUsed:
+ return "已使用"
+ case ActUserCouponUseStateUnValid:
+ return "失效"
+ default:
+ return "未知类型"
+ }
+}
diff --git a/app/hdl/hdl_cate.go b/app/hdl/hdl_cate.go
new file mode 100644
index 0000000..084cd69
--- /dev/null
+++ b/app/hdl/hdl_cate.go
@@ -0,0 +1,10 @@
+package hdl
+
+import (
+ "applet/app/svc"
+ "github.com/gin-gonic/gin"
+)
+
+func Cate(c *gin.Context) {
+ svc.Cate(c)
+}
diff --git a/app/hdl/hdl_goods.go b/app/hdl/hdl_goods.go
new file mode 100644
index 0000000..346a4be
--- /dev/null
+++ b/app/hdl/hdl_goods.go
@@ -0,0 +1,16 @@
+package hdl
+
+import (
+ "applet/app/svc"
+ "github.com/gin-gonic/gin"
+)
+
+func Goods(c *gin.Context) {
+ svc.Goods(c)
+}
+func GoodsSku(c *gin.Context) {
+ svc.GoodsSku(c)
+}
+func GoodsCoupon(c *gin.Context) {
+ svc.GoodsCoupon(c)
+}
diff --git a/app/hdl/hdl_order.go b/app/hdl/hdl_order.go
new file mode 100644
index 0000000..6f6f98f
--- /dev/null
+++ b/app/hdl/hdl_order.go
@@ -0,0 +1,28 @@
+package hdl
+
+import (
+ "applet/app/svc"
+ "github.com/gin-gonic/gin"
+)
+
+func OrderTotal(c *gin.Context) {
+ svc.OrderTotal(c)
+}
+func OrderCreate(c *gin.Context) {
+ svc.OrderCreate(c)
+}
+func OrderCoupon(c *gin.Context) {
+ svc.OrderCoupon(c)
+}
+func OrderCancel(c *gin.Context) {
+ svc.OrderCancel(c)
+}
+func OrderList(c *gin.Context) {
+ svc.OrderList(c)
+}
+func OrderCate(c *gin.Context) {
+ svc.OrderCate(c)
+}
+func OrderDetail(c *gin.Context) {
+ svc.OrderDetail(c)
+}
diff --git a/app/hdl/hdl_pay.go b/app/hdl/hdl_pay.go
new file mode 100644
index 0000000..5977a5a
--- /dev/null
+++ b/app/hdl/hdl_pay.go
@@ -0,0 +1,37 @@
+package hdl
+
+import (
+ "applet/app/e"
+ "applet/app/svc"
+ "github.com/gin-gonic/gin"
+)
+
+func Pay(c *gin.Context) {
+ orderType := c.Param("orderType")
+ payMethod := c.Param("payMethod")
+ if orderType == "" || payMethod == "" {
+ e.OutErr(c, e.ERR_INVALID_ARGS)
+ return
+ }
+ payFunc, ok := svc.PayFuncList[orderType][payMethod]
+ if !ok || payFunc == nil {
+ e.OutErr(c, e.ERR, e.NewErr(500, "不存在该支付方式"))
+ return
+ }
+ r, err := payFunc(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_PAY_ERR, e.NewErr(e.ERR_PAY_ERR, err.Error()))
+ return
+ }
+ }
+ e.OutSuc(c, r, nil)
+ return
+
+ return
+}
diff --git a/app/hdl/hdl_store.go b/app/hdl/hdl_store.go
new file mode 100644
index 0000000..cb73d25
--- /dev/null
+++ b/app/hdl/hdl_store.go
@@ -0,0 +1,29 @@
+package hdl
+
+import (
+ "applet/app/e"
+ "applet/app/svc"
+ "github.com/gin-gonic/gin"
+)
+
+func BankStoreCate(c *gin.Context) {
+ var res = []map[string]string{
+ {"name": "全部网点", "value": ""},
+ {"name": "附近网点", "value": "1"},
+ {"name": "关注网点", "value": "2"},
+ }
+ e.OutSuc(c, res, nil)
+ return
+}
+func BankStore(c *gin.Context) {
+ svc.BankStore(c)
+}
+func Store(c *gin.Context) {
+ svc.Store(c)
+}
+func StoreAddLike(c *gin.Context) {
+ svc.StoreAddLike(c)
+}
+func StoreCancelLike(c *gin.Context) {
+ svc.StoreCancelLike(c)
+}
diff --git a/app/hdl/hdl_store_order.go b/app/hdl/hdl_store_order.go
new file mode 100644
index 0000000..5dd3f41
--- /dev/null
+++ b/app/hdl/hdl_store_order.go
@@ -0,0 +1,19 @@
+package hdl
+
+import (
+ "applet/app/svc"
+ "github.com/gin-gonic/gin"
+)
+
+func StoreOrderList(c *gin.Context) {
+ svc.StoreOrderList(c)
+}
+func StoreOrderCate(c *gin.Context) {
+ svc.StoreOrderCate(c)
+}
+func StoreOrderDetail(c *gin.Context) {
+ svc.StoreOrderDetail(c)
+}
+func StoreOrderConfirm(c *gin.Context) {
+ svc.StoreOrderConfirm(c)
+}
diff --git a/app/lib/alipay/api.go b/app/lib/alipay/api.go
new file mode 100644
index 0000000..6c5da5f
--- /dev/null
+++ b/app/lib/alipay/api.go
@@ -0,0 +1,334 @@
+package alipay
+
+import (
+ "applet/app/cfg"
+ "applet/app/md"
+ "applet/app/utils/logx"
+ "fmt"
+
+ "github.com/iGoogle-ink/gopay"
+ "github.com/iGoogle-ink/gopay/alipay"
+)
+
+func commClient(appID, priKey, RSA, PKCS string, paySet *md.PayData) (*alipay.Client, error) {
+ client := alipay.NewClient(appID, priKey, true)
+ client.DebugSwitch = gopay.DebugOn
+ //判断密钥的类型
+ rsaType := alipay.RSA2
+ pkcsType := alipay.PKCS1
+ if RSA == "1" {
+ rsaType = alipay.RSA
+ }
+ if PKCS == "1" {
+ pkcsType = alipay.PKCS8
+ }
+ if paySet.PayAliUseType == "1" {
+ rsaType = alipay.RSA2
+ pkcsType = alipay.PKCS8
+ }
+ //配置公共参数
+ client.SetCharset("utf-8").
+ SetSignType(rsaType).
+ SetPrivateKeyType(pkcsType)
+ //新支付宝支付
+ if paySet.PayAliUseType == "1" {
+ appCertSN, err := alipay.GetCertSN(cfg.WxappletFilepath.URL + "/" + paySet.PayAppCertSn)
+ if err != nil {
+ return nil, err
+ }
+ if appCertSN == "" {
+ return nil, err
+ }
+ client.SetAppCertSN(appCertSN)
+ aliPayRootCertSN := "687b59193f3f462dd5336e5abf83c5d8_02941eef3187dddf3d3b83462e1dfcf6"
+ client.SetAliPayRootCertSN(aliPayRootCertSN)
+ aliPayPublicCertSN, err := alipay.GetCertSN(cfg.WxappletFilepath.URL + "/" + paySet.PayAlipayrsaPublicKey)
+ if err != nil {
+ return nil, err
+ }
+ if aliPayPublicCertSN == "" {
+ return nil, err
+ }
+ client.SetAliPayPublicCertSN(aliPayPublicCertSN)
+ }
+ return client, nil
+}
+
+//(实名证件信息比对验证预咨询)
+//https://opendocs.alipay.com/apis/api_2/alipay.user.certdoc.certverify.preconsult
+func UserCertverifyPreconsult(appID, priKey, userName, certNo, RSA, PKCS string, paySet *md.PayData) (*md.AlipayUserCertdocCertverifyPreconsult, error) {
+ client, _ := commClient(appID, priKey, RSA, PKCS, paySet)
+ //请求参数
+ body := make(gopay.BodyMap)
+ body.Set("user_name", userName)
+ body.Set("cert_type", "IDENTITY_CARD")
+ body.Set("cert_no", certNo)
+ var aliPsp md.AlipayUserCertdocCertverifyPreconsult
+ err := client.PostAliPayAPISelfV2(body, "alipay.user.certdoc.certverify.preconsult", aliPsp)
+ if err != nil {
+ return nil, err
+ }
+ return &aliPsp, err
+}
+
+//(实名证件信息比对验证咨询)
+//https://opendocs.alipay.com/apis/api_2/alipay.user.certdoc.certverify.consult
+func UserCertverifyConsult(appID, priKey, verifyId, RSA, PKCS string, paySet *md.PayData) (*md.AlipayUserCertdocCertverifyConsult, error) {
+ client, _ := commClient(appID, priKey, RSA, PKCS, paySet)
+ //请求参数
+ body := make(gopay.BodyMap)
+ body.Set("verify_id", verifyId)
+ var aliPsp md.AlipayUserCertdocCertverifyConsult
+ err := client.PostAliPayAPISelfV2(body, "alipay.user.certdoc.certverify.consult", aliPsp)
+ if err != nil {
+ return nil, err
+ }
+ return &aliPsp, err
+}
+
+// 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) {
+ //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(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(err)
+ // 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(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/arkid/api.go b/app/lib/arkid/api.go
new file mode 100644
index 0000000..685a0bc
--- /dev/null
+++ b/app/lib/arkid/api.go
@@ -0,0 +1,148 @@
+package arkid
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ "applet/app/cfg"
+ "applet/app/utils"
+ "applet/app/utils/cache"
+ "applet/app/utils/logx"
+)
+
+func arkidLogin(args map[string]interface{}) ([]byte, error) {
+ url := cfg.ArkID.Url + "/siteapi/v1/ucenter/login/"
+ b, err := json.Marshal(args)
+ if err != nil {
+ return nil, logx.Error(err)
+ }
+ var d []byte
+ d, err = utils.CurlPost(url, b, nil)
+ if err != nil {
+ return nil, logx.Error(err)
+ }
+ return d, nil
+}
+
+func arkidLogout(token string) ([]byte, error) {
+ // fmt.Println(cfg.ArkID.Url)
+ url := cfg.ArkID.Url + "/siteapi/v1/revoke/token/"
+ h := map[string]string{"authorization": fmt.Sprintf("token %s", token)}
+ d, err := utils.CurlPost(url, "", h)
+ if err != nil {
+ return nil, logx.Error(err)
+ }
+ return d, nil
+}
+
+func arkidUserInfo(token string) ([]byte, error) {
+ url := cfg.ArkID.Url + "/siteapi/v1/auth/token/"
+ h := map[string]string{"authorization": fmt.Sprintf("token %s", token)}
+ d, err := utils.CurlGet(url, h)
+ if err != nil {
+ return nil, logx.Error(err)
+ }
+ return d, nil
+}
+
+func arkidRegister(args map[string]interface{}) ([]byte, error) {
+ url := cfg.ArkID.Url + "/siteapi/oneid/user/"
+ b, err := json.Marshal(args)
+ if err != nil {
+ return nil, logx.Error(err)
+ }
+ admin, err := getArkIDAdmin()
+ if err != nil {
+ return nil, logx.Error(err)
+ }
+ h := map[string]string{"authorization": fmt.Sprintf("token %s", admin.Token)}
+ var d []byte
+ d, err = utils.CurlPost(url, b, h)
+ if err != nil {
+ return nil, logx.Error(err)
+ }
+ return d, nil
+}
+
+func arkidAppAccessWhiteList(args map[string]interface{}, permName string) ([]byte, error) {
+ if permName == "" {
+ return nil, errors.New("The perm_name arg must required")
+ }
+ path := fmt.Sprintf("/siteapi/oneid/perm/%s/owner/", permName)
+ url := cfg.ArkID.Url + path
+ b, err := json.Marshal(args)
+ if err != nil {
+ return nil, logx.Error(err)
+ }
+ admin, err := getArkIDAdmin()
+ if err != nil {
+ return nil, logx.Error(err)
+ }
+ // fmt.Println(admin.Token)
+ h := map[string]string{"authorization": fmt.Sprintf("token %s", admin.Token)}
+ var d []byte
+ d, err = utils.CurlPatch(url, b, h)
+ if err != nil {
+ return nil, logx.Error(err)
+ }
+ return d, nil
+}
+
+func arkidUserDelete(username string) ([]byte, error) {
+ if username == "" {
+ return nil, errors.New("The username arg must required")
+ }
+ path := fmt.Sprintf("/siteapi/oneid/user/%s/", username)
+ url := cfg.ArkID.Url + path
+ admin, err := getArkIDAdmin()
+ if err != nil {
+ return nil, logx.Error(err)
+ }
+ // fmt.Println(admin.Token)
+ h := map[string]string{"authorization": fmt.Sprintf("token %s", admin.Token)}
+ var d []byte
+ d, err = utils.CurlDelete(url, nil, h)
+ if err != nil {
+ return nil, logx.Error(err)
+ }
+ return d, nil
+}
+
+func arkidUserUpdate(username string, args map[string]interface{}) ([]byte, error) {
+ if username == "" {
+ return nil, errors.New("The username arg must required")
+ }
+ b, err := json.Marshal(args)
+ if err != nil {
+ return nil, logx.Error(err)
+ }
+ path := fmt.Sprintf("/siteapi/oneid/user/%s/", username)
+ url := cfg.ArkID.Url + path
+ var admin *ArkIDUser
+ admin, err = getArkIDAdmin()
+ if err != nil {
+ return nil, logx.Error(err)
+ }
+ h := map[string]string{"authorization": fmt.Sprintf("token %s", admin.Token)}
+ d, err := utils.CurlPatch(url, b, h)
+ if err != nil {
+ return nil, logx.Error(err)
+ }
+ return d, nil
+}
+
+func getArkIDAdmin() (*ArkIDUser, error) {
+ c, err := cache.Bytes(cache.Get(ARKID_ADMIN_TOKEN))
+ if err != nil {
+ logx.Error(err)
+ }
+ if c != nil && err == nil {
+ admin := new(ArkIDUser)
+ if err = json.Unmarshal(c, admin); err != nil {
+ return admin, err
+ }
+ return admin, nil
+ }
+ return Init()
+}
diff --git a/app/lib/arkid/base.go b/app/lib/arkid/base.go
new file mode 100644
index 0000000..fff9511
--- /dev/null
+++ b/app/lib/arkid/base.go
@@ -0,0 +1,6 @@
+package arkid
+
+const (
+ BASE_URL = "http://k8s.arkid.izhim.cn"
+ ARKID_ADMIN_TOKEN = "arkid_admin_token"
+)
diff --git a/app/lib/arkid/init.go b/app/lib/arkid/init.go
new file mode 100644
index 0000000..060537c
--- /dev/null
+++ b/app/lib/arkid/init.go
@@ -0,0 +1,24 @@
+package arkid
+
+import (
+ "applet/app/cfg"
+ "applet/app/utils"
+ "applet/app/utils/cache"
+)
+
+// Init is cache token to redis
+func Init() (*ArkIDUser, error) {
+ arkidsdk := NewArkID()
+ arkadmin := new(ArkIDUser)
+ err := arkidsdk.SelectFunction("arkid_login").WithArgs(RequestBody{
+ Username: cfg.ArkID.Admin,
+ Password: cfg.ArkID.AdminPassword,
+ }).Result(arkadmin)
+ if err != nil {
+ panic(err)
+ }
+
+ // token 默认30天过期
+ cache.SetEx(ARKID_ADMIN_TOKEN, utils.Serialize(arkadmin), 2592000)
+ return arkadmin, err
+}
diff --git a/app/lib/arkid/model.go b/app/lib/arkid/model.go
new file mode 100644
index 0000000..82882ab
--- /dev/null
+++ b/app/lib/arkid/model.go
@@ -0,0 +1,62 @@
+package arkid
+
+type ArkIDUser struct {
+ Token string `json:"token"`
+ UserID int `json:"user_id"`
+ Username string `json:"username"`
+ Name string `json:"name"`
+ Email string `json:"email"`
+ Mobile string `json:"mobile"`
+ EmployeeNumber string `json:"employee_number"`
+ Gender int `json:"gender"`
+ Perms []string `json:"perms"`
+ Avatar string `json:"avatar"`
+ Roles []string `json:"roles"`
+ PrivateEmail string `json:"private_email"`
+ Position string `json:"position"`
+ IsSettled bool `json:"is_settled"`
+ IsManager bool `json:"is_manager"`
+ IsAdmin bool `json:"is_admin"`
+ IsExternUser bool `json:"is_extern_user"`
+ OriginVerbose string `json:"origin_verbose"`
+ RequireResetPassword bool `json:"require_reset_password"`
+ HasPassword bool `json:"has_password"`
+}
+
+type RequestBody struct {
+ Token string `json:"token,omitempty"`
+ Username string `json:"username,omitempty"`
+ Password string `json:"password,omitempty"`
+ User struct {
+ Avatar string `json:"avatar,omitempty"`
+ Email string `json:"email,omitempty"`
+ EmployeeNumber string `json:"employee_number,omitempty"`
+ Gender int `json:"gende,omitemptyr"`
+ Mobile string `json:"mobile,omitempty"`
+ Name string `json:"name,omitempty"`
+ Position string `json:"position,omitempty"`
+ PrivateEmail string `json:"private_email,omitempty"`
+ Username string `json:"username,omitempty"`
+ Depts interface{} `json:"depts,omitempty"`
+ Roles interface{} `json:"roles,omitempty"`
+ Nodes []interface{} `json:"nodes,omitempty"`
+ IsSettled bool `json:"is_settled,omitempty"`
+ Password string `json:"password,omitempty"`
+ RequireResetPassword bool `json:"require_reset_password,omitempty"`
+ HasPassword bool `json:"has_password,omitempty"`
+ } `json:"user,omitempty"`
+ NodeUids []string `json:"node_uids,omitempty"`
+ PermName string `json:"perm_name,omitempty"`
+ UserPermStatus []struct {
+ UID string `json:"uid,omitempty"`
+ Status int `json:"status,omitempty"`
+ } `json:"user_perm_status,omitempty"`
+}
+
+type AppAccessWhiteListResult struct {
+ UserPermStatus []struct {
+ UID string `json:"uid"`
+ Status int `json:"status"`
+ } `json:"user_perm_status"`
+ NodePermStatus []interface{} `json:"node_perm_status"`
+}
diff --git a/app/lib/arkid/sdk.go b/app/lib/arkid/sdk.go
new file mode 100644
index 0000000..59b45c2
--- /dev/null
+++ b/app/lib/arkid/sdk.go
@@ -0,0 +1,165 @@
+package arkid
+
+import (
+ "applet/app/utils/cache"
+ "applet/app/utils/logx"
+ "encoding/json"
+ "errors"
+ "fmt"
+)
+
+type SDK struct {
+ response []byte
+ fmap map[string]func(RequestBody)
+ fname string
+ err error
+}
+
+//Init is init sdk
+func (s *SDK) Init() {
+ s.fmap = make(map[string]func(RequestBody))
+}
+
+//SelectFunction is choose func
+func (s *SDK) SelectFunction(fname string) *SDK {
+ s.fname = fname
+ return s
+}
+
+//WithArgs is request args
+func (s *SDK) WithArgs(r RequestBody) *SDK {
+ f := s.fmap[s.fname]
+ f(r)
+ return s
+}
+
+//Result is result to p
+func (s *SDK) Result(p interface{}) error {
+ if s.err != nil {
+ return s.err
+ }
+ if string(s.response) == "" {
+ return nil
+ }
+ if err := json.Unmarshal(s.response, p); err != nil {
+ return logx.Error(string(s.response), err)
+ }
+ return nil
+}
+
+// Register is register func
+func (s *SDK) Register(name string, f func(RequestBody)) {
+ s.fmap[name] = f
+}
+
+//getAdmin arkid 用户的信息 ,主要是token
+func (s *SDK) arkidLogin(r RequestBody) {
+ postData := map[string]interface{}{
+ "username": r.Username,
+ "password": r.Password,
+ }
+ s.response, s.err = arkidLogin(postData)
+}
+
+func (s *SDK) arkidRegister(r RequestBody) {
+ postData := map[string]interface{}{}
+ b, err := json.Marshal(r)
+ if err != nil {
+ s.err = err
+ }
+ if err := json.Unmarshal(b, &postData); err != nil {
+ s.err = err
+ }
+ s.response, s.err = arkidRegister(postData)
+}
+
+func (s *SDK) arkidAppAccessWhiteList(r RequestBody) {
+ postData := map[string]interface{}{}
+ b, err := json.Marshal(r)
+ if err != nil {
+ s.err = err
+ }
+ if err := json.Unmarshal(b, &postData); err != nil {
+ s.err = err
+ }
+ s.response, s.err = arkidAppAccessWhiteList(postData, r.PermName)
+}
+
+func (s *SDK) arkidUserInfo(r RequestBody) {
+ s.response, s.err = arkidUserInfo(r.Token)
+}
+
+func (s *SDK) arkidUserDelete(r RequestBody) {
+ s.response, s.err = arkidUserDelete(r.Username)
+}
+
+func (s *SDK) arkidUserUpdate(r RequestBody) {
+ postData := map[string]interface{}{}
+ b, err := json.Marshal(r.User)
+ if err != nil {
+ s.err = err
+ }
+ if err := json.Unmarshal(b, &postData); err != nil {
+ s.err = err
+ }
+ s.response, s.err = arkidUserUpdate(r.Username, postData)
+}
+
+func (s *SDK) arkidLogout(r RequestBody) {
+ s.response, s.err = arkidLogout(r.Token)
+}
+
+// NewArkID is con
+func NewArkID() *SDK {
+ sdk := new(SDK)
+ sdk.Init()
+ sdk.Register("arkid_login", sdk.arkidLogin)
+ sdk.Register("arkid_register", sdk.arkidRegister)
+ sdk.Register("arkid_app_access_white_list", sdk.arkidAppAccessWhiteList)
+ sdk.Register("arkid_delete_user", sdk.arkidUserDelete)
+ sdk.Register("arkid_user_info", sdk.arkidUserInfo)
+ sdk.Register("arkid_user_update", sdk.arkidUserUpdate)
+ sdk.Register("arkid_logout", sdk.arkidLogout)
+ return sdk
+}
+
+// GetArkIDUser is get arkid token if redis is existed unless send request to arkid
+func GetArkIDUser(username string, MD5passowrd string) (*ArkIDUser, error) {
+ key := fmt.Sprintf("arkid_user_%s", username)
+ arkidUser := new(ArkIDUser)
+ c, err := cache.GetBytes(key)
+ if c != nil && err == nil {
+ if err := json.Unmarshal(c, arkidUser); err != nil {
+ return arkidUser, err
+ }
+ if arkidUser.Token == "" {
+
+ return arkidUser, errors.New("Get Arkid User error, Token missing")
+ }
+
+ return arkidUser, err
+ }
+ arkidSdk := NewArkID()
+ err = arkidSdk.SelectFunction("arkid_login").WithArgs(RequestBody{
+ Username: username,
+ Password: MD5passowrd,
+ }).Result(arkidUser)
+ if arkidUser.Token == "" {
+ return arkidUser, errors.New("Get Arkid User error, Token missing")
+ }
+ // 缓存30天
+ // cache.SetEx(key, utils.Serialize(arkidUser), 2592000)
+ return arkidUser, err
+}
+
+// RegisterRollback is 注册时的错误回滚
+func RegisterRollback(username string) error {
+ sdk := NewArkID()
+ err := sdk.SelectFunction("arkid_delete_user").WithArgs(RequestBody{
+ Username: username,
+ }).Result(nil)
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/app/lib/auth/base.go b/app/lib/auth/base.go
new file mode 100644
index 0000000..a886c2e
--- /dev/null
+++ b/app/lib/auth/base.go
@@ -0,0 +1,25 @@
+package auth
+
+import (
+ "time"
+
+ "github.com/dgrijalva/jwt-go"
+)
+
+// TokenExpireDuration is jwt 过期时间
+const TokenExpireDuration = time.Hour * 4380
+
+const RefreshTokenExpireDuration = time.Hour * 4380
+
+var Secret = []byte("zyos")
+
+// JWTUser 如果想要保存更多信息,都可以添加到这个结构体中
+type JWTUser struct {
+ UID int `json:"uid"`
+ Username string `json:"username"`
+ Phone string `json:"phone"`
+ AppName string `json:"app_name"`
+ MiniOpenID string `json:"mini_open_id"` // 小程序的open_id
+ MiniSK string `json:"mini_session_key"` // 小程序的session_key
+ jwt.StandardClaims
+}
diff --git a/app/lib/mob/api.go b/app/lib/mob/api.go
new file mode 100644
index 0000000..fb6887b
--- /dev/null
+++ b/app/lib/mob/api.go
@@ -0,0 +1,297 @@
+package mob
+
+import (
+ "applet/app/db"
+ "applet/app/e"
+ "applet/app/lib/sms"
+ "applet/app/lib/zhimeng"
+ "applet/app/utils"
+ "applet/app/utils/logx"
+ "bytes"
+ "crypto/cipher"
+ "crypto/des"
+ "crypto/md5"
+ "encoding/base64"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "sort"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/tidwall/gjson"
+)
+
+const base string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
+
+// Mob is mob sdk
+var Mob *SDK
+
+// MobMap is 每个站长都要有自己的mob 对象
+var MobMap map[string]*SDK
+
+//Init 初始化
+func Init() {
+ // 后续可能要传请求的上下文来获取对应的配置
+ // mob 目前都是我们来管理每个站长的app 所以使用template 库
+ //fmt.Println("Mob SDK init ....")
+ ch := make(chan struct{}) // 只是做信号标志的话 空struct 更省点资源
+ MobMap = make(map[string]*SDK)
+ // 初始化
+ for k, e := range db.DBs {
+ m := db.SysCfgGetWithDb(e, k, "third_app_push_set")
+ if m == "" {
+ fmt.Printf("masterid:%s 找不到推送配置", k)
+ continue
+ }
+ key := gjson.Get(m, "mobAppKey").String()
+ secret := gjson.Get(m, "mobAppSecret").String()
+ if key == "" || secret == "" {
+ fmt.Println(k + ":mob no config")
+ continue
+ }
+ // fmt.Println(k, key, secret)
+ mob := new(SDK)
+ mob.AppKey = key
+ mob.AppSecret = secret
+ MobMap[k] = mob
+ fmt.Println(k + ":mob config success")
+ }
+ go func() {
+ ch <- struct{}{}
+ }()
+
+ // 定时任务
+ go func(MobMap map[string]*SDK, ch chan struct{}) {
+ <-ch
+ ticker := time.NewTicker(time.Duration(time.Second * 15))
+ //每 15s 一次更新一次mob 配置
+ for range ticker.C {
+ for k, e := range db.DBs {
+ if err := e.Ping(); err != nil {
+ logx.Info(err)
+ continue
+ }
+ m := db.SysCfgGetWithDb(e, k, "third_app_push_set")
+ if m == "" {
+ fmt.Printf("masterid:%s 找不到推送配置", k)
+ continue
+ }
+ key := gjson.Get(m, "mobAppKey").String()
+ secret := gjson.Get(m, "mobAppSecret").String()
+ if key == "" || secret == "" {
+ fmt.Println(k + ":mob no config")
+ continue
+ }
+ // fmt.Println(k, key, secret)
+ mob := new(SDK)
+ mob.AppKey = key
+ mob.AppSecret = secret
+ MobMap[k] = mob
+ // fmt.Println(k + ":mob config success")
+ }
+ }
+ }(MobMap, ch)
+}
+
+// GetMobSDK is 获取mob 的sdk
+func GetMobSDK(mid string) (*SDK, error) {
+ selectDB := db.DBs[mid]
+ m := db.SysCfgGetWithDb(selectDB, mid, "third_app_push_set")
+ if m == "" {
+ return nil, errors.New("获取不到推送配置")
+ }
+ key := gjson.Get(m, "mobAppKey").String()
+ secret := gjson.Get(m, "mobAppSecret").String()
+ if key == "" || secret == "" {
+ return nil, fmt.Errorf("%s mob not config", mid)
+ }
+
+ return &SDK{AppKey: key, AppSecret: secret}, nil
+}
+
+// SDK is mob_push 的sdk
+type SDK struct {
+ AppKey string
+ AppSecret string
+}
+
+//MobFreeLogin is 秒验
+func (s *SDK) MobFreeLogin(args map[string]interface{}) (string, error) {
+ var url string = "http://identify.verify.mob.com/auth/auth/sdkClientFreeLogin"
+ // https://www.mob.com/wiki/detailed/?wiki=miaoyan_for_fuwuduan_mianmifuwuduanjieru&id=78
+ //加appkey
+ args["appkey"] = s.AppKey
+ //加签名
+ args["sign"] = generateSign(args, s.AppSecret)
+ b, err := json.Marshal(args)
+ if err != nil {
+ return "", logx.Warn(err)
+ }
+ // 发送请求
+ respBody, err := httpPostBody(url, b)
+ if err != nil {
+ return "", logx.Warn(err)
+ }
+ // 反序列化
+ ret := struct {
+ Status int `json:"status"`
+ Error string `json:"error"`
+ Res interface{} `json:"res"`
+ }{}
+ // 要拿 ret 里面 Res 再解密
+ if err := json.Unmarshal(respBody, &ret); err != nil {
+ return "", logx.Warn(err)
+ }
+ //fmt.Println(ret)
+ // ret里面的Res 反序列化为结构体
+ res := struct {
+ IsValid int `json:"isValid"`
+ Phone string `json:"phone"`
+ }{}
+ // 判断是否返回正确 状态码
+ if ret.Status == 200 {
+ decode, _ := base64Decode([]byte(ret.Res.(string)))
+ decr, _ := desDecrypt(decode, []byte(s.AppSecret)[0:8])
+ if err := json.Unmarshal(decr, &res); err != nil {
+ return "", logx.Warn(err)
+ }
+ }
+ // 有效则拿出res 里的电话号码
+ if res.IsValid == 1 {
+ return res.Phone, nil
+ }
+ // Status 不等于200 则返回空
+ return "", fmt.Errorf("Mob error , status code %v ", ret.Status)
+}
+
+// MobSMS is mob 的短信验证
+func (s *SDK) MobSMS(c *gin.Context, args map[string]interface{}) (bool, error) {
+ // mob 的短信验证
+ // https://www.mob.com/wiki/detailed/?wiki=SMSSDK_for_yanzhengmafuwuduanxiaoyanjiekou&id=23
+ url := "https://webapi.sms.mob.com/sms/verify"
+ //加appkey
+ args["appkey"] = s.AppKey
+ fmt.Println(args)
+ //fmt.Println(args)
+ // 发送请求
+ respBody, err := utils.CurlPost(url, args, nil)
+ if err != nil {
+ fmt.Println(err)
+ return false, logx.Warn(err)
+ }
+ fmt.Println("=======================mob")
+ fmt.Println("mob", string(respBody))
+ code := gjson.GetBytes(respBody, "status").Int()
+ if code == 468 {
+ return false, errors.New("验证码错误")
+ }
+ if code != 200 {
+ utils.FilePutContents("sms", string(respBody))
+ return false, errors.New("验证码错误~")
+ }
+
+ if c.GetString("not_deduction_doing") == "1" { //这是前面扣过了
+ return true, nil
+ }
+ // TODO 成功后扣费暂时先接旧智盟
+ sdk, err := sms.NewZhimengSMS(c).SelectFunction("deduction_doing").WithSMSArgs(map[string]interface{}{
+ "mobile": args["phone"],
+ "getmsg": "1",
+ }).Result()
+ if err != nil {
+ return false, logx.Warn(err)
+ }
+ zr := sdk.ToInterface().(string)
+ if zr == "1" {
+ logx.Infof("旧智盟扣费成功 appkey %s", zhimeng.SMS_APP_KEY)
+ }
+ return true, nil
+}
+
+func pkcs5UnPadding(origData []byte) []byte {
+ length := len(origData)
+ // 去掉最后一个字节 unpadding 次
+ unpadding := int(origData[length-1])
+ return origData[:(length - unpadding)]
+}
+
+func desDecrypt(crypted, key []byte) ([]byte, error) {
+ block, err := des.NewCipher(key)
+ if err != nil {
+ return nil, err
+ }
+ blockMode := cipher.NewCBCDecrypter(block, []byte("00000000"))
+ origData := make([]byte, len(crypted))
+ // origData := crypted
+ blockMode.CryptBlocks(origData, crypted)
+ origData = pkcs5UnPadding(origData)
+ // origData = ZeroUnPadding(origData)
+ return origData, nil
+}
+
+func base64Decode(src []byte) ([]byte, error) {
+ var coder *base64.Encoding
+ coder = base64.NewEncoding(base)
+ return coder.DecodeString(string(src))
+}
+
+func httpPostBody(url string, msg []byte) ([]byte, error) {
+ resp, err := http.Post(url, "application/json;charset=utf-8", bytes.NewBuffer(msg))
+ if err != nil {
+ return []byte(""), err
+ }
+ defer resp.Body.Close()
+ body, err := ioutil.ReadAll(resp.Body)
+ return body, err
+}
+
+func generateSign(request map[string]interface{}, secret string) string {
+ ret := ""
+ var keys []string
+ for k := range request {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ for _, k := range keys {
+ ret = ret + fmt.Sprintf("%v=%v&", k, request[k])
+ }
+ ret = ret[:len(ret)-1] + secret
+
+ md5Ctx := md5.New()
+ md5Ctx.Write([]byte(ret))
+ cipherStr := md5Ctx.Sum(nil)
+ return hex.EncodeToString(cipherStr)
+}
+
+func Check(c *gin.Context, phone, zone, validCode string, ok1 bool) (bool, error) {
+ smsPlatform := sms.GetSmsPlatform(c)
+
+ if smsPlatform == "mob" {
+ mob1, errr := GetMobSDK(c.GetString("mid"))
+ if errr != nil {
+ return false, e.NewErr(400, "mob配置错误")
+ }
+ send := map[string]interface{}{
+ "phone": phone,
+ "zone": zone,
+ "code": validCode,
+ }
+ if zone == "" {
+ send["zone"] = "86"
+ }
+ c.Set("not_deduction_doing", "1")
+ ok, err := mob1.MobSMS(c, send)
+ fmt.Println(ok)
+ if err != nil {
+ fmt.Println(err)
+ return false, e.NewErr(400, "验证码校验错误")
+ }
+
+ return ok, nil
+ }
+ return ok1, nil
+}
diff --git a/app/lib/mob/main.go b/app/lib/mob/main.go
new file mode 100644
index 0000000..e76b515
--- /dev/null
+++ b/app/lib/mob/main.go
@@ -0,0 +1,15 @@
+package mob
+
+import "applet/app/svc"
+
+//NewMobSDK 构建一个Mobsdk对象
+func NewMobSDK() *SDK {
+ // 后续可能要传请求的上下文来获取对应的配置
+ // mob 目前都是我们来管理每个站长的app 所以使用template 库
+ key := svc.SysCfgGet(nil, "third_mob_app_key")
+ secret := svc.SysCfgGet(nil, "third_mob_app_secret")
+ mob := new(SDK)
+ mob.AppKey = key
+ mob.AppSecret = secret
+ return mob
+}
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..36c27ae
--- /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/sms/sms.go b/app/lib/sms/sms.go
new file mode 100644
index 0000000..f10dafd
--- /dev/null
+++ b/app/lib/sms/sms.go
@@ -0,0 +1,122 @@
+package sms
+
+import (
+ "applet/app/db"
+ "applet/app/lib/zhimeng"
+ "applet/app/svc"
+ "applet/app/utils/cache"
+ "applet/app/utils/logx"
+ "code.fnuoos.com/go_rely_warehouse/zyos_go_third_party_api.git/sms"
+ "errors"
+ "fmt"
+ "github.com/tidwall/gjson"
+
+ "github.com/gin-gonic/gin"
+)
+
+// NewZhimengSMS is 智盟的短信服务
+func NewZhimengSMS(c *gin.Context) *zhimeng.SDK {
+ sms := new(zhimeng.SDK)
+ key := svc.SysCfgGet(c, "third_zm_sms_key")
+ secret := svc.SysCfgGet(c, "third_zm_sms_secret")
+ if key == "" || secret == "" {
+ _ = logx.Warn("短信服务配置错误")
+ }
+ sms.Init("send_msg", key, secret)
+ return sms
+}
+func GetSmsPlatform(c *gin.Context) string {
+ var smsPlatform = "ljioe"
+ key := fmt.Sprintf("%s:sms_platform", c.GetString("mid"))
+ smsPlatformTmp, _ := cache.GetString(key)
+ if smsPlatformTmp == "" {
+ smsPlatformTmp = svc.GetWebSiteAppSmsPlatform(c.GetString("mid"))
+ if smsPlatformTmp != "" {
+ cache.SetEx(key, smsPlatformTmp, 300)
+ }
+ }
+ if smsPlatformTmp != "" {
+ smsPlatform = smsPlatformTmp
+ }
+ return smsPlatform
+}
+func GetTplId(c *gin.Context, zone, types string) string {
+ // 校验短信验证码
+ tplId := ""
+ if zone != "86" {
+ tplId = svc.SysCfgGet(c, "mob_sms_sdk_international_template_id")
+ } else {
+ tplId = svc.SysCfgGet(c, "mob_sms_sdk_template_id")
+ }
+ if c.GetString("app_type") == "o2o" {
+ tplId = db.SysCfgGet(c, "biz_mob_sms_sdk_template_id")
+ }
+ normal := gjson.Get(tplId, types).String()
+ if normal == "" {
+ normal = gjson.Get(tplId, "normal").String()
+ }
+ return normal
+}
+func GetSmsConfig(c *gin.Context, zone string, postData map[string]interface{}) error {
+ m := db.SysCfgGet(c, "third_app_push_set")
+ if c.GetString("app_type") == "o2o" {
+ m = db.SysCfgGet(c, "biz_third_app_push_set")
+ }
+ key := gjson.Get(m, "mobAppKey").String()
+ postData["is_mob"] = "1"
+ postData["type"] = "mob"
+ postData["sms_type"] = "putong"
+ smsPlatform := GetSmsPlatform(c)
+ if smsPlatform == "ljioe" {
+ postData["is_mob"] = "0"
+ postData["type"] = ""
+ }
+ token := c.GetHeader("Authorization")
+ if zone == "" && token != "" {
+ arkToken, _ := db.UserProfileFindByArkToken(svc.MasterDb(c), token)
+ if arkToken != nil && arkToken.Uid > 0 {
+ user, _ := db.UserFindByID(svc.MasterDb(c), arkToken.Uid)
+ if user != nil && user.Uid > 0 {
+ zone = user.Zone
+ }
+ }
+ }
+ if zone == "" {
+ zone = "86"
+ }
+ if zone != "86" { //国际短信
+ postData["is_sales"] = "2"
+ postData["sms_type"] = "international"
+ }
+ postData["templateCode"] = GetTplId(c, zone, postData["templateCode"].(string))
+ postData["zone"] = zone
+ if key != "" {
+ postData["smsmsg_key"] = key
+ }
+ if c.GetString("sms_type") == "1" { //新的
+ postData["uid"] = c.GetString("mid")
+ err := sms.SmsSend(db.Db, postData)
+ if err != nil {
+ return err
+ }
+ return nil
+ }
+ fmt.Println("===短信", postData, c.ClientIP())
+ sdk, err := NewZhimengSMS(c).SelectFunction("msg_doing").WithSMSArgs(postData).Result()
+ if err != nil {
+ msg := gjson.Get(err.Error(), "msg").String()
+ if msg == "" {
+ msg = err.Error()
+ }
+ fmt.Println("===短信", err)
+ errs := errors.New(msg)
+ return errs
+ }
+ rmap := sdk.ToInterface().(map[string]interface{})
+ fmt.Println("===短信", rmap)
+
+ if rmap["status"] == "" {
+ return err
+ }
+ return nil
+}
diff --git a/app/lib/weapp/LICENSE b/app/lib/weapp/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/app/lib/weapp/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/app/lib/weapp/Readme.md b/app/lib/weapp/Readme.md
new file mode 100644
index 0000000..c0dfa9b
--- /dev/null
+++ b/app/lib/weapp/Readme.md
@@ -0,0 +1,2992 @@
+# ![title](title.png)
+
+## `注意` ⚠️
+
+- [v1 版本入口](https://github.com/medivhzhan/weapp/tree/v1)
+- 新版本暂时不包含支付相关内容, 已有很多优秀的支付相关模块;
+- 为了保证大家及时用上新功能,已发布 v2 版本,请大家使用经过`线上测试` ✅ 的接口。
+- 未完成的接口将在经过线上测试后在新版本中提供给大家。
+- 大部分接口需要去线上测试。最近一直比较忙,有条件的朋友可以帮忙一起测试,我代表所有使用者谢谢你: )
+- 欢迎大家一起完善 :)
+
+## 获取代码
+
+```sh
+
+go get -u github.com/medivhzhan/weapp/v2
+
+```
+
+## `目录`
+
+> 文档按照[小程序服务端官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/)排版,方便您一一对照查找相关内容。
+
+✅: 代表已经通过线上测试
+⚠️: 代表还没有或者未完成
+
+- [登录](#登录)
+ - [code2Session](#code2Session) ✅
+- [用户信息](#用户信息)
+ - [getPaidUnionId](#getPaidUnionId) ✅
+- [接口调用凭证](#接口调用凭证)
+ - [getAccessToken](#getAccessToken) ✅
+- [数据分析](#数据分析)
+ - [访问留存](#访问留存)
+ - [getDailyRetain](#getDailyRetain) ✅
+ - [getWeeklyRetain](#getWeeklyRetain) ✅
+ - [getMonthlyRetain](#getMonthlyRetain) ✅
+ - [getDailySummary](#getDailySummary) ✅
+ - [访问趋势](#访问趋势)
+ - [getDailyVisitTrend](#getDailyVisitTrend) ✅
+ - [getWeeklyVisitTrend](#getWeeklyVisitTrend) ✅
+ - [getMonthlyVisitTrend](#getMonthlyVisitTrend) ✅
+ - [getUserPortrait](#getUserPortrait) ✅
+ - [getVisitDistribution](#getVisitDistribution) ✅
+ - [getVisitPage](#getVisitPage) ✅
+- [客服消息](#客服消息)
+ - [getTempMedia](#getTempMedia) ✅
+ - [sendCustomerServiceMessage](#sendCustomerServiceMessage) ✅
+ - [setTyping](#setTyping) ✅
+ - [uploadTempMedia](#uploadTempMedia) ✅
+- [统一服务消息](#统一服务消息)
+ - [sendUniformMessage](#sendUniformMessage) ✅
+- [动态消息](#动态消息)
+ - [createActivityId](#createActivityId)
+ - [setUpdatableMsg](#setUpdatableMsg)
+- [插件管理](#插件管理)
+ - [applyPlugin](#applyPlugin)
+ - [getPluginDevApplyList](#getPluginDevApplyList)
+ - [getPluginList](#getPluginList)
+ - [setDevPluginApplyStatus](#setDevPluginApplyStatus)
+ - [unbindPlugin](#unbindPlugin)
+- [附近的小程序](#附近的小程序)
+ - [addNearbyPoi](#addNearbyPoi)
+ - [deleteNearbyPoi](#deleteNearbyPoi)
+ - [getNearbyPoiList](#getNearbyPoiList)
+ - [setNearbyPoiShowStatus](#setNearbyPoiShowStatus)
+- [小程序码](#小程序码) ✅
+ - [createQRCode](#createQRCode) ✅
+ - [get](#get) ✅
+ - [getUnlimited](#getUnlimited) ✅
+- [内容安全](#内容安全)
+ - [imgSecCheck](#imgSecCheck) ✅
+ - [mediaCheckAsync](#mediaCheckAsync)✅
+ - [msgSecCheck](#msgSecCheck) ✅
+- [图像处理](#图像处理)
+ - [aiCrop](#aiCrop) ✅
+ - [scanQRCode](#scanQRCode) ✅
+ - [superResolution](#superResolution)
+- [及时配送](#及时配送) ⚠️
+ - [小程序使用](#小程序使用)
+ - [abnormalConfirm](#abnormalConfirm)
+ - [addDeliveryOrder](#addDeliveryOrder)
+ - [addDeliveryTip](#addDeliveryTip)
+ - [cancelDeliveryOrder](#cancelDeliveryOrder)
+ - [getAllImmediateDelivery](#getAllImmediateDelivery)
+ - [getBindAccount](#getBindAccount)
+ - [getDeliveryOrder](#getDeliveryOrder)
+ - [mockUpdateDeliveryOrder](#mockUpdateDeliveryOrder)
+ - [onDeliveryOrderStatus](#onDeliveryOrderStatus)
+ - [preAddDeliveryOrder](#preAddDeliveryOrder)
+ - [preCancelDeliveryOrder](#preCancelDeliveryOrder)
+ - [reDeliveryOrder](#reDeliveryOrder)
+ - [服务提供方使用](#服务提供方使用)
+ - [updateDeliveryOrder](#updateDeliveryOrder)
+ - [onAgentPosQuery](#onAgentPosQuery)
+ - [onAuthInfoGet](#onAuthInfoGet)
+ - [onCancelAuth](#onCancelAuth)
+ - [onDeliveryOrderAdd](#onDeliveryOrderAdd)
+ - [onDeliveryOrderAddTips](#onDeliveryOrderAddTips)
+ - [onDeliveryOrderCancel](#onDeliveryOrderCancel)
+ - [onDeliveryOrderConfirmReturn](#onDeliveryOrderConfirmReturn)
+ - [onDeliveryOrderPreAdd](#onDeliveryOrderPreAdd)
+ - [onDeliveryOrderPreCancel](#onDeliveryOrderPreCancel)
+ - [onDeliveryOrderQuery](#onDeliveryOrderQuery)
+ - [onDeliveryOrderReAdd](#onDeliveryOrderReAdd)
+ - [onPreAuthCodeGet](#onPreAuthCodeGet)
+ - [onRiderScoreSet](#onRiderScoreSet)
+- [物流助手](#物流助手) ⚠️
+ - [小程序使用](#小程序使用)
+ - [addExpressOrder](#addExpressOrder)
+ - [cancelExpressOrder](#cancelExpressOrder)
+ - [getAllDelivery](#getAllDelivery)
+ - [getExpressOrder](#getExpressOrder)
+ - [getExpressPath](#getExpressPath)
+ - [getExpressPrinter](#getExpressPrinter)
+ - [getExpressQuota](#getExpressQuota)
+ - [onExpressPathUpdate](#onExpressPathUpdate)
+ - [testUpdateExpressOrder](#testUpdateExpressOrder)
+ - [updateExpressPrinter](#updateExpressPrinter)
+ - [服务提供方使用](#服务提供方使用)
+ - [getExpressContact](#getExpressContact)
+ - [onAddExpressOrder](#onAddExpressOrder)
+ - [onCancelExpressOrder](#onCancelExpressOrder)
+ - [onCheckExpressBusiness](#onCheckExpressBusiness)
+ - [onGetExpressQuota](#onGetExpressQuota)
+ - [previewExpressTemplate](#previewExpressTemplate)
+ - [updateExpressBusiness](#updateExpressBusiness)
+ - [updateExpressPath](#updateExpressPath)
+- [OCR](#OCR)
+ - [bankcard](#bankcard) ✅
+ - [businessLicense](#businessLicense) ✅
+ - [driverLicense](#driverLicense) ✅
+ - [idcard](#idcard) ✅
+ - [printedText](#printedText) ✅
+ - [vehicleLicense](#vehicleLicense) ✅
+- [运维中心](#运维中心) ⚠️
+ - [realTimeLogSearch](#realTimeLogSearch)
+- [小程序搜索](#小程序搜索) ⚠️
+ - [siteSearch](#siteSearch)
+ - [submitPages](#submitPages)
+- [生物认证](#生物认证)
+ - [verifySignature](#verifySignature)
+- [订阅消息](#订阅消息) ✅
+ - [addTemplate](#addTemplate) ✅
+ - [deleteTemplate](#deleteTemplate) ✅
+ - [getCategory](#getCategory) ✅
+ - [getPubTemplateKeyWordsById](#getPubTemplateKeyWordsById)✅
+ - [getPubTemplateTitleList](#getPubTemplateTitleList) ✅
+ - [getTemplateList](#getTemplateList) ✅
+ - [sendSubscribeMessage](#sendSubscribeMessage) ✅
+- [解密](#解密)
+ - [解密手机号码](#解密手机号码) ✅
+ - [解密分享内容](#解密分享内容)
+ - [解密用户信息](#解密用户信息) ✅
+ - [解密微信运动](#解密微信运动)
+- [人脸识别](#人脸识别)
+
+---
+
+## 登录
+
+### code2Session
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.Login("appid", "secret", "code")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+---
+
+## 用户信息
+
+### getPaidUnionId
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/user-info/auth.getPaidUnionId.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.GetPaidUnionID("access-token", "open-id", "transaction-id")
+// 或者
+res, err := weapp.GetPaidUnionIDWithMCH("access-token", "open-id", "out-trade-number", "mch-id")
+
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+---
+
+## 接口调用凭证
+
+### getAccessToken
+
+> 调用次数有限制 请注意缓存
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.GetAccessToken("appid", "secret")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+---
+
+## 数据分析
+
+### 访问留存
+
+#### getDailyRetain
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/data-analysis/visit-retain/analysis.getDailyRetain.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.GetDailyRetain("access-token", "begin-date", "end-date")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### getWeeklyRetain
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/data-analysis/visit-retain/analysis.getWeeklyRetain.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.GetWeeklyRetain("access-token", "begin-date", "end-date")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### getMonthlyRetain
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/data-analysis/visit-retain/analysis.getMonthlyRetain.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.GetMonthlyRetain("access-token", "begin-date", "end-date")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+### getDailySummary
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/data-analysis/analysis.getDailySummary.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.GetDailySummary("access-token", "begin-date", "end-date")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+### 访问趋势
+
+#### getDailyVisitTrend
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/data-analysis/visit-trend/analysis.getDailyVisitTrend.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.GetDailyVisitTrend("access-token", "begin-date", "end-date")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### getWeeklyVisitTrend
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/data-analysis/visit-trend/analysis.getWeeklyVisitTrend.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.GetWeeklyVisitTrend("access-token", "begin-date", "end-date")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### getMonthlyVisitTrend
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/data-analysis/visit-trend/analysis.getMonthlyVisitTrend.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.GetMonthlyVisitTrend("access-token", "begin-date", "end-date")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+### getUserPortrait
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/data-analysis/analysis.getUserPortrait.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.GetUserPortrait("access-token", "begin-date", "end-date")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+### getVisitDistribution
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/data-analysis/analysis.getVisitDistribution.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.GetVisitDistribution("access-token", "begin-date", "end-date")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+### getVisitPage
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/data-analysis/analysis.getVisitPage.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.GetVisitPage("access-token", "begin-date", "end-date")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+---
+
+## 客服消息
+
+### getTempMedia
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/customer-message/customerServiceMessage.getTempMedia.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+resp, res, err := weapp.GetTempMedia("access-token", "media-id")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+defer resp.Close()
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+### sendCustomerServiceMessage
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/customer-message/customerServiceMessage.send.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+// 接收并处理异步结果
+srv, err := weapp.NewServer("app-id", "token", "aes-key", "mch-id", "api-key", true)
+if err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+// 文本消息
+srv.OnCustomerServiceTextMessage(func(msg *weapp.TextMessageResult) *weapp.TransferCustomerMessage {
+
+ reply := weapp.CSMsgText{
+ Content: "content",
+ }
+
+ res, err := reply.SendTo("open-id", "access-token")
+ if err != nil {
+ // 处理一般错误信息
+ return nil
+ }
+
+ if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return nil
+ }
+
+ return nil
+})
+
+// 图片消息
+srv.OnCustomerServiceImageMessage(func(msg *weapp.TextMessageResult) *weapp.TransferCustomerMessage {
+
+ reply := weapp.CSMsgImage{
+ MediaID: "media-id",
+ }
+
+ res, err := reply.SendTo("open-id", "access-token")
+ if err != nil {
+ // 处理一般错误信息
+ return nil
+ }
+
+ if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return nil
+ }
+
+ return nil
+})
+
+// 小程序卡片消息
+srv.OnCustomerServiceCardMessage(func(msg *weapp.TextMessageResult) *weapp.TransferCustomerMessage {
+
+ reply := weapp.CSMsgMPCard{
+ Title: "title",
+ PagePath: "page-path",
+ ThumbMediaID: "thumb-media-id",
+ }
+ res, err := reply.SendTo("open-id", "access-token")
+ if err != nil {
+ // 处理一般错误信息
+ return nil
+ }
+
+ if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return nil
+ }
+
+ return nil
+})
+
+if err := srv.Serve(http.ResponseWriter, *http.Request); err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+```
+
+### setTyping
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/customer-message/customerServiceMessage.setTyping.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.SetTyping("access-token", "open-id", weapp.SetTypingCommandTyping)
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+### uploadTempMedia
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/customer-message/customerServiceMessage.uploadTempMedia.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.UploadTempMedia("access-token", weapp.TempMediaTypeImage, "media-filename")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+---
+
+## 统一服务消息
+
+### sendUniformMessage
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/uniform-message/uniformMessage.send.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+sender := weapp.UniformMsgSender{
+ ToUser: "open-id",
+ UniformWeappTmpMsg: weapp.UniformWeappTmpMsg{
+ TemplateID: "template-id",
+ Page: "page",
+ FormID: "form-id",
+ Data: weapp.UniformMsgData{
+ "keyword": {Value: "value"},
+ },
+ EmphasisKeyword: "keyword.DATA",
+ },
+ UniformMpTmpMsg: weapp.UniformMpTmpMsg{
+ AppID: "app-id",
+ TemplateID: "template-id",
+ URL: "url",
+ Miniprogram: weapp.UniformMsgMiniprogram{"miniprogram-app-id", "page-path"},
+ Data: weapp.UniformMsgData{
+ "keyword": {"value", "color"},
+ },
+ },
+}
+
+res, err := sender.Send("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+---
+
+## 动态消息
+
+### createActivityId
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/updatable-message/updatableMessage.createActivityId.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.CreateActivityId("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+### setUpdatableMsg
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/updatable-message/updatableMessage.setUpdatableMsg.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+
+setter := weapp.UpdatableMsgSetter{
+ "activity-id",
+ UpdatableMsgJoining,
+ UpdatableMsgTempInfo{
+ []UpdatableMsgParameter{
+ {UpdatableMsgParamMemberCount, "parameter-value-number"},
+ {UpdatableMsgParamRoomLimit, "parameter-value-number"},
+ },
+ },
+}
+
+res, err := setter.Set("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+---
+
+## 插件管理
+
+### applyPlugin
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/plugin-management/pluginManager.applyPlugin.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.ApplyPlugin("access-token", "plugin-app-id", "reason")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+### getPluginDevApplyList
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/plugin-management/pluginManager.getPluginDevApplyList.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.GetPluginDevApplyList("access-token", 1, 2)
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+### getPluginList
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/plugin-management/pluginManager.getPluginList.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.GetPluginList("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+### setDevPluginApplyStatus
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/plugin-management/pluginManager.setDevPluginApplyStatus.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.SetDevPluginApplyStatus("access-token", "plugin-app-id", "reason", weapp.DevAgree)
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+### unbindPlugin
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/plugin-management/pluginManager.unbindPlugin.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.UnbindPlugin("access-token", "plugin-app-id")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+---
+
+## 附近的小程序
+
+### addNearbyPoi
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/nearby-poi/nearbyPoi.add.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+poi := NearbyPoi{
+ PicList: PicList{[]string{"first-picture-url", "second-picture-url", "third-picture-url"}},
+ ServiceInfos: weapp.ServiceInfos{[]weapp.ServiceInfo{
+ {1, 1, "name", "app-id", "path"},
+ }},
+ StoreName: "store-name",
+ Hour: "11:11-12:12",
+ Credential: "credential",
+ Address: "address", // 地址 必填
+ CompanyName: "company-name", // 主体名字 必填
+ QualificationList: "qualification-list", // 证明材料 必填 如果company_name和该小程序主体不一致,需要填qualification_list,详细规则见附近的小程序使用指南-如何证明门店的经营主体跟公众号或小程序帐号主体相关http://kf.qq.com/faq/170401MbUnim17040122m2qY.html
+ KFInfo: weapp.KFInfo{true, "kf-head-img", "kf-name"}, // 客服信息 选填,可自定义服务头像与昵称,具体填写字段见下方示例kf_info pic_list是字符串,内容是一个json!
+ PoiID: "poi-id", // 如果创建新的门店,poi_id字段为空 如果更新门店,poi_id参数则填对应门店的poi_id 选填
+}
+
+res, err := poi.Add("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+srv, err := weapp.NewServer("app-id", "token", "aes-key", "mch-id", "api-key", true)
+if err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+srv.OnAddNearbyPoi(func(mix *weapp.AddNearbyPoiResult) {
+ // 处理返回结果
+})
+
+if err := srv.Serve(http.ResponseWriter, *http.Request); err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+```
+
+### deleteNearbyPoi
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/nearby-poi/nearbyPoi.delete.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.DeleteNearbyPoi("access-token", "poi-id")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+### getNearbyPoiList
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/nearby-poi/nearbyPoi.getList.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.GetNearbyPoiList("access-token", 1, 10)
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+### setNearbyPoiShowStatus
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/nearby-poi/nearbyPoi.setShowStatus.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.SetNearbyPoiShowStatus("access-token", "poi-id", weapp.ShowNearbyPoi)
+// 或者
+res, err := weapp.SetNearbyPoiShowStatus("access-token", "poi-id", weapp.HideNearbyPoi)
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+---
+
+## 小程序码
+
+### createQRCode
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.createQRCode.html)
+
+```go
+
+import (
+ "ioutil"
+ "github.com/medivhzhan/weapp/v2"
+)
+
+
+creator := weapp.QRCodeCreator{
+ Path: "mock/path",
+ Width: 430,
+}
+
+resp, res, err := creator.Create("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+defer resp.Body.Close()
+
+content, err := ioutil.ReadAll(resp.Body)
+// 处理图片内容
+
+```
+
+### get
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.get.html)
+
+```go
+
+import (
+ "ioutil"
+ "github.com/medivhzhan/weapp/v2"
+)
+
+
+getter := weapp.QRCode{
+ Path: "mock/path",
+ Width: 430,
+ AutoColor: true,
+ LineColor: weapp.Color{"r", "g", "b"},
+ IsHyaline: true,
+}
+
+resp, res, err := getter.Get("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+defer resp.Body.Close()
+
+content, err := ioutil.ReadAll(resp.Body)
+// 处理图片内容
+
+```
+
+### getUnlimited
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.getUnlimited.html)
+
+```go
+
+import (
+ "ioutil"
+ "github.com/medivhzhan/weapp/v2"
+)
+
+
+getter := weapp.UnlimitedQRCode{
+ Scene: "scene-data",
+ Page: "mock/page",
+ Width: 430,
+ AutoColor: true,
+ LineColor: weapp.Color{"r", "g", "b"},
+ IsHyaline: true,
+}
+
+resp, res, err := getter.Get("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+defer resp.Body.Close()
+
+content, err := ioutil.ReadAll(resp.Body)
+// 处理图片内容
+
+```
+
+---
+
+## 内容安全
+
+### imgSecCheck
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/sec-check/security.imgSecCheck.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.IMGSecCheck("access-token", "local-filename")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+### mediaCheckAsync
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/sec-check/security.mediaCheckAsync.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.MediaCheckAsync("access-token", "image-url", weapp.MediaTypeImage)
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+// 接收并处理异步结果
+srv, err := weapp.NewServer("app-id", "token", "aes-key", "mch-id", "api-key", true)
+if err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+srv.OnMediaCheckAsync(func(mix *weapp.MediaCheckAsyncResult) {
+ // 处理返回结果
+})
+
+if err := srv.Serve(http.ResponseWriter, *http.Request); err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+```
+
+### msgSecCheck
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/sec-check/security.msgSecCheck.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.MSGSecCheck("access-token", "message-content")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+---
+
+## 图像处理
+
+### aiCrop
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/img/img.aiCrop.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.AICrop("access-token", "filename")
+// 或者
+res, err := weapp.AICropByURL("access-token", "url")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+### scanQRCode
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/img/img.scanQRCode.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.ScanQRCode("access-token", "file-path")
+// 或者
+res, err := weapp.ScanQRCodeByURL("access-token", "qr-code-url")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+### superResolution
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/img/img.superresolution.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.SuperResolution("access-token", "file-path")
+// 或者
+res, err := weapp.SuperResolutionByURL("access-token", "img-url")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+---
+
+## 及时配送
+
+### 服务提供方使用
+
+#### updateDeliveryOrder
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-provider/immediateDelivery.updateOrder.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+mocker := weapp.DeliveryOrderUpdater{
+ // ...
+}
+
+res, err := mocker.Update("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### onAgentPosQuery
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-provider/immediateDelivery.onAgentPosQuery.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+srv, err := weapp.NewServer("app-id", "token", "aes-key", "mch-id", "api-key", true)
+if err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+srv.OnAgentPosQuery(func(mix *weapp.AgentPosQueryResult) *weapp.AgentPosQueryReturn {
+ // 处理返回结果
+
+ return &weapp.AgentPosQueryReturn{
+ // ...
+ }
+})
+
+if err := srv.Serve(http.ResponseWriter, *http.Request); err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+```
+
+#### onAuthInfoGet
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-provider/immediateDelivery.onAuthInfoGet.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+srv, err := weapp.NewServer("app-id", "token", "aes-key", "mch-id", "api-key", true)
+if err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+srv.OnAuthInfoGet(func(mix *weapp.AuthInfoGetResult) *weapp.AuthInfoGetReturn {
+ // 处理返回结果
+
+ return &weapp.AuthInfoGetReturn{
+ // ...
+ }
+})
+
+if err := srv.Serve(http.ResponseWriter, *http.Request); err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+```
+
+#### onCancelAuth
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-provider/immediateDelivery.onCancelAuth.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+srv, err := weapp.NewServer("app-id", "token", "aes-key", "mch-id", "api-key", true)
+if err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+srv.OnCancelAuth(func(mix *weapp.CancelAuthResult) *weapp.CancelAuthReturn {
+ // 处理返回结果
+
+ return &weapp.CancelAuthReturn{
+ // ...
+ }
+})
+
+if err := srv.Serve(http.ResponseWriter, *http.Request); err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+```
+
+#### onDeliveryOrderAdd
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-provider/immediateDelivery.onOrderAdd.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+srv, err := weapp.NewServer("app-id", "token", "aes-key", "mch-id", "api-key", true)
+if err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+srv.OnDeliveryOrderAdd(func(mix *weapp.DeliveryOrderAddResult) *weapp.DeliveryOrderAddReturn {
+ // 处理返回结果
+
+ return &weapp.DeliveryOrderAddReturn{
+ // ...
+ }
+})
+
+if err := srv.Serve(http.ResponseWriter, *http.Request); err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+```
+
+#### onDeliveryOrderAddTips
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-provider/immediateDelivery.onOrderAddTips.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+srv, err := weapp.NewServer("app-id", "token", "aes-key", "mch-id", "api-key", true)
+if err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+srv.OnDeliveryOrderAddTips(func(mix *weapp.DeliveryOrderAddTipsResult) *weapp.DeliveryOrderAddTipsReturn {
+ // 处理返回结果
+
+ return &weapp.DeliveryOrderAddTipsReturn{
+ // ...
+ }
+})
+
+if err := srv.Serve(http.ResponseWriter, *http.Request); err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+```
+
+#### onDeliveryOrderCancel
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-provider/immediateDelivery.onOrderCancel.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+srv, err := weapp.NewServer("app-id", "token", "aes-key", "mch-id", "api-key", true)
+if err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+srv.OnDeliveryOrderCancel(func(mix *weapp.DeliveryOrderCancelResult) *weapp.DeliveryOrderCancelReturn {
+ // 处理返回结果
+
+ return &weapp.DeliveryOrderCancelReturn{
+ // ...
+ }
+})
+
+if err := srv.Serve(http.ResponseWriter, *http.Request); err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+```
+
+#### onDeliveryOrderConfirmReturn
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-provider/immediateDelivery.onOrderConfirmReturn.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+srv, err := weapp.NewServer("app-id", "token", "aes-key", "mch-id", "api-key", true)
+if err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+srv.OnDeliveryOrderReturnConfirm(func(mix *weapp.DeliveryOrderReturnConfirmResult) *weapp.DeliveryOrderReturnConfirmReturn {
+ // 处理返回结果
+
+ return &weapp.DeliveryOrderReturnConfirmReturn{
+ // ...
+ }
+})
+
+if err := srv.Serve(http.ResponseWriter, *http.Request); err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+```
+
+#### onDeliveryOrderPreAdd
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-provider/immediateDelivery.onOrderPreAdd.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+srv, err := weapp.NewServer("app-id", "token", "aes-key", "mch-id", "api-key", true)
+if err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+srv.OnDeliveryOrderPreAdd(func(mix *weapp.DeliveryOrderPreAddResult) *weapp.DeliveryOrderPreAddReturn {
+ // 处理返回结果
+
+ return &weapp.DeliveryOrderPreAddReturn{
+ // ...
+ }
+})
+
+if err := srv.Serve(http.ResponseWriter, *http.Request); err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+```
+
+#### onDeliveryOrderPreCancel
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-provider/immediateDelivery.onOrderPreCancel.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+srv, err := weapp.NewServer("app-id", "token", "aes-key", "mch-id", "api-key", true)
+if err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+srv.OnDeliveryOrderPreCancel(func(mix *weapp.DeliveryOrderPreCancelResult) *weapp.DeliveryOrderPreCancelReturn {
+ // 处理返回结果
+
+ return &weapp.DeliveryOrderPreCancelReturn{
+ // ...
+ }
+})
+
+if err := srv.Serve(http.ResponseWriter, *http.Request); err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+```
+
+#### onDeliveryOrderQuery
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-provider/immediateDelivery.onOrderQuery.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+srv, err := weapp.NewServer("app-id", "token", "aes-key", "mch-id", "api-key", true)
+if err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+srv.OnDeliveryOrderQuery(func(mix *weapp.DeliveryOrderQueryResult) *weapp.DeliveryOrderQueryReturn {
+ // 处理返回结果
+
+ return &weapp.DeliveryOrderQueryReturn{
+ // ...
+ }
+})
+
+if err := srv.Serve(http.ResponseWriter, *http.Request); err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+```
+
+#### onDeliveryOrderReAdd
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-provider/immediateDelivery.onOrderReAdd.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+srv, err := weapp.NewServer("app-id", "token", "aes-key", "mch-id", "api-key", true)
+if err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+srv.OnDeliveryOrderReadd(func(mix *weapp.DeliveryOrderReaddResult) *weapp.DeliveryOrderReaddReturn {
+ // 处理返回结果
+
+ return &weapp.DeliveryOrderReaddReturn{
+ // ...
+ }
+})
+
+if err := srv.Serve(http.ResponseWriter, *http.Request); err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+```
+
+#### onPreAuthCodeGet
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-provider/immediateDelivery.onPreAuthCodeGet.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+srv, err := weapp.NewServer("app-id", "token", "aes-key", "mch-id", "api-key", true)
+if err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+srv.OnPreAuthCodeGet(func(mix *weapp.PreAuthCodeGetResult) *weapp.PreAuthCodeGetReturn {
+ // 处理返回结果
+
+ return &weapp.PreAuthCodeGetReturn{
+ // ...
+ }
+})
+
+if err := srv.Serve(http.ResponseWriter, *http.Request); err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+```
+
+#### onRiderScoreSet
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-provider/immediateDelivery.onRiderScoreSet.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+srv, err := weapp.NewServer("app-id", "token", "aes-key", "mch-id", "api-key", true)
+if err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+srv.OnRiderScoreSet(func(mix *weapp.RiderScoreSetResult) *weapp.RiderScoreSetReturn {
+ // 处理返回结果
+
+ return &weapp.PreAuthCodeGetReturn{
+ // ...
+ }
+})
+
+if err := srv.Serve(http.ResponseWriter, *http.Request); err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+```
+
+### 小程序使用
+
+#### abnormalConfirm
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-business/immediateDelivery.abnormalConfirm.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+confirmer := weapp.AbnormalConfirmer{
+ ShopID: "123456",
+ ShopOrderID: "123456",
+ ShopNo: "shop_no_111",
+ WaybillID: "123456",
+ Remark: "remark",
+ DeliverySign: "123456",
+}
+
+res, err := confirmer.Confirm("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### addDeliveryOrder
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-business/immediateDelivery.addOrder.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+creator := weapp.DeliveryOrderCreator{
+ // ...
+}
+
+res, err := creator.Create("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### addDeliveryTip
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-business/immediateDelivery.addTip.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+adder := weapp.DeliveryTipAdder{
+ // ...
+}
+
+res, err := adder.Add("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### cancelDeliveryOrder
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-business/immediateDelivery.cancelOrder.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+canceler := weapp.DeliveryOrderCanceler{
+ // ...
+}
+
+res, err := canceler.Cancel("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### getAllImmediateDelivery
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-business/immediateDelivery.getAllImmeDelivery.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.GetAllImmediateDelivery("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### getBindAccount
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-business/immediateDelivery.getBindAccount.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.GetBindAccount("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### getDeliveryOrder
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-business/immediateDelivery.getOrder.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+getter := weapp.DeliveryOrderGetter{
+ // ...
+}
+
+res, err := getter.Get("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### mockUpdateDeliveryOrder
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-business/immediateDelivery.mockUpdateOrder.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+mocker := weapp.UpdateDeliveryOrderMocker{
+ // ...
+}
+
+res, err := mocker.Mock("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### onDeliveryOrderStatus
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-business/immediateDelivery.onOrderStatus.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+srv, err := weapp.NewServer("app-id", "token", "aes-key", "mch-id", "api-key", true)
+if err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+srv.OnDeliveryOrderStatusUpdate(func(mix *weapp.DeliveryOrderStatusUpdateResult) *weapp.DeliveryOrderStatusUpdateReturn {
+ // 处理返回结果
+
+ return &weapp.DeliveryOrderStatusUpdateReturn{
+ // ...
+ }
+})
+
+if err := srv.Serve(http.ResponseWriter, *http.Request); err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+```
+
+#### preAddDeliveryOrder
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-business/immediateDelivery.preAddOrder.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+creator := weapp.DeliveryOrderCreator{
+ // ...
+}
+
+res, err := creator.Prepare("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### preCancelDeliveryOrder
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-business/immediateDelivery.preCancelOrder.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+canceler := weapp.DeliveryOrderCanceler{
+ // ...
+}
+
+res, err := canceler.Prepare("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### reDeliveryOrder
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/immediate-delivery/by-business/immediateDelivery.reOrder.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+creator := weapp.DeliveryOrderCreator{
+ // ...
+}
+
+res, err := creator.Recreate("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+---
+
+## 物流助手
+
+### 小程序使用
+
+#### addExpressOrder
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.addOrder.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+creator := weapp.ExpressOrderCreator{
+ // ...
+}
+
+res, err := creator.Create("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### cancelExpressOrder
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.cancelOrder.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+canceler := weapp.ExpressOrderCanceler{
+ // ...
+}
+
+res, err := canceler.cancel("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### getAllDelivery
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.getAllDelivery.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.getAllDelivery("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### getExpressOrder
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.getOrder.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+getter := weapp.ExpressOrderGetter{
+ // ...
+}
+
+res, err := getter.Get("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### getExpressPath
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.getPath.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+getter := weapp.ExpressPathGetter{
+ // ...
+}
+
+res, err := getter.Get("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### getExpressPrinter
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.getPrinter.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.GetPrinter("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### getExpressQuota
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.getQuota.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+getter := weapp.QuotaGetter{
+ // ...
+}
+
+res, err := getter.Get("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### onExpressPathUpdate
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.onPathUpdate.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+srv, err := weapp.NewServer("app-id", "token", "aes-key", "mch-id", "api-key", true)
+if err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+srv.OnExpressPathUpdate(func(mix *weapp.ExpressPathUpdateResult) {
+ // 处理返回结果
+})
+
+if err := srv.Serve(http.ResponseWriter, *http.Request); err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+```
+
+#### testUpdateExpressOrder
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.testUpdateOrder.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+tester := weapp.UpdateExpressOrderTester{
+ // ...
+}
+
+res, err := tester.Test("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### updateExpressPrinter
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.updatePrinter.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+updater := weapp.PrinterUpdater{
+ // ...
+}
+
+res, err := updater.Update("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+### 服务提供方使用
+
+#### getExpressContact
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-provider/logistics.getContact.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.GetContact("access-token", "token", "wat-bill-id")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### onAddExpressOrder
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-provider/logistics.onAddOrder.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+srv, err := weapp.NewServer("app-id", "token", "aes-key", "mch-id", "api-key", true)
+if err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+srv.OnAddExpressOrder(func(mix *weapp.AddExpressOrderResult) *weapp.AddExpressOrderReturn {
+ // 处理返回结果
+
+ return &weapp.AddExpressOrderReturn{
+ // ...
+ }
+})
+
+if err := srv.Serve(http.ResponseWriter, *http.Request); err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+```
+
+#### onCancelExpressOrder
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-provider/logistics.onCancelOrder.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+srv, err := weapp.NewServer("app-id", "token", "aes-key", "mch-id", "api-key", true)
+if err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+srv.OnCancelExpressOrder(func(mix *weapp.CancelExpressOrderResult) *weapp.CancelExpressOrderReturn {
+ // 处理返回结果
+
+ return &weapp.CancelExpressOrderReturn{
+ // ...
+ }
+})
+
+if err := srv.Serve(http.ResponseWriter, *http.Request); err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+```
+
+#### onCheckExpressBusiness
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-provider/logistics.onCheckBusiness.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+srv, err := weapp.NewServer("app-id", "token", "aes-key", "mch-id", "api-key", true)
+if err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+srv.OnCheckExpressBusiness(func(mix *weapp.CheckExpressBusinessResult) *weapp.CheckExpressBusinessReturn {
+ // 处理返回结果
+
+ return &weapp.CheckExpressBusinessReturn{
+ // ...
+ }
+})
+
+if err := srv.Serve(http.ResponseWriter, *http.Request); err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+```
+
+#### onGetExpressQuota
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-provider/logistics.onGetQuota.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+srv, err := weapp.NewServer("app-id", "token", "aes-key", "mch-id", "api-key", true)
+if err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+srv.OnGetExpressQuota(func(mix *weapp.GetExpressQuotaResult) *weapp.GetExpressQuotaReturn {
+ // 处理返回结果
+
+ return &weapp.GetExpressQuotaReturn{
+ // ...
+ }
+})
+
+if err := srv.Serve(http.ResponseWriter, *http.Request); err != nil {
+ // 处理微信返回错误信息
+ return
+}
+
+```
+
+#### previewExpressTemplate
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-provider/logistics.previewTemplate.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+previewer := weapp.ExpressTemplatePreviewer{
+ // ...
+}
+
+res, err := previewer.Preview("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### updateExpressBusiness
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-provider/logistics.updateBusiness.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+updater := weapp.BusinessUpdater{
+ // ...
+}
+
+res, err := updater.Update("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+#### updateExpressPath
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-provider/logistics.updatePath.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+updater := weapp.ExpressPathUpdater{
+ // ...
+}
+
+res, err := updater.Update("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+---
+
+## OCR
+
+### bankcard
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/ocr/ocr.bankcard.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.BankCard("access-token", "file-path", weapp.RecognizeModeScan)
+// 或者
+res, err := weapp.BankCardByURL("access-token", "card-url", weapp.RecognizeModePhoto)
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+### businessLicense
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/ocr/ocr.businessLicense.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.BusinessLicense("access-token", "file-path")
+// 或者
+res, err := weapp.BusinessLicenseByURL("access-token", "card-url")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+### driverLicense
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/ocr/ocr.driverLicense.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.DriverLicense("access-token", "file-path")
+// 或者
+res, err := weapp.DriverLicenseByURL("access-token", "card-url")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+### idcard
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/ocr/ocr.idcard.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.IDCardByURL("access-token", "card-url", weapp.RecognizeModePhoto)
+// 或者
+res, err := weapp.IDCard("access-token", "file-path", weapp.RecognizeModeScan)
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+### printedText
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/ocr/ocr.printedText.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.PrintedText("access-token", "file-path")
+// 或者
+res, err := weapp.PrintedTextByURL("access-token", "card-url")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+### vehicleLicense
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/ocr/ocr.vehicleLicense.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.VehicleLicense("access-token", "file-path", weapp.RecognizeModeScan)
+// 或者
+res, err := weapp.VehicleLicenseByURL("access-token", "card-url", weapp.RecognizeModePhoto)
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+---
+
+## 小程序搜索
+
+### siteSearch
+
+### submitPages
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/search/search.submitPages.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+sender := weapp.SearchSubmitPages{
+ []weapp.SearchSubmitPage{
+ {
+ Path: "pages/index/index",
+ Query: "id=test",
+ },
+ },
+}
+
+res, err := sender.Send("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+---
+
+## 生物认证
+
+### verifySignature
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/soter/soter.verifySignature.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.VerifySignature("access-token", "open-id", "data", "signature")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+---
+
+## 订阅消息
+
+### addTemplate
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.addTemplate.html)
+
+```go
+import "github.com/medivhzhan/weapp/v2"
+
+// AddTemplate 组合模板并添加至帐号下的个人模板库
+//
+// token 微信 access_token
+// tid 模板ID
+// desc 服务场景描述,15个字以内
+// keywordIDList 关键词 ID 列表
+res, err := weapp.AddTemplate("access_token", "tid", "desc", []int32{1, 2, 3})
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+fmt.Printf("返回结果: %#v", res)
+```
+
+### deleteTemplate
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.deleteTemplate.html)
+
+```go
+import "github.com/medivhzhan/weapp/v2"
+
+// DeleteTemplate 删除帐号下的某个模板
+//
+// token 微信 access_token
+// pid 模板ID
+res, err := weapp.DeleteTemplate("access_token", "pid")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+fmt.Printf("返回结果: %#v", res)
+```
+
+### getCategory
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.getCategory.html)
+
+```go
+import "github.com/medivhzhan/weapp/v2"
+
+// GetTemplateCategory 删除帐号下的某个模板
+//
+// token 微信 access_token
+res, err := weapp.GetTemplateCategory("access_token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+fmt.Printf("返回结果: %#v", res)
+```
+
+### getPubTemplateKeyWordsById
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.getPubTemplateKeyWordsById.html)
+
+```go
+import "github.com/medivhzhan/weapp/v2"
+
+// GetPubTemplateKeyWordsById 获取模板标题下的关键词列表
+//
+// token 微信 access_token
+// tid 模板ID
+res, err := weapp.GetPubTemplateKeyWordsById("access_token", "tid")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+fmt.Printf("返回结果: %#v", res)
+```
+
+### getPubTemplateTitleList
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.getPubTemplateTitleList.html)
+
+```go
+import "github.com/medivhzhan/weapp/v2"
+
+// GetPubTemplateTitleList 获取帐号所属类目下的公共模板标题
+//
+// token 微信 access_token
+// ids 类目 id,多个用逗号隔开
+// start 用于分页,表示从 start 开始。从 0 开始计数。
+// limit 用于分页,表示拉取 limit 条记录。最大为 30
+res, err := weapp.GetPubTemplateTitleList("access_token", "1,2,3", 0, 10)
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+fmt.Printf("返回结果: %#v", res)
+```
+
+### getTemplateList
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.getTemplateList.html)
+
+```go
+import "github.com/medivhzhan/weapp/v2"
+
+// GetTemplateList 获取帐号下已存在的模板列表
+//
+// token 微信 access_token
+res, err := weapp.GetTemplateList("access_token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+fmt.Printf("返回结果: %#v", res)
+```
+
+### sendSubscribeMessage
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html)
+
+```go
+
+import "github.com/medivhzhan/weapp/v2"
+
+sender := weapp.SubscribeMessage{
+ ToUser: mpOpenID,
+ TemplateID: "template-id",
+ Page: "mock/page/path",
+ MiniprogramState: weapp.MiniprogramStateDeveloper, // 或者: "developer"
+ Data: weapp.SubscribeMessageData{
+ "first-key": {
+ Value: "value",
+ },
+ "second-key": {
+ Value: "value",
+ },
+ },
+}
+
+res, err := sender.Send("access-token")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+
+```
+
+---
+
+## 解密
+
+[官方文档](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html)
+
+> ⚠️ 前端应当先完成[登录](#登录)流程再调用获取加密数据的相关接口。
+
+### 解密手机号码
+
+```go
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.DecryptMobile("session-key", "encrypted-data", "iv" )
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+```
+
+### 解密分享内容
+
+```go
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.DecryptShareInfo("session-key", "encrypted-data", "iv" )
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+```
+
+### 解密用户信息
+
+```go
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.DecryptUserInfo( "session-key", "raw-data", "encrypted-data", "signature", "iv")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+```
+
+### 解密微信运动
+
+```go
+import "github.com/medivhzhan/weapp/v2"
+
+res, err := weapp.DecryptRunData("session-key", "encrypted-data", "iv" )
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+
+fmt.Printf("返回结果: %#v", res)
+```
+
+---
+
+## 人脸识别
+
+```go
+import "github.com/medivhzhan/weapp/v2"
+
+// FaceIdentify 获取人脸识别结果
+//
+// token 微信 access_token
+// key 小程序 verify_result
+res, err := weapp.FaceIdentify("access_token", "verify_result")
+if err != nil {
+ // 处理一般错误信息
+ return
+}
+if err := res.GetResponseError(); err !=nil {
+ // 处理微信返回错误信息
+ return
+}
+fmt.Printf("返回结果: %#v", res)
+```
+
+---
diff --git a/app/lib/weapp/analysis.go b/app/lib/weapp/analysis.go
new file mode 100644
index 0000000..9dca262
--- /dev/null
+++ b/app/lib/weapp/analysis.go
@@ -0,0 +1,211 @@
+package weapp
+
+type dateRange struct {
+ BeginDate string `json:"begin_date"`
+ EndDate string `json:"end_date"`
+}
+
+const (
+ apiGetUserPortrait = "/datacube/getweanalysisappiduserportrait"
+ apiGetVisitDistribution = "/datacube/getweanalysisappidvisitdistribution"
+ apiGetVisitPage = "/datacube/getweanalysisappidvisitpage"
+ apiGetDailySummary = "/datacube/getweanalysisappiddailysummarytrend"
+)
+
+// UserPortrait response data of get user portrait
+type UserPortrait struct {
+ CommonError
+ RefDate string `json:"ref_date"`
+ VisitUV Portrait `json:"visit_uv"` // 活跃用户画像
+ VisitUVNew Portrait `json:"visit_uv_new"` // 新用户画像
+}
+
+// Portrait 肖像
+type Portrait struct {
+ Index uint `json:"index"` // 分布类型
+ Province []Attribute `json:"province"` // 省份,如北京、广东等
+ City []Attribute `json:"city"` // 城市,如北京、广州等
+ Genders []Attribute `json:"genders"` // 性别,包括男、女、未知
+ Platforms []Attribute `json:"platforms"` // 终端类型,包括 iPhone,android,其他
+ Devices []Attribute `json:"devices"` // 机型,如苹果 iPhone 6,OPPO R9 等
+ Ages []Attribute `json:"ages"` // 年龄,包括17岁以下、18-24岁等区间
+}
+
+// Attribute 描述内容
+type Attribute struct {
+ ID uint `json:"id"` // 属性值id
+ Name string `json:"name"` // 属性值名称,与id对应。如属性为 province 时,返回的属性值名称包括「广东」等。
+ Value uint `json:"value"`
+ // TODO: 确认后删除该字段
+ AccessSourceVisitUV uint `json:"access_source_visit_uv"` // 该场景访问uv
+}
+
+// GetUserPortrait 获取小程序新增或活跃用户的画像分布数据。
+// 时间范围支持昨天、最近7天、最近30天。
+// 其中,新增用户数为时间范围内首次访问小程序的去重用户数,活跃用户数为时间范围内访问过小程序的去重用户数。
+// begin 开始日期。格式为 yyyymmdd
+// end 结束日期,开始日期与结束日期相差的天数限定为0/6/29,分别表示查询最近1/7/30天数据,允许设置的最大值为昨日。格式为 yyyymmdd
+func GetUserPortrait(accessToken, begin, end string) (*UserPortrait, error) {
+ api := baseURL + apiGetUserPortrait
+ return getUserPortrait(accessToken, begin, end, api)
+}
+
+func getUserPortrait(accessToken, begin, end, api string) (*UserPortrait, error) {
+ api, err := tokenAPI(api, accessToken)
+ if err != nil {
+ return nil, err
+ }
+
+ params := dateRange{
+ BeginDate: begin,
+ EndDate: end,
+ }
+
+ res := new(UserPortrait)
+ if err := postJSON(api, params, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// VisitDistribution 用户小程序访问分布数据
+type VisitDistribution struct {
+ CommonError
+ RefDate string `json:"ref_date"`
+ List []Distribution `json:"list"`
+}
+
+// Distribution 分布数据
+type Distribution struct {
+ // 分布类型
+ // index 的合法值
+ // access_source_session_cnt 访问来源分布
+ // access_staytime_info 访问时长分布
+ // access_depth_info 访问深度的分布
+ Index string `json:"index"`
+ ItemList []DistributionItem `json:"item_list"` // 分布数据列表
+}
+
+// DistributionItem 分布数据项
+type DistributionItem struct {
+ Key uint `json:"key"` // 场景 id,定义在各个 index 下不同,具体参见下方表格
+ Value uint `json:"value"` // 该场景 id 访问 pv
+ // TODO: 确认后删除该字段
+ AccessSourceVisitUV uint `json:"access_source_visit_uv"` // 该场景 id 访问 uv
+}
+
+// GetVisitDistribution 获取用户小程序访问分布数据
+// begin 开始日期。格式为 yyyymmdd
+// end 结束日期,限定查询 1 天数据,允许设置的最大值为昨日。格式为 yyyymmdd
+func GetVisitDistribution(accessToken, begin, end string) (*VisitDistribution, error) {
+ api := baseURL + apiGetVisitDistribution
+ return getVisitDistribution(accessToken, begin, end, api)
+}
+
+func getVisitDistribution(accessToken, begin, end, api string) (*VisitDistribution, error) {
+ url, err := tokenAPI(api, accessToken)
+ if err != nil {
+ return nil, err
+ }
+
+ params := dateRange{
+ BeginDate: begin,
+ EndDate: end,
+ }
+
+ res := new(VisitDistribution)
+ if err := postJSON(url, params, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// VisitPage 页面访问数据
+type VisitPage struct {
+ CommonError
+ RefDate string `json:"ref_date"`
+ List []Page `json:"list"`
+}
+
+// Page 页面
+type Page struct {
+ PagePath string `json:"Page_path"` // 页面路径
+ PageVisitPV uint `json:"Page_visit_pv"` // 访问次数
+ PageVisitUV uint `json:"Page_visit_uv"` // 访问人数
+ PageStaytimePV float64 `json:"page_staytime_pv"` // 次均停留时长
+ EntrypagePV uint `json:"entrypage_pv"` // 进入页次数
+ ExitpagePV uint `json:"exitpage_pv"` // 退出页次数
+ PageSharePV uint `json:"page_share_pv"` // 转发次数
+ PageShareUV uint `json:"page_share_uv"` // 转发人数
+
+}
+
+// GetVisitPage 访问页面。
+// 目前只提供按 page_visit_pv 排序的 top200。
+// begin 开始日期。格式为 yyyymmdd
+// end 结束日期,限定查询1天数据,允许设置的最大值为昨日。格式为 yyyymmdd
+func GetVisitPage(accessToken, begin, end string) (*VisitPage, error) {
+ api := baseURL + apiGetVisitPage
+ return getVisitPage(accessToken, begin, end, api)
+}
+
+func getVisitPage(accessToken, begin, end, api string) (*VisitPage, error) {
+ url, err := tokenAPI(api, accessToken)
+ if err != nil {
+ return nil, err
+ }
+
+ params := dateRange{
+ BeginDate: begin,
+ EndDate: end,
+ }
+
+ res := new(VisitPage)
+ if err := postJSON(url, params, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// DailySummary 用户访问小程序数据概况
+type DailySummary struct {
+ CommonError
+ List []Summary `json:"list"`
+}
+
+// Summary 概况
+type Summary struct {
+ RefDate string `json:"ref_date"` // 日期,格式为 yyyymmdd
+ VisitTotal uint `json:"visit_total"` // 累计用户数
+ SharePV uint `json:"share_pv"` // 转发次数
+ ShareUV uint `json:"share_uv"` // 转发人数
+}
+
+// GetDailySummary 获取用户访问小程序数据概况
+// begin 开始日期。格式为 yyyymmdd
+// end 结束日期,限定查询1天数据,允许设置的最大值为昨日。格式为 yyyymmdd
+func GetDailySummary(accessToken, begin, end string) (*DailySummary, error) {
+ api := baseURL + apiGetDailySummary
+ return getDailySummary(accessToken, begin, end, api)
+}
+func getDailySummary(accessToken, begin, end, api string) (*DailySummary, error) {
+ url, err := tokenAPI(api, accessToken)
+ if err != nil {
+ return nil, err
+ }
+
+ params := dateRange{
+ BeginDate: begin,
+ EndDate: end,
+ }
+
+ res := new(DailySummary)
+ if err := postJSON(url, params, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
diff --git a/app/lib/weapp/analysis_retain.go b/app/lib/weapp/analysis_retain.go
new file mode 100644
index 0000000..d758487
--- /dev/null
+++ b/app/lib/weapp/analysis_retain.go
@@ -0,0 +1,67 @@
+package weapp
+
+const (
+ apiGetMonthlyRetain = "/datacube/getweanalysisappidmonthlyretaininfo"
+ apiGetWeeklyRetain = "/datacube/getweanalysisappidweeklyretaininfo"
+ apiGetDailyRetain = "/datacube/getweanalysisappiddailyretaininfo"
+)
+
+// Retain 用户留存
+type Retain struct {
+ Key uint8 `json:"key"` // 标识,0开始,表示当月,1表示1月后。key取值分别是:0,1
+ Value uint `json:"value"` // key对应日期的新增用户数/活跃用户数(key=0时)或留存用户数(k>0时)
+}
+
+// RetainResponse 生物认证秘钥签名验证请求返回数据
+type RetainResponse struct {
+ CommonError
+ RefDate string `json:"ref_date"` // 时间,月格式为 yyyymm | 周格式为 yyyymmdd-yyyymmdd | 天格式为 yyyymmdd
+ VisitUV []Retain `json:"visit_uv"` // 活跃用户留存
+ VisitUVNew []Retain `json:"visit_uv_new"` // 新增用户留存
+}
+
+// GetMonthlyRetain 获取用户访问小程序月留存
+// accessToken 接口调用凭证
+// begin 开始日期,为自然月第一天。格式为 yyyymmdd
+// end 结束日期,为自然月最后一天,限定查询一个月数据。格式为 yyyymmdd
+func GetMonthlyRetain(accessToken, begin, end string) (*RetainResponse, error) {
+ api := baseURL + apiGetMonthlyRetain
+ return getRetain(accessToken, begin, end, api)
+}
+
+// GetWeeklyRetain 获取用户访问小程序周留存
+// accessToken 接口调用凭证
+// begin 开始日期,为自然月第一天。格式为 yyyymmdd
+// end 结束日期,为周日日期,限定查询一周数据。格式为 yyyymmdd
+func GetWeeklyRetain(accessToken, begin, end string) (*RetainResponse, error) {
+ api := baseURL + apiGetWeeklyRetain
+ return getRetain(accessToken, begin, end, api)
+}
+
+// GetDailyRetain 获取用户访问小程序日留存
+// accessToken 接口调用凭证
+// begin 开始日期,为自然月第一天。格式为 yyyymmdd
+// end 结束日期,限定查询1天数据,允许设置的最大值为昨日。格式为 yyyymmdd
+func GetDailyRetain(accessToken, begin, end string) (*RetainResponse, error) {
+ api := baseURL + apiGetDailyRetain
+ return getRetain(accessToken, begin, end, api)
+}
+
+func getRetain(accessToken, begin, end, api string) (*RetainResponse, error) {
+ url, err := tokenAPI(api, accessToken)
+ if err != nil {
+ return nil, err
+ }
+
+ params := dateRange{
+ BeginDate: begin,
+ EndDate: end,
+ }
+
+ res := new(RetainResponse)
+ if err := postJSON(url, params, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
diff --git a/app/lib/weapp/analysis_retain_test.go b/app/lib/weapp/analysis_retain_test.go
new file mode 100644
index 0000000..f0fa02a
--- /dev/null
+++ b/app/lib/weapp/analysis_retain_test.go
@@ -0,0 +1,208 @@
+package weapp
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+func TestGetMonthlyRetain(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiGetMonthlyRetain {
+ t.Fatalf("Except to path '%s',get '%s'", apiGetMonthlyRetain, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := make(map[string]interface{})
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+ param, ok := params["begin_date"]
+ if !ok || param == "" {
+ t.Log("param begin_date can not be empty")
+ t.Fail()
+ }
+ param, ok = params["end_date"]
+ if !ok || param == "" {
+ t.Log("param end_date can not be empty")
+ t.Fail()
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "ref_date": "201702",
+ "visit_uv_new": [
+ {
+ "key": 0,
+ "value": 346249
+ }
+ ],
+ "visit_uv": [
+ {
+ "key": 0,
+ "value": 346249
+ }
+ ]
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := getRetain("mock-access-token", "mock-begin-date", "mock-end-date", ts.URL+apiGetMonthlyRetain)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetWeeklyRetain(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiGetWeeklyRetain {
+ t.Fatalf("Except to path '%s',get '%s'", apiGetWeeklyRetain, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := make(map[string]interface{})
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+ param, ok := params["begin_date"]
+ if !ok || param == "" {
+ t.Log("param begin_date can not be empty")
+ t.Fail()
+ }
+ param, ok = params["end_date"]
+ if !ok || param == "" {
+ t.Log("param end_date can not be empty")
+ t.Fail()
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "ref_date": "20170306-20170312",
+ "visit_uv_new": [
+ {
+ "key": 0,
+ "value": 0
+ },
+ {
+ "key": 1,
+ "value": 16853
+ }
+ ],
+ "visit_uv": [
+ {
+ "key": 0,
+ "value": 0
+ },
+ {
+ "key": 1,
+ "value": 99310
+ }
+ ]
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := getRetain("mock-access-token", "mock-begin-date", "mock-end-date", ts.URL+apiGetWeeklyRetain)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetDailyRetain(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiGetDailyRetain {
+ t.Fatalf("Except to path '%s',get '%s'", apiGetDailyRetain, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := make(map[string]interface{})
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+ param, ok := params["begin_date"]
+ if !ok || param == "" {
+ t.Log("param begin_date can not be empty")
+ t.Fail()
+ }
+ param, ok = params["end_date"]
+ if !ok || param == "" {
+ t.Log("param end_date can not be empty")
+ t.Fail()
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "ref_date": "20170313",
+ "visit_uv_new": [
+ {
+ "key": 0,
+ "value": 5464
+ }
+ ],
+ "visit_uv": [
+ {
+ "key": 0,
+ "value": 55500
+ }
+ ]
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := getRetain("mock-access-token", "mock-begin-date", "mock-end-date", ts.URL+apiGetDailyRetain)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/app/lib/weapp/analysis_test.go b/app/lib/weapp/analysis_test.go
new file mode 100644
index 0000000..3447afa
--- /dev/null
+++ b/app/lib/weapp/analysis_test.go
@@ -0,0 +1,466 @@
+package weapp
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+func TestGetUserPortrait(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiGetUserPortrait {
+ t.Fatalf("Except to path '%s',get '%s'", apiGetUserPortrait, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := make(map[string]interface{})
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+ param, ok := params["begin_date"]
+ if !ok || param == "" {
+ t.Log("param begin_date can not be empty")
+ t.Fail()
+ }
+ param, ok = params["end_date"]
+ if !ok || param == "" {
+ t.Log("param end_date can not be empty")
+ t.Fail()
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "ref_date": "20170611",
+ "visit_uv_new": {
+ "province": [
+ {
+ "id": 31,
+ "name": "广东省",
+ "value": 215
+ }
+ ],
+ "city": [
+ {
+ "id": 3102,
+ "name": "广州",
+ "value": 78
+ }
+ ],
+ "genders": [
+ {
+ "id": 1,
+ "name": "男",
+ "value": 2146
+ }
+ ],
+ "platforms": [
+ {
+ "id": 1,
+ "name": "iPhone",
+ "value": 27642
+ }
+ ],
+ "devices": [
+ {
+ "name": "OPPO R9",
+ "value": 61
+ }
+ ],
+ "ages": [
+ {
+ "id": 1,
+ "name": "17岁以下",
+ "value": 151
+ }
+ ]
+ },
+ "visit_uv": {
+ "province": [
+ {
+ "id": 31,
+ "name": "广东省",
+ "value": 1341
+ }
+ ],
+ "city": [
+ {
+ "id": 3102,
+ "name": "广州",
+ "value": 234
+ }
+ ],
+ "genders": [
+ {
+ "id": 1,
+ "name": "男",
+ "value": 14534
+ }
+ ],
+ "platforms": [
+ {
+ "id": 1,
+ "name": "iPhone",
+ "value": 21750
+ }
+ ],
+ "devices": [
+ {
+ "name": "OPPO R9",
+ "value": 617
+ }
+ ],
+ "ages": [
+ {
+ "id": 1,
+ "name": "17岁以下",
+ "value": 3156
+ }
+ ]
+ }
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := getUserPortrait("mock-access-token", "mock-begin-date", "mock-end-date", ts.URL+apiGetUserPortrait)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetVisitDistribution(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiGetVisitDistribution {
+ t.Fatalf("Except to path '%s',get '%s'", apiGetVisitDistribution, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := make(map[string]interface{})
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+ param, ok := params["begin_date"]
+ if !ok || param == "" {
+ t.Log("param begin_date can not be empty")
+ t.Fail()
+ }
+ param, ok = params["end_date"]
+ if !ok || param == "" {
+ t.Log("param end_date can not be empty")
+ t.Fail()
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "ref_date": "20170313",
+ "list": [
+ {
+ "index": "access_source_session_cnt",
+ "item_list": [
+ {
+ "key": 10,
+ "value": 5
+ },
+ {
+ "key": 8,
+ "value": 687
+ },
+ {
+ "key": 7,
+ "value": 10740
+ },
+ {
+ "key": 6,
+ "value": 1961
+ },
+ {
+ "key": 5,
+ "value": 677
+ },
+ {
+ "key": 4,
+ "value": 653
+ },
+ {
+ "key": 3,
+ "value": 1120
+ },
+ {
+ "key": 2,
+ "value": 10243
+ },
+ {
+ "key": 1,
+ "value": 116578
+ }
+ ]
+ },
+ {
+ "index": "access_staytime_info",
+ "item_list": [
+ {
+ "key": 8,
+ "value": 16329
+ },
+ {
+ "key": 7,
+ "value": 19322
+ },
+ {
+ "key": 6,
+ "value": 21832
+ },
+ {
+ "key": 5,
+ "value": 19539
+ },
+ {
+ "key": 4,
+ "value": 29670
+ },
+ {
+ "key": 3,
+ "value": 19667
+ },
+ {
+ "key": 2,
+ "value": 11794
+ },
+ {
+ "key": 1,
+ "value": 4511
+ }
+ ]
+ },
+ {
+ "index": "access_depth_info",
+ "item_list": [
+ {
+ "key": 5,
+ "value": 217
+ },
+ {
+ "key": 4,
+ "value": 3259
+ },
+ {
+ "key": 3,
+ "value": 32445
+ },
+ {
+ "key": 2,
+ "value": 63542
+ },
+ {
+ "key": 1,
+ "value": 43201
+ }
+ ]
+ }
+ ]
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := getVisitDistribution("mock-access-token", "mock-begin-date", "mock-end-date", ts.URL+apiGetVisitDistribution)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetVisitPage(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiGetVisitPage {
+ t.Fatalf("Except to path '%s',get '%s'", apiGetVisitPage, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := make(map[string]interface{})
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+ param, ok := params["begin_date"]
+ if !ok || param == "" {
+ t.Log("param begin_date can not be empty")
+ t.Fail()
+ }
+ param, ok = params["end_date"]
+ if !ok || param == "" {
+ t.Log("param end_date can not be empty")
+ t.Fail()
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "ref_date": "20170313",
+ "list": [
+ {
+ "page_path": "pages/main/main.html",
+ "page_visit_pv": 213429,
+ "page_visit_uv": 55423,
+ "page_staytime_pv": 8.139198,
+ "entrypage_pv": 117922,
+ "exitpage_pv": 61304,
+ "page_share_pv": 180,
+ "page_share_uv": 166
+ },
+ {
+ "page_path": "pages/linedetail/linedetail.html",
+ "page_visit_pv": 155030,
+ "page_visit_uv": 42195,
+ "page_staytime_pv": 35.462395,
+ "entrypage_pv": 21101,
+ "exitpage_pv": 47051,
+ "page_share_pv": 47,
+ "page_share_uv": 42
+ },
+ {
+ "page_path": "pages/search/search.html",
+ "page_visit_pv": 65011,
+ "page_visit_uv": 24716,
+ "page_staytime_pv": 6.889634,
+ "entrypage_pv": 1811,
+ "exitpage_pv": 3198,
+ "page_share_pv": 0,
+ "page_share_uv": 0
+ },
+ {
+ "page_path": "pages/stationdetail/stationdetail.html",
+ "page_visit_pv": 29953,
+ "page_visit_uv": 9695,
+ "page_staytime_pv": 7.558508,
+ "entrypage_pv": 1386,
+ "exitpage_pv": 2285,
+ "page_share_pv": 0,
+ "page_share_uv": 0
+ },
+ {
+ "page_path": "pages/switch-city/switch-city.html",
+ "page_visit_pv": 8928,
+ "page_visit_uv": 4017,
+ "page_staytime_pv": 9.22659,
+ "entrypage_pv": 748,
+ "exitpage_pv": 1613,
+ "page_share_pv": 0,
+ "page_share_uv": 0
+ }
+ ]
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := getVisitPage("mock-access-token", "mock-begin-date", "mock-end-date", ts.URL+apiGetVisitPage)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetDailySummary(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiGetDailySummary {
+ t.Fatalf("Except to path '%s',get '%s'", apiGetDailySummary, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := make(map[string]interface{})
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+ param, ok := params["begin_date"]
+ if !ok || param == "" {
+ t.Log("param begin_date can not be empty")
+ t.Fail()
+ }
+ param, ok = params["end_date"]
+ if !ok || param == "" {
+ t.Log("param end_date can not be empty")
+ t.Fail()
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "list": [
+ {
+ "ref_date": "20170313",
+ "visit_total": 391,
+ "share_pv": 572,
+ "share_uv": 383
+ }
+ ]
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := getDailySummary("mock-access-token", "mock-begin-date", "mock-end-date", ts.URL+apiGetDailySummary)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/app/lib/weapp/analysis_visit_trend.go b/app/lib/weapp/analysis_visit_trend.go
new file mode 100644
index 0000000..0207fe2
--- /dev/null
+++ b/app/lib/weapp/analysis_visit_trend.go
@@ -0,0 +1,71 @@
+package weapp
+
+const (
+ apiGetMonthlyVisitTrend = "/datacube/getweanalysisappidmonthlyvisittrend"
+ apiGetWeeklyVisitTrend = "/datacube/getweanalysisappidweeklyvisittrend"
+ apiGetDailyVisitTrend = "/datacube/getweanalysisappiddailyvisittrend"
+)
+
+// Trend 用户趋势
+type Trend struct {
+ RefDate string `json:"ref_date"` // 时间,月格式为 yyyymm | 周格式为 yyyymmdd-yyyymmdd | 天格式为 yyyymmdd
+ SessionCNT uint `json:"session_cnt"` // 打开次数(自然月内汇总)
+ VisitPV uint `json:"visit_pv"` // 访问次数(自然月内汇总)
+ VisitUV uint `json:"visit_uv"` // 访问人数(自然月内去重)
+ VisitUVNew uint `json:"visit_uv_new"` // 新用户数(自然月内去重)
+ StayTimeUV float64 `json:"stay_time_uv"` // 人均停留时长 (浮点型,单位:秒)
+ StayTimeSession float64 `json:"stay_time_session"` // 次均停留时长 (浮点型,单位:秒)
+ VisitDepth float64 `json:"visit_depth"` // 平均访问深度 (浮点型)
+}
+
+// VisitTrend 生物认证秘钥签名验证请求返回数据
+type VisitTrend struct {
+ CommonError
+ List []Trend `json:"list"`
+}
+
+// GetMonthlyVisitTrend 获取用户访问小程序数据月趋势
+// accessToken 接口调用凭证
+// begin 开始日期,为自然月第一天。格式为 yyyymmdd
+// end 结束日期,为自然月最后一天,限定查询一个月数据。格式为 yyyymmdd
+func GetMonthlyVisitTrend(accessToken, begin, end string) (*VisitTrend, error) {
+ api := baseURL + apiGetMonthlyVisitTrend
+ return getVisitTrend(accessToken, begin, end, api)
+}
+
+// GetWeeklyVisitTrend 获取用户访问小程序数据周趋势
+// accessToken 接口调用凭证
+// begin 开始日期,为自然月第一天。格式为 yyyymmdd
+// end 结束日期,为周日日期,限定查询一周数据。格式为 yyyymmdd
+func GetWeeklyVisitTrend(accessToken, begin, end string) (*VisitTrend, error) {
+ api := baseURL + apiGetWeeklyVisitTrend
+ return getVisitTrend(accessToken, begin, end, api)
+}
+
+// GetDailyVisitTrend 获取用户访问小程序数据日趋势
+// accessToken 接口调用凭证
+// begin 开始日期,为自然月第一天。格式为 yyyymmdd
+// end 结束日期,限定查询1天数据,允许设置的最大值为昨日。格式为 yyyymmdd
+func GetDailyVisitTrend(accessToken, begin, end string) (*VisitTrend, error) {
+ api := baseURL + apiGetDailyVisitTrend
+ return getVisitTrend(accessToken, begin, end, api)
+}
+
+func getVisitTrend(accessToken, begin, end, api string) (*VisitTrend, error) {
+ url, err := tokenAPI(api, accessToken)
+ if err != nil {
+ return nil, err
+ }
+
+ params := dateRange{
+ BeginDate: begin,
+ EndDate: end,
+ }
+
+ res := new(VisitTrend)
+ if err := postJSON(url, params, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
diff --git a/app/lib/weapp/analysis_visit_trend_test.go b/app/lib/weapp/analysis_visit_trend_test.go
new file mode 100644
index 0000000..90e62ce
--- /dev/null
+++ b/app/lib/weapp/analysis_visit_trend_test.go
@@ -0,0 +1,194 @@
+package weapp
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+func TestGetMonthlyVisitTrend(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiGetMonthlyVisitTrend {
+ t.Fatalf("Except to path '%s',get '%s'", apiGetMonthlyVisitTrend, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := make(map[string]interface{})
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+ param, ok := params["begin_date"]
+ if !ok || param == "" {
+ t.Log("param begin_date can not be empty")
+ t.Fail()
+ }
+ param, ok = params["end_date"]
+ if !ok || param == "" {
+ t.Log("param end_date can not be empty")
+ t.Fail()
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "list": [
+ {
+ "ref_date": "201703",
+ "session_cnt": 126513,
+ "visit_pv": 426113,
+ "visit_uv": 48659,
+ "visit_uv_new": 6726,
+ "stay_time_session": 56.4112,
+ "visit_depth": 2.0189
+ }
+ ]
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := getVisitTrend("mock-access-token", "mock-begin-date", "mock-end-date", ts.URL+apiGetMonthlyVisitTrend)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetWeeklyVisitTrend(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiGetWeeklyVisitTrend {
+ t.Fatalf("Except to path '%s',get '%s'", apiGetWeeklyVisitTrend, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := make(map[string]interface{})
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+ param, ok := params["begin_date"]
+ if !ok || param == "" {
+ t.Log("param begin_date can not be empty")
+ t.Fail()
+ }
+ param, ok = params["end_date"]
+ if !ok || param == "" {
+ t.Log("param end_date can not be empty")
+ t.Fail()
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "list": [
+ {
+ "ref_date": "20170306-20170312",
+ "session_cnt": 986780,
+ "visit_pv": 3251840,
+ "visit_uv": 189405,
+ "visit_uv_new": 45592,
+ "stay_time_session": 54.5346,
+ "visit_depth": 1.9735
+ }
+ ]
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := getVisitTrend("mock-access-token", "mock-begin-date", "mock-end-date", ts.URL+apiGetWeeklyVisitTrend)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetDailyVisitTrend(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiGetDailyVisitTrend {
+ t.Fatalf("Except to path '%s',get '%s'", apiGetDailyVisitTrend, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := make(map[string]interface{})
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+ param, ok := params["begin_date"]
+ if !ok || param == "" {
+ t.Log("param begin_date can not be empty")
+ t.Fail()
+ }
+ param, ok = params["end_date"]
+ if !ok || param == "" {
+ t.Log("param end_date can not be empty")
+ t.Fail()
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "list": [
+ {
+ "ref_date": "20170313",
+ "session_cnt": 142549,
+ "visit_pv": 472351,
+ "visit_uv": 55500,
+ "visit_uv_new": 5464,
+ "stay_time_session": 0,
+ "visit_depth": 1.9838
+ }
+ ]
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := getVisitTrend("mock-access-token", "mock-begin-date", "mock-end-date", ts.URL+apiGetDailyVisitTrend)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/app/lib/weapp/auth.go b/app/lib/weapp/auth.go
new file mode 100644
index 0000000..e04e9fd
--- /dev/null
+++ b/app/lib/weapp/auth.go
@@ -0,0 +1,218 @@
+package weapp
+
+import (
+ "applet/app/cfg"
+ "applet/app/utils"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "github.com/tidwall/gjson"
+)
+
+const (
+ apiLogin = "/sns/jscode2session"
+ apiGetAccessToken = "/cgi-bin/token"
+ apiGetPaidUnionID = "/wxa/getpaidunionid"
+ apiQetticket = "/cgi-bin/ticket/getticket"
+ apiLoginWebsiteBackend = "/Wx/getJsCode2session"
+)
+
+// LoginResponse 返回给用户的数据
+type LoginResponse struct {
+ CommonError
+ OpenID string `json:"openid"`
+ SessionKey string `json:"session_key"`
+ // 用户在开放平台的唯一标识符
+ // 只在满足一定条件的情况下返回
+ UnionID string `json:"unionid"`
+}
+
+// Login 登录凭证校验。通过 wx.login 接口获得临时登录凭证 code 后传到开发者服务器调用此接口完成登录流程。
+//
+// appID 小程序 appID
+// secret 小程序的 app secret
+// code 小程序登录时获取的 code
+func Login(appID, secret, code string) (*LoginResponse, error) {
+ api := baseURL + apiLogin
+
+ return login(appID, secret, code, api)
+}
+
+// 调用总站长后台的接口
+func LoginForWebsiteBackend(uid string, appID, code string) (*LoginResponse, error) {
+ utils.FilePutContents("o2o_wechat_mini", "appId :"+appID+", code: "+code)
+ api := cfg.WebsiteBackend.URL + apiLoginWebsiteBackend + "?appid=" + appID + "&js_code=" + code + "&uid=" + uid
+ //res := new(LoginResponse)
+ resByte, err := utils.CurlGet(api, nil)
+ if err != nil {
+ utils.FilePutContents("o2o_wechat_mini", "url:"+api)
+ return nil, err
+ }
+ fmt.Println(api, string(resByte), err)
+ var data struct {
+ Code int `json:"code"`
+ Msg string `json:"msg"`
+ Data LoginResponse `json:"data"`
+ }
+ if gjson.Get(string(resByte), "code").Int() > 0 {
+ return nil, errors.New(gjson.Get(string(resByte), "msg").String())
+ }
+ err = json.Unmarshal(resByte, &data)
+ if err != nil {
+ utils.FilePutContents("o2o_wechat_mini", "err:"+err.Error()+"res: "+string(resByte))
+ return nil, err
+ }
+ if data.Code != 0 {
+ return nil, errors.New(data.Msg)
+ }
+ //if err := getJSON(api, res); err != nil {
+ // utils.FilePutContents("o2o_wechat_mini", "url:"+api)
+ // return nil, err
+ //}
+
+ utils.FilePutContents("o2o_wechat_mini", string(resByte))
+ return &data.Data, nil
+}
+
+func login(appID, secret, code, api string) (*LoginResponse, error) {
+ queries := requestQueries{
+ "appid": appID,
+ "secret": secret,
+ "js_code": code,
+ "grant_type": "authorization_code",
+ }
+
+ url, err := encodeURL(api, queries)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(LoginResponse)
+ if err := getJSON(url, res); err != nil {
+ return nil, err
+ }
+ return res, nil
+}
+
+// TokenResponse 获取 access_token 成功返回数据
+type TokenResponse struct {
+ CommonError
+ AccessToken string `json:"access_token"` // 获取到的凭证
+ ExpiresIn uint `json:"expires_in"` // 凭证有效时间,单位:秒。目前是7200秒之内的值。
+}
+
+// GetAccessToken 获取小程序全局唯一后台接口调用凭据(access_token)。
+// 调调用绝大多数后台接口时都需使用 access_token,开发者需要进行妥善保存,注意缓存。
+func GetAccessToken(appID, secret string) (*TokenResponse, error) {
+ api := baseURL + apiGetAccessToken
+ return getAccessToken(appID, secret, api)
+}
+
+func getAccessToken(appID, secret, api string) (*TokenResponse, error) {
+
+ queries := requestQueries{
+ "appid": appID,
+ "secret": secret,
+ "grant_type": "client_credential",
+ }
+
+ url, err := encodeURL(api, queries)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(TokenResponse)
+ if err := getJSON(url, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// TicketResponse 获取 Ticket 成功返回数据
+type TicketResponse struct {
+ CommonError
+ ExpiresIn uint `json:"expires_in"` // 凭证有效时间,单位:秒。目前是7200秒之内的值。
+ Errcode int `json:"errcode"`
+ Ticket string `json:"ticket"`
+}
+
+func Getticket(appID, access_token string) (*TicketResponse, error) {
+ api := baseURL + apiQetticket
+ return getticket(appID, access_token, api)
+}
+
+func getticket(appID, access_token, api string) (*TicketResponse, error) {
+ queries := requestQueries{
+ "access_token": access_token,
+ "type": "jsapi",
+ }
+
+ url, err := encodeURL(api, queries)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(TicketResponse)
+ fmt.Println(res, "res")
+ fmt.Println(access_token, "access_token")
+
+ if err := getJSON(url, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// GetPaidUnionIDResponse response data
+type GetPaidUnionIDResponse struct {
+ CommonError
+ UnionID string `json:"unionid"`
+}
+
+// GetPaidUnionID 用户支付完成后,通过微信支付订单号(transaction_id)获取该用户的 UnionId,
+func GetPaidUnionID(accessToken, openID, transactionID string) (*GetPaidUnionIDResponse, error) {
+ api := baseURL + apiGetPaidUnionID
+ return getPaidUnionID(accessToken, openID, transactionID, api)
+}
+
+func getPaidUnionID(accessToken, openID, transactionID, api string) (*GetPaidUnionIDResponse, error) {
+ queries := requestQueries{
+ "openid": openID,
+ "access_token": accessToken,
+ "transaction_id": transactionID,
+ }
+
+ return getPaidUnionIDRequest(api, queries)
+}
+
+// GetPaidUnionIDWithMCH 用户支付完成后,通过微信支付商户订单号和微信支付商户号(out_trade_no 及 mch_id)获取该用户的 UnionId,
+func GetPaidUnionIDWithMCH(accessToken, openID, outTradeNo, mchID string) (*GetPaidUnionIDResponse, error) {
+ api := baseURL + apiGetPaidUnionID
+ return getPaidUnionIDWithMCH(accessToken, openID, outTradeNo, mchID, api)
+}
+
+func getPaidUnionIDWithMCH(accessToken, openID, outTradeNo, mchID, api string) (*GetPaidUnionIDResponse, error) {
+ queries := requestQueries{
+ "openid": openID,
+ "mch_id": mchID,
+ "out_trade_no": outTradeNo,
+ "access_token": accessToken,
+ }
+
+ return getPaidUnionIDRequest(api, queries)
+}
+
+func getPaidUnionIDRequest(api string, queries requestQueries) (*GetPaidUnionIDResponse, error) {
+ url, err := encodeURL(api, queries)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(GetPaidUnionIDResponse)
+ if err := getJSON(url, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
diff --git a/app/lib/weapp/auth_test.go b/app/lib/weapp/auth_test.go
new file mode 100644
index 0000000..56eb362
--- /dev/null
+++ b/app/lib/weapp/auth_test.go
@@ -0,0 +1,181 @@
+package weapp
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+func TestLogin(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "GET" {
+ t.Fatalf("Expect 'GET' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiLogin {
+ t.Fatalf("Except to path '%s',get '%s'", apiLogin, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ queries := []string{"appid", "secret", "js_code", "grant_type"}
+ for _, v := range queries {
+ content := r.Form.Get(v)
+ if content == "" {
+ t.Fatalf("%v can not be empty", v)
+ }
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "openid": "mock-openid",
+ "session_key": "mock-session_key",
+ "unionid": "mock-unionid",
+ "errcode": 0,
+ "errmsg": "mock-errmsg"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := login("mock-appid", "mock-secret", "mock-code", ts.URL+apiLogin)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetAccessToken(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "GET" {
+ t.Fatalf("Expect 'GET' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiGetAccessToken {
+ t.Fatalf("Except to path '%s',get '%s'", apiGetAccessToken, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("grant_type") != "client_credential" {
+ t.Fatal("invalid client_credential")
+ }
+
+ queries := []string{"appid", "secret"}
+ for _, v := range queries {
+ content := r.Form.Get(v)
+ if content == "" {
+ t.Fatalf("%v can not be empty", v)
+ }
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{"access_token":"ACCESS_TOKEN","expires_in":7200}`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := getAccessToken("mock-appid", "mock-secret", ts.URL+apiGetAccessToken)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetPaidUnionID(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "GET" {
+ t.Fatalf("Expect 'GET' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiGetPaidUnionID {
+ t.Fatalf("Except to path '%s',get '%s'", apiGetPaidUnionID, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ queries := []string{"openid", "access_token", "transaction_id"}
+ for _, v := range queries {
+ content := r.Form.Get(v)
+ if content == "" {
+ t.Fatalf("%v can not be empty", v)
+ }
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "unionid": "oTmHYjg-tElZ68xxxxxxxxhy1Rgk",
+ "errcode": 0,
+ "errmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := getPaidUnionID("mock-access-token", "mock-open-id", "mock-transaction-id", ts.URL+apiGetPaidUnionID)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetPaidUnionIDWithMCH(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "GET" {
+ t.Fatalf("Expect 'GET' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiGetPaidUnionID {
+ t.Fatalf("Except to path '%s',get '%s'", apiGetPaidUnionID, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ queries := []string{"openid", "access_token", "mch_id", "out_trade_no"}
+ for _, v := range queries {
+ content := r.Form.Get(v)
+ if content == "" {
+ t.Fatalf("%v can not be empty", v)
+ }
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "unionid": "oTmHYjg-tElZ68xxxxxxxxhy1Rgk",
+ "errcode": 0,
+ "errmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := getPaidUnionIDWithMCH("mock-access-token", "mock-open-id", "mock-out-trade-number", "mock-mch-id", ts.URL+apiGetPaidUnionID)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/app/lib/weapp/common_error.go b/app/lib/weapp/common_error.go
new file mode 100644
index 0000000..f6ee34d
--- /dev/null
+++ b/app/lib/weapp/common_error.go
@@ -0,0 +1,34 @@
+package weapp
+
+import "errors"
+
+// CommonError 微信返回错误信息
+type CommonError struct {
+ ErrCode int `json:"errcode"` // 错误码
+ ErrMSG string `json:"errmsg"` // 错误描述
+}
+
+// GetResponseError 获取微信服务器错返回误信息
+func (err *CommonError) GetResponseError() error {
+ if err.ErrCode != 0 {
+ return errors.New(err.ErrMSG)
+ }
+
+ return nil
+}
+
+// CommonResult 微信返回错误信息
+type CommonResult struct {
+ ResultCode int `json:"resultcode"` // 错误码
+ ResultMsg string `json:"resultmsg"` // 错误描述
+}
+
+// GetResponseError 获取微信服务器错返回误信息
+func (err *CommonResult) GetResponseError() error {
+
+ if err.ResultCode != 0 {
+ return errors.New(err.ResultMsg)
+ }
+
+ return nil
+}
diff --git a/app/lib/weapp/crypto.go b/app/lib/weapp/crypto.go
new file mode 100644
index 0000000..f463d48
--- /dev/null
+++ b/app/lib/weapp/crypto.go
@@ -0,0 +1,114 @@
+package weapp
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "crypto/sha1"
+ "encoding/hex"
+ "errors"
+ "io"
+ "sort"
+ "strings"
+)
+
+const pkcs7blocksize = 32
+
+// pkcs7encode 对需要加密的明文进行填充补位
+// plaintext 需要进行填充补位操作的明文
+// 返回补齐明文字符串
+func pkcs7encode(plaintext []byte) []byte {
+ //计算需要填充的位数
+ pad := pkcs7blocksize - len(plaintext)%pkcs7blocksize
+ if pad == 0 {
+ pad = pkcs7blocksize
+ }
+
+ //获得补位所用的字符
+ text := bytes.Repeat([]byte{byte(pad)}, pad)
+
+ return append(plaintext, text...)
+}
+
+// pkcs7decode 对解密后的明文进行补位删除
+// plaintext 解密后的明文
+// 返回删除填充补位后的明文和
+func pkcs7decode(plaintext []byte) []byte {
+ ln := len(plaintext)
+
+ // 获取最后一个字符的 ASCII
+ pad := int(plaintext[ln-1])
+ if pad < 1 || pad > pkcs7blocksize {
+ pad = 0
+ }
+
+ return plaintext[:(ln - pad)]
+}
+
+// 对加密数据包进行签名校验,确保数据的完整性。
+func validateSignature(signature string, parts ...string) bool {
+ return signature == createSignature(parts...)
+}
+
+// 校验用户数据数据
+func validateUserInfo(signature, rawData, ssk string) bool {
+ raw := sha1.Sum([]byte(rawData + ssk))
+ return signature == hex.EncodeToString(raw[:])
+}
+
+// 拼凑签名
+func createSignature(parts ...string) string {
+ sort.Strings(parts)
+ raw := sha1.Sum([]byte(strings.Join(parts, "")))
+
+ return hex.EncodeToString(raw[:])
+}
+
+// cbcEncrypt CBC 加密数据
+func cbcEncrypt(key, plaintext, iv []byte) ([]byte, error) {
+ if len(plaintext)%aes.BlockSize != 0 {
+ return nil, errors.New("plaintext is not a multiple of the block size")
+ }
+
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, err
+ }
+
+ ciphertext := make([]byte, aes.BlockSize+len(plaintext))
+ iv = iv[:aes.BlockSize]
+ if _, err := io.ReadFull(rand.Reader, iv); err != nil {
+ return nil, err
+ }
+
+ mode := cipher.NewCBCEncrypter(block, iv)
+ mode.CryptBlocks(ciphertext[aes.BlockSize:], plaintext)
+
+ return ciphertext, nil
+}
+
+// CBC解密数据
+func cbcDecrypt(key, ciphertext, iv []byte) ([]byte, error) {
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, err
+ }
+
+ size := aes.BlockSize
+ iv = iv[:size]
+ // ciphertext = ciphertext[size:] TODO: really useless?
+
+ if len(ciphertext) < size {
+ return nil, errors.New("ciphertext too short")
+ }
+
+ if len(ciphertext)%size != 0 {
+ return nil, errors.New("ciphertext is not a multiple of the block size")
+ }
+
+ mode := cipher.NewCBCDecrypter(block, iv)
+ mode.CryptBlocks(ciphertext, ciphertext)
+
+ return pkcs7decode(ciphertext), nil
+}
diff --git a/app/lib/weapp/customer_service_message.go b/app/lib/weapp/customer_service_message.go
new file mode 100644
index 0000000..c17e3b9
--- /dev/null
+++ b/app/lib/weapp/customer_service_message.go
@@ -0,0 +1,268 @@
+package weapp
+
+import (
+ "encoding/json"
+ "errors"
+ "net/http"
+ "strings"
+)
+
+const (
+ apiSendMessage = "/cgi-bin/message/custom/send"
+ apiSetTyping = "/cgi-bin/message/custom/typing"
+ apiUploadTemplateMedia = "/cgi-bin/media/upload"
+ apiGetTemplateMedia = "/cgi-bin/media/get"
+)
+
+// csMsgType 消息类型
+type csMsgType string
+
+// 所有消息类型
+const (
+ csMsgTypeText csMsgType = "text" // 文本消息类型
+ csMsgTypeLink = "link" // 图文链接消息类型
+ csMsgTypeImage = "image" // 图片消息类型
+ csMsgTypeMPCard = "miniprogrampage" // 小程序卡片消息类型
+)
+
+// csMessage 消息体
+type csMessage struct {
+ Receiver string `json:"touser"` // user openID
+ Type csMsgType `json:"msgtype"` // text | image | link | miniprogrampage
+ Text CSMsgText `json:"text,omitempty"`
+ Image CSMsgImage `json:"image,omitempty"`
+ Link CSMsgLink `json:"link,omitempty"`
+ MPCard CSMsgMPCard `json:"miniprogrampage,omitempty"`
+}
+
+// CSMsgText 接收的文本消息
+type CSMsgText struct {
+ Content string `json:"content"`
+}
+
+// SendTo 发送文本消息
+//
+// openID 用户openID
+// token 微信 access_token
+func (msg CSMsgText) SendTo(openID, token string) (*CommonError, error) {
+
+ params := csMessage{
+ Receiver: openID,
+ Type: csMsgTypeText,
+ Text: msg,
+ }
+
+ return sendMessage(token, params)
+}
+
+// CSMsgImage 客服图片消息
+type CSMsgImage struct {
+ MediaID string `json:"media_id"` // 发送的图片的媒体ID,通过 新增素材接口 上传图片文件获得。
+}
+
+// SendTo 发送图片消息
+//
+// openID 用户openID
+// token 微信 access_token
+func (msg CSMsgImage) SendTo(openID, token string) (*CommonError, error) {
+
+ params := csMessage{
+ Receiver: openID,
+ Type: csMsgTypeImage,
+ Image: msg,
+ }
+
+ return sendMessage(token, params)
+}
+
+// CSMsgLink 图文链接消息
+type CSMsgLink struct {
+ Title string `json:"title"`
+ Description string `json:"description"`
+ URL string `json:"url"`
+ ThumbURL string `json:"thumb_url"`
+}
+
+// SendTo 发送图文链接消息
+//
+// openID 用户openID
+// token 微信 access_token
+func (msg CSMsgLink) SendTo(openID, token string) (*CommonError, error) {
+
+ params := csMessage{
+ Receiver: openID,
+ Type: csMsgTypeLink,
+ Link: msg,
+ }
+
+ return sendMessage(token, params)
+}
+
+// CSMsgMPCard 接收的卡片消息
+type CSMsgMPCard struct {
+ Title string `json:"title"` // 标题
+ PagePath string `json:"pagepath"` // 小程序页面路径
+ ThumbMediaID string `json:"thumb_media_id"` // 小程序消息卡片的封面, image 类型的 media_id,通过 新增素材接口 上传图片文件获得,建议大小为 520*416
+}
+
+// SendTo 发送卡片消息
+//
+// openID 用户openID
+// token 微信 access_token
+func (msg CSMsgMPCard) SendTo(openID, token string) (*CommonError, error) {
+
+ params := csMessage{
+ Receiver: openID,
+ Type: "miniprogrampage",
+ MPCard: msg,
+ }
+
+ return sendMessage(token, params)
+}
+
+// send 发送消息
+//
+// token 微信 access_token
+func sendMessage(token string, params interface{}) (*CommonError, error) {
+ api := baseURL + apiSendMessage
+ return doSendMessage(token, params, api)
+}
+
+func doSendMessage(token string, params interface{}, api string) (*CommonError, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(CommonError)
+ if err := postJSON(url, params, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// SetTypingCommand 下发客服当前输入状态命令
+type SetTypingCommand = string
+
+// 所有下发客服当前输入状态命令
+const (
+ SetTypingCommandTyping SetTypingCommand = "Typing" // 对用户下发"正在输入"状态
+ SetTypingCommandCancelTyping = "CancelTyping" // 取消对用户的"正在输入"状态
+)
+
+// SetTyping 下发客服当前输入状态给用户。
+//
+// token 接口调用凭证
+// openID 用户的 OpenID
+// cmd 命令
+func SetTyping(token, openID string, cmd SetTypingCommand) (*CommonError, error) {
+ api := baseURL + apiSetTyping
+ return setTyping(token, openID, cmd, api)
+}
+
+func setTyping(token, openID string, cmd SetTypingCommand, api string) (*CommonError, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ params := requestParams{
+ "touser": openID,
+ "command": cmd,
+ }
+
+ res := new(CommonError)
+ if err := postJSON(url, params, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// TempMediaType 文件类型
+type TempMediaType = string
+
+// 所有文件类型
+const (
+ TempMediaTypeImage TempMediaType = "image" // 图片
+)
+
+// UploadTempMediaResponse 上传媒体文件返回
+type UploadTempMediaResponse struct {
+ CommonError
+ Type string `json:"type"` // 文件类型
+ MediaID string `json:"media_id"` // 媒体文件上传后,获取标识,3天内有效。
+ CreatedAt uint `json:"created_at"` // 媒体文件上传时间戳
+}
+
+// UploadTempMedia 把媒体文件上传到微信服务器。目前仅支持图片。用于发送客服消息或被动回复用户消息。
+//
+// token 接口调用凭证
+// mediaType 文件类型
+// medianame 媒体文件名
+func UploadTempMedia(token string, mediaType TempMediaType, medianame string) (*UploadTempMediaResponse, error) {
+ api := baseURL + apiUploadTemplateMedia
+ return uploadTempMedia(token, mediaType, medianame, api)
+}
+
+func uploadTempMedia(token string, mediaType TempMediaType, medianame, api string) (*UploadTempMediaResponse, error) {
+ queries := requestQueries{
+ "type": mediaType,
+ "access_token": token,
+ }
+
+ url, err := encodeURL(api, queries)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(UploadTempMediaResponse)
+ if err := postFormByFile(url, "media", medianame, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// GetTempMedia 获取客服消息内的临时素材。即下载临时的多媒体文件。目前小程序仅支持下载图片文件。
+//
+// token 接口调用凭证
+// mediaID 媒体文件 ID
+func GetTempMedia(token, mediaID string) (*http.Response, *CommonError, error) {
+ api := baseURL + apiGetTemplateMedia
+ return getTempMedia(token, mediaID, api)
+}
+
+func getTempMedia(token, mediaID, api string) (*http.Response, *CommonError, error) {
+ queries := requestQueries{
+ "access_token": token,
+ "media_id": mediaID,
+ }
+
+ url, err := encodeURL(api, queries)
+ if err != nil {
+ return nil, nil, err
+ }
+ res, err := http.Get(url)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ response := new(CommonError)
+ switch header := res.Header.Get("Content-Type"); {
+ case strings.HasPrefix(header, "application/json"): // 返回错误信息
+ if err := json.NewDecoder(res.Body).Decode(response); err != nil {
+ res.Body.Close()
+ return nil, nil, err
+ }
+ return res, response, nil
+
+ case strings.HasPrefix(header, "image"): // 返回文件 TODO: 应该确认一下
+ return res, response, nil
+
+ default:
+ res.Body.Close()
+ return nil, nil, errors.New("invalid response header: " + header)
+ }
+}
diff --git a/app/lib/weapp/customer_service_message_test.go b/app/lib/weapp/customer_service_message_test.go
new file mode 100644
index 0000000..6e96743
--- /dev/null
+++ b/app/lib/weapp/customer_service_message_test.go
@@ -0,0 +1,343 @@
+package weapp
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path"
+ "reflect"
+ "testing"
+)
+
+func TestSetTyping(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiSetTyping {
+ t.Fatalf("Except to path '%s',get '%s'", apiSetTyping, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := make(map[string]interface{})
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+ param, ok := params["touser"]
+ if !ok || param == "" {
+ t.Error("param touser can not be empty")
+ }
+ param, ok = params["command"]
+ if !ok || param == "" {
+ t.Error("param command can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := setTyping("mock-access-token", "mock-open-id", "mock-command", ts.URL+apiSetTyping)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestUploadTempMedia(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiUploadTemplateMedia {
+ t.Fatalf("Except to path '%s',get '%s'", apiUploadTemplateMedia, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ queries := []string{"access_token", "type"}
+ for _, v := range queries {
+ content := r.Form.Get(v)
+ if content == "" {
+ t.Fatalf("%v can not be empty", v)
+ }
+ }
+
+ if _, _, err := r.FormFile("media"); err != nil {
+ t.Fatal(err)
+
+ }
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok",
+ "type": "image",
+ "media_id": "MEDIA_ID",
+ "created_at": 1234567890
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := uploadTempMedia("mock-access-token", "mock-media-type", testIMGName, ts.URL+apiUploadTemplateMedia)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetTempMedia(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "GET" {
+ t.Fatalf("Expect 'GET' get '%s'", r.Method)
+ }
+
+ ePath := r.URL.EscapedPath()
+ if ePath != apiGetTemplateMedia {
+ t.Fatalf("Except to path '%s',get '%s'", apiGetTemplateMedia, ePath)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ queries := []string{"access_token", "media_id"}
+ for _, v := range queries {
+ content := r.Form.Get(v)
+ if content == "" {
+ t.Fatalf("%v can not be empty", v)
+ }
+ }
+
+ filename := testIMGName
+ file, err := os.Open(filename)
+ if err != nil {
+ t.Fatal((err))
+ }
+ defer file.Close()
+
+ ext := path.Ext(filename)
+ ext = ext[1:len(ext)]
+ w.Header().Set("Content-Type", "image/"+ext)
+ w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", path.Base(filename)))
+ w.WriteHeader(http.StatusOK)
+
+ if _, err := io.Copy(w, file); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ resp, _, err := getTempMedia("mock-access-token", "mock-media-id", ts.URL+apiGetTemplateMedia)
+ if err != nil {
+ t.Fatal(err)
+ }
+ resp.Body.Close()
+}
+
+func TestSendCustomerServiceMessage(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiSendMessage {
+ t.Fatalf("Except to path '%s',get '%s'", apiSendMessage, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Error("access_token can not be empty")
+ }
+
+ params := make(object)
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ param, ok := params["touser"]
+ if !ok || param == "" {
+ t.Error("param touser can not be empty")
+ }
+ param, ok = params["msgtype"]
+ if !ok || param == "" {
+ t.Error("param command can not be empty")
+ }
+ switch param {
+
+ case "text":
+ param, ok := params["text"]
+ if !ok {
+ t.Error("param text can not be empty")
+ }
+
+ obj, ok := param.(object)
+ if !ok {
+ t.Errorf("unexpected value type of tex: %s", reflect.TypeOf(param))
+ }
+
+ param, ok = obj["content"]
+ if !ok {
+ t.Error("param text.content can not be empty")
+ }
+
+ case "image":
+ param, ok := params["image"]
+ if !ok {
+ t.Error("param command can not be empty")
+ }
+
+ obj, ok := param.(object)
+ if !ok {
+ t.Error("unexpected value type of image")
+ }
+
+ param, ok = obj["media_id"]
+ if !ok {
+ t.Error("param image.media_id can not be empty")
+ }
+
+ case "link":
+ param, ok := params["link"]
+ if !ok {
+ t.Error("param link can not be empty")
+ }
+
+ obj, ok := param.(object)
+ if !ok {
+ t.Error("unexpected value type of link")
+ }
+
+ param, ok = obj["title"]
+ if !ok {
+ t.Error("param link.title can not be empty")
+ }
+
+ param, ok = obj["description"]
+ if !ok {
+ t.Error("param link.description can not be empty")
+ }
+
+ param, ok = obj["url"]
+ if !ok {
+ t.Error("param link.url can not be empty")
+ }
+
+ param, ok = obj["thumb_url"]
+ if !ok {
+ t.Error("param link.thumb_url can not be empty")
+ }
+
+ case "miniprogrampage":
+ param, ok := params["miniprogrampage"]
+ if !ok {
+ t.Error("param miniprogrampage can not be empty")
+ }
+
+ obj, ok := param.(object)
+ if !ok {
+ t.Error("unexpected value type of miniprogrampage")
+ }
+
+ param, ok = obj["title"]
+ if !ok {
+ t.Error("param miniprogrampage.title can not be empty")
+ }
+
+ param, ok = obj["pagepath"]
+ if !ok {
+ t.Error("param miniprogrampage.pagepath can not be empty")
+ }
+
+ param, ok = obj["thumb_media_id"]
+ if !ok {
+ t.Error("param miniprogrampage.thumb_media_id can not be empty")
+ }
+
+ default:
+ t.Fatalf("unexpected msgtype: %s", param)
+ }
+
+ w.WriteHeader(http.StatusOK)
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ messages := []csMessage{
+ csMessage{
+ Receiver: "mock-open-id",
+ Type: csMsgTypeText,
+ Image: CSMsgImage{
+ MediaID: "mock-media-id",
+ },
+ },
+ csMessage{
+ Receiver: "mock-open-id",
+ Type: csMsgTypeLink,
+ Link: CSMsgLink{
+ Title: "mock-title",
+ Description: "mock-description",
+ URL: "mock-url",
+ ThumbURL: "mock-thumb-url",
+ },
+ },
+ csMessage{
+ Receiver: "mock-open-id",
+ Type: csMsgTypeMPCard,
+ MPCard: CSMsgMPCard{
+ Title: "mock-title",
+ PagePath: "mock-page-path",
+ ThumbMediaID: "mock-thumb-media-id",
+ },
+ },
+ csMessage{
+ Receiver: "mock-open-id",
+ Type: csMsgTypeText,
+ Text: CSMsgText{
+ Content: "mock-content",
+ },
+ },
+ }
+ for _, msg := range messages {
+ _, err := doSendMessage("mock-access-token", msg, ts.URL+apiSendMessage)
+ if err != nil {
+ t.Error(err)
+ }
+ }
+}
diff --git a/app/lib/weapp/decrypt.go b/app/lib/weapp/decrypt.go
new file mode 100644
index 0000000..b21199e
--- /dev/null
+++ b/app/lib/weapp/decrypt.go
@@ -0,0 +1,154 @@
+package weapp
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+)
+
+// DecryptUserData 解密用户数据
+func DecryptUserData(ssk, ciphertext, iv string) ([]byte, error) {
+ key, err := base64.StdEncoding.DecodeString(ssk)
+ if err != nil {
+ return nil, err
+ }
+ cipher, err := base64.StdEncoding.DecodeString(ciphertext)
+ if err != nil {
+ return nil, err
+ }
+
+ rawIV, err := base64.StdEncoding.DecodeString(iv)
+ if err != nil {
+ return nil, err
+ }
+ return cbcDecrypt(key, cipher, rawIV)
+}
+
+type watermark struct {
+ AppID string `json:"appid"`
+ Timestamp int64 `json:"timestamp"`
+}
+
+// Mobile 解密后的用户手机号码信息
+type Mobile struct {
+ PhoneNumber string `json:"phoneNumber"`
+ PurePhoneNumber string `json:"purePhoneNumber"`
+ CountryCode string `json:"countryCode"`
+ Watermark watermark `json:"watermark"`
+}
+
+// DecryptMobile 解密手机号码
+//
+// sessionKey 通过 Login 向微信服务端请求得到的 session_key
+// encryptedData 小程序通过 api 得到的加密数据(encryptedData)
+// iv 小程序通过 api 得到的初始向量(iv)
+func DecryptMobile(sessionKey, encryptedData, iv string) (*Mobile, error) {
+ raw, err := DecryptUserData(sessionKey, encryptedData, iv)
+ if err != nil {
+ return nil, err
+ }
+
+ mobile := new(Mobile)
+ if err := json.Unmarshal(raw, mobile); err != nil {
+ return nil, err
+ }
+
+ return mobile, nil
+}
+
+// ShareInfo 解密后的分享信息
+type ShareInfo struct {
+ GID string `json:"openGId"`
+}
+
+// DecryptShareInfo 解密转发信息的加密数据
+//
+// sessionKey 通过 Login 向微信服务端请求得到的 session_key
+// encryptedData 小程序通过 api 得到的加密数据(encryptedData)
+// iv 小程序通过 api 得到的初始向量(iv)
+//
+// gid 小程序唯一群号
+func DecryptShareInfo(sessionKey, encryptedData, iv string) (*ShareInfo, error) {
+
+ raw, err := DecryptUserData(sessionKey, encryptedData, iv)
+ if err != nil {
+ return nil, err
+ }
+
+ info := new(ShareInfo)
+ if err = json.Unmarshal(raw, info); err != nil {
+ return nil, err
+ }
+
+ return info, nil
+}
+
+// UserInfo 解密后的用户信息
+type UserInfo struct {
+ OpenID string `json:"openId"`
+ Nickname string `json:"nickName"`
+ Gender int `json:"gender"`
+ Province string `json:"province"`
+ Language string `json:"language"`
+ Country string `json:"country"`
+ City string `json:"city"`
+ Avatar string `json:"avatarUrl"`
+ UnionID string `json:"unionId"`
+ Watermark watermark `json:"watermark"`
+}
+
+// DecryptUserInfo 解密用户信息
+//
+// sessionKey 微信 session_key
+// rawData 不包括敏感信息的原始数据字符串,用于计算签名。
+// encryptedData 包括敏感数据在内的完整用户信息的加密数据
+// signature 使用 sha1( rawData + session_key ) 得到字符串,用于校验用户信息
+// iv 加密算法的初始向量
+func DecryptUserInfo(sessionKey, rawData, encryptedData, signature, iv string) (*UserInfo, error) {
+
+ if ok := validateUserInfo(signature, rawData, sessionKey); !ok {
+ return nil, errors.New("failed to validate signature")
+ }
+
+ raw, err := DecryptUserData(sessionKey, encryptedData, iv)
+ if err != nil {
+ return nil, err
+ }
+
+ info := new(UserInfo)
+ if err := json.Unmarshal(raw, info); err != nil {
+ return nil, err
+ }
+
+ return info, nil
+}
+
+// RunData 解密后的最近30天微信运动步数
+type RunData struct {
+ StepInfoList []SetpInfo `json:"stepInfoList"`
+}
+
+// SetpInfo 运动步数
+type SetpInfo struct {
+ Step int `json:"step"`
+ Timestamp int64 `json:"timestamp"`
+}
+
+// DecryptRunData 解密微信运动的加密数据
+//
+// sessionKey 通过 Login 向微信服务端请求得到的 session_key
+// encryptedData 小程序通过 api 得到的加密数据(encryptedData)
+// iv 小程序通过 api 得到的初始向量(iv)
+func DecryptRunData(sessionKey, encryptedData, iv string) (*RunData, error) {
+ raw, err := DecryptUserData(sessionKey, encryptedData, iv)
+ if err != nil {
+ return nil, err
+ }
+
+ info := new(RunData)
+ if err := json.Unmarshal(raw, info); err != nil {
+ return nil, err
+ }
+
+ return info, nil
+}
diff --git a/app/lib/weapp/express.go b/app/lib/weapp/express.go
new file mode 100644
index 0000000..492a939
--- /dev/null
+++ b/app/lib/weapp/express.go
@@ -0,0 +1,66 @@
+package weapp
+
+// ExpressOrder 物流订单
+type ExpressOrder struct {
+ OrderID string `json:"order_id"` // 订单ID,须保证全局唯一,不超过512字节
+ OpenID string `json:"openid,omitempty"` // 用户openid,当add_source=2时无需填写(不发送物流服务通知)
+ DeliveryID string `json:"delivery_id"` // 快递公司ID,参见getAllDelivery
+ BizID string `json:"biz_id"` // 快递客户编码或者现付编码
+ CustomRemark string `json:"custom_remark,omitempty"` // 快递备注信息,比如"易碎物品",不超过1024字节
+ Sender ExpreseeUserInfo `json:"sender"` // 发件人信息
+ Receiver ExpreseeUserInfo `json:"receiver"` // 收件人信息
+ Cargo ExpressCargo `json:"cargo"` // 包裹信息,将传递给快递公司
+ Shop ExpressShop `json:"shop,omitempty"` // 商家信息,会展示到物流服务通知中,当add_source=2时无需填写(不发送物流服务通知)
+ Insured ExpressInsure `json:"insured"` // 保价信息
+ Service ExpressService `json:"service"` // 服务类型
+}
+
+// ExpreseeUserInfo 收件人/发件人信息
+type ExpreseeUserInfo struct {
+ Name string `json:"name"` // 收件人/发件人姓名,不超过64字节
+ Tel string `json:"tel,omitempty"` // 收件人/发件人座机号码,若不填写则必须填写 mobile,不超过32字节
+ Mobile string `json:"mobile,omitempty"` // 收件人/发件人手机号码,若不填写则必须填写 tel,不超过32字节
+ Company string `json:"company,omitempty"` // 收件人/发件人公司名称,不超过64字节
+ PostCode string `json:"post_code,omitempty"` // 收件人/发件人邮编,不超过10字节
+ Country string `json:"country,omitempty"` // 收件人/发件人国家,不超过64字节
+ Province string `json:"province"` // 收件人/发件人省份,比如:"广东省",不超过64字节
+ City string `json:"city"` // 收件人/发件人市/地区,比如:"广州市",不超过64字节
+ Area string `json:"area"` // 收件人/发件人区/县,比如:"海珠区",不超过64字节
+ Address string `json:"address"` // 收件人/发件人详细地址,比如:"XX路XX号XX大厦XX",不超过512字节
+}
+
+// ExpressCargo 包裹信息
+type ExpressCargo struct {
+ Count uint `json:"count"` // 包裹数量
+ Weight float64 `json:"weight"` // 包裹总重量,单位是千克(kg)
+ SpaceX float64 `json:"space_x"` // 包裹长度,单位厘米(cm)
+ SpaceY float64 `json:"space_y"` // 包裹宽度,单位厘米(cm)
+ SpaceZ float64 `json:"space_z"` // 包裹高度,单位厘米(cm)
+ DetailList []CargoDetail `json:"detail_list"` // 包裹中商品详情列表
+}
+
+// CargoDetail 包裹详情
+type CargoDetail struct {
+ Name string `json:"name"` // 商品名,不超过128字节
+ Count uint `json:"count"` // 商品数量
+}
+
+// ExpressShop 商家信息
+type ExpressShop struct {
+ WXAPath string `json:"wxa_path"` // 商家小程序的路径,建议为订单页面
+ IMGUrl string `json:"img_url"` // 商品缩略图 url
+ GoodsName string `json:"goods_name"` // 商品名称
+ GoodsCount uint `json:"goods_count"` // 商品数量
+}
+
+// ExpressInsure 订单保价
+type ExpressInsure struct {
+ Used InsureStatus `json:"use_insured"` // 是否保价,0 表示不保价,1 表示保价
+ Value uint `json:"insured_value"` // 保价金额,单位是分,比如: 10000 表示 100 元
+}
+
+// ExpressService 服务类型
+type ExpressService struct {
+ Type uint8 `json:"service_type"` // 服务类型ID
+ Name string `json:"service_name"` // 服务名称
+}
diff --git a/app/lib/weapp/express_business.go b/app/lib/weapp/express_business.go
new file mode 100644
index 0000000..de49c2f
--- /dev/null
+++ b/app/lib/weapp/express_business.go
@@ -0,0 +1,432 @@
+package weapp
+
+const (
+ apiBindAccount = "/cgi-bin/express/business/account/bind"
+ apiGetAllAccount = "/cgi-bin/express/business/account/getall"
+ apiGetExpressPath = "/cgi-bin/express/business/path/get"
+ apiAddExpressOrder = "/cgi-bin/express/business/order/add"
+ apiCancelExpressOrder = "/cgi-bin/express/business/order/cancel"
+ apiGetAllDelivery = "/cgi-bin/express/business/delivery/getall"
+ apiGetExpressOrder = "/cgi-bin/express/business/order/get"
+ apiGetPrinter = "/cgi-bin/express/business/printer/getall"
+ apiGetQuota = "/cgi-bin/express/business/quota/get"
+ apiUpdatePrinter = "/cgi-bin/express/business/printer/update"
+ apiTestUpdateOrder = "/cgi-bin/express/business/test_update_order"
+)
+
+// ExpressAccount 物流账号
+type ExpressAccount struct {
+ Type BindType `json:"type"` // bind表示绑定,unbind表示解除绑定
+ BizID string `json:"biz_id"` // 快递公司客户编码
+ DeliveryID string `json:"delivery_id"` // 快递公司 ID
+ Password string `json:"password"` // 快递公司客户密码
+ RemarkContent string `json:"remark_content"` // 备注内容(提交EMS审核需要)
+}
+
+// BindType 绑定动作类型
+type BindType = string
+
+// 所有绑定动作类型
+const (
+ Bind = "bind" // 绑定
+ Unbind = "unbind" // 解除绑定
+)
+
+// Bind 绑定、解绑物流账号
+// token 接口调用凭证
+func (ea *ExpressAccount) Bind(token string) (*CommonError, error) {
+ api := baseURL + apiBindAccount
+ return ea.bind(api, token)
+}
+
+func (ea *ExpressAccount) bind(api, token string) (*CommonError, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(CommonError)
+ if err := postJSON(url, ea, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// AccountList 所有绑定的物流账号
+type AccountList struct {
+ CommonError
+ Count uint `json:"count"` // 账号数量
+ List []struct {
+ BizID string `json:"biz_id"` // 快递公司客户编码
+ DeliveryID string `json:"delivery_id"` // 快递公司 ID
+ CreateTime uint `json:"create_time"` // 账号绑定时间
+ UpdateTime uint `json:"update_time"` // 账号更新时间
+ StatusCode BindStatus `json:"status_code"` // 绑定状态
+ Alias string `json:"alias"` // 账号别名
+ RemarkWrongMsg string `json:"remark_wrong_msg"` // 账号绑定失败的错误信息(EMS审核结果)
+ RemarkContent string `json:"remark_content"` // 账号绑定时的备注内容(提交EMS审核需要))
+ QuotaNum uint `json:"quota_num"` // 电子面单余额
+ QuotaUpdateTime uint `json:"quota_update_time"` // 电子面单余额更新时间
+ } `json:"list"` // 账号列表
+}
+
+// BindStatus 账号绑定状态
+type BindStatus = int8
+
+// 所有账号绑定状态
+const (
+ BindSuccess = 0 // 成功
+ BindFailed = -1 // 系统失败
+)
+
+// GetAllAccount 获取所有绑定的物流账号
+// token 接口调用凭证
+func GetAllAccount(token string) (*AccountList, error) {
+ api := baseURL + apiGetAllAccount
+ return getAllAccount(api, token)
+}
+
+func getAllAccount(api, token string) (*AccountList, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(AccountList)
+ if err := postJSON(url, requestParams{}, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// ExpressPathGetter 查询运单轨迹所需参数
+type ExpressPathGetter ExpressOrderGetter
+
+// GetExpressPathResponse 运单轨迹
+type GetExpressPathResponse struct {
+ CommonError
+ OpenID string `json:"openid"` // 用户openid
+ DeliveryID string `json:"delivery_id"` // 快递公司 ID
+ WaybillID string `json:"waybill_id"` // 运单 ID
+ PathItemNum uint `json:"path_item_num"` // 轨迹节点数量
+ PathItemList []ExpressPathNode `json:"path_item_list"` // 轨迹节点列表
+}
+
+// ExpressPathNode 运单轨迹节点
+type ExpressPathNode struct {
+ ActionTime uint `json:"action_time"` // 轨迹节点 Unix 时间戳
+ ActionType uint `json:"action_type"` // 轨迹节点类型
+ ActionMsg string `json:"action_msg"` // 轨迹节点详情
+}
+
+// Get 查询运单轨迹
+// token 接口调用凭证
+func (ep *ExpressPathGetter) Get(token string) (*GetExpressPathResponse, error) {
+ api := baseURL + apiGetExpressPath
+ return ep.get(api, token)
+}
+
+func (ep *ExpressPathGetter) get(api, token string) (*GetExpressPathResponse, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(GetExpressPathResponse)
+ if err := postJSON(url, ep, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// ExpressOrderSource 订单来源
+type ExpressOrderSource = uint8
+
+// 所有订单来源
+const (
+ FromWeapp ExpressOrderSource = 0 // 小程序订单
+ FromAppOrH5 = 2 // APP或H5订单
+)
+
+// ExpressOrderCreator 订单创建器
+type ExpressOrderCreator struct {
+ ExpressOrder
+ AddSource ExpressOrderSource `json:"add_source"` // 订单来源,0为小程序订单,2为App或H5订单,填2则不发送物流服务通知
+ WXAppID string `json:"wx_appid,omitempty"` // App或H5的appid,add_source=2时必填,需和开通了物流助手的小程序绑定同一open帐号
+ ExpectTime uint `json:"expect_time,omitempty"` // 顺丰必须填写此字段。预期的上门揽件时间,0表示已事先约定取件时间;否则请传预期揽件时间戳,需大于当前时间,收件员会在预期时间附近上门。例如expect_time为“1557989929”,表示希望收件员将在2019年05月16日14:58:49-15:58:49内上门取货。
+ TagID uint `json:"tagid,omitempty"` //订单标签id,用于平台型小程序区分平台上的入驻方,tagid须与入驻方账号一一对应,非平台型小程序无需填写该字段
+}
+
+// InsureStatus 保价状态
+type InsureStatus = uint8
+
+// 所有保价状态
+const (
+ Uninsured = 0 // 不保价
+ Insured = 1 // 保价
+)
+
+// CreateExpressOrderResponse 创建订单返回数据
+type CreateExpressOrderResponse struct {
+ CommonError
+ OrderID string `json:"order_id"` // 订单ID,下单成功时返回
+ WaybillID string `json:"waybill_id"` // 运单ID,下单成功时返回
+ WaybillData []struct {
+ Key string `json:"key"` // 运单信息 key
+ Value string `json:"value"` // 运单信息 value
+ } `json:"waybill_data"` // 运单信息,下单成功时返回
+ DeliveryResultcode int `json:"delivery_resultcode"` // 快递侧错误码,下单失败时返回
+ DeliveryResultmsg string `json:"delivery_resultmsg"` // 快递侧错误信息,下单失败时返回
+}
+
+// Create 生成运单
+// token 接口调用凭证
+func (creator *ExpressOrderCreator) Create(token string) (*CreateExpressOrderResponse, error) {
+ api := baseURL + apiAddExpressOrder
+ return creator.create(api, token)
+}
+
+func (creator *ExpressOrderCreator) create(api, token string) (*CreateExpressOrderResponse, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(CreateExpressOrderResponse)
+ if err := postJSON(url, creator, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// CancelOrderResponse 取消订单返回数据
+type CancelOrderResponse struct {
+ CommonError
+ Count uint `json:"count"` //快递公司数量
+ Data []struct {
+ DeliveryID string `json:"delivery_id"` // 快递公司 ID
+ DeliveryName string `json:"delivery_name"` // 快递公司名称
+
+ } `json:"data"` //快递公司信息列表
+}
+
+// DeliveryList 支持的快递公司列表
+type DeliveryList struct {
+ CommonError
+ Count uint `json:"count"` // 快递公司数量
+ Data []struct {
+ ID string `json:"delivery_id"` // 快递公司 ID
+ Name string `json:"delivery_name"` // 快递公司名称
+ } `json:"data"` // 快递公司信息列表
+}
+
+// GetAllDelivery 获取支持的快递公司列表
+// token 接口调用凭证
+func GetAllDelivery(token string) (*DeliveryList, error) {
+ api := baseURL + apiGetAllDelivery
+ return getAllDelivery(api, token)
+}
+
+func getAllDelivery(api, token string) (*DeliveryList, error) {
+ queries := requestQueries{
+ "access_token": token,
+ }
+
+ url, err := encodeURL(api, queries)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(DeliveryList)
+ if err := getJSON(url, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// ExpressOrderGetter 订单获取器
+type ExpressOrderGetter struct {
+ OrderID string `json:"order_id"` // 订单 ID,需保证全局唯一
+ OpenID string `json:"openid,omitempty"` // 用户openid,当add_source=2时无需填写(不发送物流服务通知)
+ DeliveryID string `json:"delivery_id"` // 快递公司ID,参见getAllDelivery
+ WaybillID string `json:"waybill_id"` // 运单ID
+}
+
+// GetExpressOrderResponse 获取运单返回数据
+type GetExpressOrderResponse struct {
+ CommonError
+ PrintHTML string `json:"print_html"` // 运单 html 的 BASE64 结果
+ WaybillData []struct {
+ Key string `json:"key"` // 运单信息 key
+ Value string `json:"value"` // 运单信息 value
+ } `json:"waybill_data"` // 运单信息
+}
+
+// Get 获取运单数据
+// token 接口调用凭证
+func (getter *ExpressOrderGetter) Get(token string) (*GetExpressOrderResponse, error) {
+ api := baseURL + apiGetExpressOrder
+ return getter.get(api, token)
+}
+
+func (getter *ExpressOrderGetter) get(api, token string) (*GetExpressOrderResponse, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(GetExpressOrderResponse)
+ if err := postJSON(url, getter, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// ExpressOrderCanceler 订单取消器
+type ExpressOrderCanceler ExpressOrderGetter
+
+// Cancel 取消运单
+// token 接 口调用凭证
+func (canceler *ExpressOrderCanceler) Cancel(token string) (*CommonError, error) {
+ api := baseURL + apiCancelExpressOrder
+ return canceler.cancel(api, token)
+}
+
+func (canceler *ExpressOrderCanceler) cancel(api, token string) (*CommonError, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(CommonError)
+ if err := postJSON(url, canceler, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// GetPrinterResponse 获取打印员返回数据
+type GetPrinterResponse struct {
+ CommonError
+ Count uint `json:"count"` // 已经绑定的打印员数量
+ OpenID []string `json:"openid"` // 打印员 openid 列表
+ TagIDList []string `json:"tagid_list"`
+}
+
+// GetPrinter 获取打印员。若需要使用微信打单 PC 软件,才需要调用。
+// token 接口调用凭证
+func GetPrinter(token string) (*GetPrinterResponse, error) {
+ api := baseURL + apiGetPrinter
+ return getPrinter(api, token)
+}
+
+func getPrinter(api, token string) (*GetPrinterResponse, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(GetPrinterResponse)
+ if err := getJSON(url, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// QuotaGetter 电子面单余额获取器
+type QuotaGetter struct {
+ DeliveryID string `json:"delivery_id"` // 快递公司ID,参见getAllDelivery
+ BizID string `json:"biz_id"` // 快递公司客户编码
+}
+
+// QuotaGetResponse 电子面单余额
+type QuotaGetResponse struct {
+ CommonError
+ Number uint // 电子面单余额
+}
+
+// Get 获取电子面单余额。仅在使用加盟类快递公司时,才可以调用。
+func (getter *QuotaGetter) Get(token string) (*QuotaGetResponse, error) {
+ api := baseURL + apiGetQuota
+ return getter.get(api, token)
+}
+
+func (getter *QuotaGetter) get(api, token string) (*QuotaGetResponse, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(QuotaGetResponse)
+ if err := postJSON(url, getter, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// UpdateExpressOrderTester 模拟的快递公司更新订单
+type UpdateExpressOrderTester struct {
+ BizID string `json:"biz_id"` // 商户id,需填test_biz_id
+ OrderID string `json:"order_id"` // 订单ID,下单成功时返回
+ WaybillID string `json:"waybill_id"` // 运单 ID
+ DeliveryID string `json:"delivery_id"` // 快递公司 ID
+ ActionTime uint `json:"action_time"` // 轨迹变化 Unix 时间戳
+ ActionType uint `json:"action_type"` // 轨迹变化类型
+ ActionMsg string `json:"action_msg"` // 轨迹变化具体信息说明,展示在快递轨迹详情页中。若有手机号码,则直接写11位手机号码。使用UTF-8编码。
+}
+
+// Test 模拟快递公司更新订单状态, 该接口只能用户测试
+func (tester *UpdateExpressOrderTester) Test(token string) (*CommonError, error) {
+ api := baseURL + apiTestUpdateOrder
+ return tester.test(api, token)
+}
+
+func (tester *UpdateExpressOrderTester) test(api, token string) (*CommonError, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(CommonError)
+ if err := postJSON(url, tester, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// PrinterUpdater 打印员更新器
+type PrinterUpdater struct {
+ OpenID string `json:"openid"` // 打印员 openid
+ Type BindType `json:"update_type"` // 更新类型
+ TagIDList string `json:"tagid_list"` // 用于平台型小程序设置入驻方的打印员面单打印权限,同一打印员最多支持10个tagid,使用逗号分隔,如填写123,456,表示该打印员可以拉取到tagid为123和456的下的单,非平台型小程序无需填写该字段
+}
+
+// Update 更新打印员。若需要使用微信打单 PC 软件,才需要调用。
+func (updater *PrinterUpdater) Update(token string) (*CommonError, error) {
+ api := baseURL + apiUpdatePrinter
+ return updater.update(api, token)
+}
+
+func (updater *PrinterUpdater) update(api, token string) (*CommonError, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(CommonError)
+ if err := postJSON(url, updater, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
diff --git a/app/lib/weapp/express_business_test.go b/app/lib/weapp/express_business_test.go
new file mode 100644
index 0000000..3fc7717
--- /dev/null
+++ b/app/lib/weapp/express_business_test.go
@@ -0,0 +1,954 @@
+package weapp
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+)
+
+func TestAddExpressOrder(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiAddExpressOrder {
+ t.Fatalf("Except to path '%s',get '%s'", apiAddExpressOrder, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ AddSource uint8 `json:"add_source"`
+ WXAppID string `json:"wx_appid"`
+ OrderID string `json:"order_id"`
+ OpenID string `json:"openid"`
+ DeliveryID string `json:"delivery_id"`
+ BizID string `json:"biz_id"`
+ CustomRemark string `json:"custom_remark"`
+ Sender struct {
+ Name string `json:"name"`
+ Tel string `json:"tel"`
+ Mobile string `json:"mobile"`
+ Company string `json:"company"`
+ PostCode string `json:"post_code"`
+ Country string `json:"country"`
+ Province string `json:"province"`
+ City string `json:"city"`
+ Area string `json:"area"`
+ Address string `json:"address"`
+ } `json:"sender"`
+ Receiver struct {
+ Name string `json:"name"`
+ Tel string `json:"tel"`
+ Mobile string `json:"mobile"`
+ Company string `json:"company"`
+ PostCode string `json:"post_code"`
+ Country string `json:"country"`
+ Province string `json:"province"`
+ City string `json:"city"`
+ Area string `json:"area"`
+ Address string `json:"address"`
+ } `json:"receiver"`
+ Cargo struct {
+ Count uint `json:"count"`
+ Weight float64 `json:"weight"`
+ SpaceX float64 `json:"space_x"`
+ SpaceY float64 `json:"space_y"`
+ SpaceZ float64 `json:"space_z"`
+ DetailList []struct {
+ Name string `json:"name"`
+ Count uint `json:"count"`
+ } `json:"detail_list"`
+ } `json:"cargo"`
+ Shop struct {
+ WXAPath string `json:"wxa_path"`
+ IMGUrl string `json:"img_url"`
+ GoodsName string `json:"goods_name"`
+ GoodsCount uint `json:"goods_count"`
+ } `json:"shop"`
+ Insured struct {
+ Used InsureStatus `json:"use_insured"`
+ Value uint `json:"insured_value"`
+ } `json:"insured"`
+ Service struct {
+ Type uint8 `json:"service_type"`
+ Name string `json:"service_name"`
+ } `json:"service"`
+ ExpectTime uint `json:"expect_time"`
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.AddSource == 2 && params.WXAppID == "" {
+ t.Error("param wx_appid can not be empty")
+ }
+ if params.AddSource != 2 && params.OpenID == "" {
+ t.Error("param openid can not be empty")
+ }
+ if params.OrderID == "" {
+ t.Error("param order_id can not be empty")
+ }
+ if params.DeliveryID == "" {
+ t.Error("param delivery_id can not be empty")
+ }
+
+ if params.BizID == "" {
+ t.Error("param biz_id can not be empty")
+ }
+
+ if params.Sender.Name == "" {
+ t.Error("param sender.name can not be empty")
+ }
+ if params.Sender.Province == "" {
+ t.Error("param sender.province can not be empty")
+ }
+ if params.Sender.City == "" {
+ t.Error("param sender.city can not be empty")
+ }
+ if params.Sender.Area == "" {
+ t.Error("param sender.area can not be empty")
+ }
+ if params.Sender.Address == "" {
+ t.Error("param sender.address can not be empty")
+ }
+ if params.Receiver.Name == "" {
+ t.Error("param receiver.name can not be empty")
+ }
+ if params.Receiver.Province == "" {
+ t.Error("param receiver.province can not be empty")
+ }
+ if params.Receiver.City == "" {
+ t.Error("param receiver.city can not be empty")
+ }
+ if params.Receiver.Area == "" {
+ t.Error("param receiver.area can not be empty")
+ }
+ if params.Receiver.Address == "" {
+ t.Error("param receiver.address can not be empty")
+ }
+
+ if params.Cargo.Count == 0 {
+ t.Error("param cargo.count can not be zero")
+ }
+ if params.Cargo.Weight == 0 {
+ t.Error("param cargo.weight can not be zero")
+ }
+ if params.Cargo.SpaceX == 0 {
+ t.Error("param cargo.spaceX can not be zero")
+ }
+ if params.Cargo.SpaceY == 0 {
+ t.Error("param cargo.spaceY can not be zero")
+ }
+ if params.Cargo.SpaceZ == 0 {
+ t.Error("param cargo.spaceZ can not be zero")
+ }
+ if len(params.Cargo.DetailList) == 0 {
+ t.Error("param cargo.detailList can not be empty")
+ } else {
+ if (params.Cargo.DetailList[0].Name) == "" {
+ t.Error("param cargo.detailList.name can not be empty")
+ }
+ if (params.Cargo.DetailList[0].Count) == 0 {
+ t.Error("param cargo.detailList.count can not be zero")
+ }
+ }
+ if params.Shop.WXAPath == "" {
+ t.Error("param shop.wxa_path can not be empty")
+ }
+ if params.Shop.IMGUrl == "" {
+ t.Error("param shop.img_url can not be empty")
+ }
+ if params.Shop.GoodsName == "" {
+ t.Error("param shop.goods_name can not be empty")
+ }
+ if params.Shop.GoodsCount == 0 {
+ t.Error("param shop.goods_count can not be zero")
+ }
+ if params.Insured.Used == 0 {
+ t.Error("param insured.use_insured can not be zero")
+ }
+ if params.Service.Name == "" {
+ t.Error("param Service.service_name can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "errcode": 9300501,
+ "errmsg": "delivery logic fail",
+ "delivery_resultcode": 10002,
+ "delivery_resultmsg": "客户密码不正确",
+ "order_id": "01234567890123456789",
+ "waybill_id": "123456789",
+ "waybill_data": [
+ {
+ "key": "SF_bagAddr",
+ "value": "广州"
+ },
+ {
+ "key": "SF_mark",
+ "value": "101- 07-03 509"
+ }
+ ]
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ creator := ExpressOrderCreator{
+ AddSource: 0,
+ ExpressOrder: ExpressOrder{
+ OrderID: "01234567890123456789",
+ OpenID: "oABC123456",
+ DeliveryID: "SF",
+ BizID: "xyz",
+ CustomRemark: "易碎物品",
+ Sender: ExpreseeUserInfo{
+ "张三",
+ "020-88888888",
+ "18666666666",
+ "公司名",
+ "123456",
+ "中国",
+ "广东省",
+ "广州市",
+ "海珠区",
+ "XX路XX号XX大厦XX栋XX",
+ },
+ Receiver: ExpreseeUserInfo{
+ "王小蒙",
+ "020-77777777",
+ "18610000000",
+ "公司名",
+ "654321",
+ "中国",
+ "广东省",
+ "广州市",
+ "天河区",
+ "XX路XX号XX大厦XX栋XX",
+ },
+ Shop: ExpressShop{
+ "/index/index?from=waybill&id=01234567890123456789",
+ "https://mmbiz.qpic.cn/mmbiz_png/OiaFLUqewuIDNQnTiaCInIG8ibdosYHhQHPbXJUrqYSNIcBL60vo4LIjlcoNG1QPkeH5GWWEB41Ny895CokeAah8A/640",
+ "一千零一夜钻石包&爱马仕铂金包",
+ 2,
+ },
+ Cargo: ExpressCargo{
+ 2,
+ 5.5,
+ 30.5,
+ 20,
+ 20,
+ []CargoDetail{
+ {
+ "一千零一夜钻石包",
+ 1,
+ },
+ {
+ "爱马仕铂金包",
+ 1,
+ },
+ },
+ },
+ Insured: ExpressInsure{
+ 1,
+ 10000,
+ },
+ Service: ExpressService{
+ 0,
+ "标准快递",
+ },
+ },
+ }
+ _, err := creator.create(ts.URL+apiAddExpressOrder, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestCancelExpressOrder(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiCancelExpressOrder {
+ t.Fatalf("Except to path '%s',get '%s'", apiCancelExpressOrder, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ OrderID string `json:"order_id"`
+ OpenID string `json:"openid"`
+ DeliveryID string `json:"delivery_id"`
+ WaybillID string `json:"waybill_id"`
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.OrderID == "" {
+ t.Error("param order_id can not be empty")
+ }
+ if params.DeliveryID == "" {
+ t.Error("param delivery_id can not be empty")
+ }
+ if params.WaybillID == "" {
+ t.Error("param waybill_id can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ canceler := ExpressOrderCanceler{
+ OrderID: "01234567890123456789",
+ OpenID: "oABC123456",
+ DeliveryID: "SF",
+ WaybillID: "123456789",
+ }
+ _, err := canceler.cancel(ts.URL+apiCancelExpressOrder, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetAllDelivery(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "GET" {
+ t.Fatalf("Expect 'GET' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiGetAllDelivery {
+ t.Fatalf("Except to path '%s',get '%s'", apiGetAllDelivery, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "count": 8,
+ "data": [
+ {
+ "delivery_id": "BEST",
+ "delivery_name": "百世快递"
+ },
+ {
+ "delivery_id": "EMS",
+ "delivery_name": "中国邮政速递物流"
+ },
+ {
+ "delivery_id": "OTP",
+ "delivery_name": "承诺达特快"
+ },
+ {
+ "delivery_id": "PJ",
+ "delivery_name": "品骏物流"
+ },
+ {
+ "delivery_id": "SF",
+ "delivery_name": "顺丰速运"
+ },
+ {
+ "delivery_id": "YTO",
+ "delivery_name": "圆通速递"
+ },
+ {
+ "delivery_id": "YUNDA",
+ "delivery_name": "韵达快递"
+ },
+ {
+ "delivery_id": "ZTO",
+ "delivery_name": "中通快递"
+ }
+ ]
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := getAllDelivery(ts.URL+apiGetAllDelivery, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetExpressOrder(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiGetExpressOrder {
+ t.Fatalf("Except to path '%s',get '%s'", apiGetExpressOrder, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ OrderID string `json:"order_id"`
+ OpenID string `json:"openid"`
+ DeliveryID string `json:"delivery_id"`
+ WaybillID string `json:"waybill_id"`
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.OrderID == "" {
+ t.Error("param order_id can not be empty")
+ }
+ if params.DeliveryID == "" {
+ t.Error("param delivery_id can not be empty")
+ }
+ if params.WaybillID == "" {
+ t.Error("param waybill_id can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ canceler := ExpressOrderGetter{
+ OrderID: "01234567890123456789",
+ OpenID: "oABC123456",
+ DeliveryID: "SF",
+ WaybillID: "123456789",
+ }
+ _, err := canceler.get(ts.URL+apiGetExpressOrder, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetExpressPath(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ expectedPath := "/cgi-bin/express/business/path/get"
+ if path != expectedPath {
+ t.Fatalf("Except to path '%s',get '%s'", expectedPath, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ OrderID string `json:"order_id"`
+ OpenID string `json:"openid"`
+ DeliveryID string `json:"delivery_id"`
+ WaybillID string `json:"waybill_id"`
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.OrderID == "" {
+ t.Error("param order_id can not be empty")
+ }
+ if params.DeliveryID == "" {
+ t.Error("param delivery_id can not be empty")
+ }
+ if params.WaybillID == "" {
+ t.Error("param waybill_id can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "openid": "OPENID",
+ "delivery_id": "SF",
+ "waybill_id": "12345678901234567890",
+ "path_item_num": 3,
+ "path_item_list": [
+ {
+ "action_time": 1533052800,
+ "action_type": 100001,
+ "action_msg": "快递员已成功取件"
+ },
+ {
+ "action_time": 1533062800,
+ "action_type": 200001,
+ "action_msg": "快件已到达xxx集散中心,准备发往xxx"
+ },
+ {
+ "action_time": 1533072800,
+ "action_type": 300001,
+ "action_msg": "快递员已出发,联系电话xxxxxx"
+ }
+ ]
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ getter := ExpressPathGetter{
+ OrderID: "01234567890123456789",
+ OpenID: "oABC123456",
+ DeliveryID: "SF",
+ WaybillID: "123456789",
+ }
+ _, err := getter.get(ts.URL+apiGetExpressPath, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetPrinter(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "GET" {
+ t.Fatalf("Expect 'GET' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ expectedPath := "/cgi-bin/express/business/printer/getall"
+ if path != expectedPath {
+ t.Fatalf("Except to path '%s',get '%s'", expectedPath, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "count": 2,
+ "openid": [
+ "oABC",
+ "oXYZ"
+ ],
+ "tagid_list": [
+ "123",
+ "456"
+ ]
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := getPrinter(ts.URL+apiGetPrinter, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetQuota(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ expectedPath := "/cgi-bin/express/business/quota/get"
+ if path != expectedPath {
+ t.Fatalf("Except to path '%s',get '%s'", expectedPath, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ DeliveryID string `json:"delivery_id"`
+ BizID string `json:"biz_id"`
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.DeliveryID == "" {
+ t.Error("param delivery_id can not be empty")
+ }
+ if params.BizID == "" {
+ t.Error("param biz_id can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "quota_num": 210
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ getter := QuotaGetter{
+ DeliveryID: "YTO",
+ BizID: "xyz",
+ }
+
+ _, err := getter.get(ts.URL+apiGetQuota, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestOnPathUpdate(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ aesKey := base64.StdEncoding.EncodeToString([]byte("mock-aes-key"))
+ srv, err := NewServer("mock-app-id", "mock-access-token", aesKey, "mock-mch-id", "mock-api-key", false)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ srv.OnExpressPathUpdate(func(mix *ExpressPathUpdateResult) {
+ if mix.ToUserName == "" {
+ t.Error("ToUserName can not be empty")
+ }
+
+ if mix.FromUserName == "" {
+ t.Error("FromUserName can not be empty")
+ }
+ if mix.CreateTime == 0 {
+ t.Error("CreateTime can not be zero")
+ }
+ if mix.MsgType != "event" {
+ t.Error("Unexpected message type")
+ }
+
+ if mix.Event != "add_express_path" {
+ t.Error("Unexpected message event")
+ }
+
+ if mix.DeliveryID == "" {
+ t.Error("DeliveryID can not be empty")
+ }
+ if mix.WayBillID == "" {
+ t.Error("WayBillID can not be empty")
+ }
+ if mix.Version == 0 {
+ t.Error("Version can not be zero")
+ }
+ if mix.Count == 0 {
+ t.Error("Count can not be zero")
+ }
+
+ if len(mix.Actions) > 0 {
+ if mix.Actions[0].ActionTime == 0 {
+ t.Error("Actions.ActionTime can not be zero")
+ }
+ if mix.Actions[0].ActionType == 0 {
+ t.Error("Actions.ActionType can not be zero")
+ }
+ if mix.Actions[0].ActionMsg == "" {
+ t.Error("Actions.ActionMsg can not be empty")
+ }
+ }
+ })
+
+ if err := srv.Serve(w, r); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ xmlData := `
+
+
+ 1546924844
+
+
+
+
+ 3
+ 3
+
+ 1546924840
+ 100001
+
+
+
+ 1546924841
+ 200001
+
+
+
+ 1546924842
+ 200001
+
+
+ `
+ res, err := http.Post(ts.URL, "text/xml", strings.NewReader(xmlData))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer res.Body.Close()
+
+ jsonData := `{
+ "ToUserName": "toUser",
+ "FromUserName": "fromUser",
+ "CreateTime": 1546924844,
+ "MsgType": "event",
+ "Event": "add_express_path",
+ "DeliveryID": "SF",
+ "WayBillId": "123456789",
+ "Version": 2,
+ "Count": 3,
+ "Actions": [
+ {
+ "ActionTime": 1546924840,
+ "ActionType": 100001,
+ "ActionMsg": "小哥A揽件成功"
+ },
+ {
+ "ActionTime": 1546924841,
+ "ActionType": 200001,
+ "ActionMsg": "到达广州集包地"
+ },
+ {
+ "ActionTime": 1546924842,
+ "ActionType": 200001,
+ "ActionMsg": "运往目的地"
+ }
+ ]
+ }`
+ res, err = http.Post(ts.URL, "application/json", strings.NewReader(jsonData))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer res.Body.Close()
+}
+
+func TestTestUpdateOrder(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ expectedPath := "/cgi-bin/express/business/test_update_order"
+ if path != expectedPath {
+ t.Fatalf("Except to path '%s',get '%s'", expectedPath, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ BizID string `json:"biz_id"` // 商户id,需填test_biz_id
+ OrderID string `json:"order_id"` // 订单ID,下单成功时返回
+ WaybillID string `json:"waybill_id"` // 运单 ID
+ DeliveryID string `json:"delivery_id"` // 快递公司 ID
+ ActionTime uint `json:"action_time"` // 轨迹变化 Unix 时间戳
+ ActionType int `json:"action_type"` // 轨迹变化类型
+ ActionMsg string `json:"action_msg"` // 轨迹变化具体信息说明,展示在快递轨迹详情页中。若有手机号码,则直接写11位手机号码。使用UTF-8编码。
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.DeliveryID == "" {
+ t.Error("param delivery_id can not be empty")
+ }
+ if params.OrderID == "" {
+ t.Error("param order_id can not be empty")
+ }
+ if params.WaybillID == "" {
+ t.Error("param waybill_id can not be empty")
+ }
+
+ if params.BizID == "" {
+ t.Error("param biz_id can not be empty")
+ }
+ if params.ActionMsg == "" {
+ t.Error("param action_msg can not be empty")
+ }
+ if params.ActionTime == 0 {
+ t.Error("param action_time can not be empty")
+ }
+ if params.ActionType == 0 {
+ t.Error("param action_type can not be empty")
+ }
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ params := `{
+ "biz_id": "test_biz_id",
+ "order_id": "xxxxxxxxxxxx",
+ "delivery_id": "TEST",
+ "waybill_id": "xxxxxxxxxx",
+ "action_time": 123456789,
+ "action_type": 100001,
+ "action_msg": "揽件阶段"
+ }`
+
+ tester := new(UpdateExpressOrderTester)
+ err := json.Unmarshal([]byte(params), tester)
+ if err != nil {
+ t.Error(err)
+ }
+
+ _, err = tester.test(ts.URL+apiTestUpdateOrder, "mock-access-token")
+ if err != nil {
+ t.Error(err)
+ }
+}
+
+func TestUpdatePrinter(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != "/cgi-bin/express/business/printer/update" {
+ t.Error("Invalid request path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ OpenID string `json:"openid"` // 打印员 openid
+ Type BindType `json:"update_type"` // 更新类型
+ TagIDList string `json:"tagid_list"`
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.OpenID == "" {
+ t.Error("param openid can not be empty")
+ }
+ if params.Type == "" {
+ t.Error("param update_type can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+ params := `{
+ "openid": "oJ4v0wRAfiXcnIbM3SgGEUkTw3Qw",
+ "update_type": "bind",
+ "tagid_list": "123,456"
+ }`
+ updater := new(PrinterUpdater)
+ err := json.Unmarshal([]byte(params), updater)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = updater.update(ts.URL+apiUpdatePrinter, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/app/lib/weapp/express_provider.go b/app/lib/weapp/express_provider.go
new file mode 100644
index 0000000..0b7ef69
--- /dev/null
+++ b/app/lib/weapp/express_provider.go
@@ -0,0 +1,154 @@
+package weapp
+
+const (
+ apiGetContact = "/cgi-bin/express/delivery/contact/get"
+ apiPreviewTemplate = "/cgi-bin/express/delivery/template/preview"
+ apiUpdateBusiness = "/cgi-bin/express/delivery/service/business/update"
+ apiUpdatePath = "/cgi-bin/express/delivery/path/update"
+)
+
+// GetContactResponse 获取面单联系人信息返回数据
+type GetContactResponse struct {
+ CommonError
+ WaybillID string `json:"waybill_id"` // 运单 ID
+ Sender ContactUser `json:"sender"` // 发件人信息
+ Receiver ContactUser `json:"receiver"` // 收件人信息
+}
+
+// ContactUser 联系人
+type ContactUser struct {
+ Address string `json:"address"` //地址,已经将省市区信息合并
+ Name string `json:"name"` //用户姓名
+ Tel string `json:"tel"` //座机号码
+ Mobile string `json:"mobile"` //手机号码
+}
+
+// GetContact 获取面单联系人信息
+// accessToken, token, watBillID 接口调用凭证
+func GetContact(accessToken, token, watBillID string) (*GetContactResponse, error) {
+ api := baseURL + apiGetContact
+ return getContact(api, accessToken, token, watBillID)
+}
+
+func getContact(api, accessToken, token, watBillID string) (*GetContactResponse, error) {
+ url, err := tokenAPI(api, accessToken)
+ if err != nil {
+ return nil, err
+ }
+
+ params := requestParams{
+ "token": token,
+ "waybill_id": watBillID,
+ }
+
+ res := new(GetContactResponse)
+ if err := postJSON(url, params, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// ExpressTemplatePreviewer 面单模板预览器
+type ExpressTemplatePreviewer struct {
+ WaybillID string `json:"waybill_id"` // 运单 ID
+ WaybillTemplate string `json:"waybill_template"` // 面单 HTML 模板内容(需经 Base64 编码)
+ WaybillData string `json:"waybill_data"` // 面单数据。详情参考下单事件返回值中的 WaybillData
+ Custom ExpressOrder `json:"custom"` // 商户下单数据,格式是商户侧下单 API 中的请求体
+}
+
+// PreviewTemplateResponse 预览面单模板返回数据
+type PreviewTemplateResponse struct {
+ CommonError
+ WaybillID string `json:"waybill_id"` // 运单 ID
+ RenderedWaybillTemplate string `json:"rendered_waybill_template"` // 渲染后的面单 HTML 文件(已经过 Base64 编码)
+}
+
+// Preview 预览面单模板。用于调试面单模板使用。
+// token 接口调用凭证
+func (previewer *ExpressTemplatePreviewer) Preview(token string) (*PreviewTemplateResponse, error) {
+ api := baseURL + apiPreviewTemplate
+ return previewer.preview(api, token)
+}
+
+func (previewer *ExpressTemplatePreviewer) preview(api, token string) (*PreviewTemplateResponse, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(PreviewTemplateResponse)
+ if err := postJSON(url, previewer, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// BusinessResultCode 商户审核结果状态码
+type BusinessResultCode = int8
+
+// 所有商户审核结果状态码
+const (
+ ResultSuccess BusinessResultCode = 0 // 审核通过
+ ResultFailed = 1 // 审核失败
+)
+
+// BusinessUpdater 商户审核结果更新器
+type BusinessUpdater struct {
+ ShopAppID string `json:"shop_app_id"` // 商户的小程序AppID,即审核商户事件中的 ShopAppID
+ BizID string `json:"biz_id"` // 商户账户
+ ResultCode BusinessResultCode `json:"result_code"` // 审核结果,0 表示审核通过,其他表示审核失败
+ ResultMsg string `json:"result_msg,omitempty"` // 审核错误原因,仅 result_code 不等于 0 时需要设置
+}
+
+// Update 更新商户审核结果
+// token 接口调用凭证
+func (updater *BusinessUpdater) Update(token string) (*CommonError, error) {
+ api := baseURL + apiUpdateBusiness
+ return updater.update(api, token)
+}
+
+func (updater *BusinessUpdater) update(api, token string) (*CommonError, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(CommonError)
+ if err := postJSON(url, updater, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// ExpressPathUpdater 运单轨迹更新器
+type ExpressPathUpdater struct {
+ Token string `json:"token"` // 商户侧下单事件中推送的 Token 字段
+ WaybillID string `json:"waybill_id"` // 运单 ID
+ ActionTime uint `json:"action_time"` // 轨迹变化 Unix 时间戳
+ ActionType uint `json:"action_type"` // 轨迹变化类型
+ ActionMsg string `json:"action_msg"` // 轨迹变化具体信息说明,展示在快递轨迹详情页中。若有手机号码,则直接写11位手机号码。使用UTF-8编码。
+}
+
+// Update 更新运单轨迹
+// token 接口调用凭证
+func (updater *ExpressPathUpdater) Update(token string) (*CommonError, error) {
+ api := baseURL + apiUpdatePath
+ return updater.update(api, token)
+}
+
+func (updater *ExpressPathUpdater) update(api, token string) (*CommonError, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(CommonError)
+ if err := postJSON(url, updater, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
diff --git a/app/lib/weapp/express_provider_test.go b/app/lib/weapp/express_provider_test.go
new file mode 100644
index 0000000..0d6b7c9
--- /dev/null
+++ b/app/lib/weapp/express_provider_test.go
@@ -0,0 +1,1332 @@
+package weapp
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "encoding/xml"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+)
+
+func TestGetContact(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != "/cgi-bin/express/delivery/contact/get" {
+ t.Error("Invalid request path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ Token string `json:"token"`
+ WaybillID string `json:"waybill_id"`
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.Token == "" {
+ t.Error("param token can not be empty")
+ }
+ if params.WaybillID == "" {
+ t.Error("param waybill_id can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "waybill_id": "12345678901234567890",
+ "sender": {
+ "address": "广东省广州市海珠区XX路XX号XX大厦XX栋XX",
+ "name": "张三",
+ "tel": "020-88888888",
+ "mobile": "18666666666"
+ },
+ "receiver": {
+ "address": "广东省广州市天河区XX路XX号XX大厦XX栋XX",
+ "name": "王小蒙",
+ "tel": "029-77777777",
+ "mobile": "18610000000"
+ }
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := getContact(ts.URL+apiGetContact, "mock-access-token", "mock-token", "mock-wat-bill-id")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestOnAddExpressOrder(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ aesKey := base64.StdEncoding.EncodeToString([]byte("mock-aes-key"))
+ srv, err := NewServer("mock-app-id", "mock-access-token", aesKey, "mock-mch-id", "mock-api-key", false)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ srv.OnAddExpressOrder(func(result *AddExpressOrderResult) *AddExpressOrderReturn {
+ if result.ToUserName == "" {
+ t.Error("ToUserName can not be empty")
+ }
+ if result.FromUserName == "" {
+ t.Error("FromUserName can not be empty")
+ }
+ if result.CreateTime == 0 {
+ t.Error("CreateTime can not be zero")
+ }
+ if result.MsgType != "event" {
+ t.Error("Unexpected message type")
+ }
+ if result.Event != "add_waybill" {
+ t.Error("Unexpected message event")
+ }
+ if result.Token == "" {
+ t.Error("Result column 'Token' can not be empty")
+ }
+ if result.OrderID == "" {
+ t.Error("Result column 'OrderID' can not be empty")
+ }
+ if result.BizID == "" {
+ t.Error("Result column 'BizID' can not be empty")
+ }
+ if result.BizPwd == "" {
+ t.Error("Result column 'BizPwd' can not be empty")
+ }
+ if result.ShopAppID == "" {
+ t.Error("Result column 'ShopAppID' can not be empty")
+ }
+ if result.WayBillID == "" {
+ t.Error("Result column 'WayBillID' can not be empty")
+ }
+ if result.Remark == "" {
+ t.Error("Result column 'Remark' can not be empty")
+ }
+
+ if result.Sender.Name == "" {
+ t.Error("Result column 'Sender.Name' can not be empty")
+ }
+ if result.Sender.Tel == "" {
+ t.Error("Result column 'Sender.Tel' can not be empty")
+ }
+ if result.Sender.Mobile == "" {
+ t.Error("Result column 'Sender.Mobile' can not be empty")
+ }
+ if result.Sender.Company == "" {
+ t.Error("Result column 'Sender.Company' can not be empty")
+ }
+ if result.Sender.PostCode == "" {
+ t.Error("Result column 'Sender.PostCode' can not be empty")
+ }
+ if result.Sender.Country == "" {
+ t.Error("Result column 'Sender.Country' can not be empty")
+ }
+ if result.Sender.Province == "" {
+ t.Error("Result column 'Sender.Province' can not be empty")
+ }
+ if result.Sender.City == "" {
+ t.Error("Result column 'Sender.City' can not be empty")
+ }
+ if result.Sender.Area == "" {
+ t.Error("Result column 'Sender.Area' can not be empty")
+ }
+ if result.Sender.Address == "" {
+ t.Error("Result column 'Sender.Address' can not be empty")
+ }
+ if result.Receiver.Name == "" {
+ t.Error("Result column 'Receiver.Name' can not be empty")
+ }
+ if result.Receiver.Tel == "" {
+ t.Error("Result column 'Receiver.Tel' can not be empty")
+ }
+ if result.Receiver.Mobile == "" {
+ t.Error("Result column 'Receiver.Mobile' can not be empty")
+ }
+ if result.Receiver.Company == "" {
+ t.Error("Result column 'Receiver.Company' can not be empty")
+ }
+ if result.Receiver.PostCode == "" {
+ t.Error("Result column 'Receiver.PostCode' can not be empty")
+ }
+ if result.Receiver.Country == "" {
+ t.Error("Result column 'Receiver.Country' can not be empty")
+ }
+ if result.Receiver.Province == "" {
+ t.Error("Result column 'Receiver.Province' can not be empty")
+ }
+ if result.Receiver.City == "" {
+ t.Error("Result column 'Receiver.City' can not be empty")
+ }
+ if result.Receiver.Area == "" {
+ t.Error("Result column 'Receiver.Area' can not be empty")
+ }
+ if result.Receiver.Address == "" {
+ t.Error("Result column 'Receiver.Address' can not be empty")
+ }
+ if result.Cargo.Weight == 0 {
+ t.Error("Result column 'Cargo.Weight' can not be zero")
+ }
+ if result.Cargo.SpaceX == 0 {
+ t.Error("Result column 'Cargo.SpaceX' can not be zero")
+ }
+ if result.Cargo.SpaceY == 0 {
+ t.Error("Result column 'Cargo.SpaceY' can not be zero")
+ }
+ if result.Cargo.SpaceZ == 0 {
+ t.Error("Result column 'Cargo.SpaceZ' can not be zero")
+ }
+ if result.Cargo.Count == 0 {
+ t.Error("Result column 'Cargo.Count' can not be zero")
+ }
+ if result.Insured.Used == 0 {
+ t.Error("Result column 'Insured.Used' can not be zero")
+ }
+ if result.Insured.Value == 0 {
+ t.Error("Result column 'Insured.Value' can not be zero")
+ }
+ if result.Service.Type == 0 {
+ t.Error("Result column 'Service.Type' can not be zero")
+ }
+ if result.Service.Name == "" {
+ t.Error("Result column 'Service.Name' can not be empty")
+ }
+
+ res := AddExpressOrderReturn{
+ CommonServerReturn: CommonServerReturn{
+ "oABCD", "gh_abcdefg", 1533042556, "event", "add_waybill", 1, "success",
+ },
+ Token: "1234ABC234523451",
+ OrderID: "012345678901234567890123456789",
+ BizID: "xyz",
+ WayBillID: "123456789",
+ WaybillData: "##ZTO_bagAddr##广州##ZTO_mark##888-666-666##",
+ }
+
+ return &res
+ })
+
+ if err := srv.Serve(w, r); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ xmlData := `
+
+
+ 1533042556
+
+
+ 1234ABC234523451
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1.2
+ 20.5
+ 15.0
+ 10.0
+ 2
+
+
+ 1
+
+
+
+ 1
+
+
+
+ 1
+ 10000
+
+
+ 123
+
+
+ `
+ xmlResp, err := http.Post(ts.URL, "text/xml", strings.NewReader(xmlData))
+ if err != nil {
+ t.Error(err)
+ }
+ defer xmlResp.Body.Close()
+ res := new(AddExpressOrderReturn)
+ if err := xml.NewDecoder(xmlResp.Body).Decode(res); err != nil {
+ t.Error(err)
+ }
+
+ if res.ToUserName == "" {
+ t.Error("Response column 'ToUserName' can not be empty")
+ }
+ if res.FromUserName == "" {
+ t.Error("Response column 'FromUserName' can not be empty")
+ }
+ if res.CreateTime == 0 {
+ t.Error("Response column 'CreateTime' can not be zero")
+ }
+ if res.MsgType == "" {
+ t.Error("Response column 'MsgType' can not be empty")
+ }
+ if res.Event == "" {
+ t.Error("Response column 'Event' can not be empty")
+ }
+ if res.Token == "" {
+ t.Error("Response column 'Token' can not be empty")
+ }
+ if res.OrderID == "" {
+ t.Error("Response column 'OrderID' can not be empty")
+ }
+ if res.BizID == "" {
+ t.Error("Response column 'BizID' can not be empty")
+ }
+ if res.WayBillID == "" {
+ t.Error("Response column 'WayBillID' can not be empty")
+ }
+
+ if res.ResultMsg == "" {
+ t.Error("Response column 'ResultMsg' can not be empty")
+ }
+ if res.WaybillData == "" {
+ t.Error("Response column 'WaybillData' can not be empty")
+ }
+
+ jsonData := `{
+ "ToUserName": "gh_abcdefg",
+ "FromUserName": "oABCD",
+ "CreateTime": 1533042556,
+ "MsgType": "event",
+ "Event": "add_waybill",
+ "Token": "1234ABC234523451",
+ "OrderID": "012345678901234567890123456789",
+ "BizID": "xyz",
+ "BizPwd": "xyz123",
+ "ShopAppID": "wxABCD",
+ "WayBillID": "123456789",
+ "Remark": "易碎物品",
+ "Sender": {
+ "Name": "张三",
+ "Tel": "020-88888888",
+ "Mobile": "18666666666",
+ "Company": "公司名",
+ "PostCode": "123456",
+ "Country": "中国",
+ "Province": "广东省",
+ "City": "广州市",
+ "Area": "海珠区",
+ "Address": "XX路XX号XX大厦XX栋XX"
+ },
+ "Receiver": {
+ "Name": "王小蒙",
+ "Tel": "029-77777777",
+ "Mobile": "18610000000",
+ "Company": "公司名",
+ "PostCode": "654321",
+ "Country": "中国",
+ "Province": "广东省",
+ "City": "广州市",
+ "Area": "天河区",
+ "Address": "XX路XX号XX大厦XX栋XX"
+ },
+ "Cargo": {
+ "Weight": 1.2,
+ "Space_X": 20.5,
+ "Space_Y": 15,
+ "Space_Z": 10,
+ "Count": 2,
+ "DetailList": [
+ {
+ "Name": "一千零一夜钻石包",
+ "Count": 1
+ },
+ {
+ "Name": "爱马仕柏金钻石包",
+ "Count": 1
+ }
+ ]
+ },
+ "Insured": {
+ "UseInsured": 1,
+ "InsuredValue": 10000
+ },
+ "Service": {
+ "ServiceType": 123,
+ "ServiceName": "标准快递"
+ }
+ }`
+
+ jsonResp, err := http.Post(ts.URL, "application/json", strings.NewReader(jsonData))
+ if err != nil {
+ t.Error(err)
+ }
+ defer jsonResp.Body.Close()
+ res = new(AddExpressOrderReturn)
+ if err := json.NewDecoder(jsonResp.Body).Decode(res); err != nil {
+ t.Error(err)
+ }
+
+ if res.ToUserName == "" {
+ t.Error("Response column 'ToUserName' can not be empty")
+ }
+ if res.FromUserName == "" {
+ t.Error("Response column 'FromUserName' can not be empty")
+ }
+ if res.CreateTime == 0 {
+ t.Error("Response column 'CreateTime' can not be zero")
+ }
+ if res.MsgType == "" {
+ t.Error("Response column 'MsgType' can not be empty")
+ }
+ if res.Event == "" {
+ t.Error("Response column 'Event' can not be empty")
+ }
+ if res.Token == "" {
+ t.Error("Response column 'Token' can not be empty")
+ }
+ if res.OrderID == "" {
+ t.Error("Response column 'OrderID' can not be empty")
+ }
+ if res.BizID == "" {
+ t.Error("Response column 'BizID' can not be empty")
+ }
+ if res.WayBillID == "" {
+ t.Error("Response column 'WayBillID' can not be empty")
+ }
+
+ if res.ResultMsg == "" {
+ t.Error("Response column 'ResultMsg' can not be empty")
+ }
+ if res.WaybillData == "" {
+ t.Error("Response column 'WaybillData' can not be empty")
+ }
+}
+
+func TestOnCancelExpressOrder(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ aesKey := base64.StdEncoding.EncodeToString([]byte("mock-aes-key"))
+ srv, err := NewServer("mock-app-id", "mock-access-token", aesKey, "mock-mch-id", "mock-api-key", false)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ srv.OnCancelExpressOrder(func(result *CancelExpressOrderResult) *CancelExpressOrderReturn {
+ if result.ToUserName == "" {
+ t.Error("ToUserName can not be empty")
+ }
+ if result.FromUserName == "" {
+ t.Error("FromUserName can not be empty")
+ }
+ if result.CreateTime == 0 {
+ t.Error("CreateTime can not be zero")
+ }
+ if result.MsgType != "event" {
+ t.Error("Unexpected message type")
+ }
+ if result.Event != "cancel_waybill" {
+ t.Error("Unexpected message event")
+ }
+
+ if result.OrderID == "" {
+ t.Error("Result column 'OrderID' can not be empty")
+ }
+ if result.BizID == "" {
+ t.Error("Result column 'BizID' can not be empty")
+ }
+ if result.BizPwd == "" {
+ t.Error("Result column 'BizPwd' can not be empty")
+ }
+ if result.ShopAppID == "" {
+ t.Error("Result column 'ShopAppID' can not be empty")
+ }
+ if result.WayBillID == "" {
+ t.Error("Result column 'WayBillID' can not be empty")
+ }
+
+ res := CancelExpressOrderReturn{
+ CommonServerReturn: CommonServerReturn{
+ "oABCD", "gh_abcdefg", 1533042556, "event", "cancel_waybill", 1, "success",
+ },
+ OrderID: "012345678901234567890123456789",
+ BizID: "xyz",
+ WayBillID: "123456789",
+ }
+
+ return &res
+ })
+
+ if err := srv.Serve(w, r); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ xmlData := `
+
+
+ 1533042556
+
+
+
+
+
+
+
+`
+ xmlResp, err := http.Post(ts.URL, "text/xml", strings.NewReader(xmlData))
+ if err != nil {
+ t.Error(err)
+ }
+ defer xmlResp.Body.Close()
+ res := new(CancelExpressOrderReturn)
+ if err := xml.NewDecoder(xmlResp.Body).Decode(res); err != nil {
+ t.Error(err)
+ }
+
+ if res.ToUserName == "" {
+ t.Error("Response column 'ToUserName' can not be empty")
+ }
+ if res.FromUserName == "" {
+ t.Error("Response column 'FromUserName' can not be empty")
+ }
+ if res.CreateTime == 0 {
+ t.Error("Response column 'CreateTime' can not be zero")
+ }
+ if res.MsgType == "" {
+ t.Error("Response column 'MsgType' can not be empty")
+ }
+ if res.Event == "" {
+ t.Error("Response column 'Event' can not be empty")
+ }
+ if res.OrderID == "" {
+ t.Error("Response column 'OrderID' can not be empty")
+ }
+ if res.BizID == "" {
+ t.Error("Response column 'BizID' can not be empty")
+ }
+ if res.WayBillID == "" {
+ t.Error("Response column 'WayBillID' can not be empty")
+ }
+
+ if res.ResultMsg == "" {
+ t.Error("Response column 'ResultMsg' can not be empty")
+ }
+
+ jsonData := `{
+ "ToUserName": "gh_abcdefg",
+ "FromUserName": "oABCD",
+ "CreateTime": 1533042556,
+ "MsgType": "event",
+ "Event": "cancel_waybill",
+ "BizID": "xyz",
+ "BizPwd": "xyz123",
+ "ShopAppID": "wxABCD",
+ "OrderID": "012345678901234567890123456789",
+ "WayBillID": "123456789"
+ }`
+
+ jsonResp, err := http.Post(ts.URL, "application/json", strings.NewReader(jsonData))
+ if err != nil {
+ t.Error(err)
+ }
+ defer jsonResp.Body.Close()
+ res = new(CancelExpressOrderReturn)
+ if err := json.NewDecoder(jsonResp.Body).Decode(res); err != nil {
+ t.Error(err)
+ }
+
+ if res.ToUserName == "" {
+ t.Error("Response column 'ToUserName' can not be empty")
+ }
+ if res.FromUserName == "" {
+ t.Error("Response column 'FromUserName' can not be empty")
+ }
+ if res.CreateTime == 0 {
+ t.Error("Response column 'CreateTime' can not be zero")
+ }
+ if res.MsgType == "" {
+ t.Error("Response column 'MsgType' can not be empty")
+ }
+ if res.Event == "" {
+ t.Error("Response column 'Event' can not be empty")
+ }
+ if res.OrderID == "" {
+ t.Error("Response column 'OrderID' can not be empty")
+ }
+ if res.BizID == "" {
+ t.Error("Response column 'BizID' can not be empty")
+ }
+ if res.WayBillID == "" {
+ t.Error("Response column 'WayBillID' can not be empty")
+ }
+
+ if res.ResultMsg == "" {
+ t.Error("Response column 'ResultMsg' can not be empty")
+ }
+}
+
+func TestOnCheckBusiness(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ aesKey := base64.StdEncoding.EncodeToString([]byte("mock-aes-key"))
+ srv, err := NewServer("mock-app-id", "mock-access-token", aesKey, "mock-mch-id", "mock-api-key", false)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ srv.OnCheckExpressBusiness(func(result *CheckExpressBusinessResult) *CheckExpressBusinessReturn {
+ if result.ToUserName == "" {
+ t.Error("ToUserName can not be empty")
+ }
+ if result.FromUserName == "" {
+ t.Error("FromUserName can not be empty")
+ }
+ if result.CreateTime == 0 {
+ t.Error("CreateTime can not be zero")
+ }
+ if result.MsgType != "event" {
+ t.Error("Unexpected message type")
+ }
+ if result.Event != "check_biz" {
+ t.Error("Unexpected message event")
+ }
+
+ if result.BizID == "" {
+ t.Error("Result column 'BizID' can not be empty")
+ }
+ if result.BizPwd == "" {
+ t.Error("Result column 'BizPwd' can not be empty")
+ }
+ if result.ShopAppID == "" {
+ t.Error("Result column 'ShopAppID' can not be empty")
+ }
+ if result.ShopName == "" {
+ t.Error("Result column 'ShopName' can not be empty")
+ }
+
+ if result.ShopTelphone == "" {
+ t.Error("Result column 'ShopTelphone' can not be empty")
+ }
+ if result.SenderAddress == "" {
+ t.Error("Result column 'SenderAddress' can not be empty")
+ }
+ if result.ShopContact == "" {
+ t.Error("Result column 'ShopContact' can not be empty")
+ }
+ if result.ServiceName == "" {
+ t.Error("Result column 'ServiceName' can not be empty")
+ }
+
+ res := CheckExpressBusinessReturn{
+ CommonServerReturn: CommonServerReturn{
+ "oABCD", "gh_abcdefg", 1533042556, "event", "check_biz", 1, "success",
+ },
+ BizID: "xyz",
+ Quota: 3.14159265358,
+ }
+ return &res
+ })
+
+ if err := srv.Serve(w, r); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ xmlData := `
+
+
+ 1533042556
+
+
+
+
+
+
+
+
+
+
+`
+ xmlResp, err := http.Post(ts.URL, "text/xml", strings.NewReader(xmlData))
+ if err != nil {
+ t.Error(err)
+ }
+ defer xmlResp.Body.Close()
+ res := new(CheckExpressBusinessReturn)
+ if err := xml.NewDecoder(xmlResp.Body).Decode(res); err != nil {
+ t.Error(err)
+ }
+
+ if res.ToUserName == "" {
+ t.Error("Response column 'ToUserName' can not be empty")
+ }
+ if res.FromUserName == "" {
+ t.Error("Response column 'FromUserName' can not be empty")
+ }
+ if res.CreateTime == 0 {
+ t.Error("Response column 'CreateTime' can not be zero")
+ }
+ if res.MsgType == "" {
+ t.Error("Response column 'MsgType' can not be empty")
+ }
+ if res.Event == "" {
+ t.Error("Response column 'Event' can not be empty")
+ }
+ if res.Quota == 0 {
+ t.Error("Response column 'Quota' can not be zero")
+ }
+ if res.BizID == "" {
+ t.Error("Response column 'BizID' can not be empty")
+ }
+ if res.ResultMsg == "" {
+ t.Error("Response column 'ResultMsg' can not be empty")
+ }
+
+ jsonData := `{
+ "ToUserName": "gh_abcdefg",
+ "FromUserName": "oABCD",
+ "CreateTime": 1533042556,
+ "MsgType": "event",
+ "Event": "check_biz",
+ "BizID": "xyz",
+ "BizPwd": "xyz123",
+ "ShopAppID": "wxABCD",
+ "ShopName": "商户名称",
+ "ShopTelphone": "18677778888",
+ "ShopContact": "村正",
+ "ServiceName": "标准快递",
+ "SenderAddress": "广东省广州市海珠区新港中路397号"
+ }`
+
+ jsonResp, err := http.Post(ts.URL, "application/json", strings.NewReader(jsonData))
+ if err != nil {
+ t.Error(err)
+ }
+ defer jsonResp.Body.Close()
+ res = new(CheckExpressBusinessReturn)
+ if err := json.NewDecoder(jsonResp.Body).Decode(res); err != nil {
+ t.Error(err)
+ }
+
+ if res.ToUserName == "" {
+ t.Error("Response column 'ToUserName' can not be empty")
+ }
+ if res.FromUserName == "" {
+ t.Error("Response column 'FromUserName' can not be empty")
+ }
+ if res.CreateTime == 0 {
+ t.Error("Response column 'CreateTime' can not be zero")
+ }
+ if res.MsgType == "" {
+ t.Error("Response column 'MsgType' can not be empty")
+ }
+ if res.Event == "" {
+ t.Error("Response column 'Event' can not be empty")
+ }
+ if res.Quota == 0 {
+ t.Error("Response column 'Quota' can not be zero")
+ }
+ if res.BizID == "" {
+ t.Error("Response column 'BizID' can not be empty")
+ }
+ if res.ResultMsg == "" {
+ t.Error("Response column 'ResultMsg' can not be empty")
+ }
+}
+
+func TestOnGetExpressQuota(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ aesKey := base64.StdEncoding.EncodeToString([]byte("mock-aes-key"))
+ srv, err := NewServer("mock-app-id", "mock-access-token", aesKey, "mock-mch-id", "mock-api-key", false)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ srv.OnGetExpressQuota(func(result *GetExpressQuotaResult) *GetExpressQuotaReturn {
+ if result.ToUserName == "" {
+ t.Error("ToUserName can not be empty")
+ }
+ if result.FromUserName == "" {
+ t.Error("FromUserName can not be empty")
+ }
+ if result.CreateTime == 0 {
+ t.Error("CreateTime can not be zero")
+ }
+ if result.MsgType != "event" {
+ t.Error("Unexpected message type")
+ }
+ if result.Event != "get_quota" {
+ t.Error("Unexpected message event")
+ }
+
+ if result.BizID == "" {
+ t.Error("Result column 'BizID' can not be empty")
+ }
+ if result.BizPwd == "" {
+ t.Error("Result column 'BizPwd' can not be empty")
+ }
+ if result.ShopAppID == "" {
+ t.Error("Result column 'ShopAppID' can not be empty")
+ }
+
+ res := GetExpressQuotaReturn{
+ CommonServerReturn: CommonServerReturn{
+ "oABCD", "gh_abcdefg", 1533042556, "event", "get_quota", 1, "success",
+ },
+ BizID: "xyz",
+ Quota: 3.14159265358,
+ }
+ return &res
+ })
+
+ if err := srv.Serve(w, r); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ xmlData := `
+
+
+ 1533042556
+
+
+
+
+
+`
+ xmlResp, err := http.Post(ts.URL, "text/xml", strings.NewReader(xmlData))
+ if err != nil {
+ t.Error(err)
+ }
+ defer xmlResp.Body.Close()
+ res := new(GetExpressQuotaReturn)
+ if err := xml.NewDecoder(xmlResp.Body).Decode(res); err != nil {
+ t.Error(err)
+ }
+
+ if res.ToUserName == "" {
+ t.Error("Response column 'ToUserName' can not be empty")
+ }
+ if res.FromUserName == "" {
+ t.Error("Response column 'FromUserName' can not be empty")
+ }
+ if res.CreateTime == 0 {
+ t.Error("Response column 'CreateTime' can not be zero")
+ }
+ if res.MsgType == "" {
+ t.Error("Response column 'MsgType' can not be empty")
+ }
+ if res.Event != "get_quota" {
+ t.Error("Invalid event")
+ }
+ if res.Quota == 0 {
+ t.Error("Response column 'Quota' can not be zero")
+ }
+ if res.BizID == "" {
+ t.Error("Response column 'BizID' can not be empty")
+ }
+ if res.ResultMsg == "" {
+ t.Error("Response column 'ResultMsg' can not be empty")
+ }
+
+ jsonData := `{
+ "ToUserName": "gh_abcdefg",
+ "FromUserName": "oABCD",
+ "CreateTime": 1533042556,
+ "MsgType": "event",
+ "Event": "get_quota",
+ "BizID": "xyz",
+ "BizPwd": "xyz123",
+ "ShopAppID": "wxABCD"
+ }`
+
+ jsonResp, err := http.Post(ts.URL, "application/json", strings.NewReader(jsonData))
+ if err != nil {
+ t.Error(err)
+ }
+ defer jsonResp.Body.Close()
+ res = new(GetExpressQuotaReturn)
+ if err := json.NewDecoder(jsonResp.Body).Decode(res); err != nil {
+ t.Error(err)
+ }
+ if res.ToUserName == "" {
+ t.Error("Response column 'ToUserName' can not be empty")
+ }
+ if res.FromUserName == "" {
+ t.Error("Response column 'FromUserName' can not be empty")
+ }
+ if res.CreateTime == 0 {
+ t.Error("Response column 'CreateTime' can not be zero")
+ }
+ if res.MsgType == "" {
+ t.Error("Response column 'MsgType' can not be empty")
+ }
+ if res.Event != "get_quota" {
+ t.Error("Invalid event")
+ }
+ if res.Quota == 0 {
+ t.Error("Response column 'Quota' can not be zero")
+ }
+ if res.BizID == "" {
+ t.Error("Response column 'BizID' can not be empty")
+ }
+ if res.ResultMsg == "" {
+ t.Error("Response column 'ResultMsg' can not be empty")
+ }
+}
+
+func TestPreviewTemplate(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Error("UnExpect request method")
+ }
+
+ if r.URL.EscapedPath() != "/cgi-bin/express/delivery/template/preview" {
+ t.Error("Unexpected path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ WaybillTemplate string `json:"waybill_template"`
+ WaybillData string `json:"waybill_data"`
+ Custom struct {
+ OrderID string `json:"order_id"`
+ OpenID string `json:"openid"`
+ DeliveryID string `json:"delivery_id"`
+ BizID string `json:"biz_id"`
+ CustomRemark string `json:"custom_remark"`
+ Sender struct {
+ Name string `json:"name"`
+ Tel string `json:"tel"`
+ Mobile string `json:"mobile"`
+ Company string `json:"company"`
+ PostCode string `json:"post_code"`
+ Country string `json:"country"`
+ Province string `json:"province"`
+ City string `json:"city"`
+ Area string `json:"area"`
+ Address string `json:"address"`
+ } `json:"sender"`
+ Receiver struct {
+ Name string `json:"name"`
+ Tel string `json:"tel"`
+ Mobile string `json:"mobile"`
+ Company string `json:"company"`
+ PostCode string `json:"post_code"`
+ Country string `json:"country"`
+ Province string `json:"province"`
+ City string `json:"city"`
+ Area string `json:"area"`
+ Address string `json:"address"`
+ } `json:"receiver"`
+ Cargo struct {
+ Count uint `json:"count"`
+ Weight float64 `json:"weight"`
+ SpaceX float64 `json:"space_x"`
+ SpaceY float64 `json:"space_y"`
+ SpaceZ float64 `json:"space_z"`
+ DetailList []struct {
+ Name string `json:"name"`
+ Count uint `json:"count"`
+ } `json:"detail_list"`
+ } `json:"cargo"`
+ Shop struct {
+ WXAPath string `json:"wxa_path"`
+ IMGUrl string `json:"img_url"`
+ GoodsName string `json:"goods_name"`
+ GoodsCount uint `json:"goods_count"`
+ } `json:"shop"`
+ Insured struct {
+ Used InsureStatus `json:"use_insured"`
+ Value uint `json:"insured_value"`
+ } `json:"insured"`
+ Service struct {
+ Type uint8 `json:"service_type"`
+ Name string `json:"service_name"`
+ } `json:"service"`
+ } `json:"custom"`
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.WaybillTemplate == "" {
+ t.Error("Response column waybill_template can not be empty")
+ }
+ if params.WaybillData == "" {
+ t.Error("Response column waybill_data can not be empty")
+ }
+ if params.Custom.OrderID == "" {
+ t.Error("param custom.order_id can not be empty")
+ }
+ if params.Custom.DeliveryID == "" {
+ t.Error("param custom.delivery_id can not be empty")
+ }
+
+ if params.Custom.BizID == "" {
+ t.Error("param custom.biz_id can not be empty")
+ }
+
+ if params.Custom.Sender.Name == "" {
+ t.Error("param custom.sender.name can not be empty")
+ }
+ if params.Custom.Sender.Province == "" {
+ t.Error("param custom.sender.province can not be empty")
+ }
+ if params.Custom.Sender.City == "" {
+ t.Error("param custom.sender.city can not be empty")
+ }
+ if params.Custom.Sender.Area == "" {
+ t.Error("param custom.sender.area can not be empty")
+ }
+ if params.Custom.Sender.Address == "" {
+ t.Error("param custom.sender.address can not be empty")
+ }
+ if params.Custom.Receiver.Name == "" {
+ t.Error("param custom.receiver.name can not be empty")
+ }
+ if params.Custom.Receiver.Province == "" {
+ t.Error("param custom.receiver.province can not be empty")
+ }
+ if params.Custom.Receiver.City == "" {
+ t.Error("param custom.receiver.city can not be empty")
+ }
+ if params.Custom.Receiver.Area == "" {
+ t.Error("param custom.receiver.area can not be empty")
+ }
+ if params.Custom.Receiver.Address == "" {
+ t.Error("param custom.receiver.address can not be empty")
+ }
+
+ if params.Custom.Cargo.Count == 0 {
+ t.Error("param custom.cargo.count can not be zero")
+ }
+ if params.Custom.Cargo.Weight == 0 {
+ t.Error("param custom.cargo.weight can not be zero")
+ }
+ if params.Custom.Cargo.SpaceX == 0 {
+ t.Error("param custom.cargo.spaceX can not be zero")
+ }
+ if params.Custom.Cargo.SpaceY == 0 {
+ t.Error("param custom.cargo.spaceY can not be zero")
+ }
+ if params.Custom.Cargo.SpaceZ == 0 {
+ t.Error("param custom.cargo.spaceZ can not be zero")
+ }
+ if len(params.Custom.Cargo.DetailList) == 0 {
+ t.Error("param cargo.custom.detailList can not be empty")
+ } else {
+ if (params.Custom.Cargo.DetailList[0].Name) == "" {
+ t.Error("param custom.cargo.detailList.name can not be empty")
+ }
+ if (params.Custom.Cargo.DetailList[0].Count) == 0 {
+ t.Error("param custom.cargo.detailList.count can not be zero")
+ }
+ }
+ if params.Custom.Shop.WXAPath == "" {
+ t.Error("param custom.shop.wxa_path can not be empty")
+ }
+ if params.Custom.Shop.IMGUrl == "" {
+ t.Error("param custom.shop.img_url can not be empty")
+ }
+ if params.Custom.Shop.GoodsName == "" {
+ t.Error("param custom.shop.goods_name can not be empty")
+ }
+ if params.Custom.Shop.GoodsCount == 0 {
+ t.Error("param custom.shop.goods_count can not be zero")
+ }
+ if params.Custom.Insured.Used == 0 {
+ t.Error("param custom.insured.use_insured can not be zero")
+ }
+ if params.Custom.Service.Name == "" {
+ t.Error("param custom.service.service_name can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "waybill_id": "1234567890123",
+ "rendered_waybill_template": "PGh0bWw+dGVzdDwvaHRtbD4="
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ raw := `{
+ "waybill_id": "1234567890123",
+ "waybill_data": "##ZTO_mark##11-22-33##ZTO_bagAddr##广州##",
+ "waybill_template": "PGh0bWw+dGVzdDwvaHRtbD4=",
+ "custom": {
+ "order_id": "012345678901234567890123456789",
+ "openid": "oABC123456",
+ "delivery_id": "ZTO",
+ "biz_id": "xyz",
+ "custom_remark": "易碎物品",
+ "sender": {
+ "name": "张三",
+ "tel": "18666666666",
+ "mobile": "020-88888888",
+ "company": "公司名",
+ "post_code": "123456",
+ "country": "中国",
+ "province": "广东省",
+ "city": "广州市",
+ "area": "海珠区",
+ "address": "XX路XX号XX大厦XX栋XX"
+ },
+ "receiver": {
+ "name": "王小蒙",
+ "tel": "18610000000",
+ "mobile": "020-77777777",
+ "company": "公司名",
+ "post_code": "654321",
+ "country": "中国",
+ "province": "广东省",
+ "city": "广州市",
+ "area": "天河区",
+ "address": "XX路XX号XX大厦XX栋XX"
+ },
+ "shop": {
+ "wxa_path": "/index/index?from=waybill",
+ "img_url": "https://mmbiz.qpic.cn/mmbiz_png/KfrZwACMrmwbPGicysN6kibW0ibXwzmA3mtTwgSsdw4Uicabduu2pfbfwdKicQ8n0v91kRAUX6SDESQypl5tlRwHUPA/640",
+ "goods_name": "一千零一夜钻石包&爱马仕柏金钻石包",
+ "goods_count": 2
+ },
+ "cargo": {
+ "count": 2,
+ "weight": 5.5,
+ "space_x": 30.5,
+ "space_y": 20,
+ "space_z": 20,
+ "detail_list": [
+ {
+ "name": "一千零一夜钻石包",
+ "count": 1
+ },
+ {
+ "name": "爱马仕柏金钻石包",
+ "count": 1
+ }
+ ]
+ },
+ "insured": {
+ "use_insured": 1,
+ "insured_value": 10000
+ },
+ "service": {
+ "service_type": 0,
+ "service_name": "标准快递"
+ }
+ }
+ }`
+
+ previewer := new(ExpressTemplatePreviewer)
+ err := json.Unmarshal([]byte(raw), previewer)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = previewer.preview(ts.URL+apiPreviewTemplate, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestUpdateBusiness(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Error("UnExpect request method")
+ }
+
+ if r.URL.EscapedPath() != "/cgi-bin/express/delivery/service/business/update" {
+ t.Error("Unexpected path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ ShopAppID string `json:"shop_app_id"` // 商户的小程序AppID,即审核商户事件中的 ShopAppID
+ BizID string `json:"biz_id"` // 商户账户
+ ResultCode int `json:"result_code"` // 审核结果,0 表示审核通过,其他表示审核失败
+ ResultMsg string `json:"result_msg"` // 审核错误原因,仅 result_code 不等于 0 时需要设置
+ }{}
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+ if params.ShopAppID == "" {
+ t.Error("Response column shop_app_id can not be empty")
+ }
+ if params.BizID == "" {
+ t.Error("Response column biz_id can not be empty")
+ }
+ if params.ResultCode == 0 {
+ t.Error("Response column result_code can not be zero")
+ }
+ if params.ResultMsg == "" {
+ t.Error("Response column result_msg can not be empty")
+ }
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ raw := `{
+ "shop_app_id": "wxABCD",
+ "biz_id": "xyz",
+ "result_code": 1,
+ "result_msg": "审核通过"
+ }`
+
+ updater := new(BusinessUpdater)
+ err := json.Unmarshal([]byte(raw), updater)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = updater.update(ts.URL+apiUpdateBusiness, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestUpdatePath(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Error("UnExpect request method")
+ }
+
+ if r.URL.EscapedPath() != "/cgi-bin/express/delivery/path/update" {
+ t.Error("Unexpected path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ Token string `json:"token"` // 商户侧下单事件中推送的 Token 字段
+ WaybillID string `json:"waybill_id"` // 运单 ID
+ ActionTime uint `json:"action_time"` // 轨迹变化 Unix 时间戳
+ ActionType int `json:"action_type"` // 轨迹变化类型
+ ActionMsg string `json:"action_msg"` // 轨迹变化具体信息说明,展示在快递轨迹详情页中。若有手机号码,则直接写11位手机号码。使用UTF-8编码。
+ }{}
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+ if params.Token == "" {
+ t.Error("Response column token can not be empty")
+ }
+ if params.WaybillID == "" {
+ t.Error("Response column waybill_id can not be empty")
+ }
+ if params.ActionMsg == "" {
+ t.Error("Response column action_msg can not be empty")
+ }
+ if params.ActionTime == 0 {
+ t.Error("Response column action_time can not be zero")
+ }
+ if params.ActionType == 0 {
+ t.Error("Response column action_type can not be zero")
+ }
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ raw := `{
+ "token": "TOKEN",
+ "waybill_id": "12345678901234567890",
+ "action_time": 1533052800,
+ "action_type": 300002,
+ "action_msg": "丽影邓丽君【18666666666】正在派件"
+ }`
+
+ updater := new(ExpressPathUpdater)
+ err := json.Unmarshal([]byte(raw), updater)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = updater.update(ts.URL+apiUpdatePath, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/app/lib/weapp/face_identify.go b/app/lib/weapp/face_identify.go
new file mode 100644
index 0000000..3f3aaef
--- /dev/null
+++ b/app/lib/weapp/face_identify.go
@@ -0,0 +1,46 @@
+package weapp
+
+const (
+ apiFaceIdentify = "/cityservice/face/identify/getinfo"
+)
+
+// FaceIdentifyResponse 人脸识别结果返回
+type FaceIdentifyResponse struct {
+ CommonError
+ Result int `json:"identify_ret"` // 认证结果
+ Time uint32 `json:"identify_time"` // 认证时间
+ Data string `json:"validate_data"` // 用户读的数字(如是读数字)
+ OpenID string `json:"openid"` // 用户openid
+ UserIDKey string `json:"user_id_key"` // 用于后台交户表示用户姓名、身份证的凭证
+ FinishTime uint32 `json:"finish_time"` // 认证结束时间
+ IDCardNumberMD5 string `json:"id_card_number_md5"` // 身份证号的md5(最后一位X为大写)
+ NameUTF8MD5 string `json:"name_utf8_md5"` // 姓名MD5
+}
+
+// FaceIdentify 获取人脸识别结果
+//
+// token 微信 access_token
+// key 小程序 verify_result
+func FaceIdentify(token, key string) (*FaceIdentifyResponse, error) {
+ api := baseURL + apiFaceIdentify
+ return faceIdentify(api, token, key)
+}
+
+func faceIdentify(api, token, key string) (*FaceIdentifyResponse, error) {
+ api, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ params := requestParams{
+ "verify_result": key,
+ }
+
+ res := new(FaceIdentifyResponse)
+ err = postJSON(api, params, res)
+ if err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
diff --git a/app/lib/weapp/img.go b/app/lib/weapp/img.go
new file mode 100644
index 0000000..191b470
--- /dev/null
+++ b/app/lib/weapp/img.go
@@ -0,0 +1,188 @@
+package weapp
+
+const (
+ apiAICrop = "/cv/img/aicrop"
+ apiScanQRCode = "/cv/img/qrcode"
+ apiSuperResolution = "/cv/img/superResolution"
+)
+
+// AICropResponse 图片智能裁剪后的返回数据
+type AICropResponse struct {
+ CommonError
+ Results []struct {
+ CropLeft uint `json:"crop_left"`
+ CropTop uint `json:"crop_top"`
+ CropRight uint `json:"crop_right"`
+ CropBottom uint `json:"crop_bottom"`
+ } `json:"results"`
+ IMGSize struct {
+ Width uint `json:"w"`
+ Height uint `json:"h"`
+ } `json:"img_size"`
+}
+
+// AICrop 本接口提供基于小程序的图片智能裁剪能力。
+func AICrop(token, filename string) (*AICropResponse, error) {
+ api := baseURL + apiAICrop
+ return aiCrop(api, token, filename)
+}
+
+func aiCrop(api, token, filename string) (*AICropResponse, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(AICropResponse)
+ if err := postFormByFile(url, "img", filename, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// AICropByURL 本接口提供基于小程序的图片智能裁剪能力。
+func AICropByURL(token, url string) (*AICropResponse, error) {
+ api := baseURL + apiAICrop
+ return aiCropByURL(api, token, url)
+}
+
+func aiCropByURL(api, token, imgURL string) (*AICropResponse, error) {
+ queries := requestQueries{
+ "access_token": token,
+ "img_url": imgURL,
+ }
+
+ url, err := encodeURL(api, queries)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(AICropResponse)
+ if err := postJSON(url, nil, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// QRCodePoint 二维码角的位置
+type QRCodePoint struct {
+ X uint `json:"x"`
+ Y uint `json:"y"`
+}
+
+// ScanQRCodeResponse 小程序的条码/二维码识别后的返回数据
+type ScanQRCodeResponse struct {
+ CommonError
+ CodeResults []struct {
+ TypeName string `json:"type_name"`
+ Data string `json:"data"`
+ Position struct {
+ LeftTop QRCodePoint `json:"left_top"`
+ RightTop QRCodePoint `json:"right_top"`
+ RightBottom QRCodePoint `json:"right_bottom"`
+ LeftBottom QRCodePoint `json:"left_bottom"`
+ } `json:"pos"`
+ } `json:"code_results"`
+ IMGSize struct {
+ Width uint `json:"w"`
+ Height uint `json:"h"`
+ } `json:"img_size"`
+}
+
+// ScanQRCode 本接口提供基于小程序的条码/二维码识别的API。
+func ScanQRCode(token, filename string) (*ScanQRCodeResponse, error) {
+ api := baseURL + apiScanQRCode
+ return scanQRCode(api, token, filename)
+}
+
+func scanQRCode(api, token, filename string) (*ScanQRCodeResponse, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(ScanQRCodeResponse)
+ if err := postFormByFile(url, "img", filename, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// ScanQRCodeByURL 把网络文件上传到微信服务器。目前仅支持图片。用于发送客服消息或被动回复用户消息。
+func ScanQRCodeByURL(token, imgURL string) (*ScanQRCodeResponse, error) {
+ api := baseURL + apiScanQRCode
+ return scanQRCodeByURL(api, token, imgURL)
+}
+
+func scanQRCodeByURL(api, token, imgURL string) (*ScanQRCodeResponse, error) {
+ queries := requestQueries{
+ "access_token": token,
+ "img_url": imgURL,
+ }
+
+ url, err := encodeURL(api, queries)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(ScanQRCodeResponse)
+ if err := postJSON(url, nil, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// SuperResolutionResponse 图片高清化后的返回数据
+type SuperResolutionResponse struct {
+ CommonError
+ MediaID string `json:"media_id"`
+}
+
+// SuperResolution 本接口提供基于小程序的图片高清化能力。
+func SuperResolution(token, filename string) (*SuperResolutionResponse, error) {
+ api := baseURL + apiSuperResolution
+ return superResolution(api, token, filename)
+}
+
+func superResolution(api, token, filename string) (*SuperResolutionResponse, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(SuperResolutionResponse)
+ if err := postFormByFile(url, "img", filename, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// SuperResolutionByURL 把网络文件上传到微信服务器。目前仅支持图片。用于发送客服消息或被动回复用户消息。
+func SuperResolutionByURL(token, imgURL string) (*SuperResolutionResponse, error) {
+ api := baseURL + apiSuperResolution
+ return superResolutionByURL(api, token, imgURL)
+}
+
+func superResolutionByURL(api, token, imgURL string) (*SuperResolutionResponse, error) {
+ queries := requestQueries{
+ "access_token": token,
+ "img_url": imgURL,
+ }
+
+ url, err := encodeURL(api, queries)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(SuperResolutionResponse)
+ if err := postJSON(url, nil, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
diff --git a/app/lib/weapp/img_test.go b/app/lib/weapp/img_test.go
new file mode 100644
index 0000000..8c393f8
--- /dev/null
+++ b/app/lib/weapp/img_test.go
@@ -0,0 +1,474 @@
+package weapp
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path"
+ "testing"
+)
+
+func TestAICrop(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ if r.URL.EscapedPath() != "/cv/img/aicrop" {
+ t.Error("Invalid request path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ if _, _, err := r.FormFile("img"); err != nil {
+ t.Fatal(err)
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok",
+ "results": [
+ {
+ "crop_left": 112,
+ "crop_top": 0,
+ "crop_right": 839,
+ "crop_bottom": 727
+ },
+ {
+ "crop_left": 0,
+ "crop_top": 205,
+ "crop_right": 965,
+ "crop_bottom": 615
+ }
+ ],
+ "img_size": {
+ "w": 966,
+ "h": 728
+ }
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := aiCrop(ts.URL+apiAICrop, "mock-access-token", testIMGName)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestAICropByURL(t *testing.T) {
+ server := http.NewServeMux()
+ server.HandleFunc("/cv/img/aicrop", func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ if r.URL.EscapedPath() != "/cv/img/aicrop" {
+ t.Error("Invalid request path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ queries := []string{"access_token", "img_url"}
+ for _, v := range queries {
+ content := r.Form.Get(v)
+ if content == "" {
+ t.Fatalf("%v can not be empty", v)
+ }
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok",
+ "results": [
+ {
+ "crop_left": 112,
+ "crop_top": 0,
+ "crop_right": 839,
+ "crop_bottom": 727
+ },
+ {
+ "crop_left": 0,
+ "crop_top": 205,
+ "crop_right": 965,
+ "crop_bottom": 615
+ }
+ ],
+ "img_size": {
+ "w": 966,
+ "h": 728
+ }
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ server.HandleFunc("/mediaurl", func(w http.ResponseWriter, r *http.Request) {
+ filename := testIMGName
+ file, err := os.Open(filename)
+ if err != nil {
+ t.Fatal((err))
+ }
+ defer file.Close()
+
+ ext := path.Ext(filename)
+ ext = ext[1:len(ext)]
+ w.Header().Set("Content-Type", "image/"+ext)
+ w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", path.Base(filename)))
+ w.WriteHeader(http.StatusOK)
+
+ if _, err := io.Copy(w, file); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ ts := httptest.NewServer(server)
+ defer ts.Close()
+
+ _, err := aiCropByURL(ts.URL+apiAICrop, "mock-access-token", ts.URL+"/mediaurl")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+func TestScanQRCode(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiScanQRCode {
+ t.Fatalf("Except to path '%s',get '%s'", apiScanQRCode, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ if _, _, err := r.FormFile("img"); err != nil {
+ t.Fatal(err)
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok",
+ "code_results": [
+ {
+ "type_name": "QR_CODE",
+ "data": "http://www.qq.com",
+ "pos": {
+ "left_top": {
+ "x": 585,
+ "y": 378
+ },
+ "right_top": {
+ "x": 828,
+ "y": 378
+ },
+ "right_bottom": {
+ "x": 828,
+ "y": 618
+ },
+ "left_bottom": {
+ "x": 585,
+ "y": 618
+ }
+ }
+ },
+ {
+ "type_name": "QR_CODE",
+ "data": "https://mp.weixin.qq.com",
+ "pos": {
+ "left_top": {
+ "x": 185,
+ "y": 142
+ },
+ "right_top": {
+ "x": 396,
+ "y": 142
+ },
+ "right_bottom": {
+ "x": 396,
+ "y": 353
+ },
+ "left_bottom": {
+ "x": 185,
+ "y": 353
+ }
+ }
+ },
+ {
+ "type_name": "EAN_13",
+ "data": "5906789678957"
+ },
+ {
+ "type_name": "CODE_128",
+ "data": "50090500019191"
+ }
+ ],
+ "img_size": {
+ "w": 1000,
+ "h": 900
+ }
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := scanQRCode(ts.URL+apiScanQRCode, "mock-access-token", testIMGName)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestScanQRCodeByURL(t *testing.T) {
+ server := http.NewServeMux()
+ server.HandleFunc(apiScanQRCode, func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiScanQRCode {
+ t.Fatalf("Except to path '%s',get '%s'", apiScanQRCode, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ queries := []string{"access_token", "img_url"}
+ for _, v := range queries {
+ content := r.Form.Get(v)
+ if content == "" {
+ t.Fatalf("%v can not be empty", v)
+ }
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok",
+ "code_results": [
+ {
+ "type_name": "QR_CODE",
+ "data": "http://www.qq.com",
+ "pos": {
+ "left_top": {
+ "x": 585,
+ "y": 378
+ },
+ "right_top": {
+ "x": 828,
+ "y": 378
+ },
+ "right_bottom": {
+ "x": 828,
+ "y": 618
+ },
+ "left_bottom": {
+ "x": 585,
+ "y": 618
+ }
+ }
+ },
+ {
+ "type_name": "QR_CODE",
+ "data": "https://mp.weixin.qq.com",
+ "pos": {
+ "left_top": {
+ "x": 185,
+ "y": 142
+ },
+ "right_top": {
+ "x": 396,
+ "y": 142
+ },
+ "right_bottom": {
+ "x": 396,
+ "y": 353
+ },
+ "left_bottom": {
+ "x": 185,
+ "y": 353
+ }
+ }
+ },
+ {
+ "type_name": "EAN_13",
+ "data": "5906789678957"
+ },
+ {
+ "type_name": "CODE_128",
+ "data": "50090500019191"
+ }
+ ],
+ "img_size": {
+ "w": 1000,
+ "h": 900
+ }
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ server.HandleFunc("/mediaurl", func(w http.ResponseWriter, r *http.Request) {
+ filename := testIMGName
+ file, err := os.Open(filename)
+ if err != nil {
+ t.Fatal((err))
+ }
+ defer file.Close()
+
+ ext := path.Ext(filename)
+ ext = ext[1:len(ext)]
+ w.Header().Set("Content-Type", "image/"+ext)
+ w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", path.Base(filename)))
+ w.WriteHeader(http.StatusOK)
+
+ if _, err := io.Copy(w, file); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ ts := httptest.NewServer(server)
+ defer ts.Close()
+
+ _, err := scanQRCodeByURL(ts.URL+apiScanQRCode, "mock-access-token", ts.URL+"/mediaurl")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSuperResolution(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiSuperResolution {
+ t.Fatalf("Except to path '%s',get '%s'", apiSuperResolution, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ if _, _, err := r.FormFile("img"); err != nil {
+ t.Fatal(err)
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok",
+ "media_id": "6WXsIXkG7lXuDLspD9xfm5dsvHzb0EFl0li6ySxi92ap8Vl3zZoD9DpOyNudeJGB"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := superResolution(ts.URL+apiSuperResolution, "mock-access-token", testIMGName)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSuperResolutionByURL(t *testing.T) {
+ server := http.NewServeMux()
+ server.HandleFunc(apiSuperResolution, func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiSuperResolution {
+ t.Fatalf("Except to path '%s',get '%s'", apiSuperResolution, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ queries := []string{"access_token", "img_url"}
+ for _, v := range queries {
+ content := r.Form.Get(v)
+ if content == "" {
+ t.Fatalf("%v can not be empty", v)
+ }
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok",
+ "media_id": "6WXsIXkG7lXuDLspD9xfm5dsvHzb0EFl0li6ySxi92ap8Vl3zZoD9DpOyNudeJGB"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ server.HandleFunc("/mediaurl", func(w http.ResponseWriter, r *http.Request) {
+ filename := testIMGName
+ file, err := os.Open(filename)
+ if err != nil {
+ t.Fatal((err))
+ }
+ defer file.Close()
+
+ ext := path.Ext(filename)
+ ext = ext[1:len(ext)]
+ w.Header().Set("Content-Type", "image/"+ext)
+ w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", path.Base(filename)))
+ w.WriteHeader(http.StatusOK)
+
+ if _, err := io.Copy(w, file); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ ts := httptest.NewServer(server)
+ defer ts.Close()
+
+ _, err := superResolutionByURL(ts.URL+apiSuperResolution, "mock-access-token", ts.URL+"/mediaurl")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/app/lib/weapp/immediate_delivery.go b/app/lib/weapp/immediate_delivery.go
new file mode 100644
index 0000000..4319d49
--- /dev/null
+++ b/app/lib/weapp/immediate_delivery.go
@@ -0,0 +1,479 @@
+package weapp
+
+const (
+ apiAbnormalConfirm = "/cgi-bin/express/local/business/order/confirm_return"
+ apiAddDeliveryOrder = "/cgi-bin/express/local/business/order/add"
+ apiAddDeliveryTip = "/cgi-bin/express/local/business/order/addtips"
+ apiCancelDeliveryOrder = "/cgi-bin/express/local/business/order/cancel"
+ apiGetAllImmediateDelivery = "/cgi-bin/express/local/business/delivery/getall"
+ apiGetDeliveryBindAccount = "/cgi-bin/express/local/business/shop/get"
+ apiGetDeliveryOrder = "/cgi-bin/express/local/business/order/get"
+ apiPreAddDeliveryOrder = "/cgi-bin/express/local/business/order/pre_add"
+ apiPreCancelDeliveryOrder = "/cgi-bin/express/local/business/order/precancel"
+ apiReAddDeliveryOrder = "/cgi-bin/express/local/business/order/readd"
+ apiMockUpdateDeliveryOrder = "/cgi-bin/express/local/business/test_update_order"
+ apiUpdateDeliveryOrder = "/cgi-bin/express/local/delivery/update_order"
+)
+
+// AbnormalConfirmer 异常件退回商家商家确认器
+type AbnormalConfirmer struct {
+ ShopID string `json:"shopid"` // 商家id, 由配送公司分配的appkey
+ ShopOrderID string `json:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ ShopNo string `json:"shop_no"` // 商家门店编号, 在配送公司登记,如果只有一个门店,可以不填
+ DeliverySign string `json:"delivery_sign"` // 用配送公司提供的appSecret加密的校验串
+ WaybillID string `json:"waybill_id"` // 配送单id
+ Remark string `json:"remark"` // 备注
+}
+
+// Confirm 异常件退回商家商家确认收货
+func (confirmer *AbnormalConfirmer) Confirm(token string) (*CommonResult, error) {
+ api := baseURL + apiAbnormalConfirm
+ return confirmer.confirm(api, token)
+}
+
+func (confirmer *AbnormalConfirmer) confirm(api, token string) (*CommonResult, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(CommonResult)
+ if err := postJSON(url, confirmer, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// DeliveryOrderCreator 下配送单参数
+type DeliveryOrderCreator struct {
+ DeliveryToken string `json:"delivery_token,omitempty"` // 预下单接口返回的参数,配送公司可保证在一段时间内运费不变
+ ShopID string `json:"shopid"` // 商家id, 由配送公司分配的appkey
+ ShopOrderID string `json:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ ShopNo string `json:"shop_no"` // 商家门店编号, 在配送公司登记,如果只有一个门店,可以不填
+ DeliverySign string `json:"delivery_sign"` // 用配送公司提供的appSecret加密的校验串
+ DeliveryID string `json:"delivery_id"` // 配送公司ID
+ OpenID string `json:"openid"` // 下单用户的openid
+ Sender DeliveryUser `json:"sender"` // 发件人信息,闪送、顺丰同城急送必须填写,美团配送、达达,若传了shop_no的值可不填该字段
+ Receiver DeliveryUser `json:"receiver"` // 收件人信息
+ Cargo DeliveryCargo `json:"cargo"` // 货物信息
+ OrderInfo DeliveryOrderInfo `json:"order_info"` // 订单信息
+ Shop DeliveryShop `json:"shop"` // 商品信息,会展示到物流通知消息中
+ SubBizID string `json:"sub_biz_id"` // 子商户id,区分小程序内部多个子商户
+}
+
+// DeliveryUser 发件人信息,闪送、顺丰同城急送必须填写,美团配送、达达,若传了shop_no的值可不填该字段
+type DeliveryUser struct {
+ Name string `json:"name"` // 姓名,最长不超过256个字符
+ City string `json:"city"` // 城市名称,如广州市
+ Address string `json:"address"` // 地址(街道、小区、大厦等,用于定位)
+ AddressDetail string `json:"address_detail"` // 地址详情(楼号、单元号、层号)
+ Phone string `json:"phone"` // 电话/手机号,最长不超过64个字符
+ Lng float64 `json:"lng"` // 经度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,确到小数点后6位
+ Lat float64 `json:"lat"` // 纬度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,精确到小数点后6位)
+ CoordinateType uint8 `json:"coordinate_type"` // 坐标类型,0:火星坐标(高德,腾讯地图均采用火星坐标) 1:百度坐标
+}
+
+// DeliveryCargo 货物信息
+type DeliveryCargo struct {
+ GoodsValue float64 `json:"goods_value"` // 货物价格,单位为元,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-5000]
+ GoodsHeight float64 `json:"goods_height"` // 货物高度,单位为cm,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-45]
+ GoodsLength float64 `json:"goods_length"` // 货物长度,单位为cm,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-65]
+ GoodsWidth float64 `json:"goods_width"` // 货物宽度,单位为cm,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-50]
+ GoodsWeight float64 `json:"goods_weight"` // 货物重量,单位为kg,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-50]
+ GoodsDetail DeliveryGoodsDetail `json:"goods_detail"` // 货物详情,最长不超过10240个字符
+ GoodsPickupInfo string `json:"goods_pickup_info"` // 货物取货信息,用于骑手到店取货,最长不超过100个字符
+ GoodsDeliveryInfo string `json:"goods_delivery_info"` // 货物交付信息,最长不超过100个字符
+ CargoFirstClass string `json:"cargo_first_class"` // 品类一级类目
+ CargoSecondClass string `json:"cargo_second_class"` // 品类二级类目
+}
+
+// DeliveryGoodsDetail 货物详情
+type DeliveryGoodsDetail struct {
+ Goods []DeliveryGoods `json:"goods"` // 货物交付信息,最长不超过100个字符
+}
+
+// DeliveryGoods 货物
+type DeliveryGoods struct {
+ Count uint `json:"good_count"` // 货物数量
+ Name string `json:"good_name"` // 货品名称
+ Price float32 `json:"good_price"` // 货品单价,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数)
+ Unit string `json:"good_unit"` // 货品单位,最长不超过20个字符
+}
+
+// DeliveryOrderInfo 订单信息
+type DeliveryOrderInfo struct {
+ DeliveryServiceCode string `json:"delivery_service_code"` // 配送服务代码 不同配送公司自定义,微信侧不理解
+ OrderType uint8 `json:"order_type"` // 订单类型, 0: 即时单 1 预约单,如预约单,需要设置expected_delivery_time或expected_finish_time或expected_pick_time
+ ExpectedDeliveryTime uint `json:"expected_delivery_time"` // 期望派单时间(顺丰同城急送、达达、支持),unix-timestamp
+ ExpectedFinishTime uint `json:"expected_finish_time"` // 期望送达时间(顺丰同城急送、美团配送支持),unix-timestamp
+ ExpectedPickTime uint `json:"expected_pick_time"` // 期望取件时间(闪送支持),unix-timestamp
+ PoiSeq string `json:"poi_seq"` // 门店订单流水号,建议提供,方便骑手门店取货,最长不超过32个字符
+ Note string `json:"note"` // 备注,最长不超过200个字符
+ OrderTime uint `json:"order_time"` // 用户下单付款时间
+ IsInsured uint8 `json:"is_insured"` // 是否保价,0,非保价,1.保价
+ DeclaredValue float64 `json:"declared_value"` // 保价金额,单位为元,精确到分
+ Tips float64 `json:"tips"` // 小费,单位为元, 下单一般不加小费
+ IsDirectDelivery uint `json:"is_direct_delivery"` // 是否选择直拿直送(0:不需要;1:需要。选择直拿直送后,同一时间骑手只能配送此订单至完成,配送费用也相应高一些,闪送必须选1,达达可选0或1,其余配送公司不支持直拿直送)
+ CashOnDelivery uint `json:"cash_on_delivery"` // 骑手应付金额,单位为元,精确到分
+ CashOnPickup uint `json:"cash_on_pickup"` // 骑手应收金额,单位为元,精确到分
+ RiderPickMethod uint8 `json:"rider_pick_method"` // 物流流向,1:从门店取件送至用户;2:从用户取件送至门店
+ IsFinishCodeNeeded uint8 `json:"is_finish_code_needed"` // 收货码(0:不需要;1:需要。收货码的作用是:骑手必须输入收货码才能完成订单妥投)
+ IsPickupCodeNeeded uint8 `json:"is_pickup_code_needed"` // 取货码(0:不需要;1:需要。取货码的作用是:骑手必须输入取货码才能从商家取货)
+}
+
+// DeliveryShop 商品信息,会展示到物流通知消息中
+type DeliveryShop struct {
+ WxaPath string `json:"wxa_path"` // 商家小程序的路径,建议为订单页面
+ ImgURL string `json:"img_url"` // 商品缩略图 url
+ GoodsName string `json:"goods_name"` // 商品名称
+ GoodsCount uint `json:"goods_count"` // 商品数量
+}
+
+// PreDeliveryOrderResponse 返回数据
+type PreDeliveryOrderResponse struct {
+ Fee float64 `json:"fee"` // 实际运费(单位:元),运费减去优惠券费用
+ Deliverfee float64 `json:"deliverfee"` // 运费(单位:元)
+ Couponfee float64 `json:"couponfee"` // 优惠券费用(单位:元)
+ Tips float64 `json:"tips"` // 小费(单位:元)
+ Insurancefee float64 `json:"insurancefee"` // 保价费(单位:元)
+ Distance float64 `json:"distance"` // 配送距离(单位:米)
+ DispatchDuration uint `json:"dispatch_duration"` // 预计骑手接单时间,单位秒,比如5分钟,就填300, 无法预计填0
+ DeliveryToken string `json:"delivery_token"` // 配送公司可以返回此字段,当用户下单时候带上这个字段,保证在一段时间内运费不变
+}
+
+// Prepare 预下配送单接口
+func (creator *DeliveryOrderCreator) Prepare(token string) (*PreDeliveryOrderResponse, error) {
+ api := baseURL + apiPreCancelDeliveryOrder
+ return creator.prepare(api, token)
+}
+
+func (creator *DeliveryOrderCreator) prepare(api, token string) (*PreDeliveryOrderResponse, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(PreDeliveryOrderResponse)
+ if err := postJSON(url, creator, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// CreateDeliveryOrderResponse 返回数据
+type CreateDeliveryOrderResponse struct {
+ CommonResult
+ Fee uint `json:"fee"` //实际运费(单位:元),运费减去优惠券费用
+ Deliverfee uint `json:"deliverfee"` //运费(单位:元)
+ Couponfee uint `json:"couponfee"` //优惠券费用(单位:元)
+ Tips uint `json:"tips"` //小费(单位:元)
+ Insurancefee uint `json:"insurancefee"` //保价费(单位:元)
+ Distance float64 `json:"distance"` // 配送距离(单位:米)
+ WaybillID string `json:"waybill_id"` //配送单号
+ OrderStatus int `json:"order_status"` //配送状态
+ FinishCode uint `json:"finish_code"` // 收货码
+ PickupCode uint `json:"pickup_code"` //取货码
+ DispatchDuration uint `json:"dispatch_duration"` // 预计骑手接单时间,单位秒,比如5分钟,就填300, 无法预计填0
+}
+
+// Create 下配送单
+func (creator *DeliveryOrderCreator) Create(token string) (*CreateDeliveryOrderResponse, error) {
+ api := baseURL + apiAddDeliveryOrder
+ return creator.create(api, token)
+}
+
+func (creator *DeliveryOrderCreator) create(api, token string) (*CreateDeliveryOrderResponse, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(CreateDeliveryOrderResponse)
+ if err := postJSON(url, creator, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// Recreate 重新下单
+func (creator *DeliveryOrderCreator) Recreate(token string) (*CreateDeliveryOrderResponse, error) {
+ api := baseURL + apiReAddDeliveryOrder
+ return creator.recreate(api, token)
+}
+
+func (creator *DeliveryOrderCreator) recreate(api, token string) (*CreateDeliveryOrderResponse, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(CreateDeliveryOrderResponse)
+ if err := postJSON(url, creator, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// DeliveryTipAdder 增加小费参数
+type DeliveryTipAdder struct {
+ ShopID string `json:"shopid"` // 商家id, 由配送公司分配的appkey
+ ShopOrderID string `json:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ ShopNo string `json:"shop_no"` // 商家门店编号, 在配送公司登记,如果只有一个门店,可以不填
+ DeliverySign string `json:"delivery_sign"` // 用配送公司提供的appSecret加密的校验串
+ WaybillID string `json:"waybill_id"` // 配送单id
+ OpenID string `json:"openid"` // 下单用户的openid
+ Tips float64 `json:"tips"` // 小费金额(单位:元) 各家配送公司最大值不同
+ Remark string `json:"Remark"` // 备注
+}
+
+// Add 对待接单状态的订单增加小费。需要注意:订单的小费,以最新一次加小费动作的金额为准,故下一次增加小费额必须大于上一次小费额
+func (adder *DeliveryTipAdder) Add(token string) (*CommonResult, error) {
+ api := baseURL + apiAddDeliveryTip
+ return adder.add(api, token)
+}
+
+func (adder *DeliveryTipAdder) add(api, token string) (*CommonResult, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(CommonResult)
+ if err := postJSON(url, adder, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// DeliveryOrderCanceler 取消配送单参数
+type DeliveryOrderCanceler struct {
+ ShopID string `json:"shopid"` // 商家id, 由配送公司分配的appkey
+ ShopOrderID string `json:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ ShopNo string `json:"shop_no"` // 商家门店编号, 在配送公司登记,如果只有一个门店,可以不填
+ DeliverySign string `json:"delivery_sign"` // 用配送公司提供的appSecret加密的校验串
+ DeliveryID string `json:"delivery_id"` // 快递公司ID
+ WaybillID string `json:"waybill_id"` // 配送单id
+ ReasonID uint8 `json:"cancel_reason_id"` // 取消原因Id
+ Reason string `json:"cancel_reason"` // 取消原因
+}
+
+// CancelDeliveryOrderResponse 取消配送单返回数据
+type CancelDeliveryOrderResponse struct {
+ CommonResult
+ DeductFee float64 `json:"deduct_fee"` // 预计扣除的违约金(单位:元),精确到分
+ Desc string `json:"desc"` //说明
+}
+
+// Prepare 预取消配送单
+func (canceler *DeliveryOrderCanceler) Prepare(token string) (*CancelDeliveryOrderResponse, error) {
+ api := baseURL + apiCancelDeliveryOrder
+ return canceler.prepare(api, token)
+}
+
+func (canceler *DeliveryOrderCanceler) prepare(api, token string) (*CancelDeliveryOrderResponse, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(CancelDeliveryOrderResponse)
+ if err := postJSON(url, canceler, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// Cancel 取消配送单
+func (canceler *DeliveryOrderCanceler) Cancel(token string) (*CancelDeliveryOrderResponse, error) {
+ api := baseURL + apiCancelDeliveryOrder
+ return canceler.cancel(api, token)
+}
+
+func (canceler *DeliveryOrderCanceler) cancel(api, token string) (*CancelDeliveryOrderResponse, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(CancelDeliveryOrderResponse)
+ if err := postJSON(url, canceler, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// GetAllImmediateDeliveryResponse 获取已支持的配送公司列表接口返回数据
+type GetAllImmediateDeliveryResponse struct {
+ CommonResult
+ List []struct {
+ ID string `json:"delivery_id"` //配送公司Id
+ Name string `json:"delivery_name"` // 配送公司名称
+ } `json:"list"` // 配送公司列表
+}
+
+// GetAllImmediateDelivery 获取已支持的配送公司列表接口
+func GetAllImmediateDelivery(token string) (*GetAllImmediateDeliveryResponse, error) {
+ api := baseURL + apiGetAllImmediateDelivery
+ return getAllImmediateDelivery(api, token)
+}
+
+func getAllImmediateDelivery(api, token string) (*GetAllImmediateDeliveryResponse, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(GetAllImmediateDeliveryResponse)
+ if err := postJSON(url, nil, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// GetBindAccountResponse 返回数据
+type GetBindAccountResponse struct {
+ CommonResult
+ ShopList []struct {
+ DeliveryID string `json:"delivery_id"` // 配送公司Id
+ ShopID string `json:"shopid"` // 商家id
+ AuditResult uint8 `json:"audit_result"` // 审核状态
+ } `json:"shop_list"` // 配送公司列表
+}
+
+// GetBindAccount 拉取已绑定账号
+func GetBindAccount(token string) (*GetBindAccountResponse, error) {
+ api := baseURL + apiGetDeliveryBindAccount
+ return getBindAccount(api, token)
+}
+
+func getBindAccount(api, token string) (*GetBindAccountResponse, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(GetBindAccountResponse)
+ if err := postJSON(url, nil, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// DeliveryOrderGetter 请求参数
+type DeliveryOrderGetter struct {
+ ShopID string `json:"shopid"` // 商家id, 由配送公司分配的appkey
+ ShopOrderID string `json:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ ShopNo string `json:"shop_no"` // 商家门店编号, 在配送公司登记,如果只有一个门店,可以不填
+ DeliverySign string `json:"delivery_sign"` // 用配送公司提供的appSecret加密的校验串说明
+}
+
+// GetDeliveryOrderResponse 返回数据
+type GetDeliveryOrderResponse struct {
+ CommonResult
+ OrderStatus int `json:"order_status"` // 配送状态,枚举值
+ WaybillID string `json:"waybill_id"` // 配送单号
+ RiderName string `json:"rider_name"` // 骑手姓名
+ RiderPhone string `json:"rider_phone"` // 骑手电话
+ RiderLng float64 `json:"rider_lng"` // 骑手位置经度, 配送中时返回
+ RiderLat float64 `json:"rider_lat"` // 骑手位置纬度, 配送中时返回
+}
+
+// Get 下配送单
+func (getter *DeliveryOrderGetter) Get(token string) (*GetDeliveryOrderResponse, error) {
+ api := baseURL + apiGetDeliveryOrder
+ return getter.get(api, token)
+}
+
+func (getter *DeliveryOrderGetter) get(api, token string) (*GetDeliveryOrderResponse, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(GetDeliveryOrderResponse)
+ if err := postJSON(url, getter, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// UpdateDeliveryOrderMocker 请求参数
+type UpdateDeliveryOrderMocker struct {
+ ShopID string `json:"shopid"` // 商家id, 必须是 "test_shop_id"
+ ShopOrderID string `json:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ ActionTime uint `json:"action_time"` // 状态变更时间点,Unix秒级时间戳
+ OrderStatus int `json:"order_status"` // 配送状态,枚举值
+ ActionMsg string `json:"action_msg"` // 附加信息
+}
+
+// Mock 模拟配送公司更新配送单状态
+func (mocker *UpdateDeliveryOrderMocker) Mock(token string) (*CommonResult, error) {
+ api := baseURL + apiMockUpdateDeliveryOrder
+ return mocker.mock(api, token)
+}
+
+func (mocker *UpdateDeliveryOrderMocker) mock(api, token string) (*CommonResult, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(CommonResult)
+ if err := postJSON(url, mocker, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// DeliveryOrderUpdater 请求参数
+type DeliveryOrderUpdater struct {
+ WXToken string `json:"wx_token"` // 下单事件中推送的wx_token字段
+ ShopID string `json:"shopid"` // 商家id, 由配送公司分配,可以是dev_id或者appkey
+ ShopOrderID string `json:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ ShopNo string `json:"shop_no,omitempty"` // 商家门店编号, 在配送公司侧登记
+ WaybillID string `json:"waybill_id"` // 配送单id
+ ActionTime uint `json:"action_time"` // 状态变更时间点,Unix秒级时间戳
+ OrderStatus int `json:"order_status"` // 订单状态,枚举值,下附枚举值列表及说明
+ ActionMsg string `json:"action_msg,omitempty"` // 附加信息
+ WxaPath string `json:"wxa_path"` // 配送公司小程序跳转路径,用于用户收到消息会间接跳转到这个页面
+ Agent DeliveryAgent `json:"agent,omitempty"` // 骑手信息, 骑手接单时需返回
+ ExpectedDeliveryTime uint `json:"expected_delivery_time,omitempty"` // 预计送达时间戳, 骑手接单时需返回
+}
+
+// DeliveryAgent 骑手信息
+type DeliveryAgent struct {
+ Name string `json:"name"` // 骑手姓名
+ Phone string `json:"phone"` // 骑手电话
+ Encrypted uint8 `json:"is_phone_encrypted,omitempty"` // 电话是否加密
+}
+
+// Update 模拟配送公司更新配送单状态
+func (updater *DeliveryOrderUpdater) Update(token string) (*CommonResult, error) {
+ api := baseURL + apiUpdateDeliveryOrder
+ return updater.update(api, token)
+}
+
+func (updater *DeliveryOrderUpdater) update(api, token string) (*CommonResult, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(CommonResult)
+ if err := postJSON(url, updater, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
diff --git a/app/lib/weapp/immediate_delivery_test.go b/app/lib/weapp/immediate_delivery_test.go
new file mode 100644
index 0000000..cf34975
--- /dev/null
+++ b/app/lib/weapp/immediate_delivery_test.go
@@ -0,0 +1,2041 @@
+package weapp
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+)
+
+func TestAbnormalConfirm(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Error("UnExpect request method")
+ }
+
+ if r.URL.EscapedPath() != "/cgi-bin/express/local/business/order/confirm_return" {
+ t.Error("Unexpected path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ ShopID string `json:"shopid"` // 商家id, 由配送公司分配的appkey
+ ShopOrderID string `json:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ ShopNo string `json:"shop_no"` // 商家门店编号, 在配送公司登记,如果只有一个门店,可以不填
+ DeliverySign string `json:"delivery_sign"` // 用配送公司提供的appSecret加密的校验串
+ WaybillID string `json:"waybill_id"` // 配送单id
+ Remark string `json:"remark"` // 备注
+ }{}
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+ if params.ShopID == "" {
+ t.Error("Response column shopid can not be empty")
+ }
+ if params.ShopOrderID == "" {
+ t.Error("Response column shop_order_id can not be empty")
+ }
+ if params.ShopNo == "" {
+ t.Error("Response column shop_no can not be empty")
+ }
+
+ if params.DeliverySign == "" {
+ t.Error("Response column delivery_sign can not be empty")
+ }
+
+ if params.WaybillID == "" {
+ t.Error("Response column waybill_id can not be empty")
+ }
+ if params.Remark == "" {
+ t.Error("Response column remark can not be empty")
+ }
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "errcode": 1,
+ "errmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ raw := `{
+ "shopid": "123456",
+ "shop_order_id": "123456",
+ "shop_no": "shop_no_111",
+ "waybill_id": "123456",
+ "remark": "remark",
+ "delivery_sign": "123456"
+ }`
+
+ confirmer := new(AbnormalConfirmer)
+ err := json.Unmarshal([]byte(raw), confirmer)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = confirmer.confirm(ts.URL+apiAbnormalConfirm, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestAddDeliveryOrder(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Error("UnExpect request method")
+ }
+
+ if r.URL.EscapedPath() != "/cgi-bin/express/local/business/order/add" {
+ t.Error("Unexpected path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ DeliveryToken string `json:"delivery_token"` // 预下单接口返回的参数,配送公司可保证在一段时间内运费不变
+ ShopID string `json:"shopid"` // 商家id, 由配送公司分配的appkey
+ ShopOrderID string `json:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ ShopNo string `json:"shop_no"` // 商家门店编号, 在配送公司登记,如果只有一个门店,可以不填
+ DeliverySign string `json:"delivery_sign"` // 用配送公司提供的appSecret加密的校验串
+ DeliveryID string `json:"delivery_id"` // 配送公司ID
+ OpenID string `json:"openid"` // 下单用户的openid
+ SubBizID string `json:"sub_biz_id"` // 子商户id,区分小程序内部多个子商户
+ Sender struct {
+ Name string `json:"name"` // 姓名,最长不超过256个字符
+ City string `json:"city"` // 城市名称,如广州市
+ Address string `json:"address"` // 地址(街道、小区、大厦等,用于定位)
+ AddressDetail string `json:"address_detail"` // 地址详情(楼号、单元号、层号)
+ Phone string `json:"phone"` // 电话/手机号,最长不超过64个字符
+ Lng float64 `json:"lng"` // 经度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,确到小数点后6位
+ Lat float64 `json:"lat"` // 纬度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,精确到小数点后6位)
+ CoordinateType uint8 `json:"coordinate_type"` // 坐标类型,0:火星坐标(高德,腾讯地图均采用火星坐标) 1:百度坐标
+ } `json:"sender"` // 发件人信息,闪送、顺丰同城急送必须填写,美团配送、达达,若传了shop_no的值可不填该字段
+ Receiver struct {
+ Name string `json:"name"` // 姓名,最长不超过256个字符
+ City string `json:"city"` // 城市名称,如广州市
+ Address string `json:"address"` // 地址(街道、小区、大厦等,用于定位)
+ AddressDetail string `json:"address_detail"` // 地址详情(楼号、单元号、层号)
+ Phone string `json:"phone"` // 电话/手机号,最长不超过64个字符
+ Lng float64 `json:"lng"` // 经度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,确到小数点后6位
+ Lat float64 `json:"lat"` // 纬度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,精确到小数点后6位)
+ CoordinateType uint8 `json:"coordinate_type"` // 坐标类型,0:火星坐标(高德,腾讯地图均采用火星坐标) 1:百度坐标
+ } `json:"receiver"` // 收件人信息
+ Cargo struct {
+ GoodsValue float64 `json:"goods_value"` // 货物价格,单位为元,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-5000]
+ GoodsHeight float64 `json:"goods_height"` // 货物高度,单位为cm,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-45]
+ GoodsLength float64 `json:"goods_length"` // 货物长度,单位为cm,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-65]
+ GoodsWidth float64 `json:"goods_width"` // 货物宽度,单位为cm,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-50]
+ GoodsWeight float64 `json:"goods_weight"` // 货物重量,单位为kg,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-50]
+ GoodsDetail struct {
+ Goods []struct {
+ Count uint `json:"good_count"` // 货物数量
+ Name string `json:"good_name"` // 货品名称
+ Price float32 `json:"good_price"` // 货品单价,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数)
+ Unit string `json:"good_unit"` // 货品单位,最长不超过20个字符
+ } `json:"goods"` // 货物交付信息,最长不超过100个字符
+ } `json:"goods_detail"` // 货物详情,最长不超过10240个字符
+ GoodsPickupInfo string `json:"goods_pickup_info"` // 货物取货信息,用于骑手到店取货,最长不超过100个字符
+ GoodsDeliveryInfo string `json:"goods_delivery_info"` // 货物交付信息,最长不超过100个字符
+ CargoFirstClass string `json:"cargo_first_class"` // 品类一级类目
+ CargoSecondClass string `json:"cargo_second_class"` // 品类二级类目
+ } `json:"cargo"` // 货物信息
+ OrderInfo struct {
+ DeliveryServiceCode string `json:"delivery_service_code"` // 配送服务代码 不同配送公司自定义,微信侧不理解
+ OrderType uint8 `json:"order_type"` // 订单类型, 0: 即时单 1 预约单,如预约单,需要设置expected_delivery_time或expected_finish_time或expected_pick_time
+ ExpectedDeliveryTime uint `json:"expected_delivery_time"` // 期望派单时间(顺丰同城急送、达达、支持),unix-timestamp
+ ExpectedFinishTime uint `json:"expected_finish_time"` // 期望送达时间(顺丰同城急送、美团配送支持),unix-timestamp
+ ExpectedPickTime uint `json:"expected_pick_time"` // 期望取件时间(闪送支持),unix-timestamp
+ PoiSeq string `json:"poi_seq"` // 门店订单流水号,建议提供,方便骑手门店取货,最长不超过32个字符
+ Note string `json:"note"` // 备注,最长不超过200个字符
+ OrderTime uint `json:"order_time"` // 用户下单付款时间
+ IsInsured uint8 `json:"is_insured"` // 是否保价,0,非保价,1.保价
+ DeclaredValue float64 `json:"declared_value"` // 保价金额,单位为元,精确到分
+ Tips float64 `json:"tips"` // 小费,单位为元, 下单一般不加小费
+ IsDirectDelivery uint `json:"is_direct_delivery"` // 是否选择直拿直送(0:不需要;1:需要。选择直拿直送后,同一时间骑手只能配送此订单至完成,配送费用也相应高一些,闪送必须选1,达达可选0或1,其余配送公司不支持直拿直送)
+ CashOnDelivery uint `json:"cash_on_delivery"` // 骑手应付金额,单位为元,精确到分
+ CashOnPickup uint `json:"cash_on_pickup"` // 骑手应收金额,单位为元,精确到分
+ RiderPickMethod uint8 `json:"rider_pick_method"` // 物流流向,1:从门店取件送至用户;2:从用户取件送至门店
+ IsFinishCodeNeeded uint8 `json:"is_finish_code_needed"` // 收货码(0:不需要;1:需要。收货码的作用是:骑手必须输入收货码才能完成订单妥投)
+ IsPickupCodeNeeded uint8 `json:"is_pickup_code_needed"` // 取货码(0:不需要;1:需要。取货码的作用是:骑手必须输入取货码才能从商家取货)
+ } `json:"order_info"` // 订单信息
+ Shop struct {
+ WxaPath string `json:"wxa_path"` // 商家小程序的路径,建议为订单页面
+ ImgURL string `json:"img_url"` // 商品缩略图 url
+ GoodsName string `json:"goods_name"` // 商品名称
+ GoodsCount uint `json:"goods_count"` // 商品数量
+ } `json:"shop"` // 商品信息,会展示到物流通知消息中
+ }{}
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.DeliveryToken == "" {
+ t.Error("Response column 'delivery_token' can not be empty")
+ }
+ if params.ShopID == "" {
+ t.Error("Response column 'shopid' can not be empty")
+ }
+ if params.ShopOrderID == "" {
+ t.Error("Response column 'shop_order_id' can not be empty")
+ }
+ if params.ShopNo == "" {
+ t.Error("Response column 'shop_no' can not be empty")
+ }
+ if params.DeliverySign == "" {
+ t.Error("Response column 'delivery_sign' can not be empty")
+ }
+ if params.DeliveryID == "" {
+ t.Error("Response column 'delivery_id' can not be empty")
+ }
+ if params.OpenID == "" {
+ t.Error("Response column 'openid' can not be empty")
+ }
+ if params.SubBizID == "" {
+ t.Error("Response column 'sub_biz_id' can not be empty")
+ }
+
+ if params.Sender.Name == "" {
+ t.Error("Param 'sender.name' can not be empty")
+ }
+ if params.Sender.City == "" {
+ t.Error("Param 'sender.city' can not be empty")
+ }
+ if params.Sender.Address == "" {
+ t.Error("Param 'sender.address' can not be empty")
+ }
+ if params.Sender.AddressDetail == "" {
+ t.Error("Param 'sender.address_detail' can not be empty")
+ }
+ if params.Sender.Phone == "" {
+ t.Error("Param 'sender.phone' can not be empty")
+ }
+ if params.Sender.Lng == 0 {
+ t.Error("Param 'sender.lng' can not be empty")
+ }
+ if params.Sender.Lat == 0 {
+ t.Error("Param 'sender.lat' can not be empty")
+ }
+ if params.Sender.CoordinateType == 0 {
+ t.Error("Param 'sender.coordinate_type' can not be empty")
+ }
+
+ if params.Receiver.Name == "" {
+ t.Error("Param 'receiver.name' can not be empty")
+ }
+ if params.Receiver.City == "" {
+ t.Error("Param 'receiver.city' can not be empty")
+ }
+ if params.Receiver.Address == "" {
+ t.Error("Param 'receiver.address' can not be empty")
+ }
+ if params.Receiver.AddressDetail == "" {
+ t.Error("Param 'receiver.address_detail' can not be empty")
+ }
+ if params.Receiver.Phone == "" {
+ t.Error("Param 'receiver.phone' can not be empty")
+ }
+ if params.Receiver.Lng == 0 {
+ t.Error("Param 'receiver.lng' can not be empty")
+ }
+ if params.Receiver.Lat == 0 {
+ t.Error("Param 'receiver.lat' can not be empty")
+ }
+ if params.Receiver.CoordinateType == 0 {
+ t.Error("Param 'receiver.coordinate_type' can not be empty")
+ }
+ if params.Cargo.GoodsValue == 0 {
+ t.Error("Param 'cargo.goods_value' can not be empty")
+ }
+ if params.Cargo.GoodsHeight == 0 {
+ t.Error("Param 'cargo.goods_height' can not be empty")
+ }
+ if params.Cargo.GoodsLength == 0 {
+ t.Error("Param 'cargo.goods_length' can not be empty")
+ }
+ if params.Cargo.GoodsWidth == 0 {
+ t.Error("Param 'cargo.goods_width' can not be empty")
+ }
+ if params.Cargo.GoodsWeight == 0 {
+ t.Error("Param 'cargo.goods_weight' can not be empty")
+ }
+ if params.Cargo.CargoFirstClass == "" {
+ t.Error("Param 'cargo.cargo_first_class' can not be empty")
+ }
+ if params.Cargo.CargoSecondClass == "" {
+ t.Error("Param 'cargo.cargo_second_class' can not be empty")
+ }
+ if len(params.Cargo.GoodsDetail.Goods) > 0 {
+ if params.Cargo.GoodsDetail.Goods[0].Count == 0 {
+ t.Error("Param 'cargo.goods_detail.goods.good_count' can not be empty")
+ }
+ if params.Cargo.GoodsDetail.Goods[0].Name == "" {
+ t.Error("Param 'cargo.goods_detail.goods.good_name' can not be empty")
+ }
+ if params.Cargo.GoodsDetail.Goods[0].Price == 0 {
+ t.Error("Param 'cargo.goods_detail.goods.good_price' can not be empty")
+ }
+ if params.Cargo.GoodsDetail.Goods[0].Unit == "" {
+ t.Error("Param 'cargo.goods_detail.goods.good_unit' can not be empty")
+ }
+ }
+ if params.OrderInfo.DeliveryServiceCode == "" {
+ t.Error("Param 'order_info.delivery_service_code' can not be empty")
+ }
+ if params.Shop.WxaPath == "" {
+ t.Error("Param 'shop.wxa_path' can not be empty")
+ }
+ if params.Shop.ImgURL == "" {
+ t.Error("Param 'shop.img_url' can not be empty")
+ }
+ if params.Shop.GoodsName == "" {
+ t.Error("Param 'shop.goods_name' can not be empty")
+ }
+ if params.Shop.GoodsCount == 0 {
+ t.Error("Param 'shop.goods_count' can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "resultcode": 1,
+ "resultmsg": "ok",
+ "fee": 11,
+ "deliverfee": 11,
+ "couponfee": 1,
+ "tips": 1,
+ "insurancefee": 1000,
+ "insurancfee": 1,
+ "distance": 1001,
+ "waybill_id": "123456789",
+ "order_status": 101,
+ "finish_code": 1024,
+ "pickup_code": 2048,
+ "dispatch_duration": 300
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ raw := `{
+ "cargo": {
+ "cargo_first_class": "美食宵夜",
+ "cargo_second_class": "零食小吃",
+ "goods_detail": {
+ "goods": [
+ {
+ "good_count": 1,
+ "good_name": "水果",
+ "good_price": 11,
+ "good_unit": "元"
+ },
+ {
+ "good_count": 2,
+ "good_name": "蔬菜",
+ "good_price": 21,
+ "good_unit": "元"
+ }
+ ]
+ },
+ "goods_height": 1,
+ "goods_length": 3,
+ "goods_value": 5,
+ "goods_weight": 1,
+ "goods_width": 2
+ },
+ "delivery_id": "SFTC",
+ "delivery_sign": "01234567890123456789",
+ "openid": "oABC123456",
+ "order_info": {
+ "delivery_service_code": "xxx",
+ "expected_delivery_time": 1,
+ "is_direct_delivery": 1,
+ "is_finish_code_needed": 1,
+ "is_insured": 1,
+ "is_pickup_code_needed": 1,
+ "note": "test_note",
+ "order_time": 1555220757,
+ "order_type": 1,
+ "poi_seq": "1111",
+ "tips": 0
+ },
+ "receiver": {
+ "address": "xxx地铁站",
+ "address_detail": "2号楼202",
+ "city": "北京市",
+ "coordinate_type": 1,
+ "lat": 40.1529600001,
+ "lng": 116.5060300001,
+ "name": "老王",
+ "phone": "18512345678"
+ },
+ "sender": {
+ "address": "xx大厦",
+ "address_detail": "1号楼101",
+ "city": "北京市",
+ "coordinate_type": 1,
+ "lat": 40.4486120001,
+ "lng": 116.3830750001,
+ "name": "刘一",
+ "phone": "13712345678"
+ },
+ "shop": {
+ "goods_count": 2,
+ "goods_name": "宝贝",
+ "img_url": "https://mmbiz.qpic.cn/mmbiz_png/xxxxxxxxx/0?wx_fmt=png",
+ "wxa_path": "/page/index/index"
+ },
+ "shop_no": "12345678",
+ "sub_biz_id": "sub_biz_id_1",
+ "shop_order_id": "SFTC_001",
+ "shopid": "122222222",
+ "delivery_token": "xxxxxxxx"
+ }`
+
+ creator := new(DeliveryOrderCreator)
+ err := json.Unmarshal([]byte(raw), creator)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ res, err := creator.create(ts.URL+apiAddDeliveryOrder, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if res.Fee == 0 {
+ t.Error("Response 'fee' can not be empty")
+ }
+ if res.Deliverfee == 0 {
+ t.Error("Response 'deliverfee' can not be empty")
+ }
+ if res.Couponfee == 0 {
+ t.Error("Response 'couponfee' can not be empty")
+ }
+ if res.Tips == 0 {
+ t.Error("Response 'tips' can not be empty")
+ }
+ if res.Insurancefee == 0 {
+ t.Error("Response 'insurancefee' can not be empty")
+ }
+ if res.Distance == 0 {
+ t.Error("Response 'distance' can not be empty")
+ }
+ if res.WaybillID == "" {
+ t.Error("Response 'waybill_id' can not be empty")
+ }
+ if res.OrderStatus == 0 {
+ t.Error("Response 'order_status' can not be empty")
+ }
+ if res.FinishCode == 0 {
+ t.Error("Response 'finish_code' can not be empty")
+ }
+ if res.PickupCode == 0 {
+ t.Error("Response 'pickup_code' can not be empty")
+ }
+ if res.DispatchDuration == 0 {
+ t.Error("Response 'dispatch_duration' can not be empty")
+ }
+ if res.ResultCode == 0 {
+ t.Error("Response 'resultcode' can not be empty")
+ }
+ if res.ResultMsg == "" {
+ t.Error("Response 'resultmsg' can not be empty")
+ }
+}
+
+func TestAddDeliveryTip(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Error("UnExpect request method")
+ }
+
+ if r.URL.EscapedPath() != "/cgi-bin/express/local/business/order/addtips" {
+ t.Error("Unexpected path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("Query 'access_token' can not be empty")
+ }
+
+ params := struct {
+ ShopID string `json:"shopid"`
+ ShopOrderID string `json:"shop_order_id"`
+ ShopNo string `json:"shop_no"`
+ DeliverySign string `json:"delivery_sign"`
+ WaybillID string `json:"waybill_id"`
+ OpenID string `json:"openid"`
+ Tips float64 `json:"tips"`
+ Remark string `json:"remark"`
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.ShopID == "" {
+ t.Error("Param 'shopid' can not be empty")
+ }
+ if params.ShopOrderID == "" {
+ t.Error("Param 'shop_order_id' can not be empty")
+ }
+ if params.ShopNo == "" {
+ t.Error("Param 'shop_no' can not be empty")
+ }
+ if params.DeliverySign == "" {
+ t.Error("Param 'delivery_sign' can not be empty")
+ }
+ if params.WaybillID == "" {
+ t.Error("Param 'waybill_id' can not be empty")
+ }
+ if params.OpenID == "" {
+ t.Error("Param 'openid' can not be empty")
+ }
+ if params.Tips == 0 {
+ t.Error("Param 'tips' can not be empty")
+ }
+ if params.Remark == "" {
+ t.Error("Param 'remark' can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "resultcode": 1,
+ "resultmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ raw := `{
+ "shopid": "123456",
+ "shop_order_id": "123456",
+ "waybill_id": "123456",
+ "tips": 5,
+ "openid": "mock-open-id",
+ "remark": "gogogo",
+ "delivery_sign": "123456",
+ "shop_no": "shop_no_111"
+ }`
+
+ adder := new(DeliveryTipAdder)
+ err := json.Unmarshal([]byte(raw), adder)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ res, err := adder.add(ts.URL+apiAddDeliveryTip, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if res.ResultCode == 0 {
+ t.Error("Response 'resultcode' can not be empty")
+ }
+ if res.ResultMsg == "" {
+ t.Error("Response 'resultmsg' can not be empty")
+ }
+}
+
+func TestCancelDeliveryOrder(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Error("UnExpect request method")
+ }
+
+ if r.URL.EscapedPath() != "/cgi-bin/express/local/business/order/cancel" {
+ t.Error("Unexpected path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("Query 'access_token' can not be empty")
+ }
+
+ params := struct {
+ ShopID string `json:"shopid"` // 商家id, 由配送公司分配的appkey
+ ShopOrderID string `json:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ ShopNo string `json:"shop_no"` // 商家门店编号, 在配送公司登记,如果只有一个门店,可以不填
+ DeliverySign string `json:"delivery_sign"` // 用配送公司提供的appSecret加密的校验串
+ DeliveryID string `json:"delivery_id"` // 快递公司ID
+ WaybillID string `json:"waybill_id"` // 配送单id
+ ReasonID uint8 `json:"cancel_reason_id"` // 取消原因Id
+ Reason string `json:"cancel_reason"` // 取消原因
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.ShopID == "" {
+ t.Error("Param 'shopid' can not be empty")
+ }
+ if params.ShopOrderID == "" {
+ t.Error("Param 'shop_order_id' can not be empty")
+ }
+ if params.ShopNo == "" {
+ t.Error("Param 'shop_no' can not be empty")
+ }
+ if params.DeliverySign == "" {
+ t.Error("Param 'delivery_sign' can not be empty")
+ }
+ if params.WaybillID == "" {
+ t.Error("Param 'waybill_id' can not be empty")
+ }
+ if params.DeliveryID == "" {
+ t.Error("Param 'delivery_id' can not be empty")
+ }
+ if params.ReasonID == 0 {
+ t.Error("Param 'cancel_reason_id' can not be empty")
+ }
+ if params.Reason == "" {
+ t.Error("Param 'cancel_reason' can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "resultcode": 1,
+ "resultmsg": "ok",
+ "deduct_fee": 5,
+ "desc": "blabla"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ raw := `{
+ "shopid": "123456",
+ "shop_order_id": "123456",
+ "waybill_id": "123456",
+ "delivery_id": "123456",
+ "cancel_reason_id": 1,
+ "cancel_reason": "mock-cancel-reson",
+ "delivery_sign": "123456",
+ "shop_no": "shop_no_111"
+ }`
+
+ canceler := new(DeliveryOrderCanceler)
+ err := json.Unmarshal([]byte(raw), canceler)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ res, err := canceler.cancel(ts.URL+apiCancelDeliveryOrder, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if res.ResultCode == 0 {
+ t.Error("Response 'resultcode' can not be empty")
+ }
+ if res.ResultMsg == "" {
+ t.Error("Response 'resultmsg' can not be empty")
+ }
+ if res.DeductFee == 0 {
+ t.Error("Response 'deduct_fee' can not be empty")
+ }
+ if res.Desc == "" {
+ t.Error("Response 'desc' can not be empty")
+ }
+}
+
+func TestGetAllImmediateDelivery(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Error("UnExpect request method")
+ }
+
+ if r.URL.EscapedPath() != "/cgi-bin/express/local/business/delivery/getall" {
+ t.Error("Unexpected path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("Query 'access_token' can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "resultcode": 1,
+ "resultmsg": "ok",
+ "list": [
+ {
+ "delivery_id": "SFTC",
+ "delivery_name": "顺发同城"
+ },
+ {
+ "delivery_id": "MTPS",
+ "delivery_name": "美团配送"
+ }
+ ]
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+
+ res, err := getAllImmediateDelivery(ts.URL+apiGetAllImmediateDelivery, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if res.ResultCode == 0 {
+ t.Error("Response 'resultcode' can not be empty")
+ }
+ if res.ResultMsg == "" {
+ t.Error("Response 'resultmsg' can not be empty")
+ }
+ if len(res.List) == 0 {
+ t.Error("Response 'list' can not be empty")
+ } else {
+ for _, item := range res.List {
+ if item.ID == "" {
+ t.Error("Response 'list.delivery_id' can not be empty")
+ }
+ if item.Name == "" {
+ t.Error("Response 'list.delivery_name' can not be empty")
+ }
+ }
+ }
+}
+
+func TestGetBindAccount(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Error("UnExpect request method")
+ }
+
+ if r.URL.EscapedPath() != "/cgi-bin/express/local/business/shop/get" {
+ t.Error("Unexpected path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("Query 'access_token' can not be empty")
+ }
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "resultcode": 1,
+ "resultmsg": "ok",
+ "shop_list": [
+ {
+ "delivery_id": "SFTC",
+ "shopid": "123456",
+ "audit_result": 1
+ },
+ {
+ "delivery_id": "MTPS",
+ "shopid": "123456",
+ "audit_result": 1
+ }
+ ]
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+
+ res, err := getBindAccount(ts.URL+apiGetDeliveryBindAccount, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if res.ResultCode == 0 {
+ t.Error("Response 'resultcode' can not be empty")
+ }
+ if res.ResultMsg == "" {
+ t.Error("Response 'resultmsg' can not be empty")
+ }
+ if len(res.ShopList) == 0 {
+ t.Error("Response 'shop_list' can not be empty")
+ } else {
+ for _, item := range res.ShopList {
+ if item.DeliveryID == "" {
+ t.Error("Response 'shop_list.delivery_id' can not be empty")
+ }
+ if item.ShopID == "" {
+ t.Error("Response 'shop_list.shopid' can not be empty")
+ }
+ if item.AuditResult == 0 {
+ t.Error("Response 'audit_result.shopid' can not be empty")
+ }
+ }
+ }
+}
+
+func TestGetDeliveryOrder(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Error("UnExpect request method")
+ }
+
+ if r.URL.EscapedPath() != "/cgi-bin/express/local/business/order/get" {
+ t.Error("Unexpected path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("Query 'access_token' can not be empty")
+ }
+
+ params := struct {
+ ShopID string `json:"shopid"`
+ ShopOrderID string `json:"shop_order_id"`
+ ShopNo string `json:"shop_no"`
+ DeliverySign string `json:"delivery_sign"`
+ }{}
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+ if params.ShopID == "" {
+ t.Error("Response column shopid can not be empty")
+ }
+ if params.ShopOrderID == "" {
+ t.Error("Response column shop_order_id can not be empty")
+ }
+ if params.ShopNo == "" {
+ t.Error("Response column shop_no can not be empty")
+ }
+ if params.DeliverySign == "" {
+ t.Error("Response column delivery_sign can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "resultcode": 1,
+ "resultmsg": "ok",
+ "order_status": 1,
+ "waybill_id": "string",
+ "rider_name": "string",
+ "rider_phone": "string",
+ "rider_lng": 3.14,
+ "rider_lat": 3.14
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+
+ raw := `{
+ "shopid": "xxxxxx",
+ "shop_order_id": "xxxxxx",
+ "shop_no": "xxxxxx",
+ "delivery_sign": "xxxxxx"
+ }`
+
+ getter := new(DeliveryOrderGetter)
+ err := json.Unmarshal([]byte(raw), getter)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ res, err := getter.get(ts.URL+apiGetDeliveryOrder, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.ResultCode == 0 {
+ t.Error("Response 'resultcode' can not be empty")
+ }
+ if res.ResultMsg == "" {
+ t.Error("Response 'resultmsg' can not be empty")
+ }
+ if res.WaybillID == "" {
+ t.Error("Response 'waybill_id' can not be empty")
+ }
+ if res.OrderStatus == 0 {
+ t.Error("Response 'order_status' can not be empty")
+ }
+
+ if res.RiderName == "" {
+ t.Error("Response 'rider_name' can not be empty")
+ }
+ if res.RiderPhone == "" {
+ t.Error("Response 'rider_phone' can not be empty")
+ }
+ if res.RiderLng == 0 {
+ t.Error("Response 'rider_lng' can not be empty")
+ }
+ if res.RiderLat == 0 {
+ t.Error("Response 'rider_lat' can not be empty")
+ }
+}
+
+func TestMockUpdateDeliveryOrder(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Error("UnExpect request method")
+ }
+
+ if r.URL.EscapedPath() != "/cgi-bin/express/local/business/test_update_order" {
+ t.Error("Unexpected path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("Query 'access_token' can not be empty")
+ }
+
+ params := struct {
+ ShopID string `json:"shopid"`
+ ShopOrderID string `json:"shop_order_id"`
+ ActionTime uint `json:"action_time"`
+ OrderStatus int `json:"order_status"`
+ ActionMsg string `json:"action_msg"`
+ }{}
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+ if params.ShopID == "" {
+ t.Error("Response column shopid can not be empty")
+ }
+ if params.ShopOrderID == "" {
+ t.Error("Response column shop_order_id can not be empty")
+ }
+ if params.ActionTime == 0 {
+ t.Error("Response column action_time can not be empty")
+ }
+ if params.OrderStatus == 0 {
+ t.Error("Response column order_status can not be empty")
+ }
+ if params.ActionMsg == "" {
+ t.Error("Response column action_msg can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "resultcode": 1,
+ "resultmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+
+ raw := `{
+ "shopid": "test_shop_id",
+ "shop_order_id": "xxxxxxxxxxx",
+ "waybill_id": "xxxxxxxxxxxxx",
+ "action_time": 12345678,
+ "order_status": 101,
+ "action_msg": "xxxxxx"
+ }`
+
+ mocker := new(UpdateDeliveryOrderMocker)
+ err := json.Unmarshal([]byte(raw), mocker)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ res, err := mocker.mock(ts.URL+apiMockUpdateDeliveryOrder, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.ResultCode == 0 {
+ t.Error("Response 'resultcode' can not be empty")
+ }
+ if res.ResultMsg == "" {
+ t.Error("Response 'resultmsg' can not be empty")
+ }
+}
+
+func TestPreAddDeliveryOrder(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Error("UnExpect request method")
+ }
+
+ if r.URL.EscapedPath() != "/cgi-bin/express/local/business/order/pre_add" {
+ t.Error("Unexpected path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ ShopID string `json:"shopid"` // 商家id, 由配送公司分配的appkey
+ ShopOrderID string `json:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ ShopNo string `json:"shop_no"` // 商家门店编号, 在配送公司登记,如果只有一个门店,可以不填
+ DeliverySign string `json:"delivery_sign"` // 用配送公司提供的appSecret加密的校验串
+ DeliveryID string `json:"delivery_id"` // 配送公司ID
+ OpenID string `json:"openid"` // 下单用户的openid
+ SubBizID string `json:"sub_biz_id"` // 子商户id,区分小程序内部多个子商户
+ Sender struct {
+ Name string `json:"name"` // 姓名,最长不超过256个字符
+ City string `json:"city"` // 城市名称,如广州市
+ Address string `json:"address"` // 地址(街道、小区、大厦等,用于定位)
+ AddressDetail string `json:"address_detail"` // 地址详情(楼号、单元号、层号)
+ Phone string `json:"phone"` // 电话/手机号,最长不超过64个字符
+ Lng float64 `json:"lng"` // 经度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,确到小数点后6位
+ Lat float64 `json:"lat"` // 纬度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,精确到小数点后6位)
+ CoordinateType uint8 `json:"coordinate_type"` // 坐标类型,0:火星坐标(高德,腾讯地图均采用火星坐标) 1:百度坐标
+ } `json:"sender"` // 发件人信息,闪送、顺丰同城急送必须填写,美团配送、达达,若传了shop_no的值可不填该字段
+ Receiver struct {
+ Name string `json:"name"` // 姓名,最长不超过256个字符
+ City string `json:"city"` // 城市名称,如广州市
+ Address string `json:"address"` // 地址(街道、小区、大厦等,用于定位)
+ AddressDetail string `json:"address_detail"` // 地址详情(楼号、单元号、层号)
+ Phone string `json:"phone"` // 电话/手机号,最长不超过64个字符
+ Lng float64 `json:"lng"` // 经度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,确到小数点后6位
+ Lat float64 `json:"lat"` // 纬度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,精确到小数点后6位)
+ CoordinateType uint8 `json:"coordinate_type"` // 坐标类型,0:火星坐标(高德,腾讯地图均采用火星坐标) 1:百度坐标
+ } `json:"receiver"` // 收件人信息
+ Cargo struct {
+ GoodsValue float64 `json:"goods_value"` // 货物价格,单位为元,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-5000]
+ GoodsHeight float64 `json:"goods_height"` // 货物高度,单位为cm,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-45]
+ GoodsLength float64 `json:"goods_length"` // 货物长度,单位为cm,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-65]
+ GoodsWidth float64 `json:"goods_width"` // 货物宽度,单位为cm,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-50]
+ GoodsWeight float64 `json:"goods_weight"` // 货物重量,单位为kg,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-50]
+ GoodsDetail struct {
+ Goods []struct {
+ Count uint `json:"good_count"` // 货物数量
+ Name string `json:"good_name"` // 货品名称
+ Price float32 `json:"good_price"` // 货品单价,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数)
+ Unit string `json:"good_unit"` // 货品单位,最长不超过20个字符
+ } `json:"goods"` // 货物交付信息,最长不超过100个字符
+ } `json:"goods_detail"` // 货物详情,最长不超过10240个字符
+ GoodsPickupInfo string `json:"goods_pickup_info"` // 货物取货信息,用于骑手到店取货,最长不超过100个字符
+ GoodsDeliveryInfo string `json:"goods_delivery_info"` // 货物交付信息,最长不超过100个字符
+ CargoFirstClass string `json:"cargo_first_class"` // 品类一级类目
+ CargoSecondClass string `json:"cargo_second_class"` // 品类二级类目
+ } `json:"cargo"` // 货物信息
+ OrderInfo struct {
+ DeliveryServiceCode string `json:"delivery_service_code"` // 配送服务代码 不同配送公司自定义,微信侧不理解
+ OrderType uint8 `json:"order_type"` // 订单类型, 0: 即时单 1 预约单,如预约单,需要设置expected_delivery_time或expected_finish_time或expected_pick_time
+ ExpectedDeliveryTime uint `json:"expected_delivery_time"` // 期望派单时间(顺丰同城急送、达达、支持),unix-timestamp
+ ExpectedFinishTime uint `json:"expected_finish_time"` // 期望送达时间(顺丰同城急送、美团配送支持),unix-timestamp
+ ExpectedPickTime uint `json:"expected_pick_time"` // 期望取件时间(闪送支持),unix-timestamp
+ PoiSeq string `json:"poi_seq"` // 门店订单流水号,建议提供,方便骑手门店取货,最长不超过32个字符
+ Note string `json:"note"` // 备注,最长不超过200个字符
+ OrderTime uint `json:"order_time"` // 用户下单付款时间
+ IsInsured uint8 `json:"is_insured"` // 是否保价,0,非保价,1.保价
+ DeclaredValue float64 `json:"declared_value"` // 保价金额,单位为元,精确到分
+ Tips float64 `json:"tips"` // 小费,单位为元, 下单一般不加小费
+ IsDirectDelivery uint `json:"is_direct_delivery"` // 是否选择直拿直送(0:不需要;1:需要。选择直拿直送后,同一时间骑手只能配送此订单至完成,配送费用也相应高一些,闪送必须选1,达达可选0或1,其余配送公司不支持直拿直送)
+ CashOnDelivery uint `json:"cash_on_delivery"` // 骑手应付金额,单位为元,精确到分
+ CashOnPickup uint `json:"cash_on_pickup"` // 骑手应收金额,单位为元,精确到分
+ RiderPickMethod uint8 `json:"rider_pick_method"` // 物流流向,1:从门店取件送至用户;2:从用户取件送至门店
+ IsFinishCodeNeeded uint8 `json:"is_finish_code_needed"` // 收货码(0:不需要;1:需要。收货码的作用是:骑手必须输入收货码才能完成订单妥投)
+ IsPickupCodeNeeded uint8 `json:"is_pickup_code_needed"` // 取货码(0:不需要;1:需要。取货码的作用是:骑手必须输入取货码才能从商家取货)
+ } `json:"order_info"` // 订单信息
+ Shop struct {
+ WxaPath string `json:"wxa_path"` // 商家小程序的路径,建议为订单页面
+ ImgURL string `json:"img_url"` // 商品缩略图 url
+ GoodsName string `json:"goods_name"` // 商品名称
+ GoodsCount uint `json:"goods_count"` // 商品数量
+ } `json:"shop"` // 商品信息,会展示到物流通知消息中
+ }{}
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.ShopID == "" {
+ t.Error("Response column 'shopid' can not be empty")
+ }
+ if params.ShopOrderID == "" {
+ t.Error("Response column 'shop_order_id' can not be empty")
+ }
+ if params.ShopNo == "" {
+ t.Error("Response column 'shop_no' can not be empty")
+ }
+ if params.DeliverySign == "" {
+ t.Error("Response column 'delivery_sign' can not be empty")
+ }
+ if params.DeliveryID == "" {
+ t.Error("Response column 'delivery_id' can not be empty")
+ }
+ if params.OpenID == "" {
+ t.Error("Response column 'openid' can not be empty")
+ }
+ if params.SubBizID == "" {
+ t.Error("Response column 'sub_biz_id' can not be empty")
+ }
+
+ if params.Sender.Name == "" {
+ t.Error("Param 'sender.name' can not be empty")
+ }
+ if params.Sender.City == "" {
+ t.Error("Param 'sender.city' can not be empty")
+ }
+ if params.Sender.Address == "" {
+ t.Error("Param 'sender.address' can not be empty")
+ }
+ if params.Sender.AddressDetail == "" {
+ t.Error("Param 'sender.address_detail' can not be empty")
+ }
+ if params.Sender.Phone == "" {
+ t.Error("Param 'sender.phone' can not be empty")
+ }
+ if params.Sender.Lng == 0 {
+ t.Error("Param 'sender.lng' can not be empty")
+ }
+ if params.Sender.Lat == 0 {
+ t.Error("Param 'sender.lat' can not be empty")
+ }
+ if params.Sender.CoordinateType == 0 {
+ t.Error("Param 'sender.coordinate_type' can not be empty")
+ }
+
+ if params.Receiver.Name == "" {
+ t.Error("Param 'receiver.name' can not be empty")
+ }
+ if params.Receiver.City == "" {
+ t.Error("Param 'receiver.city' can not be empty")
+ }
+ if params.Receiver.Address == "" {
+ t.Error("Param 'receiver.address' can not be empty")
+ }
+ if params.Receiver.AddressDetail == "" {
+ t.Error("Param 'receiver.address_detail' can not be empty")
+ }
+ if params.Receiver.Phone == "" {
+ t.Error("Param 'receiver.phone' can not be empty")
+ }
+ if params.Receiver.Lng == 0 {
+ t.Error("Param 'receiver.lng' can not be empty")
+ }
+ if params.Receiver.Lat == 0 {
+ t.Error("Param 'receiver.lat' can not be empty")
+ }
+ if params.Receiver.CoordinateType == 0 {
+ t.Error("Param 'receiver.coordinate_type' can not be empty")
+ }
+ if params.Cargo.GoodsValue == 0 {
+ t.Error("Param 'cargo.goods_value' can not be empty")
+ }
+ if params.Cargo.GoodsHeight == 0 {
+ t.Error("Param 'cargo.goods_height' can not be empty")
+ }
+ if params.Cargo.GoodsLength == 0 {
+ t.Error("Param 'cargo.goods_length' can not be empty")
+ }
+ if params.Cargo.GoodsWidth == 0 {
+ t.Error("Param 'cargo.goods_width' can not be empty")
+ }
+ if params.Cargo.GoodsWeight == 0 {
+ t.Error("Param 'cargo.goods_weight' can not be empty")
+ }
+ if params.Cargo.CargoFirstClass == "" {
+ t.Error("Param 'cargo.cargo_first_class' can not be empty")
+ }
+ if params.Cargo.CargoSecondClass == "" {
+ t.Error("Param 'cargo.cargo_second_class' can not be empty")
+ }
+ if len(params.Cargo.GoodsDetail.Goods) > 0 {
+ if params.Cargo.GoodsDetail.Goods[0].Count == 0 {
+ t.Error("Param 'cargo.goods_detail.goods.good_count' can not be empty")
+ }
+ if params.Cargo.GoodsDetail.Goods[0].Name == "" {
+ t.Error("Param 'cargo.goods_detail.goods.good_name' can not be empty")
+ }
+ if params.Cargo.GoodsDetail.Goods[0].Price == 0 {
+ t.Error("Param 'cargo.goods_detail.goods.good_price' can not be empty")
+ }
+ if params.Cargo.GoodsDetail.Goods[0].Unit == "" {
+ t.Error("Param 'cargo.goods_detail.goods.good_unit' can not be empty")
+ }
+ }
+ if params.OrderInfo.DeliveryServiceCode == "" {
+ t.Error("Param 'order_info.delivery_service_code' can not be empty")
+ }
+ if params.Shop.WxaPath == "" {
+ t.Error("Param 'shop.wxa_path' can not be empty")
+ }
+ if params.Shop.ImgURL == "" {
+ t.Error("Param 'shop.img_url' can not be empty")
+ }
+ if params.Shop.GoodsName == "" {
+ t.Error("Param 'shop.goods_name' can not be empty")
+ }
+ if params.Shop.GoodsCount == 0 {
+ t.Error("Param 'shop.goods_count' can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+ raw := `{
+ "resultcode": 1,
+ "resultmsg": "ok",
+ "fee": 11,
+ "deliverfee": 11,
+ "couponfee": 1,
+ "insurancefee": 1000,
+ "tips": 1,
+ "insurancfee": 1,
+ "distance": 1001,
+ "dispatch_duration": 301,
+ "delivery_token": "1111111"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ raw := `{
+ "cargo": {
+ "cargo_first_class": "美食宵夜",
+ "cargo_second_class": "零食小吃",
+ "goods_detail": {
+ "goods": [
+ {
+ "good_count": 1,
+ "good_name": "水果",
+ "good_price": 11,
+ "good_unit": "元"
+ },
+ {
+ "good_count": 2,
+ "good_name": "蔬菜",
+ "good_price": 21,
+ "good_unit": "元"
+ }
+ ]
+ },
+ "goods_height": 1,
+ "goods_length": 3,
+ "goods_value": 5,
+ "goods_weight": 1,
+ "goods_width": 2
+ },
+ "delivery_id": "SFTC",
+ "delivery_sign": "01234567890123456789",
+ "openid": "oABC123456",
+ "order_info": {
+ "delivery_service_code": "xxx",
+ "expected_delivery_time": 1,
+ "is_direct_delivery": 1,
+ "is_finish_code_needed": 1,
+ "is_insured": 1,
+ "is_pickup_code_needed": 1,
+ "note": "test_note",
+ "order_time": 1555220757,
+ "order_type": 1,
+ "poi_seq": "1111",
+ "tips": 0
+ },
+ "receiver": {
+ "address": "xxx地铁站",
+ "address_detail": "2号楼202",
+ "city": "北京市",
+ "coordinate_type": 1,
+ "lat": 40.1529600001,
+ "lng": 116.5060300001,
+ "name": "老王",
+ "phone": "18512345678"
+ },
+ "sender": {
+ "address": "xx大厦",
+ "address_detail": "1号楼101",
+ "city": "北京市",
+ "coordinate_type": 1,
+ "lat": 40.4486120001,
+ "lng": 116.3830750001,
+ "name": "刘一",
+ "phone": "13712345678"
+ },
+ "shop": {
+ "goods_count": 2,
+ "goods_name": "宝贝",
+ "img_url": "https://mmbiz.qpic.cn/mmbiz_png/xxxxxxxxx/0?wx_fmt=png",
+ "wxa_path": "/page/index/index"
+ },
+ "shop_no": "12345678",
+ "sub_biz_id": "sub_biz_id_1",
+ "shop_order_id": "SFTC_001",
+ "shopid": "122222222"
+ }`
+
+ creator := new(DeliveryOrderCreator)
+ err := json.Unmarshal([]byte(raw), creator)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ res, err := creator.prepare(ts.URL+apiPreAddDeliveryOrder, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if res.Fee == 0 {
+ t.Error("Response 'fee' can not be empty")
+ }
+ if res.Deliverfee == 0 {
+ t.Error("Response 'deliverfee' can not be empty")
+ }
+ if res.Couponfee == 0 {
+ t.Error("Response 'couponfee' can not be empty")
+ }
+ if res.Tips == 0 {
+ t.Error("Response 'tips' can not be empty")
+ }
+ if res.Insurancefee == 0 {
+ t.Error("Response 'insurancefee' can not be empty")
+ }
+ if res.Distance == 0 {
+ t.Error("Response 'distance' can not be empty")
+ }
+ if res.DispatchDuration == 0 {
+ t.Error("Response 'dispatch_duration' can not be empty")
+ }
+}
+
+func TestPreCancelDeliveryOrder(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Error("UnExpect request method")
+ }
+
+ if r.URL.EscapedPath() != "/cgi-bin/express/local/business/order/precancel" {
+ t.Error("Unexpected path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("Query 'access_token' can not be empty")
+ }
+
+ params := struct {
+ ShopID string `json:"shopid"` // 商家id, 由配送公司分配的appkey
+ ShopOrderID string `json:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ ShopNo string `json:"shop_no"` // 商家门店编号, 在配送公司登记,如果只有一个门店,可以不填
+ DeliverySign string `json:"delivery_sign"` // 用配送公司提供的appSecret加密的校验串
+ DeliveryID string `json:"delivery_id"` // 快递公司ID
+ WaybillID string `json:"waybill_id"` // 配送单id
+ ReasonID uint8 `json:"cancel_reason_id"` // 取消原因Id
+ Reason string `json:"cancel_reason"` // 取消原因
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.ShopID == "" {
+ t.Error("Param 'shopid' can not be empty")
+ }
+ if params.ShopOrderID == "" {
+ t.Error("Param 'shop_order_id' can not be empty")
+ }
+ if params.ShopNo == "" {
+ t.Error("Param 'shop_no' can not be empty")
+ }
+ if params.DeliverySign == "" {
+ t.Error("Param 'delivery_sign' can not be empty")
+ }
+ if params.WaybillID == "" {
+ t.Error("Param 'waybill_id' can not be empty")
+ }
+ if params.DeliveryID == "" {
+ t.Error("Param 'delivery_id' can not be empty")
+ }
+ if params.ReasonID == 0 {
+ t.Error("Param 'cancel_reason_id' can not be empty")
+ }
+ if params.Reason == "" {
+ t.Error("Param 'cancel_reason' can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "resultcode": 1,
+ "resultmsg": "ok",
+ "deduct_fee": 5,
+ "desc": "blabla"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ raw := `{
+ "shopid": "123456",
+ "shop_order_id": "123456",
+ "waybill_id": "123456",
+ "delivery_id": "123456",
+ "cancel_reason_id": 1,
+ "cancel_reason": "xxxxxx",
+ "delivery_sign": "123456",
+ "shop_no": "shop_no_111"
+ }`
+
+ canceler := new(DeliveryOrderCanceler)
+ err := json.Unmarshal([]byte(raw), canceler)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ res, err := canceler.prepare(ts.URL+apiPreCancelDeliveryOrder, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if res.ResultCode == 0 {
+ t.Error("Response 'resultcode' can not be empty")
+ }
+ if res.ResultMsg == "" {
+ t.Error("Response 'resultmsg' can not be empty")
+ }
+ if res.DeductFee == 0 {
+ t.Error("Response 'deduct_fee' can not be empty")
+ }
+ if res.Desc == "" {
+ t.Error("Response 'desc' can not be empty")
+ }
+}
+
+func TestReAddDeliveryOrder(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Error("UnExpect request method")
+ }
+
+ if r.URL.EscapedPath() != "/cgi-bin/express/local/business/order/readd" {
+ t.Error("Unexpected path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ DeliveryToken string `json:"delivery_token"` // 预下单接口返回的参数,配送公司可保证在一段时间内运费不变
+ ShopID string `json:"shopid"` // 商家id, 由配送公司分配的appkey
+ ShopOrderID string `json:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ ShopNo string `json:"shop_no"` // 商家门店编号, 在配送公司登记,如果只有一个门店,可以不填
+ DeliverySign string `json:"delivery_sign"` // 用配送公司提供的appSecret加密的校验串
+ DeliveryID string `json:"delivery_id"` // 配送公司ID
+ OpenID string `json:"openid"` // 下单用户的openid
+ SubBizID string `json:"sub_biz_id"` // 子商户id,区分小程序内部多个子商户
+ Sender struct {
+ Name string `json:"name"` // 姓名,最长不超过256个字符
+ City string `json:"city"` // 城市名称,如广州市
+ Address string `json:"address"` // 地址(街道、小区、大厦等,用于定位)
+ AddressDetail string `json:"address_detail"` // 地址详情(楼号、单元号、层号)
+ Phone string `json:"phone"` // 电话/手机号,最长不超过64个字符
+ Lng float64 `json:"lng"` // 经度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,确到小数点后6位
+ Lat float64 `json:"lat"` // 纬度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,精确到小数点后6位)
+ CoordinateType uint8 `json:"coordinate_type"` // 坐标类型,0:火星坐标(高德,腾讯地图均采用火星坐标) 1:百度坐标
+ } `json:"sender"` // 发件人信息,闪送、顺丰同城急送必须填写,美团配送、达达,若传了shop_no的值可不填该字段
+ Receiver struct {
+ Name string `json:"name"` // 姓名,最长不超过256个字符
+ City string `json:"city"` // 城市名称,如广州市
+ Address string `json:"address"` // 地址(街道、小区、大厦等,用于定位)
+ AddressDetail string `json:"address_detail"` // 地址详情(楼号、单元号、层号)
+ Phone string `json:"phone"` // 电话/手机号,最长不超过64个字符
+ Lng float64 `json:"lng"` // 经度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,确到小数点后6位
+ Lat float64 `json:"lat"` // 纬度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,精确到小数点后6位)
+ CoordinateType uint8 `json:"coordinate_type"` // 坐标类型,0:火星坐标(高德,腾讯地图均采用火星坐标) 1:百度坐标
+ } `json:"receiver"` // 收件人信息
+ Cargo struct {
+ GoodsValue float64 `json:"goods_value"` // 货物价格,单位为元,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-5000]
+ GoodsHeight float64 `json:"goods_height"` // 货物高度,单位为cm,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-45]
+ GoodsLength float64 `json:"goods_length"` // 货物长度,单位为cm,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-65]
+ GoodsWidth float64 `json:"goods_width"` // 货物宽度,单位为cm,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-50]
+ GoodsWeight float64 `json:"goods_weight"` // 货物重量,单位为kg,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-50]
+ GoodsDetail struct {
+ Goods []struct {
+ Count uint `json:"good_count"` // 货物数量
+ Name string `json:"good_name"` // 货品名称
+ Price float32 `json:"good_price"` // 货品单价,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数)
+ Unit string `json:"good_unit"` // 货品单位,最长不超过20个字符
+ } `json:"goods"` // 货物交付信息,最长不超过100个字符
+ } `json:"goods_detail"` // 货物详情,最长不超过10240个字符
+ GoodsPickupInfo string `json:"goods_pickup_info"` // 货物取货信息,用于骑手到店取货,最长不超过100个字符
+ GoodsDeliveryInfo string `json:"goods_delivery_info"` // 货物交付信息,最长不超过100个字符
+ CargoFirstClass string `json:"cargo_first_class"` // 品类一级类目
+ CargoSecondClass string `json:"cargo_second_class"` // 品类二级类目
+ } `json:"cargo"` // 货物信息
+ OrderInfo struct {
+ DeliveryServiceCode string `json:"delivery_service_code"` // 配送服务代码 不同配送公司自定义,微信侧不理解
+ OrderType uint8 `json:"order_type"` // 订单类型, 0: 即时单 1 预约单,如预约单,需要设置expected_delivery_time或expected_finish_time或expected_pick_time
+ ExpectedDeliveryTime uint `json:"expected_delivery_time"` // 期望派单时间(顺丰同城急送、达达、支持),unix-timestamp
+ ExpectedFinishTime uint `json:"expected_finish_time"` // 期望送达时间(顺丰同城急送、美团配送支持),unix-timestamp
+ ExpectedPickTime uint `json:"expected_pick_time"` // 期望取件时间(闪送支持),unix-timestamp
+ PoiSeq string `json:"poi_seq"` // 门店订单流水号,建议提供,方便骑手门店取货,最长不超过32个字符
+ Note string `json:"note"` // 备注,最长不超过200个字符
+ OrderTime uint `json:"order_time"` // 用户下单付款时间
+ IsInsured uint8 `json:"is_insured"` // 是否保价,0,非保价,1.保价
+ DeclaredValue float64 `json:"declared_value"` // 保价金额,单位为元,精确到分
+ Tips float64 `json:"tips"` // 小费,单位为元, 下单一般不加小费
+ IsDirectDelivery uint `json:"is_direct_delivery"` // 是否选择直拿直送(0:不需要;1:需要。选择直拿直送后,同一时间骑手只能配送此订单至完成,配送费用也相应高一些,闪送必须选1,达达可选0或1,其余配送公司不支持直拿直送)
+ CashOnDelivery uint `json:"cash_on_delivery"` // 骑手应付金额,单位为元,精确到分
+ CashOnPickup uint `json:"cash_on_pickup"` // 骑手应收金额,单位为元,精确到分
+ RiderPickMethod uint8 `json:"rider_pick_method"` // 物流流向,1:从门店取件送至用户;2:从用户取件送至门店
+ IsFinishCodeNeeded uint8 `json:"is_finish_code_needed"` // 收货码(0:不需要;1:需要。收货码的作用是:骑手必须输入收货码才能完成订单妥投)
+ IsPickupCodeNeeded uint8 `json:"is_pickup_code_needed"` // 取货码(0:不需要;1:需要。取货码的作用是:骑手必须输入取货码才能从商家取货)
+ } `json:"order_info"` // 订单信息
+ Shop struct {
+ WxaPath string `json:"wxa_path"` // 商家小程序的路径,建议为订单页面
+ ImgURL string `json:"img_url"` // 商品缩略图 url
+ GoodsName string `json:"goods_name"` // 商品名称
+ GoodsCount uint `json:"goods_count"` // 商品数量
+ } `json:"shop"` // 商品信息,会展示到物流通知消息中
+ }{}
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.DeliveryToken == "" {
+ t.Error("Response column 'delivery_token' can not be empty")
+ }
+ if params.ShopID == "" {
+ t.Error("Response column 'shopid' can not be empty")
+ }
+ if params.ShopOrderID == "" {
+ t.Error("Response column 'shop_order_id' can not be empty")
+ }
+ if params.ShopNo == "" {
+ t.Error("Response column 'shop_no' can not be empty")
+ }
+ if params.DeliverySign == "" {
+ t.Error("Response column 'delivery_sign' can not be empty")
+ }
+ if params.DeliveryID == "" {
+ t.Error("Response column 'delivery_id' can not be empty")
+ }
+ if params.OpenID == "" {
+ t.Error("Response column 'openid' can not be empty")
+ }
+ if params.SubBizID == "" {
+ t.Error("Response column 'sub_biz_id' can not be empty")
+ }
+
+ if params.Sender.Name == "" {
+ t.Error("Param 'sender.name' can not be empty")
+ }
+ if params.Sender.City == "" {
+ t.Error("Param 'sender.city' can not be empty")
+ }
+ if params.Sender.Address == "" {
+ t.Error("Param 'sender.address' can not be empty")
+ }
+ if params.Sender.AddressDetail == "" {
+ t.Error("Param 'sender.address_detail' can not be empty")
+ }
+ if params.Sender.Phone == "" {
+ t.Error("Param 'sender.phone' can not be empty")
+ }
+ if params.Sender.Lng == 0 {
+ t.Error("Param 'sender.lng' can not be empty")
+ }
+ if params.Sender.Lat == 0 {
+ t.Error("Param 'sender.lat' can not be empty")
+ }
+ if params.Sender.CoordinateType == 0 {
+ t.Error("Param 'sender.coordinate_type' can not be empty")
+ }
+
+ if params.Receiver.Name == "" {
+ t.Error("Param 'receiver.name' can not be empty")
+ }
+ if params.Receiver.City == "" {
+ t.Error("Param 'receiver.city' can not be empty")
+ }
+ if params.Receiver.Address == "" {
+ t.Error("Param 'receiver.address' can not be empty")
+ }
+ if params.Receiver.AddressDetail == "" {
+ t.Error("Param 'receiver.address_detail' can not be empty")
+ }
+ if params.Receiver.Phone == "" {
+ t.Error("Param 'receiver.phone' can not be empty")
+ }
+ if params.Receiver.Lng == 0 {
+ t.Error("Param 'receiver.lng' can not be empty")
+ }
+ if params.Receiver.Lat == 0 {
+ t.Error("Param 'receiver.lat' can not be empty")
+ }
+ if params.Receiver.CoordinateType == 0 {
+ t.Error("Param 'receiver.coordinate_type' can not be empty")
+ }
+ if params.Cargo.GoodsValue == 0 {
+ t.Error("Param 'cargo.goods_value' can not be empty")
+ }
+ if params.Cargo.GoodsHeight == 0 {
+ t.Error("Param 'cargo.goods_height' can not be empty")
+ }
+ if params.Cargo.GoodsLength == 0 {
+ t.Error("Param 'cargo.goods_length' can not be empty")
+ }
+ if params.Cargo.GoodsWidth == 0 {
+ t.Error("Param 'cargo.goods_width' can not be empty")
+ }
+ if params.Cargo.GoodsWeight == 0 {
+ t.Error("Param 'cargo.goods_weight' can not be empty")
+ }
+ if params.Cargo.CargoFirstClass == "" {
+ t.Error("Param 'cargo.cargo_first_class' can not be empty")
+ }
+ if params.Cargo.CargoSecondClass == "" {
+ t.Error("Param 'cargo.cargo_second_class' can not be empty")
+ }
+ if len(params.Cargo.GoodsDetail.Goods) > 0 {
+ if params.Cargo.GoodsDetail.Goods[0].Count == 0 {
+ t.Error("Param 'cargo.goods_detail.goods.good_count' can not be empty")
+ }
+ if params.Cargo.GoodsDetail.Goods[0].Name == "" {
+ t.Error("Param 'cargo.goods_detail.goods.good_name' can not be empty")
+ }
+ if params.Cargo.GoodsDetail.Goods[0].Price == 0 {
+ t.Error("Param 'cargo.goods_detail.goods.good_price' can not be empty")
+ }
+ if params.Cargo.GoodsDetail.Goods[0].Unit == "" {
+ t.Error("Param 'cargo.goods_detail.goods.good_unit' can not be empty")
+ }
+ }
+ if params.OrderInfo.DeliveryServiceCode == "" {
+ t.Error("Param 'order_info.delivery_service_code' can not be empty")
+ }
+ if params.Shop.WxaPath == "" {
+ t.Error("Param 'shop.wxa_path' can not be empty")
+ }
+ if params.Shop.ImgURL == "" {
+ t.Error("Param 'shop.img_url' can not be empty")
+ }
+ if params.Shop.GoodsName == "" {
+ t.Error("Param 'shop.goods_name' can not be empty")
+ }
+ if params.Shop.GoodsCount == 0 {
+ t.Error("Param 'shop.goods_count' can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "resultcode": 1,
+ "resultmsg": "ok",
+ "fee": 11,
+ "deliverfee": 11,
+ "couponfee": 1,
+ "tips": 1,
+ "insurancefee": 1000,
+ "insurancfee": 1,
+ "distance": 1001,
+ "waybill_id": "123456789",
+ "order_status": 101,
+ "finish_code": 1024,
+ "pickup_code": 2048,
+ "dispatch_duration": 300
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ raw := `{
+ "cargo": {
+ "cargo_first_class": "美食宵夜",
+ "cargo_second_class": "零食小吃",
+ "goods_detail": {
+ "goods": [
+ {
+ "good_count": 1,
+ "good_name": "水果",
+ "good_price": 11,
+ "good_unit": "元"
+ },
+ {
+ "good_count": 2,
+ "good_name": "蔬菜",
+ "good_price": 21,
+ "good_unit": "元"
+ }
+ ]
+ },
+ "goods_height": 1,
+ "goods_length": 3,
+ "goods_value": 5,
+ "goods_weight": 1,
+ "goods_width": 2
+ },
+ "delivery_id": "SFTC",
+ "delivery_sign": "01234567890123456789",
+ "openid": "oABC123456",
+ "order_info": {
+ "delivery_service_code": "xxx",
+ "expected_delivery_time": 1,
+ "is_direct_delivery": 1,
+ "is_finish_code_needed": 1,
+ "is_insured": 1,
+ "is_pickup_code_needed": 1,
+ "note": "test_note",
+ "order_time": 1555220757,
+ "order_type": 1,
+ "poi_seq": "1111",
+ "tips": 0
+ },
+ "receiver": {
+ "address": "xxx地铁站",
+ "address_detail": "2号楼202",
+ "city": "北京市",
+ "coordinate_type": 1,
+ "lat": 40.1529600001,
+ "lng": 116.5060300001,
+ "name": "老王",
+ "phone": "18512345678"
+ },
+ "sender": {
+ "address": "xx大厦",
+ "address_detail": "1号楼101",
+ "city": "北京市",
+ "coordinate_type": 1,
+ "lat": 40.4486120001,
+ "lng": 116.3830750001,
+ "name": "刘一",
+ "phone": "13712345678"
+ },
+ "shop": {
+ "goods_count": 2,
+ "goods_name": "宝贝",
+ "img_url": "https://mmbiz.qpic.cn/mmbiz_png/xxxxxxxxx/0?wx_fmt=png",
+ "wxa_path": "/page/index/index"
+ },
+ "shop_no": "12345678",
+ "sub_biz_id": "sub_biz_id_1",
+ "shop_order_id": "SFTC_001",
+ "shopid": "122222222",
+ "delivery_token": "xxxxxxxx"
+ }`
+
+ creator := new(DeliveryOrderCreator)
+ err := json.Unmarshal([]byte(raw), creator)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ res, err := creator.recreate(ts.URL+apiReAddDeliveryOrder, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if res.Fee == 0 {
+ t.Error("Response 'fee' can not be empty")
+ }
+ if res.Deliverfee == 0 {
+ t.Error("Response 'deliverfee' can not be empty")
+ }
+ if res.Couponfee == 0 {
+ t.Error("Response 'couponfee' can not be empty")
+ }
+ if res.Tips == 0 {
+ t.Error("Response 'tips' can not be empty")
+ }
+ if res.Insurancefee == 0 {
+ t.Error("Response 'insurancefee' can not be empty")
+ }
+ if res.Distance == 0 {
+ t.Error("Response 'distance' can not be empty")
+ }
+ if res.WaybillID == "" {
+ t.Error("Response 'waybill_id' can not be empty")
+ }
+ if res.OrderStatus == 0 {
+ t.Error("Response 'order_status' can not be empty")
+ }
+ if res.FinishCode == 0 {
+ t.Error("Response 'finish_code' can not be empty")
+ }
+ if res.PickupCode == 0 {
+ t.Error("Response 'pickup_code' can not be empty")
+ }
+ if res.DispatchDuration == 0 {
+ t.Error("Response 'dispatch_duration' can not be empty")
+ }
+ if res.ResultCode == 0 {
+ t.Error("Response 'resultcode' can not be empty")
+ }
+ if res.ResultMsg == "" {
+ t.Error("Response 'resultmsg' can not be empty")
+ }
+}
+
+func TestUpdateDeliveryOrder(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Error("UnExpect request method")
+ }
+
+ if r.URL.EscapedPath() != "/cgi-bin/express/local/delivery/update_order" {
+ t.Error("Unexpected path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("Query 'access_token' can not be empty")
+ }
+
+ params := struct {
+ WxToken string `json:"wx_token"`
+ ShopID string `json:"shopid"`
+ ShopOrderID string `json:"shop_order_id"`
+ ShopNo string `json:"shop_no"`
+ WaybillID string `json:"waybill_id"`
+ ActionTime uint `json:"action_time"`
+ OrderStatus int `json:"order_status"`
+ ActionMsg string `json:"action_msg"`
+ WxaPath string `json:"wxa_path"`
+ Agent struct {
+ Name string `json:"name"`
+ Phone string `json:"phone"`
+ IsPhoneEncrypted uint8 `json:"is_phone_encrypted"`
+ } `json:"agent"`
+ ExpectedDeliveryTime uint `json:"expected_delivery_time"`
+ }{}
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+ if params.WxToken == "" {
+ t.Error("Response column wx_token can not be empty")
+ }
+ if params.Agent.Name == "" {
+ t.Error("Response column agent.name can not be empty")
+ }
+ if params.Agent.Phone == "" {
+ t.Error("Response column agent.phone can not be empty")
+ }
+ if params.Agent.IsPhoneEncrypted == 0 {
+ t.Error("Response column agent.is_phone_encrypted can not be empty")
+ }
+ if params.ShopID == "" {
+ t.Error("Response column shopid can not be empty")
+ }
+ if params.ShopNo == "" {
+ t.Error("Response column shop_no can not be empty")
+ }
+ if params.WaybillID == "" {
+ t.Error("Response column waybill_id can not be empty")
+ }
+ if params.ShopOrderID == "" {
+ t.Error("Response column expected_delivery_time can not be empty")
+ }
+ if params.ExpectedDeliveryTime == 0 {
+ t.Error("Response column action_time can not be empty")
+ }
+ if params.ActionTime == 0 {
+ t.Error("Response column action_time can not be empty")
+ }
+ if params.OrderStatus == 0 {
+ t.Error("Response column order_status can not be empty")
+ }
+ if params.ActionMsg == "" {
+ t.Error("Response column action_msg can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "resultcode": 1,
+ "resultmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+
+ raw := `{
+ "wx_token": "xxxxxx",
+ "shopid": "test_shop_id",
+ "shop_no": "test_shop_id",
+ "shop_order_id": "xxxxxxxxxxx",
+ "waybill_id": "xxxxxxxxxxxxx",
+ "action_time": 12345678,
+ "order_status": 101,
+ "action_msg": "xxxxxx",
+ "wxa_path": "xxxxxx",
+ "expected_delivery_time": 123456,
+ "agent": {
+ "name": "xxxxxx",
+ "phone": "xxxxxx",
+ "is_phone_encrypted": 1
+ }
+ }`
+
+ updater := new(DeliveryOrderUpdater)
+ err := json.Unmarshal([]byte(raw), updater)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ res, err := updater.update(ts.URL+apiUpdateDeliveryOrder, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.ResultCode == 0 {
+ t.Error("Response 'resultcode' can not be empty")
+ }
+ if res.ResultMsg == "" {
+ t.Error("Response 'resultmsg' can not be empty")
+ }
+}
+
+func TestOnDeliveryOrderStatusUpdate(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ aesKey := base64.StdEncoding.EncodeToString([]byte("mock-aes-key"))
+ srv, err := NewServer("mock-app-id", "mock-access-token", aesKey, "mock-mch-id", "mock-api-key", false)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ srv.OnDeliveryOrderStatusUpdate(func(mix *DeliveryOrderStatusUpdateResult) *DeliveryOrderStatusUpdateReturn {
+ if mix.ToUserName == "" {
+ t.Error("ToUserName can not be empty")
+ }
+
+ if mix.FromUserName == "" {
+ t.Error("FromUserName can not be empty")
+ }
+ if mix.CreateTime == 0 {
+ t.Error("CreateTime can not be zero")
+ }
+ if mix.MsgType != "event" {
+ t.Error("Unexpected message type")
+ }
+
+ if mix.Event != "update_waybill_status" {
+ t.Error("Unexpected message event")
+ }
+
+ if mix.ShopID == "" {
+ t.Error("Result 'shopid' can not be zero")
+ }
+
+ if mix.ShopOrderID == "" {
+ t.Error("Result 'shop_order_id' can not be zero")
+ }
+
+ if mix.ShopNo == "" {
+ t.Error("Result 'shop_no' can not be zero")
+ }
+
+ if mix.WaybillID == "" {
+ t.Error("Result 'waybill_id' can not be zero")
+ }
+
+ if mix.ActionTime == 0 {
+ t.Error("Result 'action_time' can not be zero")
+ }
+
+ if mix.OrderStatus == 0 {
+ t.Error("Result 'order_status' can not be zero")
+ }
+
+ if mix.ActionMsg == "" {
+ t.Error("Result 'action_msg' can not be zero")
+ }
+
+ if mix.Agent.Name == "" {
+ t.Error("Result 'agent.name' can not be zero")
+ }
+
+ if mix.Agent.Phone == "" {
+ t.Error("Result 'agent.phone' can not be zero")
+ }
+
+ return &DeliveryOrderStatusUpdateReturn{
+ "mock-to-user-name",
+ "mock-from-user-name",
+ 20060102150405,
+ "mock-message-type",
+ "mock-event",
+ 0,
+ "mock-result-message",
+ }
+ })
+
+ if err := srv.Serve(w, r); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ jsonData := `{
+ "ToUserName": "toUser",
+ "FromUserName": "fromUser",
+ "CreateTime": 1546924844,
+ "MsgType": "event",
+ "Event": "update_waybill_status",
+ "shopid": "123456",
+ "shop_order_id": "123456",
+ "waybill_id": "123456",
+ "action_time": 1546924844,
+ "order_status": 102,
+ "action_msg": "xxx",
+ "shop_no": "123456",
+ "agent": {
+ "name": "xxx",
+ "phone": "1234567"
+ }
+ }`
+
+ res, err := http.Post(ts.URL, "application/json", strings.NewReader(jsonData))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer res.Body.Close()
+}
diff --git a/app/lib/weapp/nearby_poi.go b/app/lib/weapp/nearby_poi.go
new file mode 100644
index 0000000..7d5849c
--- /dev/null
+++ b/app/lib/weapp/nearby_poi.go
@@ -0,0 +1,231 @@
+package weapp
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// apis
+const (
+ apiAddNearbyPoi = "/wxa/addnearbypoi"
+ apiDeleteNearbyPoi = "/wxa/delnearbypoi"
+ apiGetNearbyPoiList = "/wxa/getnearbypoilist"
+ apiSetNearbyPoiShowStatus = "/wxa/setnearbypoishowstatus"
+)
+
+// NearbyPoi 附近地点
+type NearbyPoi struct {
+ PicList PicList `json:"pic_list"` // 门店图片,最多9张,最少1张,上传门店图片如门店外景、环境设施、商品服务等,图片将展示在微信客户端的门店页。图片链接通过文档https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1444738729中的《上传图文消息内的图片获取URL》接口获取。必填,文件格式为bmp、png、jpeg、jpg或gif,大小不超过5M pic_list是字符串,内容是一个json!
+ ServiceInfos ServiceInfos `json:"service_infos"` // 必服务标签列表 选填,需要填写服务标签ID、APPID、对应服务落地页的path路径,详细字段格式见下方示例
+ StoreName string `json:"store_name"` // 门店名字 必填,门店名称需按照所选地理位置自动拉取腾讯地图门店名称,不可修改,如需修改请重现选择地图地点或重新创建地点
+ Hour string `json:"hour"` // 营业时间,格式11:11-12:12 必填
+ Credential string `json:"credential"` // 资质号 必填, 15位营业执照注册号或9位组织机构代码
+ Address string `json:"address"` // 地址 必填
+ CompanyName string `json:"company_name"` // 主体名字 必填
+ QualificationList string `json:"qualification_list"` // 证明材料 必填 如果company_name和该小程序主体不一致,需要填qualification_list,详细规则见附近的小程序使用指南-如何证明门店的经营主体跟公众号或小程序帐号主体相关http://kf.qq.com/faq/170401MbUnim17040122m2qY.html
+ KFInfo KFInfo `json:"kf_info"` // 客服信息 选填,可自定义服务头像与昵称,具体填写字段见下方示例kf_info pic_list是字符串,内容是一个json!
+ PoiID string `json:"poi_id"` // 如果创建新的门店,poi_id字段为空 如果更新门店,poi_id参数则填对应门店的poi_id 选填
+}
+
+// PicList 门店图片
+type PicList struct {
+ List []string `json:"list"`
+}
+
+// ServiceInfos 必服务标签列表
+type ServiceInfos struct {
+ ServiceInfos []ServiceInfo `json:"service_infos"`
+}
+
+// ServiceInfo 必服务标签
+type ServiceInfo struct {
+ ID uint `json:"id"`
+ Type uint8 `json:"type"`
+ Name string `json:"name"`
+ AppID string `json:"appid"`
+ Path string `json:"path"`
+}
+
+// KFInfo // 客服信息
+type KFInfo struct {
+ OpenKF bool `json:"open_kf"`
+ KFHeading string `json:"kf_headimg"`
+ KFName string `json:"kf_name"`
+}
+
+// AddNearbyPoiResponse response of add position.
+type AddNearbyPoiResponse struct {
+ CommonError
+ Data struct {
+ AuditID string `json:"audit_id"` // 审核单 ID
+ PoiID string `json:"poi_id"` // 附近地点 ID
+ RelatedCredential string `json:"related_credential"` // 经营资质证件号
+ } `json:"data"`
+}
+
+// Add 添加地点
+// token 接口调用凭证
+func (p *NearbyPoi) Add(token string) (*AddNearbyPoiResponse, error) {
+ api := baseURL + apiAddNearbyPoi
+ return p.add(api, token)
+}
+
+func (p *NearbyPoi) add(api, token string) (*AddNearbyPoiResponse, error) {
+
+ pisList, err := json.Marshal(p.PicList)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal picture list to json: %v", err)
+ }
+
+ serviceInfos, err := json.Marshal(p.ServiceInfos)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal service info list to json: %v", err)
+ }
+
+ kfInfo, err := json.Marshal(p.KFInfo)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal customer service staff info list to json: %v", err)
+ }
+
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ params := requestParams{
+ "is_comm_nearby": "1",
+ "pic_list": string(pisList),
+ "service_infos": string(serviceInfos),
+ "store_name": p.StoreName,
+ "hour": p.Hour,
+ "credential": p.Credential,
+ "address": p.Address,
+ "company_name": p.CompanyName,
+ "qualification_list": p.QualificationList,
+ "kf_info": string(kfInfo),
+ "poi_id": p.PoiID,
+ }
+
+ res := new(AddNearbyPoiResponse)
+ if err := postJSON(url, params, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// DeleteNearbyPoi 删除地点
+// token 接口调用凭证
+// id 附近地点 ID
+func DeleteNearbyPoi(token, id string) (*CommonError, error) {
+ api := baseURL + apiDeleteNearbyPoi
+ return deleteNearbyPoi(api, token, id)
+}
+
+func deleteNearbyPoi(api, token, id string) (*CommonError, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ params := requestParams{
+ "poi_id": id,
+ }
+
+ res := new(CommonError)
+ if err := postJSON(url, params, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// PositionList 地点列表
+type PositionList struct {
+ CommonError
+ Data struct {
+ LeftApplyNum uint `json:"left_apply_num"` // 剩余可添加地点个数
+ MaxApplyNum uint `json:"max_apply_num"` // 最大可添加地点个数
+ Data struct {
+ List []struct {
+ PoiID string `json:"poi_id"` // 附近地点 ID
+ QualificationAddress string `json:"qualification_address"` // 资质证件地址
+ QualificationNum string `json:"qualification_num"` // 资质证件证件号
+ AuditStatus int `json:"audit_status"` // 地点审核状态
+ DisplayStatus int `json:"display_status"` // 地点展示在附近状态
+ RefuseReason string `json:"refuse_reason"` // 审核失败原因,audit_status=4 时返回
+ } `json:"poi_list"` // 地址列表
+ } `json:"-"`
+ RawData string `json:"data"` // 地址列表的 JSON 格式字符串
+ } `json:"data"` // 返回数据
+}
+
+// GetNearbyPoiList 查看地点列表
+// token 接口调用凭证
+// page 起始页id(从1开始计数)
+// rows 每页展示个数(最多1000个)
+func GetNearbyPoiList(token string, page, rows uint) (*PositionList, error) {
+ api := baseURL + apiGetNearbyPoiList
+ return getNearbyPoiList(api, token, page, rows)
+}
+
+func getNearbyPoiList(api, token string, page, rows uint) (*PositionList, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ params := requestParams{
+ "page": page,
+ "page_rows": rows,
+ }
+
+ res := new(PositionList)
+ if err := postJSON(url, params, res); err != nil {
+ return nil, err
+ }
+
+ err = json.Unmarshal([]byte(res.Data.RawData), &res.Data.Data)
+ if err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// NearbyPoiShowStatus 展示状态
+type NearbyPoiShowStatus int8
+
+// 所有展示状态
+const (
+ HideNearbyPoi NearbyPoiShowStatus = iota // 不展示
+ ShowNearbyPoi // 展示
+)
+
+// SetNearbyPoiShowStatus 展示/取消展示附近小程序
+// token 接口调用凭证
+// poiID 附近地点 ID
+// status 是否展示
+func SetNearbyPoiShowStatus(token, poiID string, status NearbyPoiShowStatus) (*CommonError, error) {
+ api := baseURL + apiSetNearbyPoiShowStatus
+ return setNearbyPoiShowStatus(api, token, poiID, status)
+}
+
+func setNearbyPoiShowStatus(api, token, poiID string, status NearbyPoiShowStatus) (*CommonError, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ params := requestParams{
+ "poi_id": poiID,
+ "status": status,
+ }
+
+ res := new(CommonError)
+ if err := postJSON(url, params, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
diff --git a/app/lib/weapp/nearby_poi_test.go b/app/lib/weapp/nearby_poi_test.go
new file mode 100644
index 0000000..e1c1f98
--- /dev/null
+++ b/app/lib/weapp/nearby_poi_test.go
@@ -0,0 +1,348 @@
+package weapp
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+)
+
+func TestAddNearbyPoi(t *testing.T) {
+
+ localServer := http.NewServeMux()
+ localServer.HandleFunc("/notify", func(w http.ResponseWriter, r *http.Request) {
+ aesKey := base64.StdEncoding.EncodeToString([]byte("mock-aes-key"))
+ srv, err := NewServer("mock-app-id", "mock-access-token", aesKey, "mock-mch-id", "mock-api-key", false)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ srv.OnAddNearbyPoi(func(mix *AddNearbyPoiResult) {
+ if mix.ToUserName == "" {
+ t.Error("ToUserName can not be empty")
+ }
+
+ if mix.FromUserName == "" {
+ t.Error("FromUserName can not be empty")
+ }
+ if mix.CreateTime == 0 {
+ t.Error("CreateTime can not be zero")
+ }
+ if mix.MsgType != "event" {
+ t.Error("Unexpected message type")
+ }
+
+ if mix.Event != "add_nearby_poi_audit_info" {
+ t.Error("Unexpected message event")
+ }
+
+ if mix.AuditID == 0 {
+ t.Error("audit_id can not be zero")
+ }
+ if mix.Status == 0 {
+ t.Error("status can not be zero")
+ }
+ if mix.Reason == "" {
+ t.Error("reason can not be empty")
+ }
+ if mix.PoiID == 0 {
+ t.Error("poi_id can not be zero")
+ }
+
+ })
+
+ if err := srv.Serve(w, r); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ tls := httptest.NewServer(localServer)
+ defer tls.Close()
+
+ remoteServer := http.NewServeMux()
+ remoteServer.HandleFunc(apiAddNearbyPoi, func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiAddNearbyPoi {
+ t.Fatalf("Except to path '%s',get '%s'", apiAddNearbyPoi, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ IsCommNearby string `json:"is_comm_nearby"`
+ PicList string `json:"pic_list"` // 门店图片,最多9张,最少1张,上传门店图片如门店外景、环境设施、商品服务等,图片将展示在微信客户端的门店页。图片链接通过文档https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1444738729中的《上传图文消息内的图片获取URL》接口获取。必填,文件格式为bmp、png、jpeg、jpg或gif,大小不超过5M pic_list是字符串,内容是一个json!
+ ServiceInfos string `json:"service_infos"` // 必服务标签列表 选填,需要填写服务标签ID、APPID、对应服务落地页的path路径,详细字段格式见下方示例
+ StoreName string `json:"store_name"` // 门店名字 必填,门店名称需按照所选地理位置自动拉取腾讯地图门店名称,不可修改,如需修改请重现选择地图地点或重新创建地点
+ Hour string `json:"hour"` // 营业时间,格式11:11-12:12 必填
+ Credential string `json:"credential"` // 资质号 必填, 15位营业执照注册号或9位组织机构代码
+ Address string `json:"address"` // 地址 必填
+ CompanyName string `json:"company_name"` // 主体名字 必填
+ QualificationList string `json:"qualification_list"` // 证明材料 必填 如果company_name和该小程序主体不一致,需要填qualification_list,详细规则见附近的小程序使用指南-如何证明门店的经营主体跟公众号或小程序帐号主体相关http://kf.qq.com/faq/170401MbUnim17040122m2qY.html
+ KFInfo string `json:"kf_info"` // 客服信息 选填,可自定义服务头像与昵称,具体填写字段见下方示例kf_info pic_list是字符串,内容是一个json!
+ PoiID string `json:"poi_id"` // 如果创建新的门店,poi_id字段为空 如果更新门店,poi_id参数则填对应门店的poi_id 选填
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+ if params.IsCommNearby != "1" {
+ t.Error("param pic_list is invalid")
+ }
+ if params.PicList == "" {
+ t.Error("param pic_list can not be empty")
+ }
+ if params.ServiceInfos == "" {
+ t.Error("param service_infos can not be empty")
+ }
+ if params.StoreName == "" {
+ t.Error("param store_name can not be empty")
+ }
+ if params.Hour == "" {
+ t.Error("param hour can not be empty")
+ }
+ if params.Credential == "" {
+ t.Error("param credential can not be empty")
+ }
+ if params.Address == "" {
+ t.Error("param address can not be empty")
+ }
+ if params.CompanyName == "" {
+ t.Error("param company_name can not be empty")
+ }
+ if params.QualificationList == "" {
+ t.Error("param qualification_list can not be empty")
+ }
+ if params.KFInfo == "" {
+ t.Error("param kf_info can not be empty")
+ }
+ if params.PoiID == "" {
+ t.Error("param poi_id can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok",
+ "data":{
+ "audit_id": "xxxxx",
+ "poi_id": "xxxxx",
+ "related_credential":"xxxxx"
+ }
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+
+ raw = `
+
+
+ 1488856741
+
+
+ 11111
+ 3
+
+ 111111
+ `
+ reader := strings.NewReader(raw)
+ http.Post(tls.URL+"/notify", "text/xml", reader)
+ })
+ trs := httptest.NewServer(remoteServer)
+ defer trs.Close()
+
+ poi := NearbyPoi{
+ PicList: PicList{[]string{"first-mock-picture-url", "second-mock-picture-url", "third-mock-picture-url"}},
+ ServiceInfos: ServiceInfos{[]ServiceInfo{
+ {1, 1, "mock-name", "mock-app-id", "mock-path"},
+ }},
+ StoreName: "mock-store-name",
+ Hour: "11:11-12:12",
+ Credential: "mock-credential",
+ Address: "mock-address", // 地址 必填
+ CompanyName: "mock-company-name", // 主体名字 必填
+ QualificationList: "mock-qualification-list", // 证明材料 必填 如果company_name和该小程序主体不一致,需要填qualification_list,详细规则见附近的小程序使用指南-如何证明门店的经营主体跟公众号或小程序帐号主体相关http://kf.qq.com/faq/170401MbUnim17040122m2qY.html
+ KFInfo: KFInfo{true, "kf-head-img", "kf-name"}, // 客服信息 选填,可自定义服务头像与昵称,具体填写字段见下方示例kf_info pic_list是字符串,内容是一个json!
+ PoiID: "mock-poi-id", // 如果创建新的门店,poi_id字段为空 如果更新门店,poi_id参数则填对应门店的poi_id 选填
+ }
+ _, err := poi.add(trs.URL+apiAddNearbyPoi, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestDeleteNearbyPoi(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiDeleteNearbyPoi {
+ t.Fatalf("Except to path '%s',get '%s'", apiDeleteNearbyPoi, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ PoiID string `json:"poi_id"`
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.PoiID == "" {
+ t.Error("Response column poi_id can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := deleteNearbyPoi(ts.URL+apiDeleteNearbyPoi, "mock-access-token", "mock-poi-id")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetNearbyPoiList(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiGetNearbyPoiList {
+ t.Fatalf("Except to path '%s',get '%s'", apiGetNearbyPoiList, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ Page uint `json:"page"`
+ Rows uint `json:"page_rows"`
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.Page == 0 {
+ t.Error("Response column page can not be empty")
+ }
+
+ if params.Rows == 0 {
+ t.Error("Response column page_rows can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "errcode": 0,
+ "errmsg": "",
+ "data": {
+ "left_apply_num": 9,
+ "max_apply_num": 10,
+ "data": "{\"poi_list\": [{\"poi_id\": \"123456\",\"qualification_address\": \"广东省广州市海珠区新港中路123号\",\"qualification_num\": \"123456789-1\",\"audit_status\": 3,\"display_status\": 0,\"refuse_reason\": \"\"}]}"
+ }
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := getNearbyPoiList(ts.URL+apiGetNearbyPoiList, "mock-access-token", 1, 10)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+}
+
+func TestSetNearbyPoiShowStatus(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiSetNearbyPoiShowStatus {
+ t.Fatalf("Except to path '%s',get '%s'", apiSetNearbyPoiShowStatus, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ PoiID string `json:"poi_id"`
+ Status uint8 `json:"status"`
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.PoiID == "" {
+ t.Error("Response column poi_id can not be empty")
+ }
+
+ if params.Status == 0 {
+ t.Error("Response column status can not be zero")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := setNearbyPoiShowStatus(ts.URL+apiSetNearbyPoiShowStatus, "mock-access-token", "mock-poi-id", ShowNearbyPoi)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/app/lib/weapp/ocr.go b/app/lib/weapp/ocr.go
new file mode 100644
index 0000000..01e5c13
--- /dev/null
+++ b/app/lib/weapp/ocr.go
@@ -0,0 +1,386 @@
+package weapp
+
+const (
+ apiBankcard = "/cv/ocr/bankcard"
+ apiVehicleLicense = "/cv/ocr/driving"
+ apiDrivingLicense = "/cv/ocr/drivinglicense"
+ apiIDCard = "/cv/ocr/idcard"
+ apiBusinessLicense = "/cv/ocr/bizlicense"
+ apiPrintedText = "/cv/ocr/comm"
+)
+
+// RecognizeMode 图片识别模式
+type RecognizeMode = string
+
+// 所有图片识别模式
+const (
+ RecognizeModePhoto RecognizeMode = "photo" // 拍照模式
+ RecognizeModeScan RecognizeMode = "scan" // 扫描模式
+)
+
+// BankCardResponse 识别银行卡返回数据
+type BankCardResponse struct {
+ CommonError
+ Number string `json:"number"` // 银行卡号
+}
+
+// BankCardByURL 通过URL识别银行卡
+// 接口限制: 此接口需要提供对应小程序/公众号 appid,开通权限后方可调用。
+//
+// token 接口调用凭证
+// url 要检测的图片 url,传这个则不用传 img 参数。
+// mode 图片识别模式,photo(拍照模式)或 scan(扫描模式)
+func BankCardByURL(token, cardURL string, mode RecognizeMode) (*BankCardResponse, error) {
+ api := baseURL + apiBankcard
+ return bankCardByURL(api, token, cardURL, mode)
+}
+
+func bankCardByURL(api, token, cardURL string, mode RecognizeMode) (*BankCardResponse, error) {
+ res := new(BankCardResponse)
+ err := ocrByURL(api, token, cardURL, mode, res)
+ if err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// BankCard 通过文件识别银行卡
+// 接口限制: 此接口需要提供对应小程序/公众号 appid,开通权限后方可调用。
+//
+// token 接口调用凭证
+// img form-data 中媒体文件标识,有filename、filelength、content-type等信息,传这个则不用传递 img_url。
+// mode 图片识别模式,photo(拍照模式)或 scan(扫描模式)
+func BankCard(token, filename string, mode RecognizeMode) (*BankCardResponse, error) {
+ api := baseURL + apiBankcard
+ return bankCard(api, token, filename, mode)
+}
+
+func bankCard(api, token, filename string, mode RecognizeMode) (*BankCardResponse, error) {
+ res := new(BankCardResponse)
+ err := ocrByFile(api, token, filename, mode, res)
+ if err != nil {
+ return nil, err
+ }
+
+ return res, err
+}
+
+// CardType 卡片方向
+type CardType = string
+
+// 所有卡片方向
+const (
+ CardTypeFront = "Front" // 正面
+ CardTypeBack = "Back" // 背面
+)
+
+// CardResponse 识别卡片返回数据
+type CardResponse struct {
+ CommonError
+ Type CardType `json:"type"` // 正面或背面,Front / Back
+ ValidDate string `json:"valid_date"` // 有效期
+}
+
+// DrivingLicenseResponse 识别行驶证返回数据
+type DrivingLicenseResponse struct {
+ CommonError
+ IDNum string `json:"id_num"` // 证号
+ Name string `json:"name"` // 姓名
+ Nationality string `json:"nationality"` // 国家
+ Sex string `json:"sex"` // 性别
+ Address string `json:"address"` // 地址
+ BirthDate string `json:"birth_date"` // 出生日期
+ IssueDate string `json:"issue_date"` // 初次领证日期
+ CarClass string `json:"car_class"` // 准驾车型
+ ValidFrom string `json:"valid_from"` // 有效期限起始日
+ ValidTo string `json:"valid_to"` // 有效期限终止日
+ OfficialSeal string `json:"official_seal"` // 印章文构
+}
+
+// DriverLicenseByURL 通过URL识别行驶证
+// 接口限制: 此接口需要提供对应小程序/公众号 appid,开通权限后方可调用。
+//
+// token 接口调用凭证
+// url 要检测的图片 url,传这个则不用传 img 参数。
+// mode 图片识别模式,photo(拍照模式)或 scan(扫描模式)
+func DriverLicenseByURL(token, licenseURL string) (*DrivingLicenseResponse, error) {
+ api := baseURL + apiDrivingLicense
+ return driverLicenseByURL(api, token, licenseURL)
+}
+
+func driverLicenseByURL(api, token, licenseURL string) (*DrivingLicenseResponse, error) {
+ res := new(DrivingLicenseResponse)
+ err := ocrByURL(api, token, licenseURL, "", res)
+ if err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// DriverLicense 通过文件识别行驶证
+// 接口限制: 此接口需要提供对应小程序/公众号 appid,开通权限后方可调用。
+//
+// token 接口调用凭证
+// img form-data 中媒体文件标识,有filename、filelength、content-type等信息,传这个则不用传递 img_url。
+// mode 图片识别模式,photo(拍照模式)或 scan(扫描模式)
+func DriverLicense(token, filename string) (*DrivingLicenseResponse, error) {
+ api := baseURL + apiDrivingLicense
+ return driverLicense(api, token, filename)
+}
+
+func driverLicense(api, token, filename string) (*DrivingLicenseResponse, error) {
+ res := new(DrivingLicenseResponse)
+ err := ocrByFile(api, token, filename, "", res)
+ if err != nil {
+ return nil, err
+ }
+
+ return res, err
+}
+
+// IDCardResponse 识别身份证返回数据
+type IDCardResponse = CardResponse
+
+// IDCardByURL 通过URL识别身份证
+// 接口限制: 此接口需要提供对应小程序/公众号 appid,开通权限后方可调用。
+//
+// token 接口调用凭证
+// url 要检测的图片 url,传这个则不用传 img 参数。
+// mode 图片识别模式,photo(拍照模式)或 scan(扫描模式)
+func IDCardByURL(token, cardURL string, mode RecognizeMode) (*IDCardResponse, error) {
+ api := baseURL + apiIDCard
+ return idCardByURL(api, token, cardURL, mode)
+}
+
+func idCardByURL(api, token, cardURL string, mode RecognizeMode) (*IDCardResponse, error) {
+ res := new(IDCardResponse)
+ err := ocrByURL(api, token, cardURL, mode, res)
+ if err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// IDCard 通过文件识别身份证
+// 接口限制: 此接口需要提供对应小程序/公众号 appid,开通权限后方可调用。
+//
+// token 接口调用凭证
+// img form-data 中媒体文件标识,有filename、filelength、content-type等信息,传这个则不用传递 img_url。
+// mode 图片识别模式,photo(拍照模式)或 scan(扫描模式)
+func IDCard(token, filename string, mode RecognizeMode) (*IDCardResponse, error) {
+ api := baseURL + apiIDCard
+ return idCard(api, token, filename, mode)
+}
+
+func idCard(api, token, filename string, mode RecognizeMode) (*IDCardResponse, error) {
+ res := new(IDCardResponse)
+ err := ocrByFile(api, token, filename, mode, res)
+ if err != nil {
+ return nil, err
+ }
+
+ return res, err
+}
+
+// VehicleLicenseResponse 识别卡片返回数据
+type VehicleLicenseResponse struct {
+ CommonError
+ VehicleType string `json:"vehicle_type"`
+ Owner string `json:"owner"`
+ Addr string `json:"addr"`
+ UseCharacter string `json:"use_character"`
+ Model string `json:"model"`
+ Vin string `json:"vin"`
+ EngineNum string `json:"engine_num"`
+ RegisterDate string `json:"register_date"`
+ IssueDate string `json:"issue_date"`
+ PlateNumB string `json:"plate_num_b"`
+ Record string `json:"record"`
+ PassengersNum string `json:"passengers_num"`
+ TotalQuality string `json:"total_quality"`
+ TotalprepareQualityQuality string `json:"totalprepare_quality_quality"`
+}
+
+// VehicleLicenseByURL 行驶证 OCR 识别
+func VehicleLicenseByURL(token, cardURL string, mode RecognizeMode) (*VehicleLicenseResponse, error) {
+ api := baseURL + apiVehicleLicense
+ return vehicleLicenseByURL(api, token, cardURL, mode)
+}
+
+func vehicleLicenseByURL(api, token, cardURL string, mode RecognizeMode) (*VehicleLicenseResponse, error) {
+ res := new(VehicleLicenseResponse)
+ err := ocrByURL(api, token, cardURL, mode, res)
+ if err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// VehicleLicense 通过文件识别行驶证
+func VehicleLicense(token, filename string, mode RecognizeMode) (*VehicleLicenseResponse, error) {
+ api := baseURL + apiVehicleLicense
+ return vehicleLicense(api, token, filename, mode)
+}
+
+func vehicleLicense(api, token, filename string, mode RecognizeMode) (*VehicleLicenseResponse, error) {
+ res := new(VehicleLicenseResponse)
+ err := ocrByFile(api, token, filename, mode, res)
+ if err != nil {
+ return nil, err
+ }
+
+ return res, err
+}
+
+// LicensePoint 证件点
+type LicensePoint struct {
+ X uint `json:"x"`
+ Y uint `json:"y"`
+}
+
+// LicensePosition 证件位置
+type LicensePosition struct {
+ LeftTop LicensePoint `json:"left_top"`
+ RightTop LicensePoint `json:"right_top"`
+ RightBottom LicensePoint `json:"right_bottom"`
+ LeftBottom LicensePoint `json:"left_bottom"`
+}
+
+// BusinessLicenseResponse 营业执照 OCR 识别返回数据
+type BusinessLicenseResponse struct {
+ CommonError
+ RegNum string `json:"reg_num"` // 注册号
+ Serial string `json:"serial"` // 编号
+ LegalRepresentative string `json:"legal_representative"` // 法定代表人姓名
+ EnterpriseName string `json:"enterprise_name"` // 企业名称
+ TypeOfOrganization string `json:"type_of_organization"` // 组成形式
+ Address string `json:"address"` // 经营场所/企业住所
+ TypeOfEnterprise string `json:"type_of_enterprise"` // 公司类型
+ BusinessScope string `json:"business_scope"` // 经营范围
+ RegisteredCapital string `json:"registered_capital"` // 注册资本
+ PaidInCapital string `json:"paid_in_capital"` // 实收资本
+ ValidPeriod string `json:"valid_period"` // 营业期限
+ RegisteredDate string `json:"registered_date"` // 注册日期/成立日期
+ CertPosition struct {
+ Position LicensePosition `json:"pos"`
+ } `json:"cert_position"` // 营业执照位置
+ ImgSize LicensePoint `json:"img_size"` // 图片大小
+}
+
+// BusinessLicenseByURL 通过链接进行营业执照 OCR 识别
+func BusinessLicenseByURL(token, cardURL string) (*BusinessLicenseResponse, error) {
+ api := baseURL + apiBusinessLicense
+ return businessLicenseByURL(api, token, cardURL)
+}
+
+func businessLicenseByURL(api, token, cardURL string) (*BusinessLicenseResponse, error) {
+ res := new(BusinessLicenseResponse)
+ err := ocrByURL(api, token, cardURL, "", res)
+ if err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// BusinessLicense 通过文件进行营业执照 OCR 识别
+func BusinessLicense(token, filename string) (*BusinessLicenseResponse, error) {
+ api := baseURL + apiBusinessLicense
+ return businessLicense(api, token, filename)
+}
+
+func businessLicense(api, token, filename string) (*BusinessLicenseResponse, error) {
+ res := new(BusinessLicenseResponse)
+ err := ocrByFile(api, token, filename, "", res)
+ if err != nil {
+ return nil, err
+ }
+
+ return res, err
+}
+
+// PrintedTextResponse 通用印刷体 OCR 识别返回数据
+type PrintedTextResponse struct {
+ CommonError
+ Items []struct {
+ Text string `json:"text"`
+ Position LicensePosition `json:"pos"`
+ } `json:"items"` // 识别结果
+ ImgSize LicensePoint `json:"img_size"` // 图片大小
+}
+
+// PrintedTextByURL 通过链接进行通用印刷体 OCR 识别
+func PrintedTextByURL(token, cardURL string) (*PrintedTextResponse, error) {
+ api := baseURL + apiPrintedText
+ return printedTextByURL(api, token, cardURL)
+}
+
+func printedTextByURL(api, token, cardURL string) (*PrintedTextResponse, error) {
+ res := new(PrintedTextResponse)
+ err := ocrByURL(api, token, cardURL, "", res)
+ if err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// PrintedText 通过文件进行通用印刷体 OCR 识别
+func PrintedText(token, filename string) (*PrintedTextResponse, error) {
+ api := baseURL + apiPrintedText
+ return printedText(api, token, filename)
+}
+
+func printedText(api, token, filename string) (*PrintedTextResponse, error) {
+ res := new(PrintedTextResponse)
+ err := ocrByFile(api, token, filename, "", res)
+ if err != nil {
+ return nil, err
+ }
+
+ return res, err
+}
+
+func ocrByFile(api, token, filename string, mode RecognizeMode, response interface{}) error {
+ queries := requestQueries{
+ "access_token": token,
+ "type": mode,
+ }
+
+ url, err := encodeURL(api, queries)
+ if err != nil {
+ return err
+ }
+
+ if err := postFormByFile(url, "img", filename, response); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func ocrByURL(api, token, cardURL string, mode RecognizeMode, response interface{}) error {
+ queries := requestQueries{
+ "access_token": token,
+ "img_url": cardURL,
+ }
+
+ if mode != "" {
+ queries["type"] = mode
+ }
+
+ url, err := encodeURL(api, queries)
+ if err != nil {
+ return err
+ }
+
+ if err := postJSON(url, nil, response); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/app/lib/weapp/ocr_test.go b/app/lib/weapp/ocr_test.go
new file mode 100644
index 0000000..4fdac9a
--- /dev/null
+++ b/app/lib/weapp/ocr_test.go
@@ -0,0 +1,894 @@
+package weapp
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path"
+ "testing"
+)
+
+func TestBankCardByURL(t *testing.T) {
+ server := http.NewServeMux()
+ server.HandleFunc(apiBankcard, func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiBankcard {
+ t.Fatalf("Except to path '%s',get '%s'", apiBankcard, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ queries := []string{"type", "access_token", "img_url"}
+ for _, v := range queries {
+ content := r.Form.Get(v)
+ if content == "" {
+ t.Fatalf("Params [%s] can not be empty", v)
+ }
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok",
+ "id": "622213XXXXXXXXX"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ server.HandleFunc("/mediaurl", func(w http.ResponseWriter, r *http.Request) {
+ filename := testIMGName
+ file, err := os.Open(filename)
+ if err != nil {
+ t.Fatal((err))
+ }
+ defer file.Close()
+
+ ext := path.Ext(filename)
+ ext = ext[1:len(ext)]
+ w.Header().Set("Content-Type", "image/"+ext)
+ w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", path.Base(filename)))
+ w.WriteHeader(http.StatusOK)
+
+ if _, err := io.Copy(w, file); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ ts := httptest.NewServer(server)
+ defer ts.Close()
+
+ _, err := bankCardByURL(ts.URL+apiBankcard, "mock-access-token", ts.URL+"/mediaurl", RecognizeModePhoto)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = bankCardByURL(ts.URL+apiBankcard, "mock-access-token", ts.URL+"/mediaurl", RecognizeModeScan)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestBankCard(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiBankcard {
+ t.Fatalf("Except to path '%s',get '%s'", apiBankcard, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ queries := []string{"type", "access_token"}
+ for _, v := range queries {
+ content := r.Form.Get(v)
+ if content == "" {
+ t.Fatalf("Params [%s] can not be empty", v)
+ }
+ }
+
+ if _, _, err := r.FormFile("img"); err != nil {
+ t.Fatal(err)
+
+ }
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok",
+ "id": "622213XXXXXXXXX"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := bankCard(ts.URL+apiBankcard, "mock-access-token", testIMGName, RecognizeModePhoto)
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, err = bankCard(ts.URL+apiBankcard, "mock-access-token", testIMGName, RecognizeModeScan)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestDriverLicenseByURL(t *testing.T) {
+ server := http.NewServeMux()
+ server.HandleFunc(apiDrivingLicense, func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiDrivingLicense {
+ t.Fatalf("Except to path '%s',get '%s'", apiDrivingLicense, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ queries := []string{"access_token", "img_url"}
+ for _, v := range queries {
+ content := r.Form.Get(v)
+ if content == "" {
+ t.Fatalf("Params [%s] can not be empty", v)
+ }
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok",
+ "id_num": "660601xxxxxxxx1234",
+ "name": "张三",
+ "sex": "男",
+ "nationality": "中国",
+ "address": "广东省东莞市xxxxx号",
+ "birth_date": "1990-12-21",
+ "issue_date": "2012-12-21",
+ "car_class": "C1",
+ "valid_from": "2018-07-06",
+ "valid_to": "2020-07-01",
+ "official_seal": "xx市公安局公安交通管理局"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ server.HandleFunc("/mediaurl", func(w http.ResponseWriter, r *http.Request) {
+ filename := testIMGName
+ file, err := os.Open(filename)
+ if err != nil {
+ t.Fatal((err))
+ }
+ defer file.Close()
+
+ ext := path.Ext(filename)
+ ext = ext[1:len(ext)]
+ w.Header().Set("Content-Type", "image/"+ext)
+ w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", path.Base(filename)))
+ w.WriteHeader(http.StatusOK)
+
+ if _, err := io.Copy(w, file); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ ts := httptest.NewServer(server)
+ defer ts.Close()
+
+ _, err := driverLicenseByURL(ts.URL+apiDrivingLicense, "mock-access-token", ts.URL+"/mediaurl")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestDriverLicense(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiDrivingLicense {
+ t.Fatalf("Except to path '%s',get '%s'", apiDrivingLicense, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ if _, _, err := r.FormFile("img"); err != nil {
+ t.Fatal(err)
+
+ }
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok",
+ "id_num": "660601xxxxxxxx1234",
+ "name": "张三",
+ "sex": "男",
+ "nationality": "中国",
+ "address": "广东省东莞市xxxxx号",
+ "birth_date": "1990-12-21",
+ "issue_date": "2012-12-21",
+ "car_class": "C1",
+ "valid_from": "2018-07-06",
+ "valid_to": "2020-07-01",
+ "official_seal": "xx市公安局公安交通管理局"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := driverLicense(ts.URL+apiDrivingLicense, "mock-access-token", testIMGName)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestBusinessLicenseByURL(t *testing.T) {
+ server := http.NewServeMux()
+ server.HandleFunc("/cv/ocr/bizlicense", func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ if r.URL.EscapedPath() != "/cv/ocr/bizlicense" {
+ t.Error("Invalid request path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ queries := []string{"access_token", "img_url"}
+ for _, v := range queries {
+ content := r.Form.Get(v)
+ if content == "" {
+ t.Fatalf("Params [%s] can not be empty", v)
+ }
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok",
+ "reg_num": "123123",
+ "serial": "123123",
+ "legal_representative": "张三",
+ "enterprise_name": "XX饮食店",
+ "type_of_organization": "个人经营",
+ "address": "XX市XX区XX路XX号",
+ "type_of_enterprise": "xxx",
+ "business_scope": "中型餐馆(不含凉菜、不含裱花蛋糕,不含生食海产品)。",
+ "registered_capital": "200万",
+ "paid_in_capital": "200万",
+ "valid_period": "2019年1月1日",
+ "registered_date": "2018年1月1日",
+ "cert_position": {
+ "pos": {
+ "left_top": {
+ "x": 155,
+ "y": 191
+ },
+ "right_top": {
+ "x": 725,
+ "y": 157
+ },
+ "right_bottom": {
+ "x": 743,
+ "y": 512
+ },
+ "left_bottom": {
+ "x": 164,
+ "y": 525
+ }
+ }
+ },
+ "img_size": {
+ "w": 966,
+ "h": 728
+ }
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ server.HandleFunc("/mediaurl", func(w http.ResponseWriter, r *http.Request) {
+ filename := testIMGName
+ file, err := os.Open(filename)
+ if err != nil {
+ t.Fatal((err))
+ }
+ defer file.Close()
+
+ ext := path.Ext(filename)
+ ext = ext[1:len(ext)]
+ w.Header().Set("Content-Type", "image/"+ext)
+ w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", path.Base(filename)))
+ w.WriteHeader(http.StatusOK)
+
+ if _, err := io.Copy(w, file); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ ts := httptest.NewServer(server)
+ defer ts.Close()
+
+ _, err := businessLicenseByURL(ts.URL+apiBusinessLicense, "mock-access-token", ts.URL+"/mediaurl")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestBusinessLicense(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ if r.URL.EscapedPath() != "/cv/ocr/bizlicense" {
+ t.Error("Invalid request path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ if _, _, err := r.FormFile("img"); err != nil {
+ t.Fatal(err)
+
+ }
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok",
+ "reg_num": "123123",
+ "serial": "123123",
+ "legal_representative": "张三",
+ "enterprise_name": "XX饮食店",
+ "type_of_organization": "个人经营",
+ "address": "XX市XX区XX路XX号",
+ "type_of_enterprise": "xxx",
+ "business_scope": "中型餐馆(不含凉菜、不含裱花蛋糕,不含生食海产品)。",
+ "registered_capital": "200万",
+ "paid_in_capital": "200万",
+ "valid_period": "2019年1月1日",
+ "registered_date": "2018年1月1日",
+ "cert_position": {
+ "pos": {
+ "left_top": {
+ "x": 155,
+ "y": 191
+ },
+ "right_top": {
+ "x": 725,
+ "y": 157
+ },
+ "right_bottom": {
+ "x": 743,
+ "y": 512
+ },
+ "left_bottom": {
+ "x": 164,
+ "y": 525
+ }
+ }
+ },
+ "img_size": {
+ "w": 966,
+ "h": 728
+ }
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := businessLicense(ts.URL+apiBusinessLicense, "mock-access-token", testIMGName)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestPrintedTextByURL(t *testing.T) {
+ server := http.NewServeMux()
+ server.HandleFunc("/cv/ocr/comm", func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ if r.URL.EscapedPath() != "/cv/ocr/comm" {
+ t.Error("Invalid request path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ queries := []string{"access_token", "img_url"}
+ for _, v := range queries {
+ content := r.Form.Get(v)
+ if content == "" {
+ t.Fatalf("Params [%s] can not be empty", v)
+ }
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok",
+ "items": [
+ {
+ "text": "腾讯",
+ "pos": {
+ "left_top": {
+ "x": 575,
+ "y": 519
+ },
+ "right_top": {
+ "x": 744,
+ "y": 519
+ },
+ "right_bottom": {
+ "x": 744,
+ "y": 532
+ },
+ "left_bottom": {
+ "x": 573,
+ "y": 532
+ }
+ }
+ },
+ {
+ "text": "微信团队",
+ "pos": {
+ "left_top": {
+ "x": 670,
+ "y": 516
+ },
+ "right_top": {
+ "x": 762,
+ "y": 517
+ },
+ "right_bottom": {
+ "x": 762,
+ "y": 532
+ },
+ "left_bottom": {
+ "x": 670,
+ "y": 531
+ }
+ }
+ }
+ ],
+ "img_size": {
+ "w": 1280,
+ "h": 720
+ }
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ server.HandleFunc("/mediaurl", func(w http.ResponseWriter, r *http.Request) {
+ filename := testIMGName
+ file, err := os.Open(filename)
+ if err != nil {
+ t.Fatal((err))
+ }
+ defer file.Close()
+
+ ext := path.Ext(filename)
+ ext = ext[1:len(ext)]
+ w.Header().Set("Content-Type", "image/"+ext)
+ w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", path.Base(filename)))
+ w.WriteHeader(http.StatusOK)
+
+ if _, err := io.Copy(w, file); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ ts := httptest.NewServer(server)
+ defer ts.Close()
+
+ _, err := printedTextByURL(ts.URL+apiPrintedText, "mock-access-token", ts.URL+"/mediaurl")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestPrintedText(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ if r.URL.EscapedPath() != "/cv/ocr/comm" {
+ t.Error("Invalid request path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ if _, _, err := r.FormFile("img"); err != nil {
+ t.Fatal(err)
+
+ }
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok",
+ "items": [
+ {
+ "text": "腾讯",
+ "pos": {
+ "left_top": {
+ "x": 575,
+ "y": 519
+ },
+ "right_top": {
+ "x": 744,
+ "y": 519
+ },
+ "right_bottom": {
+ "x": 744,
+ "y": 532
+ },
+ "left_bottom": {
+ "x": 573,
+ "y": 532
+ }
+ }
+ },
+ {
+ "text": "微信团队",
+ "pos": {
+ "left_top": {
+ "x": 670,
+ "y": 516
+ },
+ "right_top": {
+ "x": 762,
+ "y": 517
+ },
+ "right_bottom": {
+ "x": 762,
+ "y": 532
+ },
+ "left_bottom": {
+ "x": 670,
+ "y": 531
+ }
+ }
+ }
+ ],
+ "img_size": {
+ "w": 1280,
+ "h": 720
+ }
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := printedText(ts.URL+apiPrintedText, "mock-access-token", testIMGName)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestIDCardByURL(t *testing.T) {
+ server := http.NewServeMux()
+ server.HandleFunc(apiIDCard, func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiIDCard {
+ t.Fatalf("Except to path '%s',get '%s'", apiIDCard, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ queries := []string{"type", "access_token", "img_url"}
+ for _, v := range queries {
+ content := r.Form.Get(v)
+ if content == "" {
+ t.Fatalf("Params [%s] can not be empty", v)
+ }
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok",
+ "type": "Front",
+ "id": "44XXXXXXXXXXXXXXX1"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ server.HandleFunc("/mediaurl", func(w http.ResponseWriter, r *http.Request) {
+ filename := testIMGName
+ file, err := os.Open(filename)
+ if err != nil {
+ t.Fatal((err))
+ }
+ defer file.Close()
+
+ ext := path.Ext(filename)
+ ext = ext[1:len(ext)]
+ w.Header().Set("Content-Type", "image/"+ext)
+ w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", path.Base(filename)))
+ w.WriteHeader(http.StatusOK)
+
+ if _, err := io.Copy(w, file); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ ts := httptest.NewServer(server)
+ defer ts.Close()
+
+ _, err := idCardByURL(ts.URL+apiIDCard, "mock-access-token", ts.URL+"/mediaurl", RecognizeModePhoto)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = idCardByURL(ts.URL+apiIDCard, "mock-access-token", ts.URL+"/mediaurl", RecognizeModeScan)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestIDCard(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiIDCard {
+ t.Fatalf("Except to path '%s',get '%s'", apiIDCard, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+ queries := []string{"type", "access_token"}
+ for _, v := range queries {
+ content := r.Form.Get(v)
+ if content == "" {
+ t.Fatalf("Params [%s] can not be empty", v)
+ }
+ }
+
+ if _, _, err := r.FormFile("img"); err != nil {
+ t.Fatal(err)
+
+ }
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok",
+ "type": "Front",
+ "id": "44XXXXXXXXXXXXXXX1"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := idCard(ts.URL+apiIDCard, "mock-access-token", testIMGName, RecognizeModePhoto)
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, err = idCard(ts.URL+apiIDCard, "mock-access-token", testIMGName, RecognizeModeScan)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestVehicleLicenseByURL(t *testing.T) {
+ server := http.NewServeMux()
+ server.HandleFunc(apiVehicleLicense, func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiVehicleLicense {
+ t.Fatalf("Except to path '%s',get '%s'", apiVehicleLicense, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ queries := []string{"type", "access_token", "img_url"}
+ for _, v := range queries {
+ content := r.Form.Get(v)
+ if content == "" {
+ t.Fatalf("Params [%s] can not be empty", v)
+ }
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "vhicle_type": "小型普通客⻋",
+ "owner": "东莞市xxxxx机械厂",
+ "addr": "广东省东莞市xxxxx号",
+ "use_character": "非营运",
+ "model": "江淮牌HFCxxxxxxx",
+ "vin": "LJ166xxxxxxxx51",
+ "engine_num": "J3xxxxx3",
+ "register_date": "2018-07-06",
+ "issue_date": "2018-07-01",
+ "plate_num_b": "粤xxxxx",
+ "record": "441xxxxxx3",
+ "passengers_num": "7人",
+ "total_quality": "2700kg",
+ "prepare_quality": "1995kg"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ server.HandleFunc("/mediaurl", func(w http.ResponseWriter, r *http.Request) {
+ filename := testIMGName
+ file, err := os.Open(filename)
+ if err != nil {
+ t.Fatal((err))
+ }
+ defer file.Close()
+
+ ext := path.Ext(filename)
+ ext = ext[1:len(ext)]
+ w.Header().Set("Content-Type", "image/"+ext)
+ w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", path.Base(filename)))
+ w.WriteHeader(http.StatusOK)
+
+ if _, err := io.Copy(w, file); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ ts := httptest.NewServer(server)
+ defer ts.Close()
+
+ _, err := vehicleLicenseByURL(ts.URL+apiVehicleLicense, "mock-access-token", ts.URL+"/mediaurl", RecognizeModePhoto)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = vehicleLicenseByURL(ts.URL+apiVehicleLicense, "mock-access-token", ts.URL+"/mediaurl", RecognizeModeScan)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestVehicleLicense(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiVehicleLicense {
+ t.Fatalf("Except to path '%s',get '%s'", apiVehicleLicense, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+ queries := []string{"type", "access_token"}
+ for _, v := range queries {
+ content := r.Form.Get(v)
+ if content == "" {
+ t.Fatalf("Params [%s] can not be empty", v)
+ }
+ }
+
+ if _, _, err := r.FormFile("img"); err != nil {
+ t.Fatal(err)
+
+ }
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "vhicle_type": "小型普通客⻋",
+ "owner": "东莞市xxxxx机械厂",
+ "addr": "广东省东莞市xxxxx号",
+ "use_character": "非营运",
+ "model": "江淮牌HFCxxxxxxx",
+ "vin": "LJ166xxxxxxxx51",
+ "engine_num": "J3xxxxx3",
+ "register_date": "2018-07-06",
+ "issue_date": "2018-07-01",
+ "plate_num_b": "粤xxxxx",
+ "record": "441xxxxxx3",
+ "passengers_num": "7人",
+ "total_quality": "2700kg",
+ "prepare_quality": "1995kg"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := vehicleLicense(ts.URL+apiVehicleLicense, "mock-access-token", testIMGName, RecognizeModePhoto)
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, err = vehicleLicense(ts.URL+apiVehicleLicense, "mock-access-token", testIMGName, RecognizeModeScan)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/app/lib/weapp/plugin.go b/app/lib/weapp/plugin.go
new file mode 100644
index 0000000..0769e44
--- /dev/null
+++ b/app/lib/weapp/plugin.go
@@ -0,0 +1,189 @@
+package weapp
+
+const (
+ apiPlugin = "/wxa/plugin"
+ apiDevPlugin = "/wxa/devplugin"
+)
+
+// ApplyPlugin 向插件开发者发起使用插件的申请
+// accessToken 接口调用凭证
+// action string 是 此接口下填写 "apply"
+// appID string 是 插件 appId
+// reason string 否 申请使用理由
+func ApplyPlugin(token, appID, reason string) (*CommonError, error) {
+ api := baseURL + apiPlugin
+ return applyPlugin(api, token, appID, reason)
+}
+
+func applyPlugin(api, token, appID, reason string) (*CommonError, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ params := requestParams{
+ "action": "apply",
+ "plugin_appid": appID,
+ }
+
+ if reason != "" {
+ params["reason"] = reason
+ }
+
+ res := new(CommonError)
+ if err := postJSON(url, params, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// GetPluginDevApplyListResponse 查询已添加的插件返回数据
+type GetPluginDevApplyListResponse struct {
+ CommonError
+ ApplyList []struct {
+ AppID string `json:"appid"` // 插件 appId
+ Status uint8 `json:"status"` // 插件状态
+ Nickname string `json:"nickname"` // 插件昵称
+ HeadImgURL string `json:"headimgurl"` // 插件头像
+ Categories []struct {
+ First string `json:"first"`
+ Second string `json:"second"`
+ } `json:"categories"` // 使用者的类目
+ CreateTime string `json:"create_time"` // 使用者的申请时间
+ ApplyURL string `json:"apply_url"` // 使用者的小程序码
+ Reason string `json:"reason"` // 使用者的申请说明
+ } `json:"apply_list"` // 申请或使用中的插件列表
+}
+
+// GetPluginDevApplyList 获取当前所有插件使用方
+// accessToken 接口调用凭证
+// page number 是 要拉取第几页的数据
+// num 是 每页的记录数
+func GetPluginDevApplyList(token string, page, num uint) (*GetPluginDevApplyListResponse, error) {
+ api := baseURL + apiDevPlugin
+ return getPluginDevApplyList(api, token, page, num)
+}
+
+func getPluginDevApplyList(api, token string, page, num uint) (*GetPluginDevApplyListResponse, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ params := requestParams{
+ "num": num,
+ "page": page,
+ "action": "dev_apply_list",
+ }
+
+ res := new(GetPluginDevApplyListResponse)
+ if err := postJSON(url, params, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// GetPluginListResponse 查询已添加的插件返回数据
+type GetPluginListResponse struct {
+ CommonError
+ PluginList []struct {
+ AppID string `json:"appid"` // 插件 appId
+ Status int8 `json:"status"` // 插件状态
+ Nickname string `json:"nickname"` // 插件昵称
+ HeadImgURL string `json:"headimgurl"` // 插件头像
+ } `json:"plugin_list"` // 申请或使用中的插件列表
+}
+
+// GetPluginList 查询已添加的插件
+// accessToken 接口调用凭证
+func GetPluginList(token string) (*GetPluginListResponse, error) {
+ api := baseURL + apiPlugin
+ return getPluginList(api, token)
+}
+
+func getPluginList(api, token string) (*GetPluginListResponse, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ params := requestParams{
+ "action": "list",
+ }
+
+ res := new(GetPluginListResponse)
+ if err := postJSON(url, params, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// DevAction 修改操作
+type DevAction string
+
+// 所有修改操作
+const (
+ DevAgree DevAction = "dev_agree" // 同意申请
+ DevRefuse DevAction = "dev_refuse" // 拒绝申请
+ DevDelete DevAction = "dev_refuse" // 删除已拒绝的申请者
+)
+
+// SetDevPluginApplyStatus 修改插件使用申请的状态
+// accessToken 接口调用凭证
+// appID 使用者的 appid。同意申请时填写。
+// reason 拒绝理由。拒绝申请时填写。
+// action 修改操作
+func SetDevPluginApplyStatus(token, appID, reason string, action DevAction) (*CommonError, error) {
+ api := baseURL + apiDevPlugin
+ return setDevPluginApplyStatus(api, token, appID, reason, action)
+}
+
+func setDevPluginApplyStatus(api, token, appID, reason string, action DevAction) (*CommonError, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ params := requestParams{
+ "action": action,
+ "appid": appID,
+ "reason": reason,
+ }
+
+ res := new(CommonError)
+ if err := postJSON(url, params, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// UnbindPlugin 查询已添加的插件
+// accessToken 接口调用凭证
+// appID 插件 appId
+func UnbindPlugin(token, appID string) (*CommonError, error) {
+ api := baseURL + apiPlugin
+ return unbindPlugin(api, token, appID)
+}
+
+func unbindPlugin(api, token, appID string) (*CommonError, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ params := requestParams{
+ "action": "unbind",
+ "plugin_appid": appID,
+ }
+
+ res := new(CommonError)
+ if err := postJSON(url, params, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
diff --git a/app/lib/weapp/plugin_test.go b/app/lib/weapp/plugin_test.go
new file mode 100644
index 0000000..9ecdbdb
--- /dev/null
+++ b/app/lib/weapp/plugin_test.go
@@ -0,0 +1,300 @@
+package weapp
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+func TestApplyPlugin(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiPlugin {
+ t.Fatalf("Except to path '%s',get '%s'", apiPlugin, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ Action string `json:"action"`
+ PluginAppID string `json:"plugin_appid"`
+ Reason string `json:"reason"`
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.Action != "apply" {
+ t.Error("Unexpected action")
+ }
+
+ if params.PluginAppID == "" {
+ t.Error("Response column plugin_appid can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := applyPlugin(ts.URL+apiPlugin, "mock-access-token", "plugin-app-id", "mock-reason")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetPluginDevApplyList(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiDevPlugin {
+ t.Fatalf("Except to path '%s',get '%s'", apiDevPlugin, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ Action string `json:"action"`
+ Page uint `json:"page"`
+ Number uint `json:"num"`
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.Action != "dev_apply_list" {
+ t.Error("Unexpected action")
+ }
+
+ if params.Page == 0 {
+ t.Error("Response column page can not be zero")
+ }
+
+ if params.Number == 0 {
+ t.Error("Response column num can not be zero")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok",
+ "apply_list": [{
+ "appid": "xxxxxxxxxxxxx",
+ "status": 1,
+ "nickname": "名称",
+ "headimgurl": "**********",
+ "reason": "polo has gone",
+ "apply_url": "*******",
+ "create_time": "1536305096",
+ "categories": [{
+ "first": "IT科技",
+ "second": "硬件与设备"
+ }]
+ }]
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := getPluginDevApplyList(ts.URL+apiDevPlugin, "mock-access-token", 1, 2)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetPluginList(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiPlugin {
+ t.Fatalf("Except to path '%s',get '%s'", apiPlugin, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ Action string `json:"action"`
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.Action != "list" {
+ t.Error("Unexpected action")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok",
+ "plugin_list": [{
+ "appid": "aaaa",
+ "status": 1,
+ "nickname": "插件昵称",
+ "headimgurl": "http://plugin.qq.com"
+ }]
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := getPluginList(ts.URL+apiPlugin, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSetDevPluginApplyStatus(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiDevPlugin {
+ t.Fatalf("Except to path '%s',get '%s'", apiDevPlugin, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ Action string `json:"action"`
+ AppID string `json:"appid"`
+ Reason string `json:"reason"`
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.Action == "" {
+ t.Error("Response column action can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := setDevPluginApplyStatus(ts.URL+apiDevPlugin, "mock-access-token", "mock-plugin-app-id", "mock-reason", DevAgree)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestUnbindPlugin(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiPlugin {
+ t.Fatalf("Except to path '%s',get '%s'", apiPlugin, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ Action string `json:"action"`
+ PluginAppID string `json:"plugin_appid"`
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.Action != "unbind" {
+ t.Error("Unexpected action")
+ }
+
+ if params.PluginAppID == "" {
+ t.Error("Response column plugin_appid can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := unbindPlugin(ts.URL+apiPlugin, "mock-access-token", "mock-plugin-app-id")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/app/lib/weapp/qr_code.go b/app/lib/weapp/qr_code.go
new file mode 100644
index 0000000..09e5d5f
--- /dev/null
+++ b/app/lib/weapp/qr_code.go
@@ -0,0 +1,118 @@
+package weapp
+
+import (
+ "encoding/json"
+ "errors"
+ "net/http"
+ "strings"
+)
+
+const (
+ apiGetQrCode = "/wxa/getwxacode"
+ apiGetUnlimitedQRCode = "/wxa/getwxacodeunlimit"
+ apiCreateQRCode = "/cgi-bin/wxaapp/createwxaqrcode"
+)
+
+// Color QRCode color
+type Color struct {
+ R string `json:"r"`
+ G string `json:"g"`
+ B string `json:"b"`
+}
+
+// QRCode 小程序码参数
+type QRCode struct {
+ Path string `json:"path"`
+ Width int `json:"width,omitempty"`
+ AutoColor bool `json:"auto_color,omitempty"`
+ LineColor Color `json:"line_color,omitempty"`
+ IsHyaline bool `json:"is_hyaline,omitempty"`
+}
+
+// Get 获取小程序码
+// 可接受path参数较长 生成个数受限 永久有效 适用于需要的码数量较少的业务场景
+//
+// token 微信access_token
+func (code *QRCode) Get(token string) (*http.Response, *CommonError, error) {
+ api := baseURL + apiGetQrCode
+ return code.get(api, token)
+}
+
+func (code *QRCode) get(api, token string) (*http.Response, *CommonError, error) {
+ return qrCodeRequest(api, token, code)
+}
+
+// UnlimitedQRCode 小程序码参数
+type UnlimitedQRCode struct {
+ Scene string `json:"scene"`
+ Page string `json:"page,omitempty"`
+ Width int `json:"width,omitempty"`
+ AutoColor bool `json:"auto_color,omitempty"`
+ LineColor Color `json:"line_color,omitempty"`
+ IsHyaline bool `json:"is_hyaline,omitempty"`
+}
+
+// Get 获取小程序码
+// 可接受页面参数较短 生成个数不受限 适用于需要的码数量极多的业务场景
+// 根路径前不要填加'/' 不能携带参数(参数请放在scene字段里)
+//
+// token 微信access_token
+func (code *UnlimitedQRCode) Get(token string) (*http.Response, *CommonError, error) {
+ api := baseURL + apiGetUnlimitedQRCode
+ return code.get(api, token)
+}
+
+func (code *UnlimitedQRCode) get(api, token string) (*http.Response, *CommonError, error) {
+ return qrCodeRequest(api, token, code)
+}
+
+// QRCodeCreator 二维码创建器
+type QRCodeCreator struct {
+ Path string `json:"path"` // 扫码进入的小程序页面路径,最大长度 128 字节,不能为空;对于小游戏,可以只传入 query 部分,来实现传参效果,如:传入 "?foo=bar",即可在 wx.getLaunchOptionsSync 接口中的 query 参数获取到 {foo:"bar"}。
+ Width int `json:"width,omitempty"` // 二维码的宽度,单位 px。最小 280px,最大 1280px
+}
+
+// Create 获取小程序二维码,适用于需要的码数量较少的业务场景。通过该接口生成的小程序码,永久有效,有数量限制
+// 通过该接口生成的小程序码,永久有效,有数量限制
+//
+// token 微信access_token
+func (creator *QRCodeCreator) Create(token string) (*http.Response, *CommonError, error) {
+ api := baseURL + apiCreateQRCode
+ return creator.create(api, token)
+}
+
+func (creator *QRCodeCreator) create(api, token string) (*http.Response, *CommonError, error) {
+ return qrCodeRequest(api, token, creator)
+}
+
+// 向微信服务器获取二维码
+// 返回 HTTP 请求实例
+func qrCodeRequest(api, token string, params interface{}) (*http.Response, *CommonError, error) {
+
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ res, err := postJSONWithBody(url, params)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ response := new(CommonError)
+ switch header := res.Header.Get("Content-Type"); {
+ case strings.HasPrefix(header, "application/json"): // 返回错误信息
+ if err := json.NewDecoder(res.Body).Decode(response); err != nil {
+ res.Body.Close()
+ return nil, nil, err
+ }
+ return res, response, nil
+
+ case strings.HasPrefix(header, "image"): // 返回文件
+ return res, response, nil
+
+ default:
+ res.Body.Close()
+ return nil, nil, errors.New("invalid response header: " + header)
+ }
+}
diff --git a/app/lib/weapp/qr_code_test.go b/app/lib/weapp/qr_code_test.go
new file mode 100644
index 0000000..df68e93
--- /dev/null
+++ b/app/lib/weapp/qr_code_test.go
@@ -0,0 +1,212 @@
+package weapp
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path"
+ "testing"
+)
+
+func TestCreateQRCode(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ ePath := r.URL.EscapedPath()
+ if ePath != apiCreateQRCode {
+ t.Fatalf("Except to path '%s',get '%s'", apiCreateQRCode, ePath)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ Path string `json:"path"`
+ Width int `json:"width,omitempty"`
+ }{}
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.Path == "" {
+ t.Error("Response column path can not be empty")
+ }
+
+ filename := testIMGName
+ file, err := os.Open(filename)
+ if err != nil {
+ t.Fatal((err))
+ }
+ defer file.Close()
+
+ ext := path.Ext(filename)
+ ext = ext[1:len(ext)]
+ w.Header().Set("Content-Type", "image/"+ext)
+ w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", path.Base(filename)))
+ w.WriteHeader(http.StatusOK)
+
+ if _, err := io.Copy(w, file); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ creator := QRCodeCreator{
+ Path: "mock/path",
+ Width: 430,
+ }
+ resp, _, err := creator.create(ts.URL+apiCreateQRCode, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ resp.Body.Close()
+}
+
+func TestGetQRCode(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ ePath := r.URL.EscapedPath()
+ if ePath != apiGetQrCode {
+ t.Fatalf("Except to path '%s',get '%s'", apiGetQrCode, ePath)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ Path string `json:"path"`
+ Width int `json:"width,omitempty"`
+ AutoColor bool `json:"auto_color,omitempty"`
+ LineColor Color `json:"line_color,omitempty"`
+ IsHyaline bool `json:"is_hyaline,omitempty"`
+ }{}
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.Path == "" {
+ t.Error("Response column path can not be empty")
+ }
+
+ filename := testIMGName
+ file, err := os.Open(filename)
+ if err != nil {
+ t.Fatal((err))
+ }
+ defer file.Close()
+
+ ext := path.Ext(filename)
+ ext = ext[1:len(ext)]
+ w.Header().Set("Content-Type", "image/"+ext)
+ w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", path.Base(filename)))
+ w.WriteHeader(http.StatusOK)
+
+ if _, err := io.Copy(w, file); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ qr := QRCode{
+ Path: "mock/path",
+ Width: 430,
+ AutoColor: true,
+ LineColor: Color{"r", "g", "b"},
+ IsHyaline: true,
+ }
+ resp, _, err := qr.get(ts.URL+apiGetQrCode, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ resp.Body.Close()
+}
+
+func TestGetUnlimitedQRCode(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ ePath := r.URL.EscapedPath()
+ if ePath != apiGetUnlimitedQRCode {
+ t.Fatalf("Except to path '%s',get '%s'", apiGetUnlimitedQRCode, ePath)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ Scene string `json:"scene"`
+ Page string `json:"page,omitempty"`
+ Width int `json:"width,omitempty"`
+ AutoColor bool `json:"auto_color,omitempty"`
+ LineColor Color `json:"line_color,omitempty"`
+ IsHyaline bool `json:"is_hyaline,omitempty"`
+ }{}
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.Scene == "" {
+ t.Error("Response column scene can not be empty")
+ }
+
+ filename := testIMGName
+ file, err := os.Open(filename)
+ if err != nil {
+ t.Fatal((err))
+ }
+ defer file.Close()
+
+ ext := path.Ext(filename)
+ ext = ext[1:len(ext)]
+ w.Header().Set("Content-Type", "image/"+ext)
+ w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", path.Base(filename)))
+ w.WriteHeader(http.StatusOK)
+
+ if _, err := io.Copy(w, file); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ qr := UnlimitedQRCode{
+ Scene: "mock-scene-data",
+ Page: "mock/page",
+ Width: 430,
+ AutoColor: true,
+ LineColor: Color{"r", "g", "b"},
+ IsHyaline: true,
+ }
+ resp, _, err := qr.get(ts.URL+apiGetUnlimitedQRCode, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ resp.Body.Close()
+}
diff --git a/app/lib/weapp/search_submit_pages.go b/app/lib/weapp/search_submit_pages.go
new file mode 100644
index 0000000..08770de
--- /dev/null
+++ b/app/lib/weapp/search_submit_pages.go
@@ -0,0 +1,35 @@
+package weapp
+
+const (
+ apiSearchSubmitPages = "/wxa/search/wxaapi_submitpages"
+)
+
+// SearchSubmitPages 小程序页面收录请求
+type SearchSubmitPages struct {
+ Pages []SearchSubmitPage `json:"pages"`
+}
+
+// SearchSubmitPage 请求收录的页面
+type SearchSubmitPage struct {
+ Path string `json:"path"`
+ Query string `json:"query"`
+}
+
+// Send 提交收录请求
+func (s *SearchSubmitPages) Send(token string) (*CommonError, error) {
+ return s.send(baseURL+apiSearchSubmitPages, token)
+}
+
+func (s *SearchSubmitPages) send(api, token string) (*CommonError, error) {
+ api, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(CommonError)
+ if err := postJSON(api, s, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
diff --git a/app/lib/weapp/search_submit_pages_test.go b/app/lib/weapp/search_submit_pages_test.go
new file mode 100644
index 0000000..00e1a62
--- /dev/null
+++ b/app/lib/weapp/search_submit_pages_test.go
@@ -0,0 +1,69 @@
+package weapp
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+func TestSearchSubmitPages(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiSearchSubmitPages {
+ t.Fatalf("Except to path '%s',get '%s'", apiSearchSubmitPages, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ Pages []struct {
+ Path string `json:"path"`
+ Query string `json:"query"`
+ } `json:"pages"`
+ }{}
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if len(params.Pages) != 1 {
+ t.Fatal("param pages can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ sender := SearchSubmitPages{
+ []SearchSubmitPage{
+ {
+ Path: "/pages/index/index",
+ Query: "id=test",
+ },
+ },
+ }
+
+ _, err := sender.send(ts.URL+apiSearchSubmitPages, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/app/lib/weapp/sec_check.go b/app/lib/weapp/sec_check.go
new file mode 100644
index 0000000..a46f892
--- /dev/null
+++ b/app/lib/weapp/sec_check.go
@@ -0,0 +1,105 @@
+package weapp
+
+// 检测地址
+const (
+ apiIMGSecCheck = "/wxa/img_sec_check"
+ apiMSGSecCheck = "/wxa/msg_sec_check"
+ apiMediaCheckAsync = "/wxa/media_check_async"
+)
+
+// IMGSecCheck 本地图片检测
+// 官方文档: https://developers.weixin.qq.com/miniprogram/dev/api/imgSecCheck.html
+//
+// filename 要检测的图片本地路径
+// token 接口调用凭证(access_token)
+func IMGSecCheck(token, filename string) (*CommonError, error) {
+ api := baseURL + apiIMGSecCheck
+ return imgSecCheck(api, token, filename)
+}
+
+func imgSecCheck(api, token, filename string) (*CommonError, error) {
+
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(CommonError)
+ if err := postFormByFile(url, "media", filename, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// MSGSecCheck 文本检测
+// 官方文档: https://developers.weixin.qq.com/miniprogram/dev/api/msgSecCheck.html
+//
+// content 要检测的文本内容,长度不超过 500KB,编码格式为utf-8
+// token 接口调用凭证(access_token)
+func MSGSecCheck(token, content string) (*CommonError, error) {
+ api := baseURL + apiMSGSecCheck
+ return msgSecCheck(api, token, content)
+}
+
+func msgSecCheck(api, token, content string) (*CommonError, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ params := requestParams{
+ "content": content,
+ }
+
+ res := new(CommonError)
+ if err = postJSON(url, params, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// MediaType 检测内容类型
+type MediaType = uint8
+
+// 所有检测内容类型
+const (
+ _ MediaType = iota
+ MediaTypeAudio // 音频
+ MediaTypeImage // 图片
+)
+
+// CheckMediaResponse 异步校验图片/音频返回数据
+type CheckMediaResponse struct {
+ CommonError
+ TraceID string `json:"trace_id"`
+}
+
+// MediaCheckAsync 异步校验图片/音频是否含有违法违规内容。
+//
+// mediaURL 要检测的多媒体url
+// mediaType 接口调用凭证(access_token)
+func MediaCheckAsync(token, mediaURL string, mediaType MediaType) (*CheckMediaResponse, error) {
+ api := baseURL + apiMediaCheckAsync
+ return mediaCheckAsync(api, token, mediaURL, mediaType)
+}
+
+func mediaCheckAsync(api, token, mediaURL string, mediaType MediaType) (*CheckMediaResponse, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ params := requestParams{
+ "media_url": mediaURL,
+ "media_type": mediaType,
+ }
+
+ res := new(CheckMediaResponse)
+ if err = postJSON(url, params, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
diff --git a/app/lib/weapp/sec_check_test.go b/app/lib/weapp/sec_check_test.go
new file mode 100644
index 0000000..72f5de1
--- /dev/null
+++ b/app/lib/weapp/sec_check_test.go
@@ -0,0 +1,240 @@
+package weapp
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path"
+ "strings"
+ "testing"
+)
+
+func TestIMGSecCheck(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiIMGSecCheck {
+ t.Fatalf("Except to path '%s',get '%s'", apiIMGSecCheck, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ if _, _, err := r.FormFile("media"); err != nil {
+ t.Fatal(err)
+
+ }
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := imgSecCheck(ts.URL+apiIMGSecCheck, "mock-access-token", testIMGName)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestMediaCheckAsync(t *testing.T) {
+
+ localServer := http.NewServeMux()
+ localServer.HandleFunc("/notify", func(w http.ResponseWriter, r *http.Request) {
+ aesKey := base64.StdEncoding.EncodeToString([]byte("mock-aes-key"))
+ srv, err := NewServer("mock-app-id", "mock-access-token", aesKey, "mock-mch-id", "mock-api-key", false)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ srv.OnMediaCheckAsync(func(mix *MediaCheckAsyncResult) {
+ if mix.ToUserName == "" {
+ t.Error("ToUserName can not be empty")
+ }
+
+ if mix.FromUserName == "" {
+ t.Error("FromUserName can not be empty")
+ }
+ if mix.CreateTime == 0 {
+ t.Error("CreateTime can not be empty")
+ }
+ if mix.MsgType != "event" {
+ t.Error("Unexpected message type")
+ }
+ if mix.Event != "wxa_media_check" {
+ t.Error("Unexpected message event")
+ }
+ if mix.AppID == "" {
+ t.Error("AppID can not be empty")
+ }
+ if mix.TraceID == "" {
+ t.Error("TraceID can not be empty")
+ }
+
+ })
+
+ if err := srv.Serve(w, r); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ tls := httptest.NewServer(localServer)
+ defer tls.Close()
+
+ remoteServer := http.NewServeMux()
+ remoteServer.HandleFunc(apiMediaCheckAsync, func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiMediaCheckAsync {
+ t.Fatalf("Except to path '%s',get '%s'", apiMediaCheckAsync, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ MediaURL string `json:"media_url"`
+ MediaType uint8 `json:"media_type"`
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.MediaURL == "" {
+ t.Error("Response column media_url can not be empty")
+ }
+
+ if params.MediaType == 0 {
+ t.Error("Response column media_type can not be zero")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "errcode" : 0,
+ "errmsg" : "ok",
+ "trace_id" : "967e945cd8a3e458f3c74dcb886068e9"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+
+ raw = `{
+ "ToUserName" : "gh_38cc49f9733b",
+ "FromUserName" : "oH1fu0FdHqpToe2T6gBj0WyB8iS1",
+ "CreateTime" : 1552465698,
+ "MsgType" : "event",
+ "Event" : "wxa_media_check",
+ "isrisky" : 0,
+ "extra_info_json" : "",
+ "appid" : "wxd8c59133dfcbfc71",
+ "trace_id" : "967e945cd8a3e458f3c74dcb886068e9",
+ "status_code" : 0
+ }`
+ reader := strings.NewReader(raw)
+ http.Post(tls.URL+"/notify", "application/json", reader)
+ })
+
+ remoteServer.HandleFunc("/mediaurl", func(w http.ResponseWriter, r *http.Request) {
+ filename := testIMGName
+ file, err := os.Open(filename)
+ if err != nil {
+ t.Fatal((err))
+ }
+ defer file.Close()
+
+ ext := path.Ext(filename)
+ ext = ext[1:len(ext)]
+ w.Header().Set("Content-Type", "image/"+ext)
+ w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", path.Base(filename)))
+ w.WriteHeader(http.StatusOK)
+
+ if _, err := io.Copy(w, file); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ trs := httptest.NewServer(remoteServer)
+ defer trs.Close()
+
+ _, err := mediaCheckAsync(trs.URL+apiMediaCheckAsync, "mock-access-token", trs.URL+"/mediaurl", MediaTypeImage)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestMSGSecCheck(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != "/wxa/img_sec_check" {
+ t.Error("Invalid request path")
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ Content string `json:"content"`
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.Content == "" {
+ t.Error("Response column content can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := msgSecCheck(ts.URL+apiIMGSecCheck, "mock-access-token", "mock-content")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/app/lib/weapp/server.go b/app/lib/weapp/server.go
new file mode 100644
index 0000000..fd458bf
--- /dev/null
+++ b/app/lib/weapp/server.go
@@ -0,0 +1,677 @@
+package weapp
+
+import (
+ "encoding/base64"
+ "encoding/binary"
+ "encoding/json"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// MsgType 消息类型
+type MsgType = string
+
+// 所有消息类型
+const (
+ MsgText MsgType = "text" // 文本消息类型
+ MsgImg = "image" // 图片消息类型
+ MsgCard = "miniprogrampage" // 小程序卡片消息类型
+ MsgEvent = "event" // 事件类型
+ MsgTrans = "transfer_customer_service" // 转发客服消息
+)
+
+// EventType 事件类型
+type EventType string
+
+// 所有事件类型
+const (
+ EventQuotaGet EventType = "get_quota" // 查询商户余额
+ EventCheckBusiness = "check_biz" // 取消订单事件
+ EventMediaCheckAsync = "wxa_media_check" // 异步校验图片/音频
+ EventAddExpressOrder = "add_waybill" // 请求下单事件
+ EventExpressPathUpdate = "add_express_path" // 运单轨迹更新事件
+ EventExpressOrderCancel = "cancel_waybill" // 审核商户事件
+ EventUserTempsessionEnter = "user_enter_tempsession" // 用户进入临时会话状态
+ EventNearbyPoiAuditInfoAdd = "add_nearby_poi_audit_info" // 附近小程序添加地点审核状态通知
+ EventDeliveryOrderStatusUpdate = "update_waybill_status" // 配送单配送状态更新通知
+ EventAgentPosQuery = "transport_get_agent_pos" // 查询骑手当前位置信息
+ EventAuthInfoGet = "get_auth_info" // 使用授权码拉取授权信息
+ EventAuthAccountCancel = "cancel_auth_account" // 取消授权帐号
+ EventDeliveryOrderAdd = "transport_add_order" // 真实发起下单任务
+ EventDeliveryOrderTipsAdd = "transport_add_tips" // 对待接单状态的订单增加小费
+ EventDeliveryOrderCancel = "transport_cancel_order" // 取消订单操作
+ EventDeliveryOrderReturnConfirm = "transport_confirm_return_to_biz" // 异常妥投商户收货确认
+ EventDeliveryOrderPreAdd = "transport_precreate_order" // 预下单
+ EventDeliveryOrderPreCancel = "transport_precancel_order" // 预取消订单
+ EventDeliveryOrderQuery = "transport_query_order_status" // 查询订单状态
+ EventDeliveryOrderReadd = "transport_readd_order" // 下单
+ EventPreAuthCodeGet = "get_pre_auth_code" // 获取预授权码
+ EventRiderScoreSet = "transport_set_rider_score" // 给骑手评分
+)
+
+// Server 微信通知服务处理器
+type Server struct {
+ appID string // 小程序 ID
+ mchID string // 商户号
+ apiKey string // 商户签名密钥
+ token string // 微信服务器验证令牌
+ aesKey []byte // base64 解码后的消息加密密钥
+ validate bool // 是否验证请求来自微信服务器
+
+ textMessageHandler func(*TextMessageResult) *TransferCustomerMessage
+ imageMessageHandler func(*ImageMessageResult) *TransferCustomerMessage
+ cardMessageHandler func(*CardMessageResult) *TransferCustomerMessage
+ userTempsessionEnterHandler func(*UserTempsessionEnterResult)
+ mediaCheckAsyncHandler func(*MediaCheckAsyncResult)
+ expressPathUpdateHandler func(*ExpressPathUpdateResult)
+ addNearbyPoiAuditHandler func(*AddNearbyPoiResult)
+ addExpressOrderHandler func(*AddExpressOrderResult) *AddExpressOrderReturn
+ expressOrderCancelHandler func(*CancelExpressOrderResult) *CancelExpressOrderReturn
+ checkExpressBusinessHandler func(*CheckExpressBusinessResult) *CheckExpressBusinessReturn
+ quotaGetHandler func(*GetExpressQuotaResult) *GetExpressQuotaReturn
+ deliveryOrderStatusUpdateHandler func(*DeliveryOrderStatusUpdateResult) *DeliveryOrderStatusUpdateReturn
+ agentPosQueryHandler func(*AgentPosQueryResult) *AgentPosQueryReturn
+ authInfoGetHandler func(*AuthInfoGetResult) *AuthInfoGetReturn
+ authAccountCancelHandler func(*CancelAuthResult) *CancelAuthReturn
+ deliveryOrderAddHandler func(*DeliveryOrderAddResult) *DeliveryOrderAddReturn
+ deliveryOrderTipsAddHandler func(*DeliveryOrderAddTipsResult) *DeliveryOrderAddTipsReturn
+ deliveryOrderCancelHandler func(*DeliveryOrderCancelResult) *DeliveryOrderCancelReturn
+ deliveryOrderReturnConfirmHandler func(*DeliveryOrderReturnConfirmResult) *DeliveryOrderReturnConfirmReturn
+ deliveryOrderPreAddHandler func(*DeliveryOrderPreAddResult) *DeliveryOrderPreAddReturn
+ deliveryOrderPreCancelHandler func(*DeliveryOrderPreCancelResult) *DeliveryOrderPreCancelReturn
+ deliveryOrderQueryHandler func(*DeliveryOrderQueryResult) *DeliveryOrderQueryReturn
+ deliveryOrderReaddHandler func(*DeliveryOrderReaddResult) *DeliveryOrderReaddReturn
+ preAuthCodeGetHandler func(*PreAuthCodeGetResult) *PreAuthCodeGetReturn
+ riderScoreSetHandler func(*RiderScoreSetResult) *RiderScoreSetReturn
+}
+
+// OnCustomerServiceTextMessage add handler to handle customer text service message.
+func (srv *Server) OnCustomerServiceTextMessage(fn func(*TextMessageResult) *TransferCustomerMessage) {
+ srv.textMessageHandler = fn
+}
+
+// OnCustomerServiceImageMessage add handler to handle customer image service message.
+func (srv *Server) OnCustomerServiceImageMessage(fn func(*ImageMessageResult) *TransferCustomerMessage) {
+ srv.imageMessageHandler = fn
+}
+
+// OnCustomerServiceCardMessage add handler to handle customer card service message.
+func (srv *Server) OnCustomerServiceCardMessage(fn func(*CardMessageResult) *TransferCustomerMessage) {
+ srv.cardMessageHandler = fn
+}
+
+// OnUserTempsessionEnter add handler to handle customer service message.
+func (srv *Server) OnUserTempsessionEnter(fn func(*UserTempsessionEnterResult)) {
+ srv.userTempsessionEnterHandler = fn
+}
+
+// OnMediaCheckAsync add handler to handle MediaCheckAsync.
+func (srv *Server) OnMediaCheckAsync(fn func(*MediaCheckAsyncResult)) {
+ srv.mediaCheckAsyncHandler = fn
+}
+
+// OnExpressPathUpdate add handler to handle ExpressPathUpdate.
+func (srv *Server) OnExpressPathUpdate(fn func(*ExpressPathUpdateResult)) {
+ srv.expressPathUpdateHandler = fn
+}
+
+// OnAddNearbyPoi add handler to handle AddNearbyPoiAudit.
+func (srv *Server) OnAddNearbyPoi(fn func(*AddNearbyPoiResult)) {
+ srv.addNearbyPoiAuditHandler = fn
+}
+
+// OnAddExpressOrder add handler to handle AddExpressOrder.
+func (srv *Server) OnAddExpressOrder(fn func(*AddExpressOrderResult) *AddExpressOrderReturn) {
+ srv.addExpressOrderHandler = fn
+}
+
+// OnCheckExpressBusiness add handler to handle CheckBusiness.
+func (srv *Server) OnCheckExpressBusiness(fn func(*CheckExpressBusinessResult) *CheckExpressBusinessReturn) {
+ srv.checkExpressBusinessHandler = fn
+}
+
+// OnCancelExpressOrder add handler to handle ExpressOrderCancel.
+func (srv *Server) OnCancelExpressOrder(fn func(*CancelExpressOrderResult) *CancelExpressOrderReturn) {
+ srv.expressOrderCancelHandler = fn
+}
+
+// OnGetExpressQuota add handler to handle QuotaGet.
+func (srv *Server) OnGetExpressQuota(fn func(*GetExpressQuotaResult) *GetExpressQuotaReturn) {
+ srv.quotaGetHandler = fn
+}
+
+// OnDeliveryOrderStatusUpdate add handler to handle DeliveryOrderStatusUpdate.
+// OnDeliveryOrderStatusUpdate add handler to handle deliveryOrderStatusUpdate.
+func (srv *Server) OnDeliveryOrderStatusUpdate(fn func(*DeliveryOrderStatusUpdateResult) *DeliveryOrderStatusUpdateReturn) {
+ srv.deliveryOrderStatusUpdateHandler = fn
+}
+
+// OnAgentPosQuery add handler to handle AgentPosQuery.
+func (srv *Server) OnAgentPosQuery(fn func(*AgentPosQueryResult) *AgentPosQueryReturn) {
+ srv.agentPosQueryHandler = fn
+}
+
+// OnAuthInfoGet add handler to handle AuthInfoGet.
+func (srv *Server) OnAuthInfoGet(fn func(*AuthInfoGetResult) *AuthInfoGetReturn) {
+ srv.authInfoGetHandler = fn
+}
+
+// OnCancelAuth add handler to handle deliveryOrderStatusUpdate.
+func (srv *Server) OnCancelAuth(fn func(*CancelAuthResult) *CancelAuthReturn) {
+ srv.authAccountCancelHandler = fn
+}
+
+// OnDeliveryOrderAdd add handler to handle deliveryOrderStatusUpdate.
+func (srv *Server) OnDeliveryOrderAdd(fn func(*DeliveryOrderAddResult) *DeliveryOrderAddReturn) {
+ srv.deliveryOrderAddHandler = fn
+}
+
+// OnDeliveryOrderAddTips add handler to handle deliveryOrderStatusUpdate.
+func (srv *Server) OnDeliveryOrderAddTips(fn func(*DeliveryOrderAddTipsResult) *DeliveryOrderAddTipsReturn) {
+ srv.deliveryOrderTipsAddHandler = fn
+}
+
+// OnDeliveryOrderCancel add handler to handle deliveryOrderStatusUpdate.
+func (srv *Server) OnDeliveryOrderCancel(fn func(*DeliveryOrderCancelResult) *DeliveryOrderCancelReturn) {
+ srv.deliveryOrderCancelHandler = fn
+}
+
+// OnDeliveryOrderReturnConfirm add handler to handle deliveryOrderStatusUpdate.
+func (srv *Server) OnDeliveryOrderReturnConfirm(fn func(*DeliveryOrderReturnConfirmResult) *DeliveryOrderReturnConfirmReturn) {
+ srv.deliveryOrderReturnConfirmHandler = fn
+}
+
+// OnDeliveryOrderPreAdd add handler to handle deliveryOrderStatusUpdate.
+func (srv *Server) OnDeliveryOrderPreAdd(fn func(*DeliveryOrderPreAddResult) *DeliveryOrderPreAddReturn) {
+ srv.deliveryOrderPreAddHandler = fn
+}
+
+// OnDeliveryOrderPreCancel add handler to handle deliveryOrderStatusUpdate.
+func (srv *Server) OnDeliveryOrderPreCancel(fn func(*DeliveryOrderPreCancelResult) *DeliveryOrderPreCancelReturn) {
+ srv.deliveryOrderPreCancelHandler = fn
+}
+
+// OnDeliveryOrderQuery add handler to handle deliveryOrderStatusUpdate.
+func (srv *Server) OnDeliveryOrderQuery(fn func(*DeliveryOrderQueryResult) *DeliveryOrderQueryReturn) {
+ srv.deliveryOrderQueryHandler = fn
+}
+
+// OnDeliveryOrderReadd add handler to handle deliveryOrderStatusUpdate.
+func (srv *Server) OnDeliveryOrderReadd(fn func(*DeliveryOrderReaddResult) *DeliveryOrderReaddReturn) {
+ srv.deliveryOrderReaddHandler = fn
+}
+
+// OnPreAuthCodeGet add handler to handle preAuthCodeGet.
+func (srv *Server) OnPreAuthCodeGet(fn func(*PreAuthCodeGetResult) *PreAuthCodeGetReturn) {
+ srv.preAuthCodeGetHandler = fn
+}
+
+// OnRiderScoreSet add handler to handle riderScoreSet.
+func (srv *Server) OnRiderScoreSet(fn func(*RiderScoreSetResult) *RiderScoreSetReturn) {
+ srv.riderScoreSetHandler = fn
+}
+
+type dataType = string
+
+const (
+ dataTypeJSON dataType = "application/json"
+ dataTypeXML = "text/xml"
+)
+
+// NewServer 返回经过初始化的Server
+func NewServer(appID, token, aesKey, mchID, apiKey string, validate bool) (*Server, error) {
+
+ key, err := base64.RawStdEncoding.DecodeString(aesKey)
+ if err != nil {
+ return nil, err
+ }
+
+ server := Server{
+ appID: appID,
+ mchID: mchID,
+ apiKey: apiKey,
+ token: token,
+ aesKey: key,
+ validate: validate,
+ }
+
+ return &server, nil
+}
+
+func getDataType(req *http.Request) dataType {
+ content := req.Header.Get("Content-Type")
+
+ switch {
+ case strings.Contains(content, dataTypeJSON):
+ return dataTypeJSON
+ case strings.Contains(content, dataTypeXML):
+ return dataTypeXML
+ default:
+ return content
+ }
+}
+
+func unmarshal(data []byte, tp dataType, v interface{}) error {
+ switch tp {
+ case dataTypeJSON:
+ if err := json.Unmarshal(data, v); err != nil {
+ return err
+ }
+ case dataTypeXML:
+ if err := xml.Unmarshal(data, v); err != nil {
+ return err
+ }
+ default:
+ return errors.New("invalid content type: " + tp)
+ }
+
+ return nil
+}
+
+func marshal(data interface{}, tp dataType) ([]byte, error) {
+ switch tp {
+ case dataTypeJSON:
+ return json.Marshal(data)
+ case dataTypeXML:
+ return xml.Marshal(data)
+ default:
+ return nil, errors.New("invalid content type: " + tp)
+ }
+}
+
+// 处理消息体
+func (srv *Server) handleRequest(w http.ResponseWriter, r *http.Request, isEncrpt bool, tp dataType) (interface{}, error) {
+ raw, err := ioutil.ReadAll(r.Body)
+ if err != nil {
+ return nil, err
+ }
+ if isEncrpt { // 处理加密消息
+
+ query := r.URL.Query()
+ nonce, signature, timestamp := query.Get("nonce"), query.Get("signature"), query.Get("timestamp")
+
+ // 检验消息是否来自微信服务器
+ if !validateSignature(signature, srv.token, timestamp, nonce) {
+ return nil, errors.New("failed to validate signature")
+ }
+
+ res := new(EncryptedResult)
+ if err := unmarshal(raw, tp, res); err != nil {
+ return nil, err
+ }
+
+ body, err := srv.decryptMsg(res.Encrypt)
+ if err != nil {
+ return nil, err
+ }
+ length := binary.BigEndian.Uint32(body[16:20])
+ raw = body[20 : 20+length]
+ }
+
+ res := new(CommonServerResult)
+ if err := unmarshal(raw, tp, res); err != nil {
+ return nil, err
+ }
+
+ switch res.MsgType {
+ case MsgText:
+ msg := new(TextMessageResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.textMessageHandler != nil {
+ return srv.textMessageHandler(msg), nil
+ }
+
+ case MsgImg:
+ msg := new(ImageMessageResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.imageMessageHandler != nil {
+ return srv.imageMessageHandler(msg), nil
+ }
+
+ case MsgCard:
+ msg := new(CardMessageResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.cardMessageHandler != nil {
+ return srv.cardMessageHandler(msg), nil
+ }
+ case MsgEvent:
+ switch res.Event {
+ case EventUserTempsessionEnter:
+ msg := new(UserTempsessionEnterResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.userTempsessionEnterHandler != nil {
+ srv.userTempsessionEnterHandler(msg)
+ }
+ case EventQuotaGet:
+ msg := new(GetExpressQuotaResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.quotaGetHandler != nil {
+ return srv.quotaGetHandler(msg), nil
+ }
+ case EventMediaCheckAsync:
+ msg := new(MediaCheckAsyncResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.mediaCheckAsyncHandler != nil {
+ srv.mediaCheckAsyncHandler(msg)
+ }
+ case EventAddExpressOrder:
+ msg := new(AddExpressOrderResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.addExpressOrderHandler != nil {
+ return srv.addExpressOrderHandler(msg), nil
+ }
+ case EventExpressOrderCancel:
+ msg := new(CancelExpressOrderResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.expressOrderCancelHandler != nil {
+ return srv.expressOrderCancelHandler(msg), nil
+ }
+ case EventCheckBusiness:
+ msg := new(CheckExpressBusinessResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.checkExpressBusinessHandler != nil {
+ return srv.checkExpressBusinessHandler(msg), nil
+ }
+ case EventDeliveryOrderStatusUpdate:
+ msg := new(DeliveryOrderStatusUpdateResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.deliveryOrderStatusUpdateHandler != nil {
+ return srv.deliveryOrderStatusUpdateHandler(msg), nil
+ }
+ case EventAgentPosQuery:
+ msg := new(AgentPosQueryResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.agentPosQueryHandler != nil {
+ return srv.agentPosQueryHandler(msg), nil
+ }
+ case EventAuthInfoGet:
+ msg := new(AuthInfoGetResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.authInfoGetHandler != nil {
+ return srv.authInfoGetHandler(msg), nil
+ }
+ case EventAuthAccountCancel:
+ msg := new(CancelAuthResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.authAccountCancelHandler != nil {
+ return srv.authAccountCancelHandler(msg), nil
+ }
+ case EventDeliveryOrderAdd:
+ msg := new(DeliveryOrderAddResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.deliveryOrderAddHandler != nil {
+ return srv.deliveryOrderAddHandler(msg), nil
+ }
+ case EventDeliveryOrderTipsAdd:
+ msg := new(DeliveryOrderAddTipsResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.deliveryOrderTipsAddHandler != nil {
+ return srv.deliveryOrderTipsAddHandler(msg), nil
+ }
+ case EventDeliveryOrderCancel:
+ msg := new(DeliveryOrderCancelResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.deliveryOrderCancelHandler != nil {
+ return srv.deliveryOrderCancelHandler(msg), nil
+ }
+ case EventDeliveryOrderReturnConfirm:
+ msg := new(DeliveryOrderReturnConfirmResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.deliveryOrderReturnConfirmHandler != nil {
+ return srv.deliveryOrderReturnConfirmHandler(msg), nil
+ }
+ case EventDeliveryOrderPreAdd:
+ msg := new(DeliveryOrderPreAddResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.deliveryOrderPreAddHandler != nil {
+ return srv.deliveryOrderPreAddHandler(msg), nil
+ }
+ case EventDeliveryOrderPreCancel:
+ msg := new(DeliveryOrderPreCancelResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.deliveryOrderPreCancelHandler != nil {
+ return srv.deliveryOrderPreCancelHandler(msg), nil
+ }
+ case EventDeliveryOrderQuery:
+ msg := new(DeliveryOrderQueryResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.deliveryOrderQueryHandler != nil {
+ return srv.deliveryOrderQueryHandler(msg), nil
+ }
+ case EventDeliveryOrderReadd:
+ msg := new(DeliveryOrderReaddResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.deliveryOrderReaddHandler != nil {
+ return srv.deliveryOrderReaddHandler(msg), nil
+ }
+ case EventPreAuthCodeGet:
+ msg := new(PreAuthCodeGetResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.preAuthCodeGetHandler != nil {
+ return srv.preAuthCodeGetHandler(msg), nil
+ }
+ case EventRiderScoreSet:
+ msg := new(RiderScoreSetResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.riderScoreSetHandler != nil {
+ return srv.riderScoreSetHandler(msg), nil
+ }
+ case EventExpressPathUpdate:
+ msg := new(ExpressPathUpdateResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.expressPathUpdateHandler != nil {
+ srv.expressPathUpdateHandler(msg)
+ }
+ case EventNearbyPoiAuditInfoAdd:
+ msg := new(AddNearbyPoiResult)
+ if err := unmarshal(raw, tp, msg); err != nil {
+ return nil, err
+ }
+ if srv.addNearbyPoiAuditHandler != nil {
+ srv.addNearbyPoiAuditHandler(msg)
+ }
+ default:
+ return nil, fmt.Errorf("unexpected message type '%s'", res.MsgType)
+ }
+ default:
+ return nil, fmt.Errorf("unexpected message type '%s'", res.MsgType)
+ }
+ return nil, nil
+}
+
+// 判断 interface{} 是否为空
+func isNil(i interface{}) bool {
+ defer func() {
+ recover()
+ }()
+
+ vi := reflect.ValueOf(i)
+ return vi.IsNil()
+}
+
+// Serve 接收并处理微信通知服务
+func (srv *Server) Serve(w http.ResponseWriter, r *http.Request) error {
+ switch r.Method {
+ case "POST":
+ tp := getDataType(r)
+ isEncrpt := isEncrypted(r)
+ res, err := srv.handleRequest(w, r, isEncrpt, tp)
+ if err != nil {
+ return fmt.Errorf("handle request content error: %s", err)
+ }
+
+ if !isNil(res) {
+ raw, err := marshal(res, tp)
+ if err != nil {
+ return err
+ }
+ if isEncrpt {
+ res, err := srv.encryptMsg(string(raw), time.Now().Unix())
+ if err != nil {
+ return err
+ }
+ raw, err = marshal(res, tp)
+ if err != nil {
+ return err
+ }
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", tp)
+ if _, err := w.Write(raw); err != nil {
+ return err
+ }
+ } else {
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8") // normal header
+ w.WriteHeader(http.StatusOK)
+ _, err = io.WriteString(w, "success")
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+ case "GET":
+ if srv.validate { // 请求来自微信验证成功后原样返回 echostr 参数内容
+ if !srv.validateServer(r) {
+ return errors.New("验证消息来自微信服务器失败")
+ }
+
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8") // normal header
+ w.WriteHeader(http.StatusOK)
+
+ echostr := r.URL.Query().Get("echostr")
+ _, err := io.WriteString(w, echostr)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+ default:
+ return errors.New("invalid request method: " + r.Method)
+ }
+}
+
+// 判断消息是否加密
+func isEncrypted(req *http.Request) bool {
+ return req.URL.Query().Get("encrypt_type") == "aes"
+}
+
+// 验证消息的确来自微信服务器
+// 1.将token、timestamp、nonce三个参数进行字典序排序
+// 2.将三个参数字符串拼接成一个字符串进行sha1加密
+// 3.开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
+func (srv *Server) validateServer(req *http.Request) bool {
+ query := req.URL.Query()
+ nonce := query.Get("nonce")
+ signature := query.Get("signature")
+ timestamp := query.Get("timestamp")
+
+ return validateSignature(signature, nonce, timestamp, srv.token)
+}
+
+// 加密消息
+func (srv *Server) encryptMsg(message string, timestamp int64) (*EncryptedMsgRequest, error) {
+
+ key := srv.aesKey
+
+ //获得16位随机字符串,填充到明文之前
+ nonce := randomString(16)
+ text := nonce + strconv.Itoa(len(message)) + message + srv.appID
+ plaintext := pkcs7encode([]byte(text))
+
+ cipher, err := cbcEncrypt(key, plaintext, key)
+ if err != nil {
+ return nil, err
+ }
+
+ encrypt := base64.StdEncoding.EncodeToString(cipher)
+ timestr := strconv.FormatInt(timestamp, 10)
+
+ //生成安全签名
+ signature := createSignature(srv.token, timestr, nonce, encrypt)
+
+ request := EncryptedMsgRequest{
+ Nonce: nonce,
+ Encrypt: encrypt,
+ TimeStamp: timestr,
+ MsgSignature: signature,
+ }
+
+ return &request, nil
+}
+
+// 检验消息的真实性,并且获取解密后的明文.
+func (srv *Server) decryptMsg(encrypted string) ([]byte, error) {
+
+ key := srv.aesKey
+
+ ciphertext, err := base64.StdEncoding.DecodeString(encrypted)
+ if err != nil {
+ return nil, err
+ }
+
+ data, err := cbcDecrypt(key, ciphertext, key)
+ if err != nil {
+ return nil, err
+ }
+
+ return data, nil
+}
diff --git a/app/lib/weapp/server_types.go b/app/lib/weapp/server_types.go
new file mode 100644
index 0000000..5614c21
--- /dev/null
+++ b/app/lib/weapp/server_types.go
@@ -0,0 +1,660 @@
+package weapp
+
+import "encoding/xml"
+
+// EncryptedResult 接收的加密数据
+type EncryptedResult struct {
+ XMLName xml.Name `xml:"xml" json:"-"`
+ ToUserName string `json:"ToUserName" xml:"ToUserName"` // 接收者 为小程序 AppID
+ Encrypt string `json:"Encrypt" xml:"Encrypt"` // 加密消息
+}
+
+// EncryptedMsgRequest 发送的加密消息格式
+type EncryptedMsgRequest struct {
+ XMLName xml.Name `xml:"xml"`
+ Encrypt string `json:"Encrypt" xml:"Encrypt"` // 加密消息
+ TimeStamp string `json:"TimeStamp,omitempty" xml:"TimeStamp,omitempty"` // 时间戳
+ Nonce string `json:"Nonce,omitempty" xml:"Nonce,omitempty"` // 随机数
+ MsgSignature string `json:"MsgSignature,omitempty" xml:"MsgSignature,omitempty"` // 签名
+}
+
+// CommonServerResult 基础通知数据
+type CommonServerResult struct {
+ XMLName xml.Name `xml:"xml" json:"-"`
+ ToUserName string `json:"ToUserName" xml:"ToUserName"` // 小程序的原始ID
+ FromUserName string `json:"FromUserName" xml:"FromUserName"` // 发送者的 openID | 平台推送服务UserName
+ CreateTime uint `json:"CreateTime" xml:"CreateTime"` // 消息创建时间(整型)
+ MsgType MsgType `json:"MsgType" xml:"MsgType"` // 消息类型
+ Event EventType `json:"Event" xml:"Event"` // 事件类型
+}
+
+// CommonServerReturn 没收到通知后返回的基础数据
+type CommonServerReturn struct {
+ ToUserName string `json:"ToUserName" xml:"ToUserName"` // 是 原样返回请求中的 FromUserName
+ FromUserName string `json:"FromUserName" xml:"FromUserName"` // 是 快递公司小程序 UserName
+ CreateTime uint `json:"CreateTime" xml:"CreateTime"` // 是 事件时间,Unix时间戳
+ MsgType string `json:"MsgType" xml:"MsgType"` // 是 消息类型,固定为 event
+ Event string `json:"Event" xml:"Event"` // 是 事件类型,固定为 transport_add_order,不区分大小写
+ ResultCode int `json:"resultcode" xml:"resultcode"` // 是 错误码
+ ResultMsg string `json:"resultmsg" xml:"resultmsg"` // 是 错误描述
+}
+
+// UserTempsessionEnterResult 接收的文本消息
+type UserTempsessionEnterResult struct {
+ CommonServerResult
+ SessionFrom string `json:"SessionFrom" xml:"SessionFrom"` // 开发者在客服会话按钮设置的 session-from 属性
+}
+
+// TextMessageResult 接收的文本消息
+type TextMessageResult struct {
+ CommonServerResult
+ MsgID int `json:"MsgId" xml:"MsgId"` // 消息 ID
+ Content string `json:"Content" xml:"Content"`
+}
+
+// ImageMessageResult 接收的图片消息
+type ImageMessageResult struct {
+ CommonServerResult
+ MsgID int `json:"MsgId" xml:"MsgId"` // 消息 ID
+ PicURL string `json:"PicUrl" xml:"PicUrl"`
+ MediaID string `json:"MediaId" xml:"MediaId"`
+}
+
+// CardMessageResult 接收的卡片消息
+type CardMessageResult struct {
+ CommonServerResult
+ MsgID int `json:"MsgId" xml:"MsgId"` // 消息 ID
+ Title string `json:"Title" xml:"Title"` // 标题
+ AppID string `json:"AppId" xml:"AppId"` // 小程序 appid
+ PagePath string `json:"PagePath" xml:"PagePath"` // 小程序页面路径
+ ThumbURL string `json:"ThumbUrl" xml:"ThumbUrl"` // 封面图片的临时cdn链接
+ ThumbMediaID string `json:"ThumbMediaId" xml:"ThumbMediaId"` // 封面图片的临时素材id
+}
+
+// MediaCheckAsyncResult 异步校验的图片/音频结果
+type MediaCheckAsyncResult struct {
+ CommonServerResult
+ IsRisky uint8 `json:"isrisky" xml:"isrisky"` // 检测结果,0:暂未检测到风险,1:风险
+ ExtraInfoJSON string `json:"extra_info_json" xml:"extra_info_json"` // 附加信息,默认为空
+ AppID string `json:"appid" xml:"appid"` // 小程序的appid
+ TraceID string `json:"trace_id" xml:"trace_id"` // 任务id
+ StatusCode int `json:"status_code" xml:"status_code"` // 默认为:0,4294966288(-1008)为链接无法下载
+}
+
+// AddNearbyPoiResult 附近小程序添加地点审核状态通知数据
+type AddNearbyPoiResult struct {
+ CommonServerResult
+ AuditID uint `xml:"audit_id"` // 审核单id
+ Status uint8 `xml:"status"` // 审核状态(3:审核通过,2:审核失败)
+ Reason string `xml:"reason"` // 如果status为2,会返回审核失败的原因
+ PoiID uint `xml:"poi_id"`
+}
+
+// ExpressPathUpdateResult 运单轨迹更新事件需要返回的数据
+type ExpressPathUpdateResult struct {
+ CommonServerResult
+ DeliveryID string `json:"DeliveryID" xml:"DeliveryID"` // 快递公司ID
+ WayBillID string `json:"WayBillId" xml:"WayBillId"` // 运单ID
+ Version uint `json:"Version" xml:"Version"` // 轨迹版本号(整型)
+ Count uint `json:"Count" xml:"Count"` // 轨迹节点数(整型)
+ Actions []struct {
+ ActionTime uint `json:"ActionTime" xml:"ActionTime"` // 轨迹节点 Unix 时间戳
+ ActionType uint `json:"ActionType" xml:"ActionType"` // 轨迹节点类型
+ ActionMsg string `json:"ActionMsg" xml:"ActionMsg"` // 轨迹节点详情
+ } `json:"Actions" xml:"Actions"` // 轨迹列表
+}
+
+// AddExpressOrderReturn 请求下单事件需要返回的数据
+type AddExpressOrderReturn struct {
+ CommonServerReturn
+ Token string `json:"Token" xml:"Token"` // 传入的 Token,原样返回
+ OrderID string `json:"OrderID" xml:"OrderID"` // 传入的唯一标识订单的 ID,由商户生成,原样返回
+ BizID string `json:"BizID" xml:"BizID"` // 商户 ID,原样返回
+ WayBillID string `json:"WayBillID" xml:"WayBillID"` // 运单 ID
+ WaybillData string `json:"WaybillData" xml:"WaybillData"` // 集包地、三段码、大头笔等信息,用于生成面单信息。详见后文返回值说明
+}
+
+// TransferCustomerMessage 需要转发的客服消息
+type TransferCustomerMessage struct {
+ XMLName xml.Name `xml:"xml"`
+ // 接收方帐号(收到的OpenID)
+ ToUserName string `json:"ToUserName" xml:"ToUserName"`
+ // 开发者微信号
+ FromUserName string `json:"FromUserName" xml:"FromUserName"`
+ // 消息创建时间 (整型)
+ CreateTime uint `json:"CreateTime" xml:"CreateTime"`
+ // 转发消息类型
+ MsgType MsgType `json:"MsgType" xml:"MsgType"`
+}
+
+// AddExpressOrderResult 请求下单事件参数
+type AddExpressOrderResult struct {
+ CommonServerResult
+ Token string `json:"Token" xml:"Token"` // 订单 Token。请保存该 Token,调用logistics.updatePath时需要传入
+ OrderID string `json:"OrderID" xml:"OrderID"` // 唯一标识订单的 ID,由商户生成。快递需要保证相同的 OrderID 生成相同的运单ID。
+ BizID string `json:"BizID" xml:"BizID"` // 商户 ID,即商户在快递注册的客户编码或月结账户名
+ BizPwd string `json:"BizPwd" xml:"BizPwd"` // BizID 对应的密码
+ ShopAppID string `json:"ShopAppID" xml:"ShopAppID"` // 商户的小程序 AppID
+ WayBillID string `json:"WayBillID" xml:"WayBillID"` // 运单 ID,从微信号段中生成。若为 0,则表示需要快递来生成运单 ID。
+ Remark string `json:"Remark" xml:"Remark"` // 快递备注,会打印到面单上,比如"易碎物品"
+ Sender struct {
+ Name string `json:"Name" xml:"Name"` // 收件人/发件人姓名,不超过64字节
+ Tel string `json:"Tel" xml:"Tel"` // 收件人/发件人座机号码,若不填写则必须填写 mobile,不超过32字节
+ Mobile string `json:"Mobile" xml:"Mobile"` // 收件人/发件人手机号码,若不填写则必须填写 tel,不超过32字节
+ Company string `json:"Company" xml:"Company"` // 收件人/发件人公司名称,不超过64字节
+ PostCode string `json:"PostCode" xml:"PostCode"` // 收件人/发件人邮编,不超过10字节
+ Country string `json:"Country" xml:"Country"` // 收件人/发件人国家,不超过64字节
+ Province string `json:"Province" xml:"Province"` // 收件人/发件人省份,比如:"广东省",不超过64字节
+ City string `json:"City" xml:"City"` // 收件人/发件人市/地区,比如:"广州市",不超过64字节
+ Area string `json:"Area" xml:"Area"` // 收件人/发件人区/县,比如:"海珠区",不超过64字节
+ Address string `json:"Address" xml:"Address"` // 收件人/发件人详细地址,比如:"XX路XX号XX大厦XX",不超过512字节
+ } `json:"Sender" xml:"Sender"` // 发件人信息
+ Receiver struct {
+ Name string `json:"Name" xml:"Name"` // 收件人/发件人姓名,不超过64字节
+ Tel string `json:"Tel" xml:"Tel"` // 收件人/发件人座机号码,若不填写则必须填写 mobile,不超过32字节
+ Mobile string `json:"Mobile" xml:"Mobile"` // 收件人/发件人手机号码,若不填写则必须填写 tel,不超过32字节
+ Company string `json:"Company" xml:"Company"` // 收件人/发件人公司名称,不超过64字节
+ PostCode string `json:"PostCode" xml:"PostCode"` // 收件人/发件人邮编,不超过10字节
+ Country string `json:"Country" xml:"Country"` // 收件人/发件人国家,不超过64字节
+ Province string `json:"Province" xml:"Province"` // 收件人/发件人省份,比如:"广东省",不超过64字节
+ City string `json:"City" xml:"City"` // 收件人/发件人市/地区,比如:"广州市",不超过64字节
+ Area string `json:"Area" xml:"Area"` // 收件人/发件人区/县,比如:"海珠区",不超过64字节
+ Address string `json:"Address" xml:"Address"` // 收件人/发件人详细地址,比如:"XX路XX号XX大厦XX",不超过512字节
+ } `json:"Receiver" xml:"Receiver"` // 收件人信息
+ Cargo struct {
+ Weight float64 `json:"Weight" xml:"Weight"` // 包裹总重量,单位是千克(kg)
+ SpaceX float64 `json:"Space_X" xml:"Space_X"` // 包裹长度,单位厘米(cm)
+ SpaceY float64 `json:"Space_Y" xml:"Space_Y"` // 包裹宽度,单位厘米(cm)
+ SpaceZ float64 `json:"Space_Z" xml:"Space_Z"` // 包裹高度,单位厘米(cm)
+ Count uint `json:"Count" xml:"Count"` // 包裹数量
+ } `json:"Cargo" xml:"Cargo"` // 包裹信息
+ Insured struct {
+ Used InsureStatus `json:"UseInsured" xml:"UseInsured"` // 是否保价,0 表示不保价,1 表示保价
+ Value uint `json:"InsuredValue" xml:"InsuredValue"` // 保价金额,单位是分,比如: 10000 表示 100 元
+ } `json:"Insured" xml:"Insured"` // 保价信息
+ Service struct {
+ Type uint8 `json:"ServiceType" xml:"ServiceType"` // 服务类型 ID
+ Name string `json:"ServiceName" xml:"ServiceName"` // 服务名称
+ } `json:"Service" xml:"Service"` // 服务类型
+}
+
+// GetExpressQuotaReturn 查询商户余额事件需要返回的数据
+type GetExpressQuotaReturn struct {
+ CommonServerReturn
+ BizID string `json:"BizID" xml:"BizID"` // 商户ID
+ Quota float64 `json:"Quota" xml:"Quota"` // 商户可用余额,0 表示无可用余额
+}
+
+// GetExpressQuotaResult 查询商户余额事件参数
+type GetExpressQuotaResult struct {
+ CommonServerResult
+ BizID string `json:"BizID" xml:"BizID"` // 商户ID,即商户在快递注册的客户编码或月结账户名
+ BizPwd string `json:"BizPwd" xml:"BizPwd"` // BizID 对应的密码
+ ShopAppID string `json:"ShopAppID" xml:"ShopAppID"` // 商户小程序的 AppID
+}
+
+// CancelExpressOrderResult 取消订单事件参数
+type CancelExpressOrderResult struct {
+ CommonServerResult
+ OrderID string `json:"OrderID" xml:"OrderID"` // 唯一标识订单的 ID,由商户生成
+ BizID string `json:"BizID" xml:"BizID"` // 商户 ID
+ BizPwd string `json:"BizPwd" xml:"BizPwd"` // 商户密码
+ ShopAppID string `json:"ShopAppID" xml:"ShopAppID"` // 商户的小程序 AppID
+ WayBillID string `json:"WayBillID" xml:"WayBillID"` // 运单 ID,从微信号段中生成
+}
+
+// CancelExpressOrderReturn 取消订单事件需要返回的数据
+type CancelExpressOrderReturn struct {
+ CommonServerReturn
+ BizID string `json:"BizID" xml:"BizID"` // 商户ID,请原样返回
+ OrderID string `json:"OrderID" xml:"OrderID"` // 唯一标识订单的ID,由商户生成。请原样返回
+ WayBillID string `json:"WayBillID" xml:"WayBillID"` // 运单ID,请原样返回
+}
+
+// CheckExpressBusinessResult 审核商户事件参数
+type CheckExpressBusinessResult struct {
+ CommonServerResult
+ BizID string `json:"BizID" xml:"BizID"` // 商户ID,即商户在快递注册的客户编码或月结账户名
+ BizPwd string `json:"BizPwd" xml:"BizPwd"` // BizID 对应的密码
+ ShopAppID string `json:"ShopAppID" xml:"ShopAppID"` // 商户的小程序 AppID
+ ShopName string `json:"ShopName" xml:"ShopName"` // 商户名称,即小程序昵称(仅EMS可用)
+ ShopTelphone string `json:"ShopTelphone" xml:"ShopTelphone"` // 商户联系电话(仅EMS可用)
+ ShopContact string `json:"ShopContact" xml:"ShopContact"` // 商户联系人姓名(仅EMS可用)
+ ServiceName string `json:"ServiceName" xml:"ServiceName"` // 预开通的服务类型名称(仅EMS可用)
+ SenderAddress string `json:"SenderAddress" xml:"SenderAddress"` // 商户发货地址(仅EMS可用)
+}
+
+// CheckExpressBusinessReturn 审核商户事件需要需要返回的数据
+type CheckExpressBusinessReturn struct {
+ CommonServerReturn
+ BizID string `json:"BizID" xml:"BizID"` // 商户ID
+ Quota float64 `json:"Quota" xml:"Quota"` // 商户可用余额,0 表示无可用余额
+}
+
+// DeliveryOrderStatusUpdateResult 服务器携带的参数
+type DeliveryOrderStatusUpdateResult struct {
+ CommonServerResult
+ ShopID string `json:"shopid" xml:"shopid"` // 商家id, 由配送公司分配的appkey
+ ShopOrderID string `json:"shop_order_id" xml:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ ShopNo string `json:"shop_no" xml:"shop_no"` // 商家门店编号, 在配送公司侧登记
+ WaybillID string `json:"waybill_id" xml:"waybill_id"` // 配送单id
+ ActionTime uint `json:"action_time" xml:"action_time"` // Unix时间戳
+ OrderStatus int `json:"order_status" xml:"order_status"` // 配送状态,枚举值
+ ActionMsg string `json:"action_msg" xml:"action_msg"` // 附加信息
+ Agent struct {
+ Name string `json:"name" xml:"name"` // 骑手姓名
+ Phone string `json:"phone" xml:"phone"` // 骑手电话
+ } `json:"agent" xml:"agent"` // 骑手信息
+}
+
+// DeliveryOrderStatusUpdateReturn 需要返回的数据
+type DeliveryOrderStatusUpdateReturn CommonServerReturn
+
+// AgentPosQueryReturn 需要返回的数据
+type AgentPosQueryReturn struct {
+ CommonServerReturn
+ Lng float64 `json:"lng" xml:"lng"` // 必填 经度,火星坐标,精确到小数点后6位
+ Lat float64 `json:"lat" xml:"lat"` // 必填 纬度,火星坐标,精确到小数点后6位
+ Distance float64 `json:"distance" xml:"distance"` // 必填 和目的地距离,已取货配送中需返回,单位米
+ ReachTime uint `json:"reach_time" xml:"reach_time"` // 必填 预计还剩多久送达时间, 单位秒, 已取货配送中需返回,比如5分钟后送达,填300
+}
+
+// AgentPosQueryResult 服务器携带的参数
+type AgentPosQueryResult struct {
+ CommonServerResult
+ ShopID string `json:"shopid" xml:"shopid"` // 商家id, 由配送公司分配,可以是dev_id或者appkey
+ ShopOrderID string `json:"shop_order_id" xml:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ ShopNo string `json:"shop_no" xml:"shop_no"` // 商家门店编号, 在配送公司侧登记
+ WaybillID string `json:"waybill_id" xml:"waybill_id"` // 配送单id
+}
+
+// AuthInfoGetReturn 需要返回的数据
+type AuthInfoGetReturn struct {
+ CommonServerReturn
+ AppKey string `json:"appkey" xml:"appkey"` // 必填 配送公司分配的appkey,对应shopid
+ Account string `json:"account" xml:"account"` // 必填 帐号名称
+ AccountType uint `json:"account_type" xml:"account_type"` // 必填 帐号类型:0.不确定,1.预充值,2,月结,3,其它
+}
+
+// AuthInfoGetResult 服务器携带的参数
+type AuthInfoGetResult struct {
+ CommonServerResult
+ WxAppID string `json:"wx_appid" xml:"wx_appid"` // 发起授权的商户小程序appid
+ Code string `json:"code" xml:"code"` // 授权码
+}
+
+// CancelAuthReturn 需要返回的数据
+type CancelAuthReturn CommonServerReturn
+
+// CancelAuthResult 服务器携带的参数
+type CancelAuthResult struct {
+ CommonServerResult
+ ShopID string `json:"shopid" xml:"shopid"` // 商家id, 配送公司唯一标识
+ WxAppID string `json:"wx_appid" xml:"wx_appid"` // 发起授权的商户小程序appid
+}
+
+// DeliveryOrderAddReturn 需要返回的数据
+type DeliveryOrderAddReturn struct {
+ CommonServerReturn
+ Event string `json:"Event" xml:"Event"` // 是 事件类型,固定为 transport_add_order,不区分大小写
+ Fee uint `json:"fee" xml:"fee"` // 是 实际运费(单位:元),运费减去优惠券费用
+ Deliverfee uint `json:"deliverfee" xml:"deliverfee"` // 是 运费(单位:元)
+ Couponfee uint `json:"couponfee" xml:"couponfee"` // 是 优惠券费用(单位:元)
+ Tips uint `json:"tips" xml:"tips"` // 是 小费(单位:元)
+ Insurancefee uint `json:"insurancefee" xml:"insurancefee"` // 是 保价费(单位:元)
+ Distance float64 `json:"distance,omitempty" xml:"distance,omitempty"` // 否 配送距离(单位:米)
+ WaybillID string `json:"waybill_id,omitempty" xml:"waybill_id,omitempty"` // 否 配送单号, 可以在API1更新配送单状态异步返回
+ OrderStatus int `json:"order_status" xml:"order_status"` // 是 配送单状态
+ FinishCode uint `json:"finish_code,omitempty" xml:"finish_code,omitempty"` // 否 收货码
+ PickupCode uint `json:"pickup_code,omitempty" xml:"pickup_code,omitempty"` // 否 取货码
+ DispatchDuration uint `json:"dispatch_duration,omitempty" xml:"dispatch_duration,omitempty"` // 否 预计骑手接单时间,单位秒,比如5分钟,就填300, 无法预计填0
+ SenderLng float64 `json:"sender_lng,omitempty" xml:"sender_lng,omitempty"` // 否 发货方经度,火星坐标,精确到小数点后6位, 用于消息通知,如果下单请求里有发货人信息则不需要
+ SenderLat float64 `json:"sender_lat,omitempty" xml:"sender_lat,omitempty"` // 否 发货方纬度,火星坐标,精确到小数点后6位, 用于消息通知,如果下单请求里有发货人信息则不需要
+}
+
+// DeliveryOrderAddResult 服务器携带的参数
+type DeliveryOrderAddResult struct {
+ CommonServerResult
+ WxToken string `json:"wx_token" xml:"wx_token"` // 微信订单 Token。请保存该Token,调用更新配送单状态接口(updateOrder)时需要传入
+ DeliveryToken string `json:"delivery_token" xml:"delivery_token"` // 配送公司侧在预下单时候返回的token,用于保证运费不变
+ ShopID string `json:"shopid" xml:"shopid"` // 商家id, 由配送公司分配的appkey
+ ShopNo string `json:"shop_no" xml:"shop_no"` // 商家门店编号, 在配送公司侧登记
+ ShopOrderID string `json:"shop_order_id" xml:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ DeliverySign string `json:"delivery_sign" xml:"delivery_sign"` // 用配送公司侧提供的appSecret加密的校验串
+ Sender struct {
+ Name string `json:"name" xml:"name"` // 姓名,最长不超过256个字符
+ City string `json:"city" xml:"city"` // 城市名称,如广州市
+ Address string `json:"address" xml:"address"` // 地址(街道、小区、大厦等,用于定位)
+ AddressDetail string `json:"address_detail" xml:"address_detail"` // 地址详情(楼号、单元号、层号)
+ Phone string `json:"phone" xml:"phone"` // 电话/手机号,最长不超过64个字符
+ Lng float64 `json:"lng" xml:"lng"` // 经度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,确到小数点后6位
+ Lat float64 `json:"lat" xml:"lat"` // 纬度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,精确到小数点后6位)
+ CoordinateType uint8 `json:"coordinate_type" xml:"coordinate_type"` // 坐标类型,0:火星坐标(高德,腾讯地图均采用火星坐标) 1:百度坐标
+ } `json:"sender" xml:"sender"` // 发件人信息,如果配送公司能从shopid+shop_no对应到门店地址,则不需要填写,否则需填写
+ Receiver struct {
+ Name string `json:"name" xml:"name"` // 姓名,最长不超过256个字符
+ City string `json:"city" xml:"city"` // 城市名称,如广州市
+ Address string `json:"address" xml:"address"` // 地址(街道、小区、大厦等,用于定位)
+ AddressDetail string `json:"address_detail" xml:"address_detail"` // 地址详情(楼号、单元号、层号)
+ Phone string `json:"phone" xml:"phone"` // 电话/手机号,最长不超过64个字符
+ Lng float64 `json:"lng" xml:"lng"` // 经度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,确到小数点后6位
+ Lat float64 `json:"lat" xml:"lat"` // 纬度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,精确到小数点后6位)
+ CoordinateType uint8 `json:"coordinate_type" xml:"coordinate_type"` // 坐标类型,0:火星坐标(高德,腾讯地图均采用火星坐标) 1:百度坐标
+ } `json:"receiver" xml:"receiver"` // 收件人信息
+ Cargo struct {
+ GoodsValue float64 `json:"goods_value" xml:"goods_value"` // 货物价格,单位为元,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-5000]
+ GoodsHeight float64 `json:"goods_height" xml:"goods_height"` // 货物高度,单位为cm,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-45]
+ GoodsLength float64 `json:"goods_length" xml:"goods_length"` // 货物长度,单位为cm,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-65]
+ GoodsWidth float64 `json:"goods_width" xml:"goods_width"` // 货物宽度,单位为cm,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-50]
+ GoodsWeight float64 `json:"goods_weight" xml:"goods_weight"` // 货物重量,单位为kg,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-50]
+ GoodsDetail struct {
+ Goods []struct {
+ Count uint `json:"good_count" xml:"good_count"` // 货物数量
+ Name string `json:"good_name" xml:"good_name"` // 货品名称
+ Price float64 `json:"good_price" xml:"good_price"` // 货品单价,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数)
+ Unit string `json:"good_unit" xml:"good_unit"` // 货品单位,最长不超过20个字符
+ } `json:"goods" xml:"goods"` // 货物列表
+ } `json:"goods_detail" xml:"goods_detail"` // 货物详情,最长不超过10240个字符
+ GoodsPickupInfo string `json:"goods_pickup_info" xml:"goods_pickup_info"` // 货物取货信息,用于骑手到店取货,最长不超过100个字符
+ GoodsDeliveryInfo string `json:"goods_delivery_info" xml:"goods_delivery_info"` // 货物交付信息,最长不超过100个字符
+ CargoFirstClass string `json:"cargo_first_class" xml:"cargo_first_class"` // 品类一级类目
+ CargoSecondClass string `json:"cargo_second_class" xml:"cargo_second_class"` // 品类二级类目
+ } `json:"cargo" xml:"cargo"` // 货物信息
+ OrderInfo struct {
+ DeliveryServiceCode string `json:"delivery_service_code" xml:"delivery_service_code"` // 配送服务代码 不同配送公司自定义,微信侧不理解
+ OrderType uint8 `json:"order_type" xml:"order_type"` // 订单类型, 0: 即时单 1 预约单,如预约单,需要设置expected_delivery_time或expected_finish_time或expected_pick_time
+ ExpectedDeliveryTime uint `json:"expected_delivery_time" xml:"expected_delivery_time"` // 期望派单时间(达达支持,表示达达系统调度时间),unix-timestamp
+ ExpectedFinishTime uint `json:"expected_finish_time" xml:"expected_finish_time"` // 期望送达时间(美团、顺丰同城急送支持),unix-timestamp)
+ ExpectedPickTime uint `json:"expected_pick_time" xml:"expected_pick_time"` // 期望取件时间(闪送、顺丰同城急送支持,顺丰同城急送只需传expected_finish_time或expected_pick_time其中之一即可,同时都传则以expected_finish_time为准),unix-timestamp
+ PoiSeq string `json:"poi_seq" xml:"poi_seq"` // 门店订单流水号,建议提供,方便骑手门店取货,最长不超过32个字符
+ Note string `json:"note" xml:"note"` // 备注,最长不超过200个字符
+ OrderTime uint `json:"order_time" xml:"order_time"` // 用户下单付款时间
+ IsInsured uint8 `json:"is_insured" xml:"is_insured"` // 是否保价,0,非保价,1.保价
+ DeclaredValue float64 `json:"declared_value" xml:"declared_value"` // 保价金额,单位为元,精确到分
+ Tips float64 `json:"tips" xml:"tips"` // 小费,单位为元, 下单一般不加小费
+ IsDirectDelivery float64 `json:"is_direct_delivery" xml:"is_direct_delivery"` // 是否选择直拿直送(0:不需要;1:需要。选择直拿直送后,同一时间骑手只能配送此订单至完成,配送费用也相应高一些,闪送必须选1,达达可选0或1,其余配送公司不支持直拿直送)
+ CashOnDelivery float64 `json:"cash_on_delivery" xml:"cash_on_delivery"` // 骑手应付金额,单位为元,精确到分
+ CashOnPickup float64 `json:"cash_on_pickup" xml:"cash_on_pickup"` // 骑手应收金额,单位为元,精确到分
+ RiderPickMethod uint8 `json:"rider_pick_method" xml:"rider_pick_method"` // 物流流向,1:从门店取件送至用户;2:从用户取件送至门店
+ IsFinishCodeNeeded uint8 `json:"is_finish_code_needed" xml:"is_finish_code_needed"` // 收货码(0:不需要;1:需要。收货码的作用是:骑手必须输入收货码才能完成订单妥投)
+ IsPickupCodeNeeded uint8 `json:"is_pickup_code_needed" xml:"is_pickup_code_needed"` // 取货码(0:不需要;1:需要。取货码的作用是:骑手必须输入取货码才能从商家取货)
+ } `json:"order_info" xml:"order_info"` // 订单信息
+}
+
+// DeliveryOrderAddTipsReturn 需要返回的数据
+type DeliveryOrderAddTipsReturn CommonServerReturn
+
+// DeliveryOrderAddTipsResult 服务器携带的参数
+type DeliveryOrderAddTipsResult struct {
+ CommonServerResult
+ ShopID string `json:"shopid" xml:"shopid"` // 商家id, 由配送公司分配,可以是dev_id或者appkey
+ ShopOrderID string `json:"shop_order_id" xml:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ ShopNo string `json:"shop_no" xml:"shop_no"` // 商家门店编号, 在配送公司侧登记
+ WaybillID string `json:"waybill_id" xml:"waybill_id"` // 配送单id
+ DeliverySign string `json:"delivery_sign" xml:"delivery_sign"` // 用配送公司侧提供的appSecret加密的校验串
+ Tips float64 `json:"tips" xml:"tips"` // 小费金额(单位:元)
+ Remark string `json:"remark" xml:"remark"` // 备注
+}
+
+// DeliveryOrderCancelReturn 需要返回的数据
+type DeliveryOrderCancelReturn struct {
+ CommonServerReturn
+ DeductFee uint `json:"deduct_fee" xml:"deduct_fee"` // 是 预计扣除的违约金(单位:元),可能没有
+ Desc string `json:"desc" xml:"desc"` // 是 扣费说明
+}
+
+// DeliveryOrderCancelResult 服务器携带的参数
+type DeliveryOrderCancelResult struct {
+ CommonServerResult
+ ShopID string `json:"shopid" xml:"shopid"` // 商家id, 由配送公司分配,可以是dev_id或者appkey
+ ShopOrderID string `json:"shop_order_id" xml:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ ShopNo string `json:"shop_no" xml:"shop_no"` // 商家门店编号, 在配送公司侧登记
+ WaybillID string `json:"waybill_id" xml:"waybill_id"` // 配送单id
+ DeliverySign string `json:"delivery_sign" xml:"delivery_sign"` // 用配送公司侧提供的appSecret加密的校验串
+ CancelReasonID uint `json:"cancel_reason_id" xml:"cancel_reason_id"` // 取消原因id
+ CancelReason string `json:"cancel_reason" xml:"cancel_reason"` // 取消原因
+}
+
+// DeliveryOrderReturnConfirmReturn 需要返回的数据
+type DeliveryOrderReturnConfirmReturn CommonServerReturn
+
+// DeliveryOrderReturnConfirmResult 服务器携带的参数
+type DeliveryOrderReturnConfirmResult struct {
+ CommonServerResult
+ ShopID string `json:"shopid" xml:"shopid"` // 商家id, 由配送公司分配,可以是dev_id或者appkey
+ ShopOrderID string `json:"shop_order_id" xml:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ ShopNo string `json:"shop_no" xml:"shop_no"` // 商家门店编号, 在配送公司侧登记
+ WaybillID string `json:"waybill_id" xml:"waybill_id"` // 配送单id
+ DeliverySign string `json:"delivery_sign" xml:"delivery_sign"` // 用配送公司侧提供的appSecret加密的校验串
+}
+
+// DeliveryOrderPreAddReturn 需要返回的数据
+type DeliveryOrderPreAddReturn struct {
+ CommonServerReturn
+ Fee uint `json:"fee" xml:"fee"` // 是 实际运费(单位:元),运费减去优惠券费用
+ Deliverfee uint `json:"deliverfee" xml:"deliverfee"` // 是 运费(单位:元)
+ Couponfee uint `json:"couponfee" xml:"couponfee"` // 是 优惠券费用(单位:元)
+ Tips float64 `json:"tips" xml:"tips"` // 是 小费(单位:元)
+ Insurancefee uint `json:"insurancefee" xml:"insurancefee"` // 是 保价费(单位:元)
+ Distance uint `json:"distance" xml:"distance"` // 否 配送距离(单位:米)
+ DispatchDuration uint `json:"dispatch_duration" xml:"dispatch_duration"` // 否 预计骑手接单时间,单位秒,比如5分钟,就填300, 无法预计填0
+ DeliveryToken string `json:"delivery_token" xml:"delivery_token"` // 否 配送公司可以返回此字段,当用户下单时候带上这个字段,配送公司可保证在一段时间内运费不变
+}
+
+// DeliveryOrderPreAddResult 服务器携带的参数
+type DeliveryOrderPreAddResult struct {
+ CommonServerResult
+ ShopID string `json:"shopid" xml:"shopid"` // 商家id, 由配送公司分配的appkey
+ ShopNo string `json:"shop_no" xml:"shop_no"` // 商家门店编号, 在配送公司侧登记
+ ShopOrderID string `json:"shop_order_id" xml:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ DeliverySign string `json:"delivery_sign" xml:"delivery_sign"` // 用配送公司侧提供的appSecret加密的校验串
+ Sender struct {
+ Name string `json:"name" xml:"name"` // 姓名,最长不超过256个字符
+ City string `json:"city" xml:"city"` // 城市名称,如广州市
+ Address string `json:"address" xml:"address"` // 地址(街道、小区、大厦等,用于定位)
+ AddressDetail string `json:"address_detail" xml:"address_detail"` // 地址详情(楼号、单元号、层号)
+ Phone string `json:"phone" xml:"phone"` // 电话/手机号,最长不超过64个字符
+ Lng float64 `json:"lng" xml:"lng"` // 经度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,确到小数点后6位
+ Lat float64 `json:"lat" xml:"lat"` // 纬度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,精确到小数点后6位)
+ CoordinateType uint8 `json:"coordinate_type" xml:"coordinate_type"` // 坐标类型,0:火星坐标(高德,腾讯地图均采用火星坐标) 1:百度坐标
+ } `json:"sender" xml:"sender"` // 发件人信息,如果配送公司能从shopid+shop_no对应到门店地址,则不需要填写,否则需填写
+ Receiver struct {
+ Name string `json:"name" xml:"name"` // 姓名,最长不超过256个字符
+ City string `json:"city" xml:"city"` // 城市名称,如广州市
+ Address string `json:"address" xml:"address"` // 地址(街道、小区、大厦等,用于定位)
+ AddressDetail string `json:"address_detail" xml:"address_detail"` // 地址详情(楼号、单元号、层号)
+ Phone string `json:"phone" xml:"phone"` // 电话/手机号,最长不超过64个字符
+ Lng float64 `json:"lng" xml:"lng"` // 经度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,确到小数点后6位
+ Lat float64 `json:"lat" xml:"lat"` // 纬度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,精确到小数点后6位)
+ CoordinateType uint8 `json:"coordinate_type" xml:"coordinate_type"` // 坐标类型,0:火星坐标(高德,腾讯地图均采用火星坐标) 1:百度坐标
+ } `json:"receiver" xml:"receiver"` // 收件人信息
+ Cargo struct {
+ GoodsValue float64 `json:"goods_value" xml:"goods_value"` // 货物价格,单位为元,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-5000]
+ GoodsHeight float64 `json:"goods_height" xml:"goods_height"` // 货物高度,单位为cm,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-45]
+ GoodsLength float64 `json:"goods_length" xml:"goods_length"` // 货物长度,单位为cm,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-65]
+ GoodsWidth float64 `json:"goods_width" xml:"goods_width"` // 货物宽度,单位为cm,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-50]
+ GoodsWeight float64 `json:"goods_weight" xml:"goods_weight"` // 货物重量,单位为kg,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-50]
+ GoodsDetail struct {
+ Goods []struct {
+ Count uint `json:"good_count" xml:"good_count"` // 货物数量
+ Name string `json:"good_name" xml:"good_name"` // 货品名称
+ Price float64 `json:"good_price" xml:"good_price"` // 货品单价,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数)
+ Unit string `json:"good_unit" xml:"good_unit"` // 货品单位,最长不超过20个字符
+ } `json:"goods" xml:"goods"` // 货物列表
+ } `json:"goods_detail" xml:"goods_detail"` // 货物详情,最长不超过10240个字符
+ GoodsPickupInfo string `json:"goods_pickup_info" xml:"goods_pickup_info"` // 货物取货信息,用于骑手到店取货,最长不超过100个字符
+ GoodsDeliveryInfo string `json:"goods_delivery_info" xml:"goods_delivery_info"` // 货物交付信息,最长不超过100个字符
+ CargoFirstClass string `json:"cargo_first_class" xml:"cargo_first_class"` // 品类一级类目
+ CargoSecondClass string `json:"cargo_second_class" xml:"cargo_second_class"` // 品类二级类目
+ } `json:"cargo" xml:"cargo"` // 货物信息
+ OrderInfo struct {
+ DeliveryServiceCode string `json:"delivery_service_code" xml:"delivery_service_code"` // 配送服务代码 不同配送公司自定义,微信侧不理解
+ OrderType uint8 `json:"order_type" xml:"order_type"` // 订单类型, 0: 即时单 1 预约单,如预约单,需要设置expected_delivery_time或expected_finish_time或expected_pick_time
+ ExpectedDeliveryTime uint `json:"expected_delivery_time" xml:"expected_delivery_time"` // 期望派单时间(达达支持,表示达达系统调度时间),unix-timestamp
+ ExpectedFinishTime uint `json:"expected_finish_time" xml:"expected_finish_time"` // 期望送达时间(美团、顺丰同城急送支持),unix-timestamp)
+ ExpectedPickTime uint `json:"expected_pick_time" xml:"expected_pick_time"` // 期望取件时间(闪送、顺丰同城急送支持,顺丰同城急送只需传expected_finish_time或expected_pick_time其中之一即可,同时都传则以expected_finish_time为准),unix-timestamp
+ PoiSeq string `json:"poi_seq" xml:"poi_seq"` // 门店订单流水号,建议提供,方便骑手门店取货,最长不超过32个字符
+ Note string `json:"note" xml:"note"` // 备注,最长不超过200个字符
+ OrderTime uint `json:"order_time" xml:"order_time"` // 用户下单付款时间
+ IsInsured uint8 `json:"is_insured" xml:"is_insured"` // 是否保价,0,非保价,1.保价
+ DeclaredValue float64 `json:"declared_value" xml:"declared_value"` // 保价金额,单位为元,精确到分
+ Tips float64 `json:"tips" xml:"tips"` // 小费,单位为元, 下单一般不加小费
+ IsDirectDelivery float64 `json:"is_direct_delivery" xml:"is_direct_delivery"` // 是否选择直拿直送(0:不需要;1:需要。选择直拿直送后,同一时间骑手只能配送此订单至完成,配送费用也相应高一些,闪送必须选1,达达可选0或1,其余配送公司不支持直拿直送)
+ CashOnDelivery float64 `json:"cash_on_delivery" xml:"cash_on_delivery"` // 骑手应付金额,单位为元,精确到分
+ CashOnPickup float64 `json:"cash_on_pickup" xml:"cash_on_pickup"` // 骑手应收金额,单位为元,精确到分
+ RiderPickMethod uint8 `json:"rider_pick_method" xml:"rider_pick_method"` // 物流流向,1:从门店取件送至用户;2:从用户取件送至门店
+ IsFinishCodeNeeded uint8 `json:"is_finish_code_needed" xml:"is_finish_code_needed"` // 收货码(0:不需要;1:需要。收货码的作用是:骑手必须输入收货码才能完成订单妥投)
+ IsPickupCodeNeeded uint8 `json:"is_pickup_code_needed" xml:"is_pickup_code_needed"` // 取货码(0:不需要;1:需要。取货码的作用是:骑手必须输入取货码才能从商家取货)
+ } `json:"order_info" xml:"order_info"` // 订单信息
+}
+
+// DeliveryOrderPreCancelReturn 需要返回的数据
+type DeliveryOrderPreCancelReturn struct {
+ CommonServerReturn
+ DeductFee uint `json:"deduct_fee" xml:"deduct_fee"` // 是 预计扣除的违约金(单位:元),可能没有
+ Desc string `json:"desc" xml:"desc"` // 是 扣费说明
+}
+
+// DeliveryOrderPreCancelResult 服务器携带的参数
+type DeliveryOrderPreCancelResult struct {
+ CommonServerResult
+ ShopID string `json:"shopid" xml:"shopid"` // 商家id, 由配送公司分配,可以是dev_id或者appkey
+ ShopOrderID string `json:"shop_order_id" xml:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ ShopNo string `json:"shop_no" xml:"shop_no"` // 商家门店编号, 在配送公司侧登记
+ WaybillID string `json:"waybill_id" xml:"waybill_id"` // 配送单id
+ DeliverySign string `json:"delivery_sign" xml:"delivery_sign"` // 用配送公司侧提供的appSecret加密的校验串
+ CancelReasonID uint `json:"cancel_reason_id" xml:"cancel_reason_id"` // 取消原因id
+ CancelReason string `json:"cancel_reason" xml:"cancel_reason"` // 取消原因
+}
+
+// DeliveryOrderQueryReturn 需要返回的数据
+type DeliveryOrderQueryReturn struct {
+ CommonServerReturn
+ OrderStatus float64 `json:"order_status" xml:"order_status"` // 是 当前订单状态,枚举值
+ ActionMsg string `json:"action_msg" xml:"action_msg"` // 否 附加信息
+ WaybillID string `json:"waybill_id" xml:"waybill_id"` // 是 配送单id
+}
+
+// DeliveryOrderQueryResult 服务器携带的参数
+type DeliveryOrderQueryResult struct {
+ CommonServerResult
+ ShopID string `json:"shopid" xml:"shopid"` // 商家id, 由配送公司分配,可以是dev_id或者appkey
+ ShopOrderID string `json:"shop_order_id" xml:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ ShopNo string `json:"shop_no" xml:"shop_no"` // 商家门店编号, 在配送公司侧登记
+ WaybillID string `json:"waybill_id" xml:"waybill_id"` // 配送单id
+ DeliverySign string `json:"delivery_sign" xml:"delivery_sign"` // 用配送公司侧提供的appSecret加密的校验串
+}
+
+// DeliveryOrderReaddReturn 需要返回的数据
+type DeliveryOrderReaddReturn struct {
+ CommonServerReturn
+ Fee uint `json:"fee" xml:"fee"` // 是 实际运费(单位:元),运费减去优惠券费用
+ Deliverfee uint `json:"deliverfee" xml:"deliverfee"` // 是 运费(单位:元)
+ Couponfee uint `json:"couponfee" xml:"couponfee"` // 是 优惠券费用(单位:元)
+ Tips float64 `json:"tips" xml:"tips"` // 是 小费(单位:元)
+ Insurancefee uint `json:"insurancefee" xml:"insurancefee"` // 是 保价费(单位:元)
+ Distance uint `json:"distance" xml:"distance"` // 否 配送距离(单位:米)
+ WaybillID string `json:"waybill_id" xml:"waybill_id"` // 否 配送单号, 可以在API1更新配送单状态异步返回
+ OrderStatus float64 `json:"order_status" xml:"order_status"` // 是 配送单状态
+ FinishCode uint `json:"finish_code" xml:"finish_code"` // 否 收货码
+ PickupCode uint `json:"pickup_code" xml:"pickup_code"` // 否 取货码
+ DispatchDuration uint `json:"dispatch_duration" xml:"dispatch_duration"` // 否 预计骑手接单时间,单位秒,比如5分钟,就填300, 无法预计填0
+ SenderLng float64 `json:"sender_lng" xml:"sender_lng"` // 否 发货方经度,火星坐标,精确到小数点后6位, 用于消息通知,如果下单请求里有发货人信息则不需要
+ SenderLat float64 `json:"sender_lat" xml:"sender_lat"` // 否 发货方纬度,火星坐标,精确到小数点后6位, 用于消息通知,如果下单请求里有发货人信息则不需要
+}
+
+// DeliveryOrderReaddResult 服务器携带的参数
+type DeliveryOrderReaddResult struct {
+ CommonServerResult
+ WxToken string `json:"wx_token" xml:"wx_token"` // 微信订单 Token。请保存该Token,调用更新配送单状态接口(updateOrder)时需要传入
+ DeliveryToken string `json:"delivery_token" xml:"delivery_token"` // 配送公司侧在预下单时候返回的token,用于保证运费不变
+ ShopID string `json:"shopid" xml:"shopid"` // 商家id, 由配送公司分配的appkey
+ ShopNo string `json:"shop_no" xml:"shop_no"` // 商家门店编号, 在配送公司侧登记
+ ShopOrderID string `json:"shop_order_id" xml:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ DeliverySign string `json:"delivery_sign" xml:"delivery_sign"` // 用配送公司侧提供的appSecret加密的校验串
+ Sender struct {
+ Name string `json:"name" xml:"name"` // 姓名,最长不超过256个字符
+ City string `json:"city" xml:"city"` // 城市名称,如广州市
+ Address string `json:"address" xml:"address"` // 地址(街道、小区、大厦等,用于定位)
+ AddressDetail string `json:"address_detail" xml:"address_detail"` // 地址详情(楼号、单元号、层号)
+ Phone string `json:"phone" xml:"phone"` // 电话/手机号,最长不超过64个字符
+ Lng float64 `json:"lng" xml:"lng"` // 经度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,确到小数点后6位
+ Lat float64 `json:"lat" xml:"lat"` // 纬度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,精确到小数点后6位)
+ CoordinateType uint8 `json:"coordinate_type" xml:"coordinate_type"` // 坐标类型,0:火星坐标(高德,腾讯地图均采用火星坐标) 1:百度坐标
+ } `json:"sender" xml:"sender"` // 发件人信息,如果配送公司能从shopid+shop_no对应到门店地址,则不需要填写,否则需填写
+ Receiver struct {
+ Name string `json:"name" xml:"name"` // 姓名,最长不超过256个字符
+ City string `json:"city" xml:"city"` // 城市名称,如广州市
+ Address string `json:"address" xml:"address"` // 地址(街道、小区、大厦等,用于定位)
+ AddressDetail string `json:"address_detail" xml:"address_detail"` // 地址详情(楼号、单元号、层号)
+ Phone string `json:"phone" xml:"phone"` // 电话/手机号,最长不超过64个字符
+ Lng float64 `json:"lng" xml:"lng"` // 经度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,确到小数点后6位
+ Lat float64 `json:"lat" xml:"lat"` // 纬度(火星坐标或百度坐标,和 coordinate_type 字段配合使用,精确到小数点后6位)
+ CoordinateType uint8 `json:"coordinate_type" xml:"coordinate_type"` // 坐标类型,0:火星坐标(高德,腾讯地图均采用火星坐标) 1:百度坐标
+ } `json:"receiver" xml:"receiver"` // 收件人信息
+ Cargo struct {
+ GoodsValue float64 `json:"goods_value" xml:"goods_value"` // 货物价格,单位为元,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-5000]
+ GoodsHeight float64 `json:"goods_height" xml:"goods_height"` // 货物高度,单位为cm,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-45]
+ GoodsLength float64 `json:"goods_length" xml:"goods_length"` // 货物长度,单位为cm,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-65]
+ GoodsWidth float64 `json:"goods_width" xml:"goods_width"` // 货物宽度,单位为cm,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-50]
+ GoodsWeight float64 `json:"goods_weight" xml:"goods_weight"` // 货物重量,单位为kg,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数),范围为(0-50]
+ GoodsDetail struct {
+ Goods []struct {
+ Count uint `json:"good_count" xml:"good_count"` // 货物数量
+ Name string `json:"good_name" xml:"good_name"` // 货品名称
+ Price float64 `json:"good_price" xml:"good_price"` // 货品单价,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数)
+ Unit string `json:"good_unit" xml:"good_unit"` // 货品单位,最长不超过20个字符
+ } `json:"goods" xml:"goods"` // 货物列表
+ } `json:"goods_detail" xml:"goods_detail"` // 货物详情,最长不超过10240个字符
+ GoodsPickupInfo string `json:"goods_pickup_info" xml:"goods_pickup_info"` // 货物取货信息,用于骑手到店取货,最长不超过100个字符
+ GoodsDeliveryInfo string `json:"goods_delivery_info" xml:"goods_delivery_info"` // 货物交付信息,最长不超过100个字符
+ CargoFirstClass string `json:"cargo_first_class" xml:"cargo_first_class"` // 品类一级类目
+ CargoSecondClass string `json:"cargo_second_class" xml:"cargo_second_class"` // 品类二级类目
+ } `json:"cargo" xml:"cargo"` // 货物信息
+ OrderInfo struct {
+ DeliveryServiceCode string `json:"delivery_service_code" xml:"delivery_service_code"` // 配送服务代码 不同配送公司自定义,微信侧不理解
+ OrderType uint8 `json:"order_type" xml:"order_type"` // 订单类型, 0: 即时单 1 预约单,如预约单,需要设置expected_delivery_time或expected_finish_time或expected_pick_time
+ ExpectedDeliveryTime uint `json:"expected_delivery_time" xml:"expected_delivery_time"` // 期望派单时间(达达支持,表示达达系统调度时间),unix-timestamp
+ ExpectedFinishTime uint `json:"expected_finish_time" xml:"expected_finish_time"` // 期望送达时间(美团、顺丰同城急送支持),unix-timestamp)
+ ExpectedPickTime uint `json:"expected_pick_time" xml:"expected_pick_time"` // 期望取件时间(闪送、顺丰同城急送支持,顺丰同城急送只需传expected_finish_time或expected_pick_time其中之一即可,同时都传则以expected_finish_time为准),unix-timestamp
+ PoiSeq string `json:"poi_seq" xml:"poi_seq"` // 门店订单流水号,建议提供,方便骑手门店取货,最长不超过32个字符
+ Note string `json:"note" xml:"note"` // 备注,最长不超过200个字符
+ OrderTime uint `json:"order_time" xml:"order_time"` // 用户下单付款时间
+ IsInsured uint8 `json:"is_insured" xml:"is_insured"` // 是否保价,0,非保价,1.保价
+ DeclaredValue float64 `json:"declared_value" xml:"declared_value"` // 保价金额,单位为元,精确到分
+ Tips float64 `json:"tips" xml:"tips"` // 小费,单位为元, 下单一般不加小费
+ IsDirectDelivery float64 `json:"is_direct_delivery" xml:"is_direct_delivery"` // 是否选择直拿直送(0:不需要;1:需要。选择直拿直送后,同一时间骑手只能配送此订单至完成,配送费用也相应高一些,闪送必须选1,达达可选0或1,其余配送公司不支持直拿直送)
+ CashOnDelivery float64 `json:"cash_on_delivery" xml:"cash_on_delivery"` // 骑手应付金额,单位为元,精确到分
+ CashOnPickup float64 `json:"cash_on_pickup" xml:"cash_on_pickup"` // 骑手应收金额,单位为元,精确到分
+ RiderPickMethod uint8 `json:"rider_pick_method" xml:"rider_pick_method"` // 物流流向,1:从门店取件送至用户;2:从用户取件送至门店
+ IsFinishCodeNeeded uint8 `json:"is_finish_code_needed" xml:"is_finish_code_needed"` // 收货码(0:不需要;1:需要。收货码的作用是:骑手必须输入收货码才能完成订单妥投)
+ IsPickupCodeNeeded uint8 `json:"is_pickup_code_needed" xml:"is_pickup_code_needed"` // 取货码(0:不需要;1:需要。取货码的作用是:骑手必须输入取货码才能从商家取货)
+ } `json:"order_info" xml:"order_info"` // 订单信息
+}
+
+// PreAuthCodeGetReturn 需要返回的数据
+type PreAuthCodeGetReturn struct {
+ CommonServerReturn
+ PreAuthCode string `json:"pre_auth_code" xml:"pre_auth_code"` // 是 预授权码
+}
+
+// PreAuthCodeGetResult 服务器携带的参数
+type PreAuthCodeGetResult struct {
+ CommonServerResult
+ WxAppID string `json:"wx_appid" xml:"wx_appid"` // 发起授权的商户小程序appid
+}
+
+// RiderScoreSetReturn 需要返回的数据
+type RiderScoreSetReturn CommonServerReturn
+
+// RiderScoreSetResult 服务器携带的参数
+type RiderScoreSetResult struct {
+ CommonServerResult
+ ShopID string `json:"shopid" xml:"shopid"` // 商家id, 由配送公司分配,可以是dev_id或者appkey
+ ShopOrderID string `json:"shop_order_id" xml:"shop_order_id"` // 唯一标识订单的 ID,由商户生成
+ ShopNo string `json:"shop_no" xml:"shop_no"` // 商家门店编号, 在配送公司侧登记
+ WaybillID string `json:"waybill_id" xml:"waybill_id"` // 配送单id
+ DeliveryOntimeScore uint `json:"delivery_ontime_score" xml:"delivery_ontime_score"` // 配送准时分数,范围 1 - 5
+ CargoIntactScore uint `json:"cargo_intact_score" xml:"cargo_intact_score"` // 货物完整分数,范围1-5
+ AttitudeScore uint `json:"attitude_score" xml:"attitude_score"` // 服务态度分数 范围1-5
+}
diff --git a/app/lib/weapp/soter.go b/app/lib/weapp/soter.go
new file mode 100644
index 0000000..490ed55
--- /dev/null
+++ b/app/lib/weapp/soter.go
@@ -0,0 +1,41 @@
+package weapp
+
+const (
+ apiVerifySignature = "/cgi-bin/soter/verify_signature"
+)
+
+// VerifySignatureResponse 生物认证秘钥签名验证请求返回数据
+type VerifySignatureResponse struct {
+ CommonError
+ IsOk bool `json:"is_ok"`
+}
+
+// VerifySignature 生物认证秘钥签名验证
+// accessToken 接口调用凭证
+// openID 用户 openid
+// data 通过 wx.startSoterAuthentication 成功回调获得的 resultJSON 字段
+// signature 通过 wx.startSoterAuthentication 成功回调获得的 resultJSONSignature 字段
+func VerifySignature(token, openID, data, signature string) (*VerifySignatureResponse, error) {
+ api := baseURL + apiVerifySignature
+ return verifySignature(api, token, openID, data, signature)
+}
+
+func verifySignature(api, token, openID, data, signature string) (*VerifySignatureResponse, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ params := requestParams{
+ "openid": openID,
+ "json_string": data,
+ "json_signature": signature,
+ }
+
+ res := new(VerifySignatureResponse)
+ if err := postJSON(url, params, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
diff --git a/app/lib/weapp/soter_test.go b/app/lib/weapp/soter_test.go
new file mode 100644
index 0000000..737124b
--- /dev/null
+++ b/app/lib/weapp/soter_test.go
@@ -0,0 +1,67 @@
+package weapp
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+func TestVerifySignature(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiVerifySignature {
+ t.Fatalf("Except to path '%s',get '%s'", apiVerifySignature, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ OpenID string `json:"openid"`
+ JSONString string `json:"json_string"`
+ JSONSignature string `json:"json_signature"`
+ }{}
+
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.OpenID == "" {
+ t.Error("Response column openid can not be empty")
+ }
+
+ if params.JSONString == "" {
+ t.Error("Response column json_string can not be empty")
+ }
+ if params.JSONSignature == "" {
+ t.Error("Response column json_signature can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok",
+ "is_ok": true
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ _, err := verifySignature(ts.URL+apiVerifySignature, "mock-access-token", "mock-open-id", "mock-data", "mock-signature")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/app/lib/weapp/subscribe_message.go b/app/lib/weapp/subscribe_message.go
new file mode 100644
index 0000000..dd8ab45
--- /dev/null
+++ b/app/lib/weapp/subscribe_message.go
@@ -0,0 +1,277 @@
+package weapp
+
+import (
+ "strconv"
+)
+
+const (
+ apiAddTemplate = "/wxaapi/newtmpl/addtemplate"
+ apiDeleteTemplate = "/wxaapi/newtmpl/deltemplate"
+ apiGetTemplateCategory = "/wxaapi/newtmpl/getcategory"
+ apiGetPubTemplateKeyWordsById = "/wxaapi/newtmpl/getpubtemplatekeywords"
+ apiGetPubTemplateTitleList = "/wxaapi/newtmpl/getpubtemplatetitles"
+ apiGetTemplateList = "/wxaapi/newtmpl/gettemplate"
+ apiSendSubscribeMessage = "/cgi-bin/message/subscribe/send"
+)
+
+// AddTemplateResponse 添加模版消息返回数据
+type AddTemplateResponse struct {
+ CommonError
+ Pid string `json:"priTmplId"` // 添加至帐号下的模板id,发送小程序订阅消息时所需
+}
+
+// AddTemplate 组合模板并添加至帐号下的个人模板库
+//
+// token 微信 access_token
+// tid 模板ID
+// desc 服务场景描述,15个字以内
+// keywordIDList 关键词 ID 列表
+func AddTemplate(token, tid, desc string, keywordIDList []int32) (*AddTemplateResponse, error) {
+ api := baseURL + apiAddTemplate
+ return addTemplate(api, token, tid, desc, keywordIDList)
+}
+
+func addTemplate(api, token, tid, desc string, keywordIDList []int32) (*AddTemplateResponse, error) {
+ api, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ params := requestParams{
+ "tid": tid,
+ "kidList": keywordIDList,
+ "sceneDesc": desc,
+ }
+
+ res := new(AddTemplateResponse)
+ err = postJSON(api, params, res)
+ if err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// DeleteTemplate 删除帐号下的某个模板
+//
+// token 微信 access_token
+// pid 模板ID
+func DeleteTemplate(token, pid string) (*CommonError, error) {
+ api := baseURL + apiDeleteTemplate
+ return deleteTemplate(api, token, pid)
+}
+
+func deleteTemplate(api, token, pid string) (*CommonError, error) {
+ api, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ params := requestParams{
+ "priTmplId": pid,
+ }
+
+ res := new(CommonError)
+ err = postJSON(api, params, res)
+ if err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// GetTemplateCategoryResponse 删除帐号下的某个模板返回数据
+type GetTemplateCategoryResponse struct {
+ CommonError
+ Data []struct {
+ ID int `json:"id"` // 类目id,查询公共库模版时需要
+ Name string `json:"name"` // 类目的中文名
+ } `json:"data"` // 类目列表
+}
+
+// GetTemplateCategory 删除帐号下的某个模板
+//
+// token 微信 access_token
+func GetTemplateCategory(token string) (*GetTemplateCategoryResponse, error) {
+ api := baseURL + apiGetTemplateCategory
+ return getTemplateCategory(token, api)
+}
+
+func getTemplateCategory(token, api string) (*GetTemplateCategoryResponse, error) {
+ api, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(GetTemplateCategoryResponse)
+ err = getJSON(api, res)
+ if err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// GetPubTemplateKeyWordsByIdResponse 模板标题下的关键词列表
+type GetPubTemplateKeyWordsByIdResponse struct {
+ CommonError
+ Count int32 `json:"count"` // 模版标题列表总数
+ Data []struct {
+ Kid int `json:"kid"` // 关键词 id,选用模板时需要
+ Name string `json:"name"` // 关键词内容
+ Example string `json:"example"` // 关键词内容对应的示例
+ Rule string `json:"rule"` // 参数类型
+ } `json:"data"` // 关键词列表
+}
+
+// GetPubTemplateKeyWordsById 获取模板标题下的关键词列表
+//
+// token 微信 access_token
+// tid 模板ID
+func GetPubTemplateKeyWordsById(token, tid string) (*GetPubTemplateKeyWordsByIdResponse, error) {
+ api := baseURL + apiGetPubTemplateKeyWordsById
+ return getPubTemplateKeyWordsById(api, token, tid)
+}
+
+func getPubTemplateKeyWordsById(api, token, tid string) (*GetPubTemplateKeyWordsByIdResponse, error) {
+ queries := requestQueries{
+ "access_token": token,
+ "tid": tid,
+ }
+ url, err := encodeURL(api, queries)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(GetPubTemplateKeyWordsByIdResponse)
+ if err = getJSON(url, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// GetPubTemplateTitleListResponse 帐号所属类目下的公共模板标题
+type GetPubTemplateTitleListResponse struct {
+ CommonError
+ Count uint `json:"count"` // 模版标题列表总数
+ Data []struct {
+ Tid int `json:"tid"` // 模版标题 id
+ Title string `json:"title"` // 模版标题
+ Type int32 `json:"type"` // 模版类型,2 为一次性订阅,3 为长期订阅
+ CategoryId string `json:"categoryId"` // 模版所属类目 id
+ } `json:"data"` // 模板标题列表
+}
+
+// GetPubTemplateTitleList 获取帐号所属类目下的公共模板标题
+//
+// token 微信 access_token
+// ids 类目 id,多个用逗号隔开
+// start 用于分页,表示从 start 开始。从 0 开始计数。
+// limit 用于分页,表示拉取 limit 条记录。最大为 30
+func GetPubTemplateTitleList(token, ids string, start, limit int) (*GetPubTemplateTitleListResponse, error) {
+ api := baseURL + apiGetPubTemplateTitleList
+ return getPubTemplateTitleList(api, token, ids, start, limit)
+}
+
+func getPubTemplateTitleList(api, token, ids string, start, limit int) (*GetPubTemplateTitleListResponse, error) {
+
+ queries := requestQueries{
+ "access_token": token,
+ "ids": ids,
+ "start": strconv.Itoa(start),
+ "limit": strconv.Itoa(limit),
+ }
+
+ url, err := encodeURL(api, queries)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(GetPubTemplateTitleListResponse)
+ if err := getJSON(url, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// GetTemplateListResponse 获取模板列表返回的数据
+type GetTemplateListResponse struct {
+ CommonError
+ Data []struct {
+ Pid string `json:"priTmplId"` // 添加至帐号下的模板 id,发送小程序订阅消息时所需
+ Title string `json:"title"` // 模版标题
+ Content string `json:"content"` // 模版内容
+ Example string `json:"example"` // 模板内容示例
+ Type int32 `json:"type"` // 模版类型,2 为一次性订阅,3 为长期订阅
+ } `json:"data"` // 个人模板列表
+}
+
+// GetTemplateList 获取帐号下已存在的模板列表
+//
+// token 微信 access_token
+func GetTemplateList(token string) (*GetTemplateListResponse, error) {
+ api := baseURL + apiGetTemplateList
+ return getTemplateList(api, token)
+}
+
+func getTemplateList(api, token string) (*GetTemplateListResponse, error) {
+ url, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(GetTemplateListResponse)
+ if err := getJSON(url, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// SubscribeMessage 订阅消息
+type SubscribeMessage struct {
+ ToUser string `json:"touser"`
+ TemplateID string `json:"template_id"`
+ Page string `json:"page,omitempty"`
+ MiniprogramState MiniprogramState `json:"miniprogram_state,omitempty"`
+ Data SubscribeMessageData `json:"data"`
+}
+
+// MiniprogramState 跳转小程序类型
+type MiniprogramState = string
+
+// developer为开发版;trial为体验版;formal为正式版;默认为正式版
+const (
+ MiniprogramStateDeveloper = "developer"
+ MiniprogramStateTrial = "trial"
+ MiniprogramStateFormal = "formal"
+)
+
+// SubscribeMessageData 订阅消息模板数据
+type SubscribeMessageData map[string]struct {
+ Value string `json:"value"`
+}
+
+// Send 发送订阅消息
+//
+// token access_token
+func (sm *SubscribeMessage) Send(token string) (*CommonError, error) {
+ api := baseURL + apiSendSubscribeMessage
+ return sm.send(api, token)
+}
+
+func (sm *SubscribeMessage) send(api, token string) (*CommonError, error) {
+ api, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := &CommonError{}
+ if err := postJSON(api, sm, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
diff --git a/app/lib/weapp/subscribe_message_test.go b/app/lib/weapp/subscribe_message_test.go
new file mode 100644
index 0000000..0662999
--- /dev/null
+++ b/app/lib/weapp/subscribe_message_test.go
@@ -0,0 +1,75 @@
+package weapp
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+func TestSendSubscribeMessage(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != "/cgi-bin/message/subscribe/send" {
+ t.Fatalf("Except path '/cgi-bin/message/subscribe/send' got '%s'", path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ ToUser string `json:"touser"` // 用户 openid
+
+ TemplateID string `json:"template_id"`
+ Page string `json:"page,omitempty"`
+ Data map[string]struct {
+ Value string `json:"value"`
+ } `json:"data"`
+ }{}
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.ToUser == "" {
+ t.Fatal("param touser can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ sender := SubscribeMessage{
+ ToUser: "mock-open-id",
+ TemplateID: "mock-template-id",
+ Page: "mock-page",
+ MiniprogramState: MiniprogramStateDeveloper,
+ Data: SubscribeMessageData{
+ "mock01.DATA": {
+ Value: "mock-value",
+ },
+ },
+ }
+
+ _, err := sender.send(ts.URL+apiSendSubscribeMessage, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/app/lib/weapp/title.png b/app/lib/weapp/title.png
new file mode 100644
index 0000000..631f403
Binary files /dev/null and b/app/lib/weapp/title.png differ
diff --git a/app/lib/weapp/uniform_message.go b/app/lib/weapp/uniform_message.go
new file mode 100644
index 0000000..31cdb42
--- /dev/null
+++ b/app/lib/weapp/uniform_message.go
@@ -0,0 +1,73 @@
+package weapp
+
+const (
+ apiSendUniformMessage = "/cgi-bin/message/wxopen/template/uniform_send"
+)
+
+// UniformMsgData 模板消息内容
+type UniformMsgData map[string]UniformMsgKeyword
+
+// UniformMsgKeyword 关键字
+type UniformMsgKeyword struct {
+ Value string `json:"value"`
+ Color string `json:"color,omitempty"`
+}
+
+// UniformWeappTmpMsg 小程序模板消息
+type UniformWeappTmpMsg struct {
+ TemplateID string `json:"template_id"`
+ Page string `json:"page"`
+ FormID string `json:"form_id"`
+ Data UniformMsgData `json:"data"`
+ EmphasisKeyword string `json:"emphasis_keyword,omitempty"`
+}
+
+// UniformMsgMiniprogram 小程序
+type UniformMsgMiniprogram struct {
+ AppID string `json:"appid"`
+ PagePath string `json:"pagepath"`
+}
+
+// UniformMpTmpMsg 公众号模板消息
+type UniformMpTmpMsg struct {
+ AppID string `json:"appid"`
+ TemplateID string `json:"template_id"`
+ URL string `json:"url"`
+ Miniprogram UniformMsgMiniprogram `json:"miniprogram"`
+ Data UniformMsgData `json:"data"`
+}
+
+// Miniprogram 小程序
+type Miniprogram struct {
+ AppID string `json:"appid"`
+ PagePath string `json:"pagepath"`
+}
+
+// UniformMsgSender 统一服务消息
+type UniformMsgSender struct {
+ ToUser string `json:"touser"` // 用户 openid
+ UniformWeappTmpMsg UniformWeappTmpMsg `json:"weapp_template_msg,omitempty"`
+ UniformMpTmpMsg UniformMpTmpMsg `json:"mp_template_msg,omitempty"`
+}
+
+// Send 统一服务消息
+//
+// token access_token
+func (msg *UniformMsgSender) Send(token string) (*CommonError, error) {
+ api := baseURL + apiSendUniformMessage
+ return msg.send(api, token)
+}
+
+func (msg *UniformMsgSender) send(api, token string) (*CommonError, error) {
+ api, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(CommonError)
+ if err := postJSON(api, msg, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
diff --git a/app/lib/weapp/uniform_message_test.go b/app/lib/weapp/uniform_message_test.go
new file mode 100644
index 0000000..2ad80f1
--- /dev/null
+++ b/app/lib/weapp/uniform_message_test.go
@@ -0,0 +1,102 @@
+package weapp
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+func TestSendUniformMessage(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ path := r.URL.EscapedPath()
+ if path != apiSendUniformMessage {
+ t.Fatalf("Except to path '%s',get '%s'", apiSendUniformMessage, path)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ ToUser string `json:"touser"` // 用户 openid
+
+ UniformWeappTmpMsg struct {
+ TemplateID string `json:"template_id"`
+ Page string `json:"page"`
+ FormID string `json:"form_id"`
+ Data map[string]struct {
+ Value string `json:"value"`
+ } `json:"data"`
+ EmphasisKeyword string `json:"emphasis_keyword"`
+ } `json:"weapp_template_msg"`
+ UniformMpTmpMsg struct {
+ AppID string `json:"appid"`
+ TemplateID string `json:"template_id"`
+ URL string `json:"url"`
+ Miniprogram struct {
+ AppID string `json:"appid"`
+ PagePath string `json:"pagepath"`
+ } `json:"miniprogram"`
+ Data map[string]struct {
+ Value string `json:"value"`
+ Color string `json:"color,omitempty"`
+ } `json:"data"`
+ } `json:"mp_template_msg"`
+ }{}
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.ToUser == "" {
+ t.Fatal("param touser can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ sender := UniformMsgSender{
+ ToUser: "mock-open-id",
+ UniformWeappTmpMsg: UniformWeappTmpMsg{
+ TemplateID: "mock-template-id",
+ Page: "mock-page",
+ FormID: "mock-form-id",
+ Data: UniformMsgData{
+ "mock-keyword": UniformMsgKeyword{Value: "mock-value"},
+ },
+ EmphasisKeyword: "mock-keyword.DATA",
+ },
+ UniformMpTmpMsg: UniformMpTmpMsg{
+ AppID: "mock-app-id",
+ TemplateID: "mock-template-id",
+ URL: "mock-url",
+ Miniprogram: UniformMsgMiniprogram{"mock-miniprogram-app-id", "mock-page-path"},
+ Data: UniformMsgData{
+ "mock-keyword": UniformMsgKeyword{"mock-value", "mock-color"},
+ },
+ },
+ }
+
+ _, err := sender.send(ts.URL+apiSendUniformMessage, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/app/lib/weapp/updatable_message.go b/app/lib/weapp/updatable_message.go
new file mode 100644
index 0000000..d6016fa
--- /dev/null
+++ b/app/lib/weapp/updatable_message.go
@@ -0,0 +1,99 @@
+package weapp
+
+const (
+ apiCreateActivityID = "/cgi-bin/message/wxopen/activityid/create"
+ apiSetUpdatableMsg = "/cgi-bin/message/wxopen/updatablemsg/send"
+)
+
+// CreateActivityIDResponse 动态消息
+type CreateActivityIDResponse struct {
+ CommonError
+ ActivityID string `json:"activity_id"` // 动态消息的 ID
+ ExpirationTime uint `json:"expiration_time"` // activity_id 的过期时间戳。默认24小时后过期。
+}
+
+// CreateActivityID 创建被分享动态消息的 activity_id。
+// token 接口调用凭证
+func CreateActivityID(token string) (*CreateActivityIDResponse, error) {
+ api := baseURL + apiCreateActivityID
+ return createActivityID(api, token)
+}
+
+func createActivityID(api, token string) (*CreateActivityIDResponse, error) {
+ api, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(CreateActivityIDResponse)
+ if err := getJSON(api, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+// UpdatableMsgTempInfo 动态消息对应的模板信息
+type UpdatableMsgTempInfo struct {
+ ParameterList []UpdatableMsgParameter `json:"parameter_list"` // 模板中需要修改的参数列表
+}
+
+// UpdatableMsgParameter 参数
+// member_count target_state = 0 时必填,文字内容模板中 member_count 的值
+// room_limit target_state = 0 时必填,文字内容模板中 room_limit 的值
+// path target_state = 1 时必填,点击「进入」启动小程序时使用的路径。
+// 对于小游戏,没有页面的概念,可以用于传递查询字符串(query),如 "?foo=bar"
+// version_type target_state = 1 时必填,点击「进入」启动小程序时使用的版本。
+// 有效参数值为:develop(开发版),trial(体验版),release(正式版)
+type UpdatableMsgParameter struct {
+ Name UpdatableMsgParamName `json:"name"` // 要修改的参数名
+ Value string `json:"value"` // 修改后的参数值
+}
+
+// UpdatableMsgTargetState 动态消息修改后的状态
+type UpdatableMsgTargetState = uint8
+
+// 动态消息状态
+const (
+ UpdatableMsgJoining UpdatableMsgTargetState = iota // 未开始
+ UpdatableMsgStarted // 已开始
+)
+
+// UpdatableMsgParamName 参数 name 的合法值
+type UpdatableMsgParamName = string
+
+// 动态消息状态
+const (
+ UpdatableMsgParamMemberCount UpdatableMsgParamName = "member_count" // target_state = 0 时必填,文字内容模板中 member_count 的值
+ UpdatableMsgParamRoomLimit = "room_limit" // target_state = 0 时必填,文字内容模板中 room_limit 的值
+ UpdatableMsgParamPath = "path" // target_state = 1 时必填,点击「进入」启动小程序时使用的路径。 对于小游戏,没有页面的概念,可以用于传递查询字符串(query),如 "?foo=bar"
+ UpdatableMsgParamVersionType = "version_type" // target_state = 1 时必填,点击「进入」启动小程序时使用的版本。有效参数值为:develop(开发版),trial(体验版),release(正式版)
+)
+
+// UpdatableMsgSetter 动态消息
+type UpdatableMsgSetter struct {
+ ActivityID string `json:"activity_id"` // 动态消息的 ID,通过 updatableMessage.createActivityId 接口获取
+ TargetState UpdatableMsgTargetState `json:"target_state"` // 动态消息修改后的状态(具体含义见后文)
+ TemplateInfo UpdatableMsgTempInfo `json:"template_info"` // 动态消息对应的模板信息
+}
+
+// Set 修改被分享的动态消息。
+// accessToken 接口调用凭证
+func (setter *UpdatableMsgSetter) Set(token string) (*CommonError, error) {
+ api := baseURL + apiSetUpdatableMsg
+ return setter.set(api, token)
+}
+
+func (setter *UpdatableMsgSetter) set(api, token string) (*CommonError, error) {
+ api, err := tokenAPI(api, token)
+ if err != nil {
+ return nil, err
+ }
+
+ res := new(CommonError)
+ if err := postJSON(api, setter, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
diff --git a/app/lib/weapp/updatable_message_test.go b/app/lib/weapp/updatable_message_test.go
new file mode 100644
index 0000000..3e200d3
--- /dev/null
+++ b/app/lib/weapp/updatable_message_test.go
@@ -0,0 +1,129 @@
+package weapp
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+func TestCreateActivityID(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "GET" {
+ t.Fatalf("Expect 'GET' get '%s'", r.Method)
+ }
+
+ realPath := r.URL.EscapedPath()
+ expectPath := "/cgi-bin/message/wxopen/activityid/create"
+ if realPath != expectPath {
+ t.Fatalf("Expect to path '%s',get '%s'", expectPath, realPath)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "expiration_time": 1000,
+ "activity_id": "ok",
+ "errcode": 0,
+ "errmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ res, err := createActivityID(ts.URL+apiCreateActivityID, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if res.ActivityID == "" {
+ t.Error("Response column activity_id can not be empty")
+ }
+
+ if res.ExpirationTime == 0 {
+ t.Error("Response column expiration_time can not be zero")
+ }
+}
+
+func TestSetUpdatableMsg(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ if r.Method != "POST" {
+ t.Fatalf("Expect 'POST' get '%s'", r.Method)
+ }
+
+ realPath := r.URL.EscapedPath()
+ expectPath := "/cgi-bin/message/wxopen/updatablemsg/send"
+ if realPath != expectPath {
+ t.Fatalf("Expect to path '%s',get '%s'", expectPath, realPath)
+ }
+
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+
+ if r.Form.Get("access_token") == "" {
+ t.Fatalf("access_token can not be empty")
+ }
+
+ params := struct {
+ ActivityID string `json:"activity_id"`
+ TargetState uint8 `json:"target_state"`
+ TemplateInfo struct {
+ ParameterList []struct {
+ Name string `json:"name"`
+ Value string `json:"value"`
+ } `json:"parameter_list"`
+ } `json:"template_info"`
+ }{}
+ if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
+ t.Fatal(err)
+ }
+
+ if params.ActivityID == "" {
+ t.Fatal("param activity_id can not be empty")
+ }
+
+ if len(params.TemplateInfo.ParameterList) == 0 {
+ t.Fatal("param template_info.parameter_list can not be empty")
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ raw := `{
+ "errcode": 0,
+ "errmsg": "ok"
+ }`
+ if _, err := w.Write([]byte(raw)); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer ts.Close()
+
+ setter := UpdatableMsgSetter{
+ "mock-activity-id",
+ UpdatableMsgJoining,
+ UpdatableMsgTempInfo{
+ []UpdatableMsgParameter{
+ {UpdatableMsgParamMemberCount, "mock-parameter-value-number"},
+ {UpdatableMsgParamRoomLimit, "mock-parameter-value-number"},
+ },
+ },
+ }
+
+ _, err := setter.set(ts.URL+apiSetUpdatableMsg, "mock-access-token")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/app/lib/weapp/util.go b/app/lib/weapp/util.go
new file mode 100644
index 0000000..edf9b46
--- /dev/null
+++ b/app/lib/weapp/util.go
@@ -0,0 +1,149 @@
+package weapp
+
+import (
+ "bytes"
+ "crypto/tls"
+ "encoding/json"
+ "io"
+ "log"
+ "math/rand"
+ "mime/multipart"
+ "net/http"
+ "net/url"
+ "os"
+ "time"
+)
+
+// tokenAPI 获取带 token 的 API 地址
+func tokenAPI(api, token string) (string, error) {
+ queries := requestQueries{
+ "access_token": token,
+ }
+
+ return encodeURL(api, queries)
+}
+
+// encodeURL add and encode parameters.
+func encodeURL(api string, params requestQueries) (string, error) {
+ url, err := url.Parse(api)
+ if err != nil {
+ return "", err
+ }
+
+ query := url.Query()
+
+ for k, v := range params {
+ query.Set(k, v)
+ }
+
+ url.RawQuery = query.Encode()
+
+ return url.String(), nil
+}
+
+// randomString random string generator
+//
+// ln length of return string
+func randomString(ln int) string {
+ letters := []rune("1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
+ b := make([]rune, ln)
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+ for i := range b {
+ b[i] = letters[r.Intn(len(letters))]
+ }
+
+ return string(b)
+}
+
+// postJSON perform a HTTP/POST request with json body
+func postJSON(url string, params interface{}, response interface{}) error {
+ resp, err := postJSONWithBody(url, params)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return json.NewDecoder(resp.Body).Decode(response)
+}
+
+func getJSON(url string, response interface{}) error {
+ resp, err := httpClient().Get(url)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ return json.NewDecoder(resp.Body).Decode(response)
+}
+
+// postJSONWithBody return with http body.
+func postJSONWithBody(url string, params interface{}) (*http.Response, error) {
+ b := &bytes.Buffer{}
+ if params != nil {
+ enc := json.NewEncoder(b)
+ enc.SetEscapeHTML(false)
+ err := enc.Encode(params)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return httpClient().Post(url, "application/json; charset=utf-8", b)
+}
+
+func postFormByFile(url, field, filename string, response interface{}) error {
+ // Add your media file
+ file, err := os.Open(filename)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ return postForm(url, field, filename, file, response)
+}
+
+func postForm(url, field, filename string, reader io.Reader, response interface{}) error {
+ // Prepare a form that you will submit to that URL.
+ buf := new(bytes.Buffer)
+ w := multipart.NewWriter(buf)
+ fw, err := w.CreateFormFile(field, filename)
+ if err != nil {
+ return err
+ }
+
+ if _, err = io.Copy(fw, reader); err != nil {
+ return err
+ }
+
+ // Don't forget to close the multipart writer.
+ // If you don't close it, your request will be missing the terminating boundary.
+ w.Close()
+
+ // Now that you have a form, you can submit it to your handler.
+ req, err := http.NewRequest("POST", url, buf)
+ if err != nil {
+ return err
+ }
+ // Don't forget to set the content type, this will contain the boundary.
+ req.Header.Set("Content-Type", w.FormDataContentType())
+
+ // Submit the request
+ client := httpClient()
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return json.NewDecoder(resp.Body).Decode(response)
+}
+
+func httpClient() *http.Client {
+ log.Print("myweapp use http")
+ return &http.Client{
+ Timeout: 10 * time.Second,
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+ },
+ }
+}
diff --git a/app/lib/weapp/weapp.go b/app/lib/weapp/weapp.go
new file mode 100644
index 0000000..5577192
--- /dev/null
+++ b/app/lib/weapp/weapp.go
@@ -0,0 +1,12 @@
+package weapp
+
+const (
+ // baseURL 微信请求基础URL
+ baseURL = "https://api.weixin.qq.com"
+)
+
+// POST 参数
+type requestParams map[string]interface{}
+
+// URL 参数
+type requestQueries map[string]string
diff --git a/app/lib/wechat/.gitignore b/app/lib/wechat/.gitignore
new file mode 100644
index 0000000..c96a04f
--- /dev/null
+++ b/app/lib/wechat/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
\ No newline at end of file
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..275623d
--- /dev/null
+++ b/app/lib/wxpay/api.go
@@ -0,0 +1,303 @@
+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 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_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")*/
+ // 预下单
+ 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
+ }
+ 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
+ }
+ fmt.Println(wxRsp)
+ 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/alipay.go b/app/md/alipay.go
new file mode 100644
index 0000000..0eafa67
--- /dev/null
+++ b/app/md/alipay.go
@@ -0,0 +1,69 @@
+package md
+
+// AliPayCallback 支付宝的回调结构体
+type AliPayCallback struct {
+ AppID string `json:"app_id"`
+ AuthAppID string `json:"auth_app_id"`
+ BuyerID string `json:"buyer_id"`
+ BuyerLogonID string `json:"buyer_logon_id"`
+ BuyerPayAmount string `json:"buyer_pay_amount"`
+ Charset string `json:"charset"`
+ FundBillList string `json:"fund_bill_list"`
+ GmtCreate string `json:"gmt_create"`
+ GmtPayment string `json:"gmt_payment"`
+ InvoiceAmount string `json:"invoice_amount"`
+ OrderType string `json:"order_type"`
+ MasterID string `json:"master_id"`
+ NotifyID string `json:"notify_id"`
+ NotifyTime string `json:"notify_time"`
+ NotifyType string `json:"notify_type"`
+ OutTradeNo string `json:"out_trade_no"`
+ PassbackParams string `json:"passback_params"`
+ PointAmount string `json:"point_amount"`
+ ReceiptAmount string `json:"receipt_amount"`
+ SellerEmail string `json:"seller_email"`
+ SellerID string `json:"seller_id"`
+ Sign string `json:"sign"`
+ SignType string `json:"sign_type"`
+ Subject string `json:"subject"`
+ TotalAmount string `json:"total_amount"`
+ TradeNo string `json:"trade_no"`
+ TradeStatus string `json:"trade_status"`
+ Version string `json:"version"`
+ PayMethod string `json:"pay_method"`
+}
+
+type AliPayPayParams struct {
+ Subject string `json:"subject" binding:"required"`
+ Amount string `json:"amount" binding:"required"`
+ OrderType string `json:"order_type" binding:"required"`
+ OrdId string `json:"ord_id"`
+ Uid string `json:"uid"`
+ Phone string `json:"phone"`
+}
+type PayData struct {
+ PayAppCertSn string `json:"pay_app_cert_sn"`
+ PayAlipayRootCertSn string `json:"pay_alipay_root_cert_sn"`
+ PayAlipayrsaPublicKey string `json:"pay_alipayrsa_public_key"`
+ PayAliUseType string `json:"pay_ali_use_type"`
+ PriKey string `json:"pay_ali_new_private_key"`
+}
+
+type AlipayUserCertdocCertverifyPreconsult struct {
+ AlipayUserCertdocCertverifyPreconsultResponse struct {
+ Code string `json:"code"`
+ Msg string `json:"msg"`
+ VerifyID string `json:"verify_id"`
+ } `json:"alipay_user_certdoc_certverify_preconsult_response"`
+ Sign string `json:"sign"`
+}
+type AlipayUserCertdocCertverifyConsult struct {
+ AlipayUserCertdocCertverifyConsultResponse struct {
+ Code string `json:"code"`
+ FailParams string `json:"fail_params"`
+ FailReason string `json:"fail_reason"`
+ Msg string `json:"msg"`
+ Passed string `json:"passed"`
+ } `json:"alipay_user_certdoc_certverify_consult_response"`
+ Sign string `json:"sign"`
+}
diff --git a/app/md/app_redis_key.go b/app/md/app_redis_key.go
new file mode 100644
index 0000000..3ffa25f
--- /dev/null
+++ b/app/md/app_redis_key.go
@@ -0,0 +1,23 @@
+package md
+
+// 缓存key统一管理, %s格式化为masterId
+const (
+ AppCfgCacheKey = "%s:app_cfg_cache:" // 占位符: masterId, key的第一个字母
+ VirtualCoinCfgCacheKey = "%s:virtual_coin_cfg"
+ PlanRewardCfgCacheKey = "%s:plan_reward_cfg"
+ UnionSetCacheCfg = "%s:union_set_cfg:%s" // 联盟设置缓存key
+
+ UserFinValidUpdateLock = "%s:user_fin_valid_update_lock:%s" // 用户余额更新锁(能拿到锁才能更新余额)
+
+ WithdrawApplyQueueListKey = "withdraw_apply_queue" // 提现队列
+
+ TplBottomNavRedisKey = "%s:tpl_nav_bottom_key:%s" // master_id platform
+
+ SysModByIdRedisKey = "%s:sys_mod_tpl_by_id:%s"
+
+ AppLimiterLock = "%s:ZhiOs_app_limiter_lock:%s" // 限流器锁
+ DealAppLimiterRequestIdPrefix = "%s:ZhiOs_app_comm_limiter_request_id:%s"
+ DealAppNewcomersLimiterRequestIdPrefix = "%s:ZhiOs_app_newcomers_limiter_request_id:%s"
+
+ CfgCacheTime = 60 * 60 * 4
+)
diff --git a/app/md/cfg_key.go b/app/md/cfg_key.go
new file mode 100644
index 0000000..f93828d
--- /dev/null
+++ b/app/md/cfg_key.go
@@ -0,0 +1,282 @@
+package md
+
+// 获取用户的缓存key
+const (
+ KEY_SYS_CFG_CACHE = "sys_cfg_cache"
+ FunctionPermissionCfgCacheKey = "%s:function_permission_cfg"
+ // 文件缓存的key
+ KEY_CFG_FILE_PVD = "file_provider" // 文件供应商
+ KEY_CFG_FILE_BUCKET = "file_bucket"
+ KEY_CFG_FILE_REGION = "file_bucket_region"
+ KEY_CFG_FILE_HOST = "file_bucket_host"
+ KEY_CFG_FILE_SCHEME = "file_bucket_scheme"
+ KEY_CFG_FILE_AK = "file_access_key"
+ KEY_CFG_FILE_SK = "file_secret_key"
+ KEY_CFG_FILE_MAX_SIZE = "file_user_upload_max_size"
+ KEY_CFG_FILE_EXT = "file_ext"
+ KEY_CFG_FILE_AVATAR_THUMBNAIL = "file_avatar_thumbnail" // 默认头像缩略图参数,宽高120px,格式webp.
+ // 智盟
+ KEY_CFG_ZM_JD_SITE_ID = "third_zm_jd_site_id" // 智盟京东联盟id
+ KEY_CFG_ZM_WEB_ID = "third_zm_web_id" // 智盟网站ID
+ KEY_CFG_ZM_AK = "third_zm_app_key"
+ KEY_CFG_ZM_SK = "third_zm_app_secret"
+ KEY_CFG_ZM_SMS_AK = "third_zm_sms_ak"
+ KEY_CFG_ZM_SMS_SK = "third_zm_sms_sk"
+ KEY_CFG_APP_NAME = "app_name"
+
+ KEY_CFG_WHITELIST = "api_cfg_whitelist" // API允许的访问的设置白名单
+
+ // 淘宝
+ KEY_CFG_TB_AUTH_AK = "third_taobao_auth_ak"
+ KEY_CFG_TB_AUTH_SK = "third_taobao_auth_sk"
+ KEY_CFG_TB_INVITER_CODE = "third_taobao_auth_inviter_code"
+ KEY_CFG_TB_AK = "third_taobao_ak"
+ KEY_CFG_TB_SK = "third_taobao_sk"
+ KEY_CFG_TB_PID = "third_taobao_pid" // 淘宝推广ID,如:mm_123_456_789,123是联盟ID,456是site_id,789是adzone_id
+ KEY_CFG_TB_SID = "third_taobao_sid" // 淘宝session id ,又称access_token
+
+ // 苏宁
+ KEY_CFG_SN_AK = "third_suning_ak"
+ KEY_CFG_SN_SK = "third_suning_sk"
+
+ KEY_CFG_JD_AK = ""
+ KEY_CFG_JD_SK = ""
+
+ KEY_CFG_KL_AK = "third_kaola_ak"
+ KEY_CFG_KL_SK = "third_kaola_sk"
+
+ KEY_CFG_VIP_AK = ""
+ KEY_CFG_VIP_SK = ""
+
+ // 自动任务配置
+ KEY_CFG_CRON_TB = "cron_order_taobao"
+ KEY_CFG_CRON_TBSETTLEORDER = "cron_order_taobao_settle_order"
+ KEY_CFG_CRON_JD = "cron_order_jd"
+ KEY_CFG_CRON_PDD = "cron_order_pdd"
+ KEY_CFG_CRON_PDD_SUCC = "cron_order_pdd_succ"
+ KEY_CFG_CRON_PDDBYCREATETIME = "cron_order_pdd_by_create_time"
+ KEY_CFG_CRON_PDDBYLOOPTIME = "cron_order_pdd_by_loop_time"
+ KEY_CFG_CRON_PDDBYLOOPMONTHTIME = "cron_order_pdd_by_loop_month_ago_time"
+ KEY_CFG_CRON_JDBYCREATETIME = "cron_order_jd_by_create_time"
+ KEY_CFG_CRON_JDBYSUCCESS = "cron_order_jd_by_success"
+ KEY_CFG_CRON_JDFAILBYCREATETIME = "cron_order_jd_fail_by_create_time"
+ KEY_CFG_CRON_PDDBYAGOTIME = "cron_order_pdd_by_ago_time"
+ KEY_CFG_CRON_PDDBYSTATUS = "cron_order_pdd_by_status"
+ KEY_CFG_CRON_PDDBYSTATUSSUCCESS = "cron_order_pdd_by_status_success"
+ KEY_CFG_CRON_PDDBYSTATUSFAIL = "cron_order_pdd_by_status_fail"
+ KEY_CFG_CRON_JDBYSTATUS = "cron_order_jd_by_status"
+ KEY_CFG_CRON_TBBYAGOTIME = "cron_order_tb_by_ago_time"
+ KEY_CFG_CRON_TBBYPAY = "cron_order_tb_by_pay"
+ KEY_CFG_CRON_TB12 = "cron_order_tb12"
+ KEY_CFG_CRON_TB13 = "cron_order_tb13"
+ KEY_CFG_CRON_TB3 = "cron_order_tb3"
+ KEY_CFG_CRON_TB14 = "cron_order_tb14"
+
+ KEY_CFG_CRON_PDDREFUND = "cron_order_pdd_refund"
+ KEY_CFG_CRON_TBREFUND = "cron_order_tb_refund"
+ KEY_CFG_CRON_WPHREFUND = "cron_order_wph_refund"
+ KEY_CFG_CRON_JDREFUND = "cron_order_jd_refund"
+ KEY_CFG_CRON_SN = "cron_order_suning"
+ KEY_CFG_CRON_VIP = "cron_order_vip"
+ KEY_CFG_CRON_KL = "cron_order_kaola"
+ KEY_CFG_CRON_DUOMAI = "cron_order_duomai"
+ KEY_CFG_CRON_HIS = "cron_order_his" // 迁移到历史订单
+ KEY_CFG_CRON_SETTLE = "cron_order_settle" //结算
+ KEY_CFG_CRON_FREE_SETTLE = "cron_order_free_settle" //结算
+ KEY_CFG_CRON_SECOND_FREE_SETTLE = "cron_order_second_free_settle"
+ KEY_CFG_CRON_THIRD_FREE_SETTLE = "cron_order_third_free_settle"
+ KEY_CFG_CRON_ACQUISTION_SETTLE = "cron_acquistion_settle" // 拉新结算
+ KEY_CFG_CRON_NEW_ACQUISTION_SETTLE = "cron_new_acquistion_settle" // 拉新结算
+ KEY_CFG_CRON_PUBLISHER = "cron_taobao_publisher" // 跟踪淘宝备案信息绑定会员运营id 针对小程序
+ KEY_CFG_CRON_AUTO_UN_FREEZE = "cron_auto_un_freeze"
+ KEY_CFG_CRON_MEITUAN = "cron_order_meituan_fxlm" //美团
+ KEY_CFG_CRON_MEITUANLM = "cron_order_meituan_lm" //美团联盟
+ KEY_CFG_CRON_MEITUANLM_START = "cron_order_meituan_lm_start" //美团联盟
+ KEY_CFG_CRON_ORDER_SUCCESS_CHECK = "cron_order_success_check"
+ KEY_CFG_CRON_MEITUAN_START = "cron_order_meituan_start" //美团联盟
+ KEY_CFG_CRON_STARBUCKS = "cron_order_starbucks" //海威星巴克
+ KEY_CFG_CRON_HWMOVIE = "cron_order_hw_movie" //海威电影票
+ KEY_CFG_CRON_MCDONALD = "cron_order_mcdonald" //海威麦当劳
+ KEY_CFG_CRON_NAYUKI = "cron_order_nayuki" //海威奈雪
+ KEY_CFG_CRON_BURGERKING = "cron_order_burger_king" //海威汉堡王
+ KEY_CFG_CRON_HEYTEA = "cron_order_heytea" //海威喜茶
+ KEY_CFG_CRON_TIKTOKLIFE = "cron_order_tik_tok_life" //
+ KEY_CFG_CRON_FAST_REFUND = "cron_order_fast_refund"
+ KEY_CFG_CRON_CHECK_GUIDE_STORE_ORDER = "cron_check_guide_store_order"
+ KEY_CFG_CRON_CHECK_BUCKLE_ORDER = "cron_check_buckle_order"
+ KEY_CFG_CRON_BUCKLE = "cron_order_buckle"
+ KEY_CFG_CRON_FAST_SUCCESS = "cron_order_fast_success"
+ KEY_CFG_CRON_PIZZA = "cron_order_pizza" //海威
+ KEY_CFG_CRON_WALLACE = "cron_order_wallace" //海威
+ KEY_CFG_CRON_TOURISM = "cron_order_tourism" //海威
+ KEY_CFG_CRON_NEAR = "cron_order_near" //海威
+ KEY_CFG_CRON_FLOWERCAKE = "cron_order_flowerCake" //海威
+ KEY_CFG_CRON_DELIVERY = "cron_order_delivery" //海威
+ KEY_CFG_CRON_TO_KFC = "cron_order_to_kfc" //
+ KEY_CFG_CRON_PAGODA = "cron_order_pagoda" //
+ KEY_CFG_CRON_LUCKIN = "cron_order_luckin" //
+ KEY_CFG_CRON_STATIONMEITUANLM = "cron_order_station_meituan_lm" //站长美团联盟
+ KEY_CFG_CRON_MEITUANOFFICIAL = "cron_order_meituan_official" //美团联盟智莺
+ KEY_CFG_CRON_OILSTATION = "cron_order_oilstation" //加油
+ KEY_CFG_CRON_BRIGHTOILSTATION = "cron_order_bright_oilstation" //加油
+ KEY_CFG_CRON_KFC = "cron_order_kfc" //肯德基
+ KEY_CFG_CRON_CINEMA = "cron_order_cinema" //电影票
+ KEY_CFG_CRON_OilRequest = "cron_order_oilrequest" //加入主动请求抓单
+ KEY_CFG_CRON_AGOTB = "cron_order_agotaobao" //n天前的淘宝订单
+ KEY_CFG_CRON_CREDIT_CARD = "cron_order_credit_card"
+ KEY_CFG_CRON_ORDER_STAT = "cron_order_stat" // 订单统计任务
+ KEY_CFG_CRON_CARD_UPDATE = "cron_card_update" // 权益卡更新
+ KEY_CFG_CRON_USER_LV_UP_SETTLE = "cron_user_lv_up_settle" //会员费订单结算
+ KEY_CFG_CRON_DUOYOUORD_SETTLE = "cron_duoyou_settle" //会员费订单结算
+ KEY_CFG_CRON_LIANLIAN_SETTLE = "cron_lianlian_settle" //会员费订单结算
+ KEY_CFG_CRON_SWIPE_SETTLE = "cron_swipe_settle"
+ KEY_CFG_CRON_AGGREGATION_RECHARGE_SETTLE = "cron_aggregation_recharge_settle"
+ KEY_CFG_CRON_ACQUISITION_CONDITION = "cron_acquisition_condition"
+ KEY_CFG_CRON_ACQUISITION_CONDITION_BY_LV = "cron_acquisition_condition_by_lv"
+ KEY_CFG_CRON_ACQUISITION_REWARD = "cron_acquisition_reward"
+ KEY_CFG_CRON_PLAYLET_SETTLE = "cron_playlet_settle"
+ KEY_CFG_CRON_TIKTOK_AUTH = "cron_tik_tok_auth"
+ KEY_CFG_CRON_TASKBOX_SETTLE = "cron_task_box_settle" //会员费订单结算
+ KEY_CFG_CRON_PRIVILEGE_CARD_SETTLE = "cron_privilege_card_settle" //权益卡订单结算
+ KEY_CFG_CRON_CARD_RETURN = "cron_card_return" //权益卡退款
+ KEY_CFG_CRON_PUBLISHER_RELATION = "cron_taobao_publisher_relation" //获取淘宝渠道
+ KEY_CFG_CRON_PUBLISHER_RELATION_NEW = "cron_taobao_publisher_relation_new" //获取淘宝渠道
+
+ KEY_CFG_CRON_DTKBRAND = "cron_dtk_brand" //大淘客品牌信息
+ KEY_CFG_CRON_PUBLISHER_RELATION_BIND = "cron_taobao_publisher_relation_bind" //获取淘宝渠道绑定
+ KEY_CFG_CRON_GOODS_SHELF = "cron_goods_shelf" //商品上下架定时任务
+ KEY_CFG_CRON_DIDI_ENERGY = "cron_order_didi_energy" //
+ KEY_CFG_CRON_T3_CAR = "cron_order_t3_car" //
+ KEY_CFG_CRON_DIDI_ONLINE_CAR = "cron_order_didi_online_car" //
+ KEY_CFG_CRON_KING_FLOWER = "cron_order_king_flower" //
+ KEY_CFG_CRON_DIDI_CHAUFFEUR = "cron_order_didi_chauffeur" //
+ KEY_CFG_CRON_PLAYLET_ORDER = "cron_order_playlet_order" //
+ KEY_CFG_CRON_PLAYLET_GOODS = "cron_order_playlet_goods" //
+ KEY_CFG_CRON_CARD_CHECK_RETURN = "cron_card_check_return" //
+ KEY_CFG_CRON_CARD_CHECK_UPDATE = "cron_card_check_update" //
+ KEY_CFG_CRON_DIDI_FREIGHT = "cron_order_didi_freight" //
+ KEY_CFG_CRON_TB_PUNISH_REFUND = "cron_order_tb_punish_refund"
+ KEY_CFG_CRON_TIKTOK = "cron_order_tikTok"
+ KEY_CFG_CRON_ELM = "cron_order_elm"
+ KEY_CFG_CRON_AUTO_ADD_TIKTOK_GOODS = "cron_order_auto_add_tiktok_goods"
+ KEY_CFG_CRON_TIKTOKOwn = "cron_order_tikTokOwn"
+ KEY_CFG_CRON_TIKTOKCsjp = "cron_order_tikTokCsjp"
+ KEY_CFG_CRON_TIKTOKCsjpLive = "cron_order_tikTokCsjpLive"
+ KEY_CFG_CRON_TIKTOKOwnCsjp = "cron_order_tikTokOwnCsjp"
+ KEY_CFG_CRON_TIKTOKOwnCsjpLive = "cron_order_tikTokOwnCsjpLive"
+ KEY_CFG_CRON_TIKTOKOwnCsjpActivity = "cron_order_tikTokOwnCsjpActivity"
+ KEY_CFG_CRON_PlayLet_Total = "cron_playlet_total"
+ KEY_CFG_CRON_TIKTOKOwnCreate = "cron_order_tikTokOwnCreate"
+ KEY_CFG_CRON_KuaishouOwn = "cron_order_kuaishouOwn"
+ KEY_CFG_CRON_KuaishouOwnCreate = "cron_order_kuaishouOwnCreate"
+ KEY_CFG_CRON_TIKTOKOwnACtivity = "cron_order_tikTokOwnActivity"
+ KEY_CFG_CRON_DUOYOUORD = "cron_order_DouYouOrd"
+ KEY_CFG_CRON_TASKBOX = "cron_order_TaskBox"
+ KEY_CFG_CRON_TASKBOXSECOND = "cron_order_TaskBoxSecond"
+ KEY_CFG_CRON_TIKTOKOwnMixH5 = "cron_order_tikTokOwnMixH5"
+
+ KEY_CFG_CRON_TIKTOKLIVE_UPDATE = "cron_order_tikTokLive_update"
+ KEY_CFG_CRON_KUAISHOU = "cron_order_kuaishou"
+ KEY_CFG_CRON_KUAISHOUOFFICIAL = "cron_order_kuaishou_official"
+ KEY_CFG_CRON_KUAISHOUOFFICIALLive = "cron_order_kuaishou_official_live"
+ KEY_CFG_CRON_MEITUANFFICIAL = "cron_order_meituan_official"
+ KEY_CFG_CRON_TIKTOKLIVE = "cron_order_tikTok_live"
+ KEY_CFG_CRON_TIKTOKLIVEOWN = "cron_order_tikTok_live_own"
+ KEY_CFG_CRON_TIKTOKACTIVITY = "cron_order_tikTok_activity"
+ KEY_CFG_CRON_KUAISHOULIVE = "cron_order_kuaishou_live"
+
+ ZhimengCronPlayletVideoOrder = "cron_playlet_video_order" //短剧订单
+ ZhimengCronPlayletAdvOrder = "cron_playlet_adv_order" //短剧广告订单
+ ZhimengCronPlayletVideoOrderYesterDay = "cron_playlet_video_order_yesterday"
+ ZhimengCronPlayletVideoOrderMonth = "cron_playlet_video_order_month"
+ ZhimengCronPlayletAdvOrderYesterDay = "cron_playlet_adv_order_yesterday"
+ ZhimengCronPlayletAdvOrderMonth = "cron_playlet_adv_order_month"
+ ZhimengCronPlayletAdvOrderYesterDayToMoney = "cron_playlet_adv_order_yesterday_to_money"
+ KEY_CFG_TIK_TOK_TEAM_ORDER_PAY = "cron_tik_tok_team_order_pay"
+ KEY_CFG_KUAISHOU_TEAM_ORDER_PAY = "cron_kuaishou_team_order_pay"
+ KEY_CFG_KUAISHOU_TEAM_ORDER_UPDATE = "cron_kuaishou_team_order_update"
+ KEY_CFG_KUAISHOU_AUTH = "cron_kuaishou_auth"
+ KEY_CFG_VERIFY = "cron_verify"
+ KEY_CFG_TIK_TOK_TEAM_ORDER_UPDATE = "cron_tik_tok_team_order_update"
+ KEY_CFG_TIK_TOK_TEAM_USER_BIND_BUYINID = "cron_tik_tok_team_user_bind_buyinid"
+ // 自动任务运行时设置
+ KEY_CFG_CRON_TIME_PIZZA = "crontab_order_time_pizza"
+ KEY_CFG_CRON_TIME_WALLACE = "crontab_order_time_wallace"
+ KEY_CFG_CRON_TIME_TOURISM = "crontab_order_time_tourism"
+ KEY_CFG_CRON_TIME_NEAR = "crontab_order_time_pizza"
+ KEY_CFG_CRON_TIME_FLOWERCAKE = "crontab_order_time_flowerCake"
+ KEY_CFG_CRON_TIME_DELIVERY = "crontab_order_time_delivery"
+ KEY_CFG_CRON_TIME_TIKTOK = "crontab_order_time_tikTok"
+ KEY_CFG_CRON_TIME_ELM = "crontab_order_time_elm"
+ KEY_CFG_CRON_TIME_TIKTOKOwn = "crontab_order_time_tikTokOwn"
+ KEY_CFG_CRON_TIME_TIKTOKOwnCreate = "crontab_order_time_tikTokOwnCreate"
+ KEY_CFG_CRON_TIME_KuaishouOwn = "crontab_order_time_kuaishouOwn"
+ KEY_CFG_CRON_TIME_KuaishouOwnCreate = "crontab_order_time_kuaishouOwnCreate"
+ KEY_CFG_CRON_TIME_TIKTOKOwnActivity = "KEY_CFG_CRON_TIME_TIKTOKOwnActivity"
+ KEY_CFG_CRON_TIME_TIKTOKOwnMix = "KEY_CFG_CRON_TIME_TIKTOKOwnMix"
+ KEY_CFG_CRON_TIME_TIKTOKOwnLive = "crontab_order_time_tikTokOwnLive"
+ KEY_CFG_CRON_TIME_KUAISHOU = "crontab_order_time_kuaishou"
+ KEY_CFG_CRON_TIME_TIKTOKLIVE = "crontab_order_time_tikTok_live"
+ KEY_CFG_CRON_TIME_KUAISHOULIVE = "crontab_order_time_kuaishou_live"
+ KEY_CFG_CRON_TIME_TB = "crontab_order_time_taobao"
+ KEY_CFG_CRON_TIME_CSJP = "crontab_order_time_csjp"
+ KEY_CFG_CRON_TIME_KUAISHOU_OFFICIAL = "crontab_order_time_kuaishou_official"
+ KEY_CFG_CRON_TIME_KUAISHOU_OFFICIAL_LIVE = "crontab_order_time_kuaishou_official_live"
+ KEY_CFG_CRON_TIME_MEITUAN_OFFICIAL = "crontab_order_time_meituan_official"
+ KEY_CFG_CRON_TIME_CSJP_Live = "crontab_order_time_csjp_live"
+ KEY_CFG_CRON_TIME_OWN_CSJP = "crontab_order_time_own_csjp"
+ KEY_CFG_CRON_TIME_TIKTOK_TEAM_ORDER = "crontab_order_time_tiktok_team_order"
+ KEY_CFG_CRON_TIME_OWN_CSJP_Live = "crontab_order_time_own_csjp_live"
+ KEY_CFG_CRON_TIME_OWN_CSJP_ACTIVITY = "crontab_order_time_own_csjp_activity"
+ KEY_CFG_CRON_TIME_TBREFUND = "crontab_order_time_taobao_refund"
+ KEY_CFG_CRON_TIME_TBPUNISHREFUND = "crontab_order_time_taobao_punish_refund_new"
+ KEY_CFG_CRON_TIME_JD = "crontab_order_time_jd"
+ KEY_CFG_CRON_TIME_PDD = "crontab_order_time_pdd"
+ KEY_CFG_CRON_TIME_TBBYCREATETIME = "crontab_order_time_tb_by_create_time"
+ KEY_CFG_CRON_TIME_TBBYPAY = "crontab_order_time_tb_by_pay"
+ KEY_CFG_CRON_TIME_TB12 = "crontab_order_time_tb12"
+ KEY_CFG_CRON_TIME_TB13 = "crontab_order_time_tb13"
+ KEY_CFG_CRON_TIME_TB14 = "crontab_order_time_tb14"
+ KEY_CFG_CRON_TIME_TB3 = "crontab_order_time_tb3"
+ KEY_CFG_CRON_TIME_TBBYSETTLE = "crontab_order_time_tb_by_settle"
+ KEY_CFG_CRON_TIME_PDDBYCREATETIME = "crontab_order_time_pdd_by_create_time"
+ KEY_CFG_CRON_TIME_JDBYCREATETIME = "crontab_order_time_jd_by_create_time"
+ KEY_CFG_CRON_TIME_JDBYSUCCESS = "crontab_order_time_jd_by_success"
+ KEY_CFG_CRON_TIME_JDFAILBYCREATETIME = "crontab_order_time_jd_fail_by_create_time"
+ KEY_CFG_CRON_TIME_PDDBYAGOTIME = "crontab_order_time_pdd_by_ago_time"
+ KEY_CFG_CRON_TIME_PDDBYSTATUSSUCCESS = "crontab_order_time_pdd_by_status_success"
+ KEY_CFG_CRON_TIME_PDDBYSTATUSFAIL = "crontab_order_time_pdd_by_status_fail"
+ KEY_CFG_CRON_TIME_PDDBYSTATUS = "crontab_order_time_pdd_by_status"
+ KEY_CFG_CRON_TIME_JDBYSTATUS = "crontab_order_time_jd_by_status"
+ KEY_CFG_CRON_TIME_SN = "crontab_order_time_suning"
+ KEY_CFG_CRON_TIME_VIP = "crontab_order_time_vip"
+ KEY_CFG_CRON_TIME_KL = "crontab_order_time_kaola"
+ KEY_CFG_CRON_TIME_DUOMAI = "crontab_order_time_duomai"
+ KEY_CFG_CRON_TIME_PUBLISHER = "crontab_taobao_time_publisher" // 跟踪淘宝备案信息绑定会员运营id 针对小程序
+ KEY_CFG_CRON_TIME_MEITUAN = "crontab_order_time_meituan" //美团
+ KEY_CFG_CRON_TIME_MEITUANLM = "crontab_order_time_meituan_lm" //美团联盟
+ KEY_CFG_CRON_TIME_MEITUANLMSTART = "crontab_order_time_meituan_lm_start" //美团联盟
+ KEY_CFG_CRON_TIME_MEITUANSTART = "crontab_order_time_meituan_start" //美团联盟
+ KEY_CFG_CRON_TIME_STATIONMEITUANLM = "crontab_order_time_station_meituan_lm" //美团联盟
+ KEY_CFG_CRON_TIME_OILSTATION = "crontab_order_time_oilstation" //加油
+ KEY_CFG_CRON_TIME_BRIGHT_OILSTATION = "crontab_order_time_bright_oilstation" //加油
+ KEY_CFG_CRON_TIME_KFC = "crontab_order_time_kfc" //肯德基
+ KEY_CFG_CRON_TIME_CINEMA = "crontab_order_time_cinema" //电影票
+ KEY_CFG_CRON_TIME_STARBUCKS = "crontab_order_time_starbucks" //海威星巴克
+ KEY_CFG_CRON_TIME_MCDONALD = "crontab_order_time_mcdonald" //海威麦当劳
+ KEY_CFG_CRON_TIME_NAYUKI = "crontab_order_time_nayuki" //海威奈雪
+ KEY_CFG_CRON_TIME_BURGERKING = "crontab_order_time_burger_king" //海威汉堡王
+ KEY_CFG_CRON_TIME_HEYTEA = "crontab_order_time_heytea" //海威喜茶
+ KEY_CFG_CRON_TIME_HWMOVIE = "crontab_order_time_hw_movie" //海威电影票
+ KEY_CFG_CRON_TIME_TIKTOKLIFE = "crontab_order_time_tik_tok_life" //海威电影票
+ KEY_CFG_CRON_TIME_PAGODA = "crontab_order_time_pagoda" //
+ KEY_CFG_CRON_TIME_TO_KFC = "crontab_order_time_to_kfc" //
+ KEY_CFG_CRON_TIME_LUCKIN = "crontab_order_time_luckin" //
+ KEY_CFG_CRON_TIME_DIDI_ENERGY = "crontab_order_time_didi_energy" //
+ KEY_CFG_CRON_TIME_T3_CAR = "crontab_order_time_t3_car" //
+ KEY_CFG_CRON_TIME_DIDI_ONLINE_CAR = "crontab_order_time_didi_online_car" //
+ KEY_CFG_CRON_TIME_KING_FLOWER = "crontab_order_time_king_flower" //
+ KEY_CFG_CRON_TIME_DIDI_FREIGHT = "crontab_order_time_didi_freight" //
+ KEY_CFG_CRON_TIME_DIDI_CHAUFFEUR = "crontab_order_time_didi_chauffeur" //
+ KEY_CFG_CRON_USER_RELATE = "cron_user_relate"
+)
diff --git a/app/md/md_order.go b/app/md/md_order.go
new file mode 100644
index 0000000..5ee6618
--- /dev/null
+++ b/app/md/md_order.go
@@ -0,0 +1,47 @@
+package md
+
+type OrderTotal struct {
+ StoreId string `json:"store_id"`
+ BuyPhone string `json:"buy_phone"`
+ MealNum string `json:"meal_num"`
+ Memo string `json:"memo"`
+ CouponId string `json:"coupon_id"`
+ IsNow string `json:"is_now"`
+ Timer string `json:"timer"`
+ GoodsInfo []GoodsInfo `json:"goods_info"`
+}
+type GoodsInfo struct {
+ GoodsId string `json:"goods_id"`
+ SkuId string `json:"sku_id"`
+ Num string `json:"num"`
+}
+
+type OneSkuPriceInfo struct {
+ Num string `json:"num"`
+ Amount string `json:"amount"`
+ GoodsId int `json:"goods_id"`
+}
+type SkuPriceStruct struct {
+ SkuId int64 `json:"skuId"`
+ Ratio string `json:"ratio"`
+ OneSkuPriceInfo OneSkuPriceInfo `json:"oneSkuPriceInfo"`
+}
+
+// 每一种sku价格结构
+type SkuId2priceInfo map[int64]OneSkuPriceInfo
+
+type SkuId2originPrice map[int64]string
+type CouponList struct {
+ Id string `json:"id"`
+ Title string `json:"title"`
+ Timer string `json:"timer"`
+ Label string `json:"label"`
+ Img string `json:"img"`
+ Content string `json:"content"`
+ IsCanUse string `json:"is_can_use"`
+ NotUseStr string `json:"not_use_str"`
+}
+type Sku struct {
+ Name string `json:"name"`
+ Value string `json:"value"`
+}
diff --git a/app/md/pay.go b/app/md/pay.go
new file mode 100644
index 0000000..4994c8f
--- /dev/null
+++ b/app/md/pay.go
@@ -0,0 +1,60 @@
+package md
+
+const (
+ CALLBACK_URL = "%s/api/v1/communityTeam/pay/callback?master_id=%s&order_type=%s&pay_method=%s"
+ RECHARGGE_CALLBACK_URL = "http://%s/api/v1/new_recharge/callback/%s"
+
+ BALANCE_PAY = "balance_pay"
+ ALIPAY = "alipay"
+ WX_PAY = "wxpay"
+ FB_PAY_ALI = "fb_pay_ali"
+ FB_PAY_WX = "fb_pay_wx"
+ FB_PAY_WX_SUB = "fb_pay_wx_sub"
+ FBCALLBACK_URL = "%s/api/v1/pay/fb/callback"
+)
+
+var PayMethod = map[string]string{
+ BALANCE_PAY: "余额支付",
+ ALIPAY: "支付宝支付",
+ WX_PAY: "微信支付",
+ FB_PAY_ALI: "乐刷支付宝",
+ FB_PAY_WX: "乐刷微信",
+ FB_PAY_WX_SUB: "乐刷微信",
+}
+var PayMethodIdToName = map[int]string{
+ 1: "余额支付",
+ 2: "支付宝支付",
+ 3: "微信支付",
+ 18: "乐刷支付宝",
+ 19: "乐刷微信",
+}
+var PayMethodIDs = map[string]int{
+ BALANCE_PAY: 1,
+ ALIPAY: 2,
+ WX_PAY: 3,
+ FB_PAY_ALI: 18,
+ FB_PAY_WX: 19,
+ FB_PAY_WX_SUB: 19,
+}
+
+const (
+ CommunityTeam = "community_team"
+ PrivilegeCard = "privilege_card"
+ LianlianPay = "lianlian_pay"
+ BusinessCollege = "business_college"
+ UserLevel = "user_level"
+ PrivilegeOpenCard = "privilege_open_card"
+ BusinessCollegeSub = "business_college_sub"
+ UserLevelSub = "user_level_sub"
+ PrivilegeOpenCardSub = "privilege_open_card_sub"
+ AggregationRecharge = "aggregation_recharge"
+ Swipe = "swipe"
+)
+
+var NeedPayPart = map[string]string{
+ PrivilegeCard: "权益卡",
+ BusinessCollege: "商学院",
+ UserLevel: "会员VIP升级",
+ PrivilegeOpenCard: "权益卡开卡",
+ AggregationRecharge: "聚合充值",
+}
diff --git a/app/md/platform.go b/app/md/platform.go
new file mode 100644
index 0000000..b479ab5
--- /dev/null
+++ b/app/md/platform.go
@@ -0,0 +1,40 @@
+package md
+
+const (
+ /*********** DEVICE ***********/
+ PLATFORM_WX_APPLET = "wx_applet" // 小程序
+ PLATFORM_TOUTIAO_APPLET = "toutiao_applet"
+ PLATFORM_TIKTOK_APPLET = "tiktok_applet"
+ PLATFORM_BAIDU_APPLET = "baidu_applet"
+ PLATFORM_ALIPAY_APPLET = "alipay_applet"
+ PLATFORM_WAP = "wap" //h5
+ PLATFORM_ANDROID = "android"
+ PLATFORM_IOS = "ios"
+ PLATFORM_PC = "pc"
+ PLATFORM_JSAPI = "jsapi" // 公众号
+)
+
+const WX_PAY_BROWSER = "wx_pay_browser" // 用于判断显示支付方式
+
+var PlatformList = map[string]struct{}{
+ PLATFORM_WX_APPLET: {},
+ PLATFORM_TOUTIAO_APPLET: {},
+ PLATFORM_TIKTOK_APPLET: {},
+ PLATFORM_BAIDU_APPLET: {},
+ PLATFORM_ALIPAY_APPLET: {},
+ PLATFORM_WAP: {},
+ PLATFORM_ANDROID: {},
+ PLATFORM_IOS: {},
+ PLATFORM_PC: {},
+}
+
+var PlatformMap = map[string]string{
+ "android": "2",
+ "ios": "2",
+ "wap": "4", // 和小程序公用模板
+ "wx_applet": "4", //微信小程序
+ "tiktok_applet": "4",
+ "baidu_applet": "4",
+ "alipay_applet": "4",
+ "toutiao_applet": "4",
+}
diff --git a/app/md/user_info.go b/app/md/user_info.go
new file mode 100644
index 0000000..f6c5648
--- /dev/null
+++ b/app/md/user_info.go
@@ -0,0 +1,43 @@
+package md
+
+import (
+ "applet/app/db/model"
+ "applet/app/lib/arkid"
+)
+
+type UserInfoResponse struct {
+ Avatar string `json:"avatar"`
+ NickName string `json:"nickname"`
+ Gender string `json:"gender"`
+ Birthday string `json:"birthday"`
+ RegisterTime string `json:"register_time"`
+ FileBucketURL string `json:"file_bucket_url"`
+ FileFormat string `json:"file_format"`
+ IsNoChange string `json:"is_no_change"`
+ IsUpLoadWx string `json:"is_upload_wx"`
+ IsShowDelUserBtn string `json:"is_show_del_user_btn"`
+ IsShowQq string `json:"is_show_qq"`
+ IsShowSalePhone string `json:"is_show_sale_phone"`
+ IsShowWallet string `json:"is_show_wallet"`
+ IsShowBankCard string `json:"is_show_bank_card"`
+ Qq string `json:"qq"`
+ SalePhone string `json:"sale_phone"`
+ LevelName string `json:"level_name"`
+ Phone string `json:"phone"`
+}
+
+type User struct {
+ Ark *arkid.ArkIDUser
+ Info *model.User
+ Profile *model.UserProfile
+ Level *model.UserLevel
+ Tags []string
+}
+
+type UserRelation struct {
+ Uid int
+ CurUid int
+ Diff int // 与当前用户级别差
+ Level int // 用户当前等级
+ OldDiff int // 旧的级别
+}
diff --git a/app/md/wxpay.go b/app/md/wxpay.go
new file mode 100644
index 0000000..88a9f8d
--- /dev/null
+++ b/app/md/wxpay.go
@@ -0,0 +1,30 @@
+package md
+
+type WxPayParams struct {
+ Subject string `json:"subject" binding:"required"`
+ Amount string `json:"amount" binding:"required"`
+ OrderType string `json:"order_type" binding:"required"`
+ OrdId string `json:"ord_id"`
+}
+
+type WxPayCallback struct {
+ AppId string `json:"appid"`
+ BankType string `json:"bank_type"`
+ CashFee string `json:"cash_fee"`
+ FeeType string `json:"fee_type"`
+ IsSubscribe string `json:"is_subscribe"`
+ MasterID string `json:"master_id"`
+ MchID string `json:"mch_id"`
+ NonceStr string `json:"nonce_str"`
+ Openid string `json:"openid"`
+ OrderType string `json:"order_type"`
+ OutTradeNo string `json:"out_trade_no"`
+ PayMethod string `json:"pay_method"`
+ ResultCode string `json:"result_code"`
+ ReturnCode string `json:"return_code"`
+ Sign string `json:"sign"`
+ TimeEnd string `json:"time_end"`
+ TotalFee string `json:"total_fee"`
+ TradeType string `json:"trade_type"`
+ TransactionID string `json:"transaction_id"`
+}
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..645dbe3
--- /dev/null
+++ b/app/mw/mw_auth.go
@@ -0,0 +1,72 @@
+package mw
+
+import (
+ "errors"
+
+ "applet/app/db"
+ "applet/app/e"
+ "applet/app/lib/arkid"
+ "applet/app/md"
+ "applet/app/utils"
+
+ "github.com/gin-gonic/gin"
+)
+
+// 检查权限, 签名等等
+func Auth(c *gin.Context) {
+
+ for k, v := range c.Request.Header {
+ c.Set(k, v[0])
+ }
+ token, ok := c.Get("Token")
+ if !ok {
+ e.OutErr(c, e.ERR_UNAUTHORIZED, errors.New("没有找到token"))
+ return
+ }
+ if token == "" {
+ e.OutErr(c, e.ERR_UNAUTHORIZED, errors.New("token 不能为空"))
+ return
+ }
+ tokenStr := utils.AnyToString(token)
+ arkIdSdk := arkid.NewArkID()
+ var err error
+ signUser := &md.User{}
+ arkIdUser := new(arkid.ArkIDUser)
+ if err = arkIdSdk.SelectFunction("arkid_user_info").
+ WithArgs(arkid.RequestBody{Token: tokenStr}).
+ Result(arkIdUser); err != nil {
+ e.OutErr(c, e.ERR_TOKEN_AUTH, err) //token 不存在
+ return
+ }
+ if arkIdUser.Username == "" {
+ e.OutErr(c, e.ERR_UNAUTHORIZED, errors.New("Token error"))
+ return
+ }
+ if err = arkIdSdk.SelectFunction("arkid_login").
+ WithArgs(arkid.RequestBody{Username: arkIdUser.Username, Password: utils.Md5(arkIdUser.Username)}).
+ Result(arkIdUser); err != nil {
+ e.OutErr(c, e.ERR_TOKEN_AUTH, err)
+ return
+ }
+ signUser.Ark = arkIdUser
+ if signUser.Ark == nil {
+ e.OutErr(c, e.ERR_TOKEN_AUTH, errors.New("无效token"))
+ return
+ }
+ signUser.Info, err = db.UserFindByArkidUserName(db.DBs[c.GetString("mid")], arkIdUser.Username)
+ if err != nil {
+ e.OutErr(c, e.ERR_TOKEN_AUTH, err)
+ return
+ }
+ if signUser.Info == nil {
+ e.OutErr(c, e.ERR_TOKEN_AUTH, errors.New("无效token"))
+ return
+ }
+ signUser.Profile, err = db.UserProfileFindByArkID(db.DBs[c.GetString("mid")], utils.IntToStr(arkIdUser.UserID))
+ if err != nil {
+ e.OutErr(c, e.ERR_TOKEN_AUTH, err)
+ return
+ }
+ c.Set("user", signUser)
+ c.Next()
+}
diff --git a/app/mw/mw_auth_jwt.go b/app/mw/mw_auth_jwt.go
new file mode 100644
index 0000000..cd4875b
--- /dev/null
+++ b/app/mw/mw_auth_jwt.go
@@ -0,0 +1,122 @@
+package mw
+
+import (
+ "applet/app/db"
+ "applet/app/db/model"
+ "applet/app/e"
+ "applet/app/lib/auth"
+ "applet/app/md"
+ "applet/app/svc"
+ "applet/app/utils"
+ "applet/app/utils/cache"
+ "applet/app/utils/logx"
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+)
+
+// AuthJWT is jwt middleware
+func AuthJWT(c *gin.Context) {
+ authHeader := c.Request.Header.Get("Authorization")
+ if authHeader == "" {
+ e.OutErr(c, e.ERR_UNAUTHORIZED, errors.New("token 不能为空"))
+ return
+ }
+
+ // 按空格分割
+ parts := strings.SplitN(authHeader, " ", 2)
+ if !(len(parts) == 2 && parts[0] == "Bearer") {
+ e.OutErr(c, e.ERR_TOKEN_FORMAT, errors.New("token 格式不对"))
+ return
+ }
+
+ // parts[1]是token
+ mc, err := utils.ParseToken(parts[1])
+ if err != nil {
+ e.OutErr(c, e.ERR_UNAUTHORIZED, errors.New("token 过期或无效"))
+ return
+ }
+
+ // 获取user
+ u, err := db.UserFindByID(db.DBs[c.GetString("mid")], mc.UID)
+ if err != nil {
+ e.OutErr(c, e.ERR_DB_ORM, err)
+ return
+ }
+ if u == nil {
+ e.OutErr(c, e.ERR_UNAUTHORIZED, errors.New("token 过期或无效"))
+ return
+ }
+
+ // 检验账号是否未激活或被冻结
+ switch u.State {
+ case 0:
+ e.OutErr(c, e.ERR_USER_NO_ACTIVE)
+ return
+ case 2:
+ if c.GetString("mid") == "31585332" {
+ utils.FilePutContents("ERR_USER_IS_BAN", utils.SerializeStr(map[string]interface{}{
+ "token": parts[1],
+ "mc": mc,
+ "user": u,
+ }))
+ }
+ e.OutErr(c, e.ERR_USER_IS_BAN)
+ return
+ }
+
+ // 校验是否和缓存的token一致,只能有一个token 是真实有效
+ key := fmt.Sprintf("%s:token:%s", c.GetString("mid"), u.Username)
+ cjwt, err := cache.GetString(key)
+ fmt.Println("====================token", u.Username, key, cjwt, parts[1])
+ if err != nil {
+ fmt.Println("====================token", err)
+ logx.Warn(err)
+ NOCACHE(c, parts, mc, u, false)
+ return
+ }
+ if parts[1] != cjwt {
+ e.OutErr(c, e.ERR_TOKEN_AUTH, errors.New("token expired"))
+ return
+ }
+ NOCACHE(c, parts, mc, u, true)
+}
+
+func NOCACHE(c *gin.Context, parts []string, mc *auth.JWTUser, u *model.User, isTrue bool) {
+ // 获取user profile
+ up, err := db.UserProfileFindByID(db.DBs[c.GetString("mid")], mc.UID)
+ if err != nil || up == nil {
+ e.OutErr(c, e.ERR_DB_ORM, err)
+ return
+ }
+ if parts[1] != up.ArkidToken && isTrue == false || up.ArkidToken == "" {
+ e.OutErr(c, e.ERR_TOKEN_AUTH, errors.New("token expired"))
+ return
+ }
+ if parts[1] != up.ArkidToken && isTrue {
+ up.ArkidToken = parts[1]
+ db.UserProfileUpdate(svc.MasterDb(c), up.Uid, up, "arkid_token")
+ }
+ if up.AvatarUrl == "" {
+ up.AvatarUrl = c.GetString("appUserDefaultAvatar")
+ }
+ // 获取user 等级
+ ul, err := db.UserLevelByID(db.DBs[c.GetString("mid")], u.Level)
+ if err != nil {
+ e.OutErr(c, e.ERR_DB_ORM, err)
+ return
+ }
+
+ user := &md.User{
+ Info: u,
+ Profile: up,
+ Level: ul,
+ }
+
+ // 将当前请求的username信息保存到请求的上下文c上
+ c.Set("user", user)
+ c.Next() // 后续的处理函数可以用过c.Get("user")来获取当前请求的用户信息
+
+}
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..c10bdb9
--- /dev/null
+++ b/app/mw/mw_change_header.go
@@ -0,0 +1,18 @@
+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..599c512
--- /dev/null
+++ b/app/mw/mw_check_sign.go
@@ -0,0 +1,37 @@
+package mw
+
+import (
+ "applet/app/e"
+ "applet/app/utils"
+ "bytes"
+ "errors"
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "io/ioutil"
+)
+
+// CheckSign is 中间件 用来检查签名
+func CheckSign(c *gin.Context) {
+ if utils.SignCheck(c) == false {
+ e.OutErr(c, 400, errors.New("请求失败~~"))
+ return
+ }
+ c.Next()
+}
+
+func CheckBody(c *gin.Context) {
+ c.Set("api_version", "1")
+ if utils.GetApiVersion(c) > 0 {
+ body, _ := ioutil.ReadAll(c.Request.Body)
+ fmt.Println("check_", c.GetString("mid"), string(body))
+ if string(body) != "" {
+ str := utils.ResultAesDecrypt(c, string(body))
+ fmt.Println("check_de", c.GetString("mid"), str)
+ if str != "" {
+ c.Set("body_str", str)
+ c.Request.Body = ioutil.NopCloser(bytes.NewBuffer([]byte(str)))
+ }
+ }
+ }
+ c.Next()
+}
diff --git a/app/mw/mw_checker.go b/app/mw/mw_checker.go
new file mode 100644
index 0000000..44ee434
--- /dev/null
+++ b/app/mw/mw_checker.go
@@ -0,0 +1,22 @@
+package mw
+
+import (
+ "strings"
+
+ "github.com/gin-gonic/gin"
+
+ "applet/app/e"
+ "applet/app/md"
+)
+
+// 检查设备等, 把头部信息下放到hdl可以获取
+func Checker(c *gin.Context) {
+ // 校验平台支持
+ platform := strings.ToLower(c.GetHeader("Platform"))
+ //fmt.Println(platform)
+ if _, ok := md.PlatformList[platform]; !ok {
+ e.OutErr(c, e.ERR_PLATFORM)
+ return
+ }
+ 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..6e2d41f
--- /dev/null
+++ b/app/mw/mw_db.go
@@ -0,0 +1,149 @@
+package mw
+
+import (
+ "applet/app/svc"
+ "applet/app/utils"
+ "code.fnuoos.com/go_rely_warehouse/zyos_go_order_relate_rule.git/rule/mw"
+ "errors"
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "strings"
+
+ "applet/app/db"
+ "applet/app/e"
+ "applet/app/md"
+)
+
+// DB is 中间件 用来检查master_id是否有对应的数据库engine
+func DB(c *gin.Context) {
+ fmt.Println(c.Request.Header)
+ masterID := mw.GetMasterId(db.Db, c)
+ _, 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)
+ //判断应用是不是过期了
+ isOverTime := svc.SysCfgGet(c, "is_over_time")
+ if isOverTime == "1" {
+ str := "应用已过期"
+ overTimeStr := svc.SysCfgGet(c, "over_time_str")
+ if overTimeStr != "" {
+ str = overTimeStr
+ }
+ e.OutErr(c, 400, e.NewErr(400, str))
+ return
+ }
+ closeStation := svc.SysCfgGet(c, "close_station")
+ closeAppVersion := svc.SysCfgGet(c, "close_app_version")
+ platform := c.GetHeader("Platform")
+ if strings.Contains(closeStation, platform) && closeStation != "" && utils.StrToInt64(c.GetHeader("app_version")) <= utils.StrToInt64(closeAppVersion) {
+ str := "应用关闭"
+ overTimeStr := svc.SysCfgGet(c, "over_time_str")
+ if overTimeStr != "" {
+ str = overTimeStr
+ }
+ e.OutErr(c, 400, e.NewErr(400, str))
+ return
+ }
+ //判断是否有独立域名
+ domainWapBase := svc.GetWebSiteDomainInfo(c, "wap")
+
+ httpStr := "http://"
+ if c.GetHeader("Platform") == md.PLATFORM_WX_APPLET || c.GetHeader("Platform") == md.PLATFORM_ALIPAY_APPLET || c.GetHeader("Platform") == md.PLATFORM_BAIDU_APPLET || c.GetHeader("Platform") == md.PLATFORM_TOUTIAO_APPLET || c.GetHeader("Platform") == md.PLATFORM_TIKTOK_APPLET {
+ httpStr = "https://"
+ domainWapBase = strings.Replace(domainWapBase, "http://", httpStr, 1)
+ }
+ c.Set("domain_wap_base", domainWapBase)
+ c.Set("domain_wap_base_new", svc.GetWebSiteLiveBroadcastDomainInfo(c, "wap", masterID))
+ c.Set("domain_wap_base_second", svc.GetWebSiteDomainInfoSecond(c, "wap"))
+
+ c.Set("http_host", httpStr)
+
+ c.Set("h5_api_secret_key", svc.SysCfgGet(c, "h5_api_secret_key"))
+ c.Set("app_api_secret_key", svc.SysCfgGet(c, "app_api_secret_key"))
+ c.Set("applet_api_secret_key", svc.SysCfgGet(c, "applet_api_secret_key"))
+ c.Set("integral_prec", svc.SysCfgGet(c, "integral_prec"))
+ fanOrderCommissionPrec := svc.SysCfgGet(c, "fan_order_commission_prec")
+ if fanOrderCommissionPrec == "" {
+ fanOrderCommissionPrec = "2"
+ }
+ c.Set("fan_order_commission_prec", fanOrderCommissionPrec)
+ areaOrderCommissionPrec := svc.SysCfgGet(c, "area_order_commission_prec")
+ if areaOrderCommissionPrec == "" {
+ areaOrderCommissionPrec = "2"
+ }
+ c.Set("area_order_commission_prec", areaOrderCommissionPrec)
+
+ commissionPrec := svc.SysCfgGet(c, "commission_prec")
+ c.Set("commission_prec", commissionPrec)
+ pricePrec := svc.SysCfgGet(c, "price_prec")
+ if pricePrec == "" {
+ pricePrec = commissionPrec
+ }
+ dsChcek := svc.SysCfgGet(c, "ds_check")
+ if dsChcek == "1" {
+ pricePrec = commissionPrec
+ }
+ c.Set("price_prec", pricePrec)
+ c.Set("is_show_point", svc.SysCfgGet(c, "is_show_point"))
+ c.Set("appUserDefaultAvatar", svc.SysCfgGet(c, "app_user_default_avatar"))
+
+ translateOpen := ""
+ if strings.Contains(c.GetHeader("locale"), "zh_Hant_") {
+ translateOpen = "zh_Hant_"
+ }
+ if strings.Contains(c.GetHeader("locale"), "ug_CN") {
+ translateOpen = "ug_CN"
+ }
+ c.Set("translate_open", translateOpen)
+ orderVirtualCoinType := db.SysCfgGet(c, "order_virtual_coin_type")
+ c.Set("order_virtual_coin_type", orderVirtualCoinType)
+ orderVirtualCoinName := db.SysCfgGet(c, "order_virtual_coin_name")
+ if orderVirtualCoinName == "" {
+ orderVirtualCoinName = "收益:¥"
+ }
+ c.Set("orderVirtualCoinName", orderVirtualCoinName)
+ h5AppletMustSign := svc.SysCfgGet(c, "h5_applet_must_sign")
+ c.Set("h5_applet_must_sign", h5AppletMustSign)
+ androidMustSign := svc.SysCfgGet(c, "android_must_sign")
+ c.Set("android_must_sign", androidMustSign)
+ iosMustSign := svc.SysCfgGet(c, "ios_must_sign")
+ c.Set("ios_must_sign", iosMustSign)
+ c.Set("is_not_change_url", "0")
+ smsType := svc.SysCfgGet(c, "sms_type")
+ c.Set("sms_type", smsType)
+ if utils.StrToInt64(c.GetHeader("app_version")) > 1678445020 {
+ // || utils.StrToInt64(c.GetHeader("BuildVersion")) > 1678676004
+ c.Set("is_not_change_url", "1")
+ }
+ if utils.InArr(c.GetHeader("platform"), []string{md.PLATFORM_ANDROID, md.PLATFORM_IOS}) == false {
+ c.Set("is_not_change_url", "1")
+ }
+ GetHeaderParam(c)
+ c.Next()
+}
+
+func GetHeaderParam(c *gin.Context) {
+ var appTypeList = []string{"app_type", "App_type", "AppType"}
+ appType := ""
+ for _, v := range appTypeList {
+ val := c.GetHeader(v)
+ if val != "" {
+ appType = val
+ }
+ }
+ c.Set("app_type", appType)
+ var storeIdList = []string{"store_id", "Store_id", "StoreId"}
+ storeId := ""
+ for _, v := range storeIdList {
+ val := c.GetHeader(v)
+ if val != "" {
+ storeId = val
+ }
+ }
+ c.Set("store_id", storeId)
+}
diff --git a/app/mw/mw_limiter.go b/app/mw/mw_limiter.go
new file mode 100644
index 0000000..3c9fb79
--- /dev/null
+++ b/app/mw/mw_limiter.go
@@ -0,0 +1,77 @@
+package mw
+
+import (
+ "applet/app/e"
+ "applet/app/md"
+ "applet/app/svc"
+ "applet/app/utils"
+ "applet/app/utils/cache"
+ "bytes"
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "io/ioutil"
+)
+
+func Limiter(c *gin.Context) {
+ limit := 200 // 限流次数
+ ttl := 2 // 限流过期时间
+ ip := c.ClientIP()
+ // 读取token或者ip
+ token := c.GetHeader("Authorization")
+ mid := c.GetString("mid")
+ // 判断是否已经超出限额次数
+ method := c.Request.Method
+ host := c.Request.Host
+ uri := c.Request.URL.String()
+
+ buf := make([]byte, 5120*10)
+ num, _ := c.Request.Body.Read(buf)
+ body := buf[:num]
+ // Write body back
+ c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
+ //queryValue := utils.SerializeStr(c.Request.URL.Query()) //不建议开启,失去限流的意义
+
+ //TODO::分布式锁阻拦(保证原子性)
+ requestIdPrefix := fmt.Sprintf(md.DealAppLimiterRequestIdPrefix, mid, ip)
+ cb, err := svc.HandleLimiterDistributedLock(mid, ip, requestIdPrefix)
+ if err != nil {
+ e.OutErr(c, e.ERR, err.Error())
+ return
+ }
+ if cb != nil {
+ defer cb() // 释放锁
+ }
+
+ Md5 := utils.Md5(ip + token + method + host + uri + string(body))
+ //Md5 := utils.Md5(ip + token + method + host + uri + string(body) + queryValue)
+ if cache.Exists(Md5) {
+ c.AbortWithStatusJSON(428, gin.H{
+ "code": 428,
+ "msg": "don't repeat the request",
+ "data": struct{}{},
+ })
+ return
+ }
+
+ // 2s后没返回自动释放
+ go cache.SetEx(Md5, "0", ttl)
+
+ key := "NEW_LIMITER_APP_COMM_" + 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)
+ go cache.SetEx(key, reqs+1, ttl)
+ } else {
+ go cache.SetEx(key, 1, ttl)
+ }
+ c.Next()
+ go cache.Del(Md5)
+}
diff --git a/app/mw/mw_limiter_newcomers.go b/app/mw/mw_limiter_newcomers.go
new file mode 100644
index 0000000..7cf7d2b
--- /dev/null
+++ b/app/mw/mw_limiter_newcomers.go
@@ -0,0 +1,77 @@
+package mw
+
+import (
+ "applet/app/e"
+ "applet/app/md"
+ "applet/app/svc"
+ "applet/app/utils"
+ "applet/app/utils/cache"
+ "bytes"
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "io/ioutil"
+)
+
+func LimiterNewComers(c *gin.Context) {
+ limit := 200 // 限流次数
+ ttl := 2 // 限流过期时间
+ ip := c.ClientIP()
+ // 读取token或者ip
+ token := c.GetHeader("Authorization")
+ mid := c.GetString("mid")
+ // 判断是否已经超出限额次数
+ method := c.Request.Method
+ host := c.Request.Host
+ uri := c.Request.URL.String()
+
+ buf := make([]byte, 5120*10)
+ num, _ := c.Request.Body.Read(buf)
+ body := buf[:num]
+ // Write body back
+ c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
+ //queryValue := utils.SerializeStr(c.Request.URL.Query()) //不建议开启,失去限流的意义
+
+ //TODO::分布式锁阻拦(保证原子性)
+ requestIdPrefix := fmt.Sprintf(md.DealAppNewcomersLimiterRequestIdPrefix, mid, ip)
+ cb, err := svc.HandleLimiterDistributedLock(mid, ip, requestIdPrefix)
+ if err != nil {
+ e.OutErr(c, e.ERR, err.Error())
+ return
+ }
+ if cb != nil {
+ defer cb() // 释放锁
+ }
+
+ Md5 := utils.Md5(ip + token + method + host + uri + string(body))
+ //Md5 := utils.Md5(ip + token + method + host + uri + string(body) + queryValue)
+ if cache.Exists(Md5) {
+ c.AbortWithStatusJSON(428, gin.H{
+ "code": 428,
+ "msg": "请求频繁",
+ "data": struct{}{},
+ })
+ return
+ }
+
+ // 2s后没返回自动释放
+ go cache.SetEx(Md5, "0", ttl)
+
+ key := "NEW_LIMITER_APP_NEWCOMERS_COMM_" + ip
+ reqs, _ := cache.GetInt(key)
+ if reqs >= limit {
+ c.AbortWithStatusJSON(429, gin.H{
+ "code": 429,
+ "msg": "请求频繁",
+ "data": struct{}{},
+ })
+ return
+ }
+ if reqs > 0 {
+ //go cache.Incr(key)
+ go cache.SetEx(key, reqs+1, ttl)
+ } 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..94edf78
--- /dev/null
+++ b/app/router/router.go
@@ -0,0 +1,72 @@
+package router
+
+import (
+ "applet/app/cfg"
+ "applet/app/hdl"
+ "applet/app/mw"
+ _ "applet/docs"
+ "github.com/gin-gonic/gin"
+)
+
+// 初始化路由
+// 1
+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.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)
+ routeCommunityTeam(r.Group("/api/v1/communityTeam"))
+ return r
+}
+func routeCommunityTeam(r *gin.RouterGroup) {
+ r.Use(mw.DB) // 下面接口再根据mid 获取数据库名
+ r.Use(mw.CheckBody) //body参数转换
+ r.Use(mw.CheckSign) //签名校验
+ r.Use(mw.Checker)
+ r.GET("/cate", hdl.Cate)
+ r.GET("/bank/store/cate", hdl.BankStoreCate)
+ r.POST("/bank/store/list", hdl.BankStore)
+ r.POST("/store", hdl.Store)
+ // 用户授权后调用的接口
+ r.Use(mw.AuthJWT)
+ r.POST("/store/addLike", hdl.StoreAddLike)
+ r.POST("/store/cancelLike", hdl.StoreCancelLike)
+ r.POST("/goods", hdl.Goods)
+ r.POST("/goods/sku", hdl.GoodsSku)
+ r.POST("/goods/coupon", hdl.GoodsCoupon)
+ r.POST("/order/total", hdl.OrderTotal)
+ r.POST("/order/create", hdl.OrderCreate)
+ r.POST("/order/cancel", hdl.OrderCancel)
+ r.POST("/order/coupon", hdl.OrderCoupon)
+ r.POST("/order/list", hdl.OrderList)
+ r.POST("/order/detail", hdl.OrderDetail)
+ r.GET("/order/cate", hdl.OrderCate)
+ r.POST("/pay/:payMethod/:orderType", hdl.Pay)
+
+ r.POST("/store/order/list", hdl.StoreOrderList)
+ r.POST("/store/order/detail", hdl.StoreOrderDetail)
+ r.POST("/store/order/confirm", hdl.StoreOrderConfirm)
+ r.GET("/store/order/cate", hdl.StoreOrderCate)
+}
diff --git a/app/svc/svc_alipay.go b/app/svc/svc_alipay.go
new file mode 100644
index 0000000..b2b384f
--- /dev/null
+++ b/app/svc/svc_alipay.go
@@ -0,0 +1,106 @@
+package svc
+
+import (
+ "applet/app/cfg"
+ "applet/app/db"
+ "applet/app/e"
+ "applet/app/md"
+ "applet/app/utils"
+ "applet/app/utils/logx"
+ "code.fnuoos.com/go_rely_warehouse/zyos_go_pay.git/pay"
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "github.com/iGoogle-ink/gopay/alipay"
+)
+
+// 支付宝回调处理
+func AlipayCallback(c *gin.Context) (string, error) {
+ data, ok := c.Get("callback")
+ if data == nil || !ok {
+ return "", e.NewErrCode(e.ERR_INVALID_ARGS)
+ }
+ args := data.(*md.AliPayCallback)
+ _, ok = db.DBs[args.MasterID]
+ if !ok {
+ return "", logx.Warn("Alipay Failed : master_id not found")
+ }
+ c.Set("mid", args.MasterID)
+ // 回调交易状态失败
+ if args.TradeStatus != "TRADE_SUCCESS" {
+ return "", logx.Warn("Alipay Failed : trade status failed")
+ }
+ return args.OutTradeNo, nil
+}
+
+func PrepareAlipayCode(c *gin.Context, p *md.AliPayPayParams) (interface{}, error) {
+ req, err := CommAlipayConfig(c, p)
+ if err != nil {
+ return "", err
+ }
+ var param interface{}
+ switch req["platform"] {
+ case md.PLATFORM_ALIPAY_APPLET:
+ param, err = pay.AlipayApplet(req)
+ case md.PLATFORM_WAP:
+ param, err = pay.AlipayWap(req)
+ case md.PLATFORM_ANDROID, md.PLATFORM_IOS:
+ param, err = pay.AlipayApp(req)
+ default:
+ return "", e.NewErrCode(e.ERR_PLATFORM)
+ }
+ if err != nil {
+ fmt.Println("支付宝错误日志")
+ fmt.Println(param)
+ fmt.Println(err)
+ return "", e.NewErrCode(e.ERR_ALIPAY_ORDER_ERR)
+ }
+ return utils.AnyToString(param), nil
+
+}
+
+func CommAlipayConfig(c *gin.Context, p *md.AliPayPayParams) (map[string]string, error) {
+ //获取支付配置
+ req := map[string]string{
+ "pay_ali_use_type": SysCfgGet(c, "pay_ali_use_type"),
+ "private_key": SysCfgGet(c, "pay_ali_private_key"),
+ "app_id": SysCfgGet(c, "pay_ali_app_id"),
+ "rsa": SysCfgGet(c, "pay_ali_key_len_type"),
+ "pkcs": SysCfgGet(c, "pay_ali_key_format_type"),
+ }
+ if req["pay_ali_use_type"] == "1" {
+ req["private_key"] = SysCfgGet(c, "pay_ali_new_private_key")
+ req["app_id"] = SysCfgGet(c, "pay_ali_new_app_id")
+ appCertSN, err := alipay.GetCertSN(cfg.WxappletFilepath.URL + "/" + SysCfgGet(c, "pay_app_cert_sn"))
+ if err != nil {
+ fmt.Println(err)
+ return nil, err
+ }
+ if appCertSN == "" {
+ fmt.Println(err)
+ return nil, err
+ }
+ req["pay_app_cert_sn"] = appCertSN
+ aliPayPublicCertSN, err := alipay.GetCertSN(cfg.WxappletFilepath.URL + "/" + SysCfgGet(c, "pay_alipayrsa_public_key"))
+ if err != nil {
+ fmt.Println(err)
+ return nil, err
+ }
+ if aliPayPublicCertSN == "" {
+ fmt.Println(err)
+ return nil, err
+ }
+ req["pay_alipayrsa_public_key"] = aliPayPublicCertSN
+ }
+ if req["private_key"] == "" || req["app_id"] == "" {
+ return req, e.NewErr(400, "请在后台正确配置支付宝")
+ }
+ req["ord_id"] = p.OrdId
+ req["amount"] = p.Amount
+ req["subject"] = p.Subject
+ req["order_type"] = p.OrderType
+ req["notify_url"] = fmt.Sprintf(md.CALLBACK_URL, c.Request.Host, c.GetString("mid"), p.OrderType, md.ALIPAY)
+ req["platform"] = c.GetHeader("Platform")
+ req["page_url"] = c.Query("page_url")
+ utils.FilePutContents(c.GetString("mid")+"alipay", utils.SerializeStr(req))
+ return req, nil
+}
diff --git a/app/svc/svc_auth.go b/app/svc/svc_auth.go
new file mode 100644
index 0000000..988a3d7
--- /dev/null
+++ b/app/svc/svc_auth.go
@@ -0,0 +1,69 @@
+package svc
+
+import (
+ "applet/app/db"
+ "applet/app/md"
+ "applet/app/utils"
+ "errors"
+ "github.com/gin-gonic/gin"
+ "strings"
+)
+
+// 因为在mw_auth已经做完所有校验, 因此在此不再做任何校验
+// GetUser is get user model
+func GetUser(c *gin.Context) *md.User {
+ user, _ := c.Get("user")
+ if user == nil {
+ return nil
+ }
+ return user.(*md.User)
+}
+
+func GetUid(c *gin.Context) string {
+ user, _ := c.Get("user")
+ u := user.(*md.User)
+ return utils.IntToStr(u.Info.Uid)
+}
+
+func CheckUser(c *gin.Context) (*md.User, 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 := utils.ParseToken(parts[1])
+ if err != nil {
+ return nil, err
+ }
+
+ // 获取user
+ u, err := db.UserFindByID(db.DBs[c.GetString("mid")], mc.UID)
+ if err != nil {
+ return nil, err
+ }
+ if u == nil {
+ return nil, errors.New("token can not find user")
+ }
+ // 获取user profile
+ up, err := db.UserProfileFindByID(db.DBs[c.GetString("mid")], mc.UID)
+ if err != nil {
+ return nil, err
+ }
+ // 获取user 等级
+ ul, err := db.UserLevelByID(db.DBs[c.GetString("mid")], u.Level)
+ if err != nil {
+ return nil, err
+ }
+
+ user := &md.User{
+ Info: u,
+ Profile: up,
+ Level: ul,
+ }
+ return user, nil
+}
diff --git a/app/svc/svc_balance.go b/app/svc/svc_balance.go
new file mode 100644
index 0000000..6e30eec
--- /dev/null
+++ b/app/svc/svc_balance.go
@@ -0,0 +1,74 @@
+package svc
+
+import (
+ "applet/app/db"
+ "applet/app/db/model"
+ "applet/app/e"
+ "applet/app/md"
+ "applet/app/utils"
+ "applet/app/utils/logx"
+ "github.com/gin-gonic/gin"
+ "time"
+ "xorm.io/xorm"
+)
+
+func BalancePay(c *gin.Context, money, oid string, types string) error {
+
+ user, err := CheckUser(c)
+ if user == nil || err != nil {
+ return err
+ }
+ // 获取余额更新锁
+ cb, err := HandleBalanceDistributedLock(c.GetString("mid"), utils.IntToStr(user.Info.Uid), "balance_pay")
+ if err != nil {
+ return err
+ }
+ // 释放锁
+ if cb != nil {
+ defer cb()
+ }
+ finValid := utils.AnyToFloat64(user.Profile.FinValid)
+ needMoney := utils.AnyToFloat64(money)
+ if finValid < needMoney {
+ return e.NewErrCode(e.ERR_BALANCE_NOT_ENOUGH)
+ }
+ user.Profile.FinValid = utils.AnyToString(finValid - needMoney)
+ affect, err := db.UserProfileUpdate(db.DBs[c.GetString("mid")], user.Profile.Uid, user.Profile, "fin_valid")
+ if err != nil || affect != 1 {
+ return err
+ }
+ var str = ""
+ if types == md.CommunityTeam {
+ str = "小店下单"
+ }
+ flowInsert(db.DBs[c.GetString("mid")], user.Profile.Uid, money, 21, utils.StrToInt64(oid), 0, 0, str+"余额支付", types, 1, utils.Float64ToStr(finValid), user.Profile.FinValid)
+ return nil
+}
+func flowInsert(eg *xorm.Engine, uid int, paidPrice string, orderAction int, ordId int64, id int64, goodsId int, ItemTitle string, ordType string, types int, beforeAmount string, afterAmount string) {
+ session := eg.NewSession()
+
+ now := time.Now()
+ if err := db.FinUserFlowInsertOneWithSession(
+ session,
+ &model.FinUserFlow{
+ Type: types,
+ Uid: uid,
+ Amount: paidPrice,
+ BeforeAmount: beforeAmount,
+ AfterAmount: afterAmount,
+ OrdType: ordType,
+ OrdId: utils.Int64ToStr(ordId),
+ OrdAction: orderAction,
+ OrdDetail: utils.IntToStr(goodsId),
+ State: 2,
+ OtherId: id,
+ OrdTitle: ItemTitle,
+ OrdTime: int(now.Unix()),
+ CreateAt: now,
+ UpdateAt: now,
+ }); err != nil {
+ _ = session.Rollback()
+ _ = logx.Warn(err)
+ return
+ }
+}
diff --git a/app/svc/svc_cate.go b/app/svc/svc_cate.go
new file mode 100644
index 0000000..1b02dbe
--- /dev/null
+++ b/app/svc/svc_cate.go
@@ -0,0 +1,24 @@
+package svc
+
+import (
+ "applet/app/db"
+ "applet/app/e"
+ "applet/app/utils"
+ "github.com/gin-gonic/gin"
+)
+
+func Cate(c *gin.Context) {
+ cate := db.GetCate(MasterDb(c), "0")
+ cateList := make([]map[string]string, 0)
+ if cate != nil {
+ for _, v := range *cate {
+ tmp := map[string]string{
+ "id": utils.IntToStr(v.Id),
+ "name": v.Title,
+ }
+ cateList = append(cateList, tmp)
+ }
+ }
+ e.OutSuc(c, cateList, nil)
+ return
+}
diff --git a/app/svc/svc_comm.go b/app/svc/svc_comm.go
new file mode 100644
index 0000000..f5b28e3
--- /dev/null
+++ b/app/svc/svc_comm.go
@@ -0,0 +1,25 @@
+package svc
+
+import (
+ "applet/app/utils"
+ "github.com/gin-gonic/gin"
+ "strings"
+)
+
+func GetCommissionPrec(c *gin.Context, sum, commPrec, isShowPoint string) string {
+ if sum == "" {
+ sum = "0"
+ }
+ sum = utils.StrToFormat(c, sum, utils.StrToInt(commPrec))
+ ex := strings.Split(sum, ".")
+ if len(ex) == 2 && isShowPoint != "1" {
+ if utils.StrToFloat64(ex[1]) == 0 {
+ sum = ex[0]
+ } else {
+ val := utils.Float64ToStrByPrec(utils.StrToFloat64(ex[1]), 0)
+ valNew := strings.ReplaceAll(val, "0", "")
+ sum = ex[0] + "." + strings.ReplaceAll(ex[1], val, valNew)
+ }
+ }
+ return sum
+}
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_default_user.go b/app/svc/svc_default_user.go
new file mode 100644
index 0000000..63bdaaa
--- /dev/null
+++ b/app/svc/svc_default_user.go
@@ -0,0 +1,50 @@
+package svc
+
+import (
+ "applet/app/db"
+ "applet/app/md"
+ "errors"
+ "github.com/gin-gonic/gin"
+ "strings"
+)
+
+// GetDefaultUser is 获取默认账号,uid =0 为系统默认账号,用于一些请求需要渠道id之类的东西
+func GetDefaultUser(c *gin.Context, token string) (*md.User, error) {
+ user := new(md.User)
+ if c.GetString("convert_url") == "1" { //转链接口
+ goto DEFALUT
+ } else {
+ // Token 不为空时拿对应的用户数据
+ if token != "" && strings.Contains(token, "Bearer") {
+
+ user, err := CheckUser(c)
+ if user == nil {
+ return nil, errors.New("token is expired")
+ }
+ if err != nil {
+ // 有报错自己拿默认用户
+ goto DEFALUT
+ }
+ return user, nil
+ }
+ }
+
+DEFALUT:
+ // 默认拿uid 等于0的用户数据
+ profile, err := db.UserProfileFindByID(db.DBs[c.GetString("mid")], 0)
+ if err != nil {
+ return nil, err
+ }
+ info, err := db.UserFindByID(db.DBs[c.GetString("mid")], 0)
+ if err != nil {
+ return nil, err
+ }
+ ul, err := db.UserLevelInIDescByWeightLowWithOne(db.DBs[c.GetString("mid")])
+ if err != nil {
+ return nil, err
+ }
+ user.Info = info
+ user.Profile = profile
+ user.Level = ul
+ return user, nil
+}
diff --git a/app/svc/svc_domain_info.go b/app/svc/svc_domain_info.go
new file mode 100644
index 0000000..9318ad9
--- /dev/null
+++ b/app/svc/svc_domain_info.go
@@ -0,0 +1,206 @@
+package svc
+
+import (
+ "applet/app/db"
+ "applet/app/db/model"
+ "applet/app/db/offical"
+ "applet/app/md"
+ "applet/app/utils"
+ "applet/app/utils/logx"
+ "github.com/gin-gonic/gin"
+ "github.com/tidwall/gjson"
+ "strings"
+)
+
+// 获取指定类型的域名:admin、wap、api
+func GetWebSiteDomainInfo(c *gin.Context, domainType string) string {
+ if domainType == "" {
+ domainType = "wap"
+ }
+
+ domainSetting := SysCfgGet(c, "domain_setting")
+
+ domainTypePath := domainType + ".type"
+ domainSslPath := domainType + ".isOpenHttps"
+ domainPath := domainType + ".domain"
+
+ domainTypeValue := gjson.Get(domainSetting, domainTypePath).String()
+ domainSslValue := gjson.Get(domainSetting, domainSslPath).String()
+ domain := gjson.Get(domainSetting, domainPath).String()
+
+ scheme := "http://"
+ if domainSslValue == "1" {
+ scheme = "https://"
+ }
+
+ // 有自定义域名 返回自定义的
+ if domainTypeValue == "own" && domain != "" {
+ return scheme + domain
+ }
+ // 否则返回官方的
+ official, err := db.GetOfficialDomainInfoByType(db.Db, c.GetString("mid"), domainType)
+ if err != nil {
+ _ = logx.Errorf("Get Official Domain Fail! %s", err)
+ return ""
+ }
+ if strings.Contains(official, "http") {
+ return official
+ }
+ return scheme + official
+}
+func GetWebSiteDomainInfoToAgent(c *gin.Context, domainType string) string {
+ if domainType == "" {
+ domainType = "wap"
+ }
+
+ domainSetting := SysCfgGet(c, "domain_setting")
+
+ domainTypePath := domainType + ".type"
+ domainSslPath := domainType + ".isOpenHttps"
+ domainPath := domainType + ".domain"
+
+ domainTypeValue := gjson.Get(domainSetting, domainTypePath).String()
+ domainSslValue := gjson.Get(domainSetting, domainSslPath).String()
+ domain := gjson.Get(domainSetting, domainPath).String()
+
+ scheme := "http://"
+ if domainSslValue == "1" {
+ scheme = "https://"
+ }
+
+ // 有自定义域名 返回自定义的
+ if domainTypeValue == "own" && domain != "" {
+ return scheme + domain
+ }
+ // 否则返回官方的
+ puid := AppUserListPuid(c)
+ var official = ""
+ var err error
+ if puid != "" && puid != "0" {
+ official, err = db.GetOfficialDomainInfoByTypeToAgent(db.Db, c.GetString("mid"), puid, domainType)
+ if err != nil {
+ _ = logx.Errorf("Get Official Domain Fail! %s", err)
+ return ""
+ }
+ } else {
+ official, err = db.GetOfficialDomainInfoByType(db.Db, c.GetString("mid"), domainType)
+ if err != nil {
+ _ = logx.Errorf("Get Official Domain Fail! %s", err)
+ return ""
+ }
+ }
+ if strings.Contains(official, "http") {
+ return official
+ }
+ return scheme + official
+}
+func GetWebSiteDomainInfoOfficial(c *gin.Context, domainType string) string {
+ if domainType == "" {
+ domainType = "wap"
+ }
+
+ domainSetting := SysCfgGet(c, "domain_setting")
+
+ domainSslPath := domainType + ".isOpenHttps"
+
+ domainSslValue := gjson.Get(domainSetting, domainSslPath).String()
+
+ scheme := "http://"
+ if domainSslValue == "1" {
+ scheme = "https://"
+ }
+
+ // 有自定义域名 返回自定义的
+ // 否则返回官方的
+ official, err := db.GetOfficialDomainInfoByType(db.Db, c.GetString("mid"), domainType)
+ if err != nil {
+ _ = logx.Errorf("Get Official Domain Fail! %s", err)
+ return ""
+ }
+ if strings.Contains(official, "http") {
+ return official
+ }
+ return scheme + official
+}
+
+// 获取指定类型的域名对应的masterId:admin、wap、api
+func GetWebSiteDomainMasterId(domainType string, host string) string {
+ obj := new(model.UserAppDomain)
+ has, err := db.Db.Where("domain=? and type=?", host, domainType).Get(obj)
+ if err != nil || !has {
+ return ""
+ }
+ return utils.AnyToString(obj.Uuid)
+}
+
+func GetWebSiteAppSmsPlatform(mid string) string {
+ obj := new(model.UserAppList)
+ has, err := db.Db.Where("uuid=? ", mid).Asc("id").Get(obj)
+ if err != nil || !has {
+ return ""
+ }
+ return utils.AnyToString(obj.SmsPlatform)
+}
+
+// 获取指定类型的域名:admin、wap、api
+func GetWebSiteLiveBroadcastDomainInfo(c *gin.Context, domainType, mid string) string {
+ if domainType == "" {
+ domainType = "wap"
+ }
+
+ domainSetting := SysCfgGet(c, "domain_setting")
+ domainSslPath := domainType + ".isOpenHttps"
+ domainSslValue := gjson.Get(domainSetting, domainSslPath).String()
+ //masterid.izhyin.cn
+ domain := mid + ".izhim.net"
+
+ scheme := "http://"
+ if domainSslValue == "1" {
+ scheme = "https://"
+ }
+ if c.GetHeader("platform") == md.PLATFORM_WX_APPLET { //小程序需要https
+ scheme = "https://"
+ }
+ return scheme + domain
+}
+func GetWebSiteDomainInfoSecond(c *gin.Context, domainType string) string {
+ if domainType == "" {
+ domainType = "wap"
+ }
+
+ domainSetting := SysCfgGet(c, "domain_setting")
+ domainSslPath := domainType + ".isOpenHttps"
+ domainSslValue := gjson.Get(domainSetting, domainSslPath).String()
+ domain := c.GetString("mid") + ".izhim.net"
+ domainTypePath := domainType + ".type"
+ domainTypeValue := gjson.Get(domainSetting, domainTypePath).String()
+ scheme := "http://"
+ if domainSslValue == "1" {
+ scheme = "https://"
+ }
+ if c.GetHeader("platform") == md.PLATFORM_WX_APPLET { //小程序需要https
+ scheme = "https://"
+ }
+ // 有自定义域名 返回自定义的
+ if domainTypeValue == "own" {
+ domainPath := domainType + ".domain"
+ domain = gjson.Get(domainSetting, domainPath).String()
+ }
+ return scheme + domain
+}
+func AppUserListPuid(c *gin.Context) string {
+ appList := offical.GetUserAppList(c.GetString("mid"))
+ uid := "0"
+ if appList != nil && appList.Puid > 0 {
+ uid = utils.IntToStr(appList.Puid)
+ }
+ return uid
+}
+func AppUserListPuidWithDb(dbName string) string {
+ appList := offical.GetUserAppList(dbName)
+ uid := "0"
+ if appList != nil && appList.Puid > 0 {
+ uid = utils.IntToStr(appList.Puid)
+ }
+ return uid
+}
diff --git a/app/svc/svc_file_img_format.go b/app/svc/svc_file_img_format.go
new file mode 100644
index 0000000..4131981
--- /dev/null
+++ b/app/svc/svc_file_img_format.go
@@ -0,0 +1,81 @@
+package svc
+
+import (
+ "applet/app/utils"
+ "fmt"
+ "github.com/syyongx/php2go"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+)
+
+// ImageFormat is 格式化 图片
+func ImageFormat(c *gin.Context, name string) string {
+ if strings.Contains(name, "https:") || strings.Contains(name, "http:") {
+ return name
+ }
+ scheme := SysCfgGet(c, "file_bucket_scheme")
+ domain := SysCfgGet(c, "file_bucket_host")
+ name, _ = php2go.URLDecode(name)
+ name = php2go.URLEncode(name)
+ return fmt.Sprintf("%s://%s/%s", scheme, domain, name)
+}
+
+// OffImageFormat is 格式化官方 图片
+func OffImageFormat(c *gin.Context, name string) string {
+ if strings.Contains(name, "https:") || strings.Contains(name, "http:") {
+ return name
+ }
+ name, _ = php2go.URLDecode(name)
+ name = php2go.URLEncode(name)
+
+ return fmt.Sprintf("%s://%s/%s", "http", "ossn.izhim.net", name)
+}
+
+// ImageBucket is 获取域名
+func ImageBucket(c *gin.Context) (string, string) {
+ return SysCfgGet(c, "file_bucket_scheme"), SysCfgGet(c, "file_bucket_host")
+}
+
+// ImageFormatWithBucket is 格式化成oss 域名
+func ImageFormatWithBucket(scheme, domain, name string) string {
+ if strings.Contains(name, "http") || name == "" {
+ return name
+ }
+ name, _ = php2go.URLDecode(name)
+ name = php2go.URLEncode(name)
+ return fmt.Sprintf("%s://%s/%s", scheme, domain, name)
+}
+
+// ImageBucketNew is 获取域名
+func ImageBucketNew(c *gin.Context) (string, string, string, map[string]string) {
+ var list = make(map[string]string, 0)
+ for i := 1; i < 10; i++ {
+ keys := "file_bucket_sub_host" + utils.IntToStr(i)
+ list[keys] = SysCfgGet(c, keys)
+ }
+ return SysCfgGet(c, "file_bucket_scheme"), SysCfgGet(c, "file_bucket_host"), SysCfgGet(c, "file_bucket_sub_host"), list
+}
+
+// ImageFormatWithBucket is 格式化成oss 域名
+func ImageFormatWithBucketNew(scheme, domain, subDomain string, moreSubDomain map[string]string, name string) string {
+ if strings.Contains(name, "http") {
+ return name
+ }
+ if strings.Contains(name, "{{subhost}}") && subDomain != "" { //读副域名 有可能是其他平台的
+ domain = subDomain
+ }
+ //为了兼容一些客户自营商城导到不同系统 并且七牛云不一样
+ for i := 1; i < 10; i++ {
+ keys := "file_bucket_sub_host" + utils.IntToStr(i)
+ if strings.Contains(name, "{{subhost"+utils.IntToStr(i)+"}}") && moreSubDomain[keys] != "" {
+ domain = moreSubDomain[keys]
+ }
+ name = strings.ReplaceAll(name, "{{subhost"+utils.IntToStr(i)+"}}", "")
+ }
+ name = strings.ReplaceAll(name, "{{host}}", "")
+ name = strings.ReplaceAll(name, "{{subhost}}", "")
+ name, _ = php2go.URLDecode(name)
+ name = php2go.URLEncode(name)
+ return fmt.Sprintf("%s://%s/%s", scheme, domain, name)
+}
diff --git a/app/svc/svc_goods.go b/app/svc/svc_goods.go
new file mode 100644
index 0000000..7c6f19e
--- /dev/null
+++ b/app/svc/svc_goods.go
@@ -0,0 +1,75 @@
+package svc
+
+import (
+ "applet/app/db"
+ "applet/app/e"
+ "applet/app/utils"
+ "encoding/json"
+ "github.com/gin-gonic/gin"
+)
+
+func Goods(c *gin.Context) {
+ var arg map[string]string
+ if err := c.ShouldBindJSON(&arg); err != nil {
+ e.OutErr(c, e.ERR_INVALID_ARGS, err)
+ return
+ }
+ goods := db.GetGoods(MasterDb(c), arg)
+ goodsList := make([]map[string]interface{}, 0)
+ if goods != nil {
+ for _, v := range *goods {
+ speImageList := make([]string, 0)
+ if v.IsSpeImageOn == 1 {
+ json.Unmarshal([]byte(v.SpeImages), &speImageList)
+ }
+ tmp := map[string]interface{}{
+ "id": utils.IntToStr(v.Id),
+ "title": v.Title,
+ "img": v.Img,
+ "info": v.Info,
+ "price": v.Price,
+ "stock": utils.IntToStr(v.Stock),
+ "is_single_sku": utils.IntToStr(v.IsSingleSku),
+ "sale_count": utils.IntToStr(v.SaleCount),
+ "spe_image_list": speImageList,
+ }
+ goodsList = append(goodsList, tmp)
+ }
+ }
+ e.OutSuc(c, goodsList, nil)
+ return
+}
+func GoodsSku(c *gin.Context) {
+ var arg map[string]string
+ if err := c.ShouldBindJSON(&arg); err != nil {
+ e.OutErr(c, e.ERR_INVALID_ARGS, err)
+ return
+ }
+ sku := db.GetGoodsSku(MasterDb(c), arg["goods_id"])
+ skuList := make([]map[string]string, 0)
+ if sku != nil {
+ for _, v := range *sku {
+ tmp := map[string]string{
+ "sku_id": utils.Int64ToStr(v.SkuId),
+ "goods_id": utils.IntToStr(v.GoodsId),
+ "price": v.Price,
+ "stock": utils.IntToStr(v.Stock),
+ "indexes": v.Indexes,
+ "sku": v.Sku,
+ }
+ skuList = append(skuList, tmp)
+ }
+ }
+ e.OutSuc(c, skuList, nil)
+ return
+}
+func GoodsCoupon(c *gin.Context) {
+ var arg map[string]string
+ if err := c.ShouldBindJSON(&arg); err != nil {
+ e.OutErr(c, e.ERR_INVALID_ARGS, err)
+ return
+ }
+ returnData := CommCoupon(c, arg["amount"])
+ e.OutSuc(c, returnData, nil)
+ return
+}
diff --git a/app/svc/svc_order.go b/app/svc/svc_order.go
new file mode 100644
index 0000000..d0be942
--- /dev/null
+++ b/app/svc/svc_order.go
@@ -0,0 +1,656 @@
+package svc
+
+import (
+ "applet/app/db"
+ "applet/app/db/model"
+ "applet/app/e"
+ "applet/app/enum"
+ "applet/app/md"
+ "applet/app/utils"
+ "applet/app/utils/cache"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "github.com/shopspring/decimal"
+ "time"
+ "xorm.io/xorm"
+)
+
+func OrderCate(c *gin.Context) {
+ var cate = []map[string]string{
+ {"name": "全部", "value": ""},
+ {"name": "待付款", "value": "0"},
+ {"name": "待提货", "value": "1"},
+ {"name": "已完成", "value": "2"},
+ {"name": "已取消", "value": "3"},
+ }
+ e.OutSuc(c, cate, nil)
+ return
+}
+func OrderList(c *gin.Context) {
+ var arg map[string]string
+ if err := c.ShouldBindJSON(&arg); err != nil {
+ e.OutErr(c, e.ERR_INVALID_ARGS, err)
+ return
+ }
+ user := GetUser(c)
+ arg["uid"] = utils.IntToStr(user.Info.Uid)
+ data := db.GetOrderList(MasterDb(c), arg)
+ var state = []string{"待付款", "待提货", "已完成", "已取消"}
+ list := make([]map[string]string, 0)
+ if data != nil {
+ now := time.Now().Unix()
+ for _, v := range *data {
+ store := db.GetStoreIdEg(MasterDb(c), utils.IntToStr(v.StoreUid))
+ info := db.GetOrderInfoEg(MasterDb(c), utils.Int64ToStr(v.Oid))
+ downTime := "0"
+ if v.State == 0 {
+ downTime = utils.IntToStr(int(v.CreateAt.Unix() + 15*60 - now))
+ if now > v.CreateAt.Unix()+15*60 {
+ v.State = 3
+ }
+ if utils.StrToInt(downTime) < 0 {
+ downTime = "0"
+ }
+ }
+ img := ""
+ title := ""
+ storeName := ""
+ if store != nil {
+ storeName = store.Name
+ }
+ if info != nil {
+ img = info.Img
+ title = info.Title
+ }
+ tmp := map[string]string{
+ "oid": utils.Int64ToStr(v.Oid),
+ "label": "自提",
+ "state": utils.IntToStr(v.State),
+ "state_str": state[v.State],
+ "store_name": storeName,
+ "img": img,
+ "title": title,
+ "amount": v.Amount,
+ "num": utils.IntToStr(v.Num),
+ "timer": "",
+ "down_time": downTime,
+ }
+ if v.Type == 1 {
+ tmp["label"] = "外卖"
+ }
+ if v.IsNow == 1 {
+ tmp["timer"] = "立即提货"
+ } else if v.Timer != "" {
+ tmp["timer"] = "提货时间:" + v.Timer
+ }
+ list = append(list, tmp)
+ }
+ }
+ e.OutSuc(c, list, nil)
+ return
+}
+func OrderDetail(c *gin.Context) {
+ var arg map[string]string
+ if err := c.ShouldBindJSON(&arg); err != nil {
+ e.OutErr(c, e.ERR_INVALID_ARGS, err)
+ return
+ }
+ data := db.GetOrderEg(MasterDb(c), arg["oid"])
+ var state = []string{"待付款", "待提货", "已完成", "已取消"}
+ now := time.Now().Unix()
+ store := db.GetStoreIdEg(MasterDb(c), utils.IntToStr(data.StoreUid))
+ downTime := "0"
+ if data.State == 0 {
+ downTime = utils.IntToStr(int(data.CreateAt.Unix() + 15*60 - now))
+ if now > data.CreateAt.Unix()+15*60 {
+ data.State = 3
+ }
+ if utils.StrToInt(downTime) < 0 {
+ downTime = "0"
+ }
+ }
+ img := ""
+ title := ""
+ storeName := ""
+ storeAddress := ""
+ lat := ""
+ lng := ""
+ km := ""
+ if store != nil {
+ storeName = store.Name
+ storeAddress = store.Address
+ lat = store.Lat
+ lng = store.Lng
+ km = ""
+ if arg["lat"] != "" && arg["lng"] != "" {
+ km1 := utils.CalculateDistance(utils.StrToFloat64(lat), utils.StrToFloat64(lng), utils.StrToFloat64(arg["lat"]), utils.StrToFloat64(arg["lng"]))
+ if km1 < 1 {
+ km = utils.Float64ToStr(km1*1000) + "m"
+ } else {
+ km = utils.Float64ToStr(km1) + "km"
+ }
+ }
+ }
+ confirmAt := ""
+ if data.ConfirmAt.IsZero() == false {
+ confirmAt = data.ConfirmAt.Format("2006-01-02 15:04:05")
+ }
+ payMethod := "-"
+ if data.PayMethod > 0 {
+ payMethod = md.PayMethodIdToName[data.PayMethod]
+ }
+ orderInfo := []map[string]string{
+ {"title": "订单编号", "content": utils.Int64ToStr(data.Oid)},
+ {"title": "下单时间", "content": data.CreateAt.Format("2006-01-02 15:04:05")},
+ {"title": "提货时间", "content": confirmAt},
+ {"title": "预留电话", "content": data.Phone},
+ {"title": "支付方式", "content": payMethod},
+ {"title": "备注信息", "content": data.Memo},
+ }
+ goodsInfo := make([]map[string]string, 0)
+ info := db.GetOrderInfoAllEg(MasterDb(c), utils.Int64ToStr(data.Oid))
+ if info != nil {
+ for _, v := range *info {
+ tmp := map[string]string{
+ "img": v.Img,
+ "title": v.Title,
+ "price": v.Price,
+ "num": utils.IntToStr(v.Num),
+ "sku_str": "",
+ }
+ skuData := make([]md.Sku, 0)
+ json.Unmarshal([]byte(v.SkuInfo), &skuData)
+ skuStr := ""
+ for _, v1 := range skuData {
+ if skuStr != "" {
+ skuStr += ";"
+ }
+ skuStr += v1.Value
+ }
+ tmp["sku_str"] = skuStr
+ goodsInfo = append(goodsInfo, tmp)
+ }
+ }
+ tmp := map[string]interface{}{
+ "oid": utils.Int64ToStr(data.Oid),
+ "label": "自提",
+ "state": utils.IntToStr(data.State),
+ "state_str": state[data.State],
+ "store_name": storeName,
+ "store_address": storeAddress,
+ "lat": lat,
+ "lng": lng,
+ "km": km,
+ "img": img,
+ "title": title,
+ "amount": data.Amount,
+ "num": utils.IntToStr(data.Num),
+ "timer": "",
+ "code": data.Code,
+ "down_time": downTime,
+ "order_info": orderInfo,
+ "goods_info": goodsInfo,
+ "goods_count": utils.IntToStr(len(goodsInfo)),
+ }
+ if data.Type == 1 {
+ tmp["label"] = "外卖"
+ }
+ if data.IsNow == 1 {
+ tmp["timer"] = "立即提货"
+ } else if data.Timer != "" {
+ tmp["timer"] = data.Timer
+ }
+ e.OutSuc(c, tmp, nil)
+ return
+}
+func OrderCoupon(c *gin.Context) {
+ var arg md.OrderTotal
+ if err := c.ShouldBindJSON(&arg); err != nil {
+ e.OutErr(c, e.ERR_INVALID_ARGS, err)
+ return
+ }
+ totalPrice := commGoods(c, arg)
+ returnData := CommCoupon(c, totalPrice)
+ e.OutSuc(c, returnData, nil)
+ return
+}
+func CommCoupon(c *gin.Context, totalPrice string) map[string]interface{} {
+ var err error
+ engine := MasterDb(c)
+ user := GetUser(c)
+ now := time.Now().Format("2006-01-02 15:04:05")
+ var ActCouponUserList []*model.CommunityTeamCouponUser
+ sess := engine.Table("act_coupon_user").
+ Where("store_type=? and uid = ? AND is_use = ? AND (valid_time_start < ? AND valid_time_end > ?)", 0,
+ user.Info.Uid, 0, now, now)
+ err = sess.Find(&ActCouponUserList)
+ if err != nil {
+ return map[string]interface{}{}
+ }
+ var ids = make([]int, 0)
+ for _, v := range ActCouponUserList {
+ ids = append(ids, v.MerchantSchemeId)
+ }
+ var merchantScheme []model.CommunityTeamCoupon
+ engine.In("id", ids).Find(&merchantScheme)
+ var merchantSchemeMap = make(map[int]model.CommunityTeamCoupon)
+ for _, v := range merchantScheme {
+ merchantSchemeMap[v.Id] = v
+ }
+
+ var couponList []md.CouponList // 可使用的
+ couponList = make([]md.CouponList, 0)
+ count := 0
+ for _, item := range ActCouponUserList {
+ var coupon = md.CouponList{
+ Id: utils.Int64ToStr(item.Id),
+ Title: item.Name,
+ Timer: item.ValidTimeStart.Format("2006.01.02") + "-" + item.ValidTimeEnd.Format("2006.01.02"),
+ Label: "全部商品可用",
+ Img: item.Img,
+ Content: item.ActivityStatement,
+ IsCanUse: "0",
+ NotUseStr: "",
+ }
+ var cal struct {
+ Reach string `json:"reach"`
+ Reduce string `json:"reduce"`
+ }
+ err = json.Unmarshal([]byte(item.Cal), &cal)
+ if err != nil {
+ return map[string]interface{}{}
+ }
+ switch item.Kind {
+ case int(enum.ActCouponTypeImmediate):
+ if utils.AnyToFloat64(totalPrice) >= utils.AnyToFloat64(cal.Reduce) {
+ coupon.IsCanUse = "1"
+ }
+ case int(enum.ActCouponTypeReachReduce):
+ if utils.AnyToFloat64(totalPrice) >= utils.AnyToFloat64(cal.Reduce) {
+ coupon.IsCanUse = "1"
+ }
+ case int(enum.ActCouponTypeReachDiscount):
+ if utils.AnyToFloat64(totalPrice) >= utils.AnyToFloat64(cal.Reduce) && utils.AnyToFloat64(cal.Reduce) > 0 {
+ coupon.IsCanUse = "1"
+ }
+ if utils.AnyToFloat64(cal.Reduce) == 0 {
+ coupon.IsCanUse = "1"
+ }
+ }
+ if coupon.IsCanUse != "1" {
+ coupon.NotUseStr = "订单金额未满" + cal.Reduce + "元"
+ }
+ if coupon.IsCanUse == "1" {
+ count++
+ }
+ couponList = append(couponList, coupon)
+ }
+
+ returnData := map[string]interface{}{
+ "total": utils.IntToStr(count),
+ "coupon_list": couponList,
+ }
+
+ return returnData
+}
+func OrderCancel(c *gin.Context) {
+ var arg map[string]string
+ if err := c.ShouldBindJSON(&arg); err != nil {
+ e.OutErr(c, e.ERR_INVALID_ARGS, err)
+ return
+ }
+ // 加锁 防止并发提取
+ mutexKey := fmt.Sprintf("%s:team.OrderCancel:%s", c.GetString("mid"), arg["oid"])
+ withdrawAvailable, err := cache.Do("SET", mutexKey, 1, "EX", 5, "NX")
+ if err != nil {
+ e.OutErr(c, e.ERR, err)
+ return
+ }
+ if withdrawAvailable != "OK" {
+ e.OutErr(c, e.ERR, e.NewErr(400000, "请求过于频繁,请稍后再试"))
+ return
+ }
+ sess := MasterDb(c).NewSession()
+ defer sess.Close()
+ sess.Begin()
+ order := db.GetOrder(sess, arg["oid"])
+ if order == nil {
+ sess.Rollback()
+ e.OutErr(c, 400, e.NewErr(400, "订单不存在"))
+ return
+ }
+ if order.State > 0 {
+ sess.Rollback()
+ e.OutErr(c, 400, e.NewErr(400, "订单不能取消"))
+ return
+ }
+ orderInfo := db.GetOrderInfo(sess, arg["oid"])
+ if orderInfo != nil {
+ goodsMap := make(map[int]int)
+ skuMap := make(map[int]int)
+ for _, v := range *orderInfo {
+ goodsMap[v.GoodsId] += v.Num
+ skuMap[v.SkuId] += v.Num
+ }
+ for k, v := range goodsMap {
+ sql := `update community_team_goods set stock=stock+%d where id=%d`
+ sql = fmt.Sprintf(sql, v, k)
+ _, err := db.QueryNativeStringWithSess(sess, sql)
+ if err != nil {
+ sess.Rollback()
+ e.OutErr(c, 400, e.NewErr(400, "订单取消失败"))
+ return
+ }
+ }
+ for k, v := range skuMap {
+ sql := `update community_team_sku set stock=stock+%d where sku_id=%d`
+ sql = fmt.Sprintf(sql, v, k)
+ _, err := db.QueryNativeStringWithSess(sess, sql)
+ if err != nil {
+ sess.Rollback()
+ e.OutErr(c, 400, e.NewErr(400, "订单取消失败"))
+ return
+ }
+ }
+ }
+ order.State = 3
+ order.UpdateAt = time.Now()
+ update, err := sess.Where("id=?", order.Id).Cols("state,update_at").Update(order)
+ if update == 0 || err != nil {
+ sess.Rollback()
+ e.OutErr(c, 400, e.NewErr(400, "订单取消失败"))
+ return
+ }
+ if order.CouponId > 0 {
+ update, err = sess.Where("id=?", order.CouponId).Cols("is_use").Update(&model.CommunityTeamCouponUser{IsUse: 0})
+ if update == 0 || err != nil {
+ sess.Rollback()
+ e.OutErr(c, 400, e.NewErr(400, "订单取消失败"))
+ return
+ }
+ }
+ sess.Commit()
+ e.OutSuc(c, "success", nil)
+ return
+}
+func OrderCreate(c *gin.Context) {
+ var arg md.OrderTotal
+ if err := c.ShouldBindJSON(&arg); err != nil {
+ e.OutErr(c, e.ERR_INVALID_ARGS, err)
+ return
+ }
+ user := GetUser(c)
+ // 加锁 防止并发提取
+ mutexKey := fmt.Sprintf("%s:team.OrderCreate:%s", c.GetString("mid"), utils.IntToStr(user.Info.Uid))
+ withdrawAvailable, err := cache.Do("SET", mutexKey, 1, "EX", 5, "NX")
+ if err != nil {
+ e.OutErr(c, e.ERR, err)
+ return
+ }
+ if withdrawAvailable != "OK" {
+ e.OutErr(c, e.ERR, e.NewErr(400000, "请求过于频繁,请稍后再试"))
+ return
+ }
+ sess := MasterDb(c).NewSession()
+ defer sess.Close()
+ err = sess.Begin()
+ if err != nil {
+ e.OutErr(c, 400, err.Error())
+ return
+ }
+ totalPrice := commGoods(c, arg)
+ coupon := "0"
+ totalPrice, coupon, err = CouponProcess(c, sess, totalPrice, arg)
+ if err != nil {
+ sess.Rollback()
+ e.OutErr(c, 400, err.Error())
+ return
+ }
+ ordId := utils.OrderUUID(user.Info.Uid)
+ // 获取店铺信息
+ store := db.GetStoreId(sess, arg.StoreId)
+ num := 0
+ for _, item := range arg.GoodsInfo {
+ num += utils.StrToInt(item.Num)
+ }
+ var order = &model.CommunityTeamOrder{
+ Uid: user.Info.Uid,
+ StoreUid: utils.StrToInt(arg.StoreId),
+ Commission: utils.Float64ToStr(utils.FloatFormat(utils.AnyToFloat64(totalPrice)*(utils.AnyToFloat64(store.Commission)/100), 2)),
+ CreateAt: time.Now(),
+ UpdateAt: time.Now(),
+ BuyPhone: arg.BuyPhone,
+ Coupon: coupon,
+ Num: num,
+ IsNow: utils.StrToInt(arg.IsNow),
+ Timer: arg.Timer,
+ Memo: arg.Memo,
+ Oid: utils.StrToInt64(ordId),
+ Amount: totalPrice,
+ MealNum: utils.StrToInt(arg.MealNum),
+ }
+ if utils.StrToFloat64(coupon) > 0 {
+ order.CouponId = utils.StrToInt(arg.CouponId)
+ }
+ insert, err := sess.Insert(order)
+ if insert == 0 || err != nil {
+ sess.Rollback()
+ e.OutErr(c, 400, e.NewErr(400, "下单失败"))
+ return
+ }
+ for _, item := range arg.GoodsInfo {
+ // 获取详细信息
+ goodsInterface, has, err := db.GetComm(MasterDb(c), &model.CommunityTeamGoods{Id: utils.StrToInt(item.GoodsId)})
+ if err != nil || !has {
+ sess.Rollback()
+ e.OutErr(c, 400, e.NewErr(400, "商品不存在"))
+ return
+ }
+ goodsModel := goodsInterface.(*model.CommunityTeamGoods)
+ var skuInterface interface{}
+ if item.SkuId != "-1" {
+ skuInterface, _, _ = db.GetComm(MasterDb(c), &model.CommunityTeamSku{GoodsId: utils.StrToInt(item.GoodsId), SkuId: utils.StrToInt64(item.SkuId)})
+ } else {
+ skuInterface, _, _ = db.GetComm(MasterDb(c), &model.CommunityTeamSku{GoodsId: utils.StrToInt(item.GoodsId)})
+ }
+ if err != nil || !has {
+ sess.Rollback()
+ e.OutErr(c, 400, e.NewErr(400, "商品不存在"))
+ return
+ }
+ skuModel := skuInterface.(*model.CommunityTeamSku)
+ var goodsSaleCount int
+ // 走普通逻辑
+ stock := skuModel.Stock - utils.StrToInt(item.Num)
+ saleCount := skuModel.SaleCount + utils.StrToInt(item.Num)
+ goodsSaleCount = goodsModel.SaleCount + utils.StrToInt(item.Num)
+ if stock < 0 {
+ sess.Rollback()
+ e.OutErr(c, 400, e.NewErr(400, "库存不足"))
+ return
+ }
+ update, err := sess.Where("sku_id=?", skuModel.SkuId).Cols("stock", "sale_count").Update(&model.CommunityTeamSku{Stock: stock, SaleCount: saleCount})
+ if err != nil {
+ sess.Rollback()
+ e.OutErr(c, 400, e.NewErr(400, "商品不存在"))
+ return
+ }
+ if update != 1 {
+ sess.Rollback()
+ e.OutErr(c, 400, e.NewErr(400, "商品不存在"))
+ return
+ }
+ // 更新销量
+ goodsModel.SaleCount = goodsSaleCount
+ goodsModel.Stock = goodsModel.Stock - utils.StrToInt(item.Num)
+ _, err = sess.Where("id = ?", goodsModel.Id).Cols("sale_count,stock").Update(goodsModel)
+ if err != nil {
+ sess.Rollback()
+ e.OutErr(c, 400, e.NewErr(400, "商品不存在"))
+ return
+ }
+
+ // 插入订单
+ insert, err := sess.Insert(&model.CommunityTeamOrderInfo{
+ Oid: utils.StrToInt64(ordId),
+ Title: goodsModel.Title,
+ Img: goodsModel.Img,
+ Price: skuModel.Price,
+ Num: utils.StrToInt(item.Num),
+ SkuInfo: skuModel.Sku,
+ GoodsId: skuModel.GoodsId,
+ SkuId: int(skuModel.SkuId),
+ })
+
+ if err != nil {
+ sess.Rollback()
+ e.OutErr(c, 400, e.NewErr(400, "下单失败"))
+ return
+ }
+ if insert != 1 {
+ sess.Rollback()
+ e.OutErr(c, 400, e.NewErr(400, "下单失败"))
+ return
+ }
+ }
+ // 更新优惠券使用状态
+ if utils.StrToInt(arg.CouponId) > 0 {
+ affect, err := sess.Where("id = ?", arg.CouponId).
+ Update(&model.CommunityTeamCouponUser{IsUse: 1})
+ if err != nil {
+ e.OutErr(c, 400, e.NewErr(400, "下单失败"))
+ return
+ }
+ if affect != 1 {
+ e.OutErr(c, 400, e.NewErr(400, "下单失败"))
+ return
+ }
+ }
+
+ err = sess.Commit()
+ if err != nil {
+ sess.Rollback()
+ e.OutErr(c, 400, err.Error())
+ return
+ }
+ sess.Commit()
+ return
+}
+func OrderTotal(c *gin.Context) {
+ var arg md.OrderTotal
+ if err := c.ShouldBindJSON(&arg); err != nil {
+ e.OutErr(c, e.ERR_INVALID_ARGS, err)
+ return
+ }
+ sess := MasterDb(c).NewSession()
+ defer sess.Close()
+ err := sess.Begin()
+ if err != nil {
+ e.OutErr(c, 400, err.Error())
+ return
+ }
+ totalPrice := commGoods(c, arg)
+ oldTotalPrice := totalPrice
+ coupon := "0"
+ totalPrice, coupon, err = CouponProcess(c, sess, totalPrice, arg)
+ if err != nil {
+ sess.Rollback()
+ e.OutErr(c, 400, err.Error())
+ return
+ }
+ user := GetUser(c)
+ result := map[string]interface{}{
+ "balance_money": GetCommissionPrec(c, user.Profile.FinValid, SysCfgGet(c, "commission_prec"), SysCfgGet(c, "is_show_point")),
+ "small_amount": GetCommissionPrec(c, oldTotalPrice, SysCfgGet(c, "commission_prec"), SysCfgGet(c, "is_show_point")),
+ "all_amount": GetCommissionPrec(c, totalPrice, SysCfgGet(c, "commission_prec"), SysCfgGet(c, "is_show_point")),
+ "coupon": GetCommissionPrec(c, coupon, SysCfgGet(c, "commission_prec"), SysCfgGet(c, "is_show_point")),
+ }
+ sess.Commit()
+ e.OutSuc(c, result, nil)
+ return
+}
+func CouponProcess(c *gin.Context, sess *xorm.Session, total string, args md.OrderTotal) (string, string, error) {
+ if utils.StrToInt(args.CouponId) == 0 {
+ return total, "0", nil
+ }
+ now := time.Now().Format("2006-01-02 15:04:05")
+ user := GetUser(c)
+ var goodsIds []int
+ var skuIds []string
+ for _, item := range args.GoodsInfo {
+ goodsIds = append(goodsIds, utils.StrToInt(item.GoodsId))
+ skuIds = append(skuIds, utils.AnyToString(item.SkuId))
+ }
+ // 获取优惠券信息
+ var mallUserCoupon model.CommunityTeamCouponUser
+ isExist, err := sess.
+ Where("id = ? AND uid = ? AND is_use = ? AND (valid_time_start < ? AND valid_time_end > ?)", args.CouponId, user.Info.Uid, 0, now, now).
+ Get(&mallUserCoupon)
+ if err != nil {
+ return "", "", err
+ }
+ if !isExist {
+ return "", "", errors.New("无相关优惠券信息")
+ }
+
+ var cal struct {
+ Reach string `json:"reach"`
+ Reduce string `json:"reduce"`
+ }
+ _ = json.Unmarshal([]byte(mallUserCoupon.Cal), &cal)
+ reach, err := decimal.NewFromString(cal.Reach)
+ reduce, err := decimal.NewFromString(cal.Reduce)
+ if err != nil {
+ return "", "", err
+ }
+
+ var specialTotal = total
+ // 是否满足优惠条件
+ if !reach.IsZero() { // 满减及有门槛折扣
+ if utils.StrToFloat64(specialTotal) < utils.StrToFloat64(reach.String()) {
+ return "", "", errors.New("不满足优惠条件")
+ }
+ } else {
+ if mallUserCoupon.Kind == 1 { //立减
+ if utils.StrToFloat64(specialTotal) < utils.StrToFloat64(reduce.String()) {
+ return "", "", errors.New("付款金额有误")
+ }
+ }
+ }
+ // 计算优惠后支付金额
+ couponTotal := "0"
+ if mallUserCoupon.Kind == int(enum.ActCouponTypeImmediate) ||
+ mallUserCoupon.Kind == int(enum.ActCouponTypeReachReduce) { // 立减 || 满减
+ couponTotal = reduce.String()
+ total = utils.Float64ToStr(utils.StrToFloat64(total) - utils.StrToFloat64(reduce.String()))
+ } else { // 折扣
+ couponTotal = utils.Float64ToStr(utils.StrToFloat64(total) - utils.StrToFloat64(total)*utils.StrToFloat64(reduce.String())/10)
+ total = utils.Float64ToStr(utils.StrToFloat64(total) * utils.StrToFloat64(reduce.String()) / 10)
+ }
+ return total, couponTotal, nil
+}
+
+func commGoods(c *gin.Context, arg md.OrderTotal) (totalPrice string) {
+ engine := MasterDb(c)
+ var totalPriceAmt float64 = 0
+ for _, item := range arg.GoodsInfo {
+ goodsInterface, _, _ := db.GetComm(engine, &model.CommunityTeamGoods{Id: utils.StrToInt(item.GoodsId)})
+ goodsModel := goodsInterface.(*model.CommunityTeamGoods)
+ var skuInterface interface{}
+ if item.SkuId != "-1" {
+ skuInterface, _, _ = db.GetComm(engine, &model.CommunityTeamSku{GoodsId: utils.StrToInt(item.GoodsId), SkuId: utils.StrToInt64(item.SkuId)})
+ } else {
+ skuInterface, _, _ = db.GetComm(engine, &model.CommunityTeamSku{GoodsId: utils.StrToInt(item.GoodsId)})
+ }
+ skuModel := skuInterface.(*model.CommunityTeamSku)
+ priceOne := goodsModel.Price
+ if item.SkuId != "-1" {
+ priceOne = skuModel.Price
+ }
+ totalPriceAmt += utils.StrToFloat64(priceOne) * utils.StrToFloat64(item.Num)
+ }
+ return utils.Float64ToStr(totalPriceAmt)
+
+}
diff --git a/app/svc/svc_pay.go b/app/svc/svc_pay.go
new file mode 100644
index 0000000..a888629
--- /dev/null
+++ b/app/svc/svc_pay.go
@@ -0,0 +1,21 @@
+package svc
+
+import (
+ "applet/app/md"
+ "github.com/gin-gonic/gin"
+)
+
+var PayFuncList = map[string]map[string]func(*gin.Context) (interface{}, error){
+ md.CommunityTeam: {
+ md.BALANCE_PAY: BalanceCommunityTeam,
+ md.ALIPAY: AlipayCommunityTeam,
+ md.WX_PAY: WxPayCommunityTeam,
+ },
+}
+var PayCallbackFuncList = map[string]map[string]func(*gin.Context){
+ md.CommunityTeam: {
+ md.BALANCE_PAY: nil,
+ md.ALIPAY: AlipayCallbackCommunityTeam,
+ md.WX_PAY: WxPayCallbackCommunityTeam,
+ },
+}
diff --git a/app/svc/svc_pay_community_team.go b/app/svc/svc_pay_community_team.go
new file mode 100644
index 0000000..e88b408
--- /dev/null
+++ b/app/svc/svc_pay_community_team.go
@@ -0,0 +1,142 @@
+package svc
+
+import (
+ "applet/app/db"
+ "applet/app/db/model"
+ "applet/app/e"
+ "applet/app/md"
+ "applet/app/utils"
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "github.com/shopspring/decimal"
+ "math"
+ "math/rand"
+ "time"
+)
+
+func BalanceCommunityTeam(c *gin.Context) (interface{}, error) {
+
+ ord, err := CheckCommunityTeam(c)
+ if err != nil || ord == nil {
+ return nil, err
+ }
+ err = BalancePay(c, ord.Amount, utils.Int64ToStr(ord.Oid), md.CommunityTeam)
+ if err != nil {
+ return nil, err
+ }
+ // 回调
+ CommonCallbackCommunityTeam(c, utils.AnyToString(ord.Oid), md.BALANCE_PAY)
+ return nil, nil
+}
+func AlipayCommunityTeam(c *gin.Context) (interface{}, error) {
+ ord, err := CheckCommunityTeam(c)
+ if err != nil {
+ return nil, err
+ }
+ payParams := &md.AliPayPayParams{
+ Subject: "小店下单",
+ Amount: ord.Amount,
+ OrdId: utils.AnyToString(ord.Oid),
+ OrderType: md.CommunityTeam,
+ Uid: utils.IntToStr(ord.Uid),
+ }
+ r, err := PrepareAlipayCode(c, payParams)
+ if err != nil {
+ return nil, err
+ }
+ return r, nil
+}
+func WxPayCommunityTeam(c *gin.Context) (interface{}, error) {
+ var r interface{}
+ var err error
+ ord, err := CheckCommunityTeam(c)
+ if err != nil {
+ return nil, err
+ }
+ params := map[string]string{
+ "subject": md.NeedPayPart[md.AggregationRecharge],
+ "amount": wxMoneyMulHundred(ord.Amount),
+ "order_type": md.AggregationRecharge,
+ "ord_id": utils.AnyToString(ord.Oid),
+ "pay_wx_mch_id": SysCfgGet(c, "pay_wx_mch_id"),
+ "pay_wx_api_key": SysCfgGet(c, "pay_wx_api_key"),
+ "uid": utils.IntToStr(ord.Uid),
+ }
+ params["notify_url"] = fmt.Sprintf(md.CALLBACK_URL, c.Request.Host, c.GetString("mid"), params["order_type"], md.WX_PAY)
+ r, err = CommPayData(c, params)
+ if err != nil {
+ return nil, err
+ }
+
+ return r, nil
+}
+func AlipayCallbackCommunityTeam(c *gin.Context) {
+ orderId, err := AlipayCallback(c)
+ if err != nil || orderId == "" {
+ return
+ }
+ CommonCallbackCommunityTeam(c, orderId, md.ALIPAY)
+}
+func WxPayCallbackCommunityTeam(c *gin.Context) {
+ orderId, err := wxPayCallback(c)
+ if err != nil || orderId == "" {
+ return
+ }
+ CommonCallbackCommunityTeam(c, orderId, md.WX_PAY)
+}
+
+// 微信金额乘100
+func wxMoneyMulHundred(m string) string {
+ amount, _ := decimal.NewFromString(m)
+ newM := amount.Mul(decimal.NewFromInt(100))
+ return newM.String()
+}
+func CommonCallbackCommunityTeam(c *gin.Context, orderId string, payMethod string) {
+ ord := db.GetOrderEg(db.DBs[c.GetString("mid")], orderId)
+ if ord == nil {
+ return
+ }
+ // 判断是否失效
+ if ord.State != 0 {
+ return
+ }
+ ord.State = 1
+ ord.UpdateAt = time.Now()
+ ord.Code = Code()
+ ord.PayAt = time.Now()
+ ord.PayMethod = md.PayMethodIDs[payMethod]
+ MasterDb(c).Where("id=?", ord.Id).Cols("state,update_at,code,pay_at,pay_method").Update(ord)
+ return
+}
+func Code() string {
+ rand.Seed(time.Now().UnixNano())
+ var digits = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
+ b := make([]rune, 6)
+ for i := range b {
+ if fl := float64(rand.Intn(10)); fl > math.Log10(float64(i+1)) {
+ b[i] = digits[rand.Intn(len(digits))]
+ } else {
+ b[i] = digits[rand.Intn(10)]
+ }
+ }
+ fmt.Println(string(b))
+ return string(b)
+}
+func CheckCommunityTeam(c *gin.Context) (*model.CommunityTeamOrder, error) {
+ var args struct {
+ MainOrdId string `json:"main_ord_id"`
+ }
+ if err := c.ShouldBindJSON(&args); err != nil || args.MainOrdId == "" {
+ return nil, e.NewErrCode(e.ERR_INVALID_ARGS)
+ }
+ // 查询订单
+ ord := db.GetOrderEg(db.DBs[c.GetString("mid")], args.MainOrdId)
+ if ord == nil {
+ return nil, e.NewErr(403000, "不存在该订单")
+ }
+ // 判断是否失效
+ if ord.State != 0 {
+ return nil, e.NewErr(403000, "订单已处理")
+ }
+ return ord, 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..f35e0f9
--- /dev/null
+++ b/app/svc/svc_redis_mutex_lock.go
@@ -0,0 +1,100 @@
+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
+}
+
+func HandleLimiterDistributedLock(masterId, ip, requestIdPrefix string) (cb func(), err error) {
+ balanceLockKey := fmt.Sprintf(md.AppLimiterLock, masterId, ip)
+ 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_store.go b/app/svc/svc_store.go
new file mode 100644
index 0000000..0ec26ab
--- /dev/null
+++ b/app/svc/svc_store.go
@@ -0,0 +1,135 @@
+package svc
+
+import (
+ "applet/app/db"
+ "applet/app/db/model"
+ "applet/app/e"
+ "applet/app/utils"
+ "github.com/gin-gonic/gin"
+ "time"
+)
+
+func BankStore(c *gin.Context) {
+ var arg map[string]string
+ if err := c.ShouldBindJSON(&arg); err != nil {
+ e.OutErr(c, e.ERR_INVALID_ARGS, err)
+ return
+ }
+ arg["store_type"] = "0"
+ user, _ := GetDefaultUser(c, c.GetHeader("Authorization"))
+ var store = make([]map[string]string, 0)
+ if arg["cid"] == "2" {
+ store = db.GetStoreLike(MasterDb(c), arg)
+ } else {
+ store = db.GetStore(MasterDb(c), arg)
+ }
+ storeList := make([]map[string]interface{}, 0)
+ for _, v := range store {
+ if utils.StrToFloat64(v["km"]) < 1 {
+ v["km"] = utils.IntToStr(int(utils.StrToFloat64(v["km"])*1000)) + "m"
+ } else {
+ v["km"] += "km"
+ }
+ if utils.StrToFloat64(v["km"]) <= 0 {
+ v["km"] = "-"
+ }
+ tmp := map[string]interface{}{
+ "lat": v["lat"],
+ "lng": v["lng"],
+ "address": v["address"],
+ "name": v["name"],
+ "id": v["id"],
+ "km": v["km"],
+ "time_str": v["timer"],
+ "phone": v["phone"],
+ "logo": v["logo"],
+ "is_like": "0",
+ "fan": "",
+ }
+ if user != nil {
+ count, _ := MasterDb(c).Where("uid=? and store_id=?", user.Info.Uid, v["id"]).Count(&model.CommunityTeamStoreLike{})
+ if count > 0 {
+ tmp["is_like"] = "1"
+ }
+ }
+ storeList = append(storeList, tmp)
+ }
+ e.OutSuc(c, storeList, nil)
+ return
+}
+func Store(c *gin.Context) {
+ var arg map[string]string
+ if err := c.ShouldBindJSON(&arg); err != nil {
+ e.OutErr(c, e.ERR_INVALID_ARGS, err)
+ return
+ }
+ arg["store_type"] = "0"
+
+ user, _ := GetDefaultUser(c, c.GetHeader("Authorization"))
+ store := db.GetStore(MasterDb(c), arg)
+ storeList := make([]map[string]interface{}, 0)
+ for _, v := range store {
+ if utils.StrToFloat64(v["km"]) < 1 {
+ v["km"] = utils.IntToStr(int(utils.StrToFloat64(v["km"])*1000)) + "m"
+ } else {
+ v["km"] += "km"
+ }
+ label := make([]string, 0)
+ tmp := map[string]interface{}{
+ "lat": v["lat"],
+ "lng": v["lng"],
+ "address": v["address"],
+ "work_state": v["work_state"],
+ "name": v["name"],
+ "id": v["id"],
+ "km": v["km"],
+ "time_str": v["timer"],
+ "phone": v["phone"],
+ "label": label,
+ "is_like": "0",
+ }
+ if user != nil {
+ count, _ := MasterDb(c).Where("uid=? and store_id=?", user.Info.Uid, v["id"]).Count(&model.CommunityTeamStoreLike{})
+ if count > 0 {
+ tmp["is_like"] = "1"
+ }
+ }
+
+ storeList = append(storeList, tmp)
+ }
+ e.OutSuc(c, storeList, nil)
+ return
+}
+func StoreAddLike(c *gin.Context) {
+ var arg map[string]string
+ if err := c.ShouldBindJSON(&arg); err != nil {
+ e.OutErr(c, e.ERR_INVALID_ARGS, err)
+ return
+ }
+ user := GetUser(c)
+ count, _ := MasterDb(c).Where("uid=? and store_id=?", user.Info.Uid, arg["id"]).Count(&model.CommunityTeamStoreLike{})
+ if count > 0 {
+ e.OutErr(c, 400, e.NewErr(400, "已收藏"))
+ return
+ }
+ var data = model.CommunityTeamStoreLike{
+ Uid: user.Info.Uid,
+ StoreId: utils.StrToInt(arg["id"]),
+ Time: time.Now(),
+ }
+ MasterDb(c).Insert(&data)
+ e.OutSuc(c, "success", nil)
+ return
+}
+
+func StoreCancelLike(c *gin.Context) {
+ var arg map[string]string
+ if err := c.ShouldBindJSON(&arg); err != nil {
+ e.OutErr(c, e.ERR_INVALID_ARGS, err)
+ return
+ }
+ user := GetUser(c)
+ MasterDb(c).Where("uid=? and store_id=?", user.Info.Uid, arg["id"]).Delete(&model.CommunityTeamStoreLike{})
+ e.OutSuc(c, "success", nil)
+ return
+}
diff --git a/app/svc/svc_store_order.go b/app/svc/svc_store_order.go
new file mode 100644
index 0000000..79cc221
--- /dev/null
+++ b/app/svc/svc_store_order.go
@@ -0,0 +1,277 @@
+package svc
+
+import (
+ "applet/app/db"
+ "applet/app/e"
+ "applet/app/md"
+ "applet/app/utils"
+ "applet/app/utils/cache"
+ "encoding/json"
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "time"
+)
+
+func StoreOrderCate(c *gin.Context) {
+ var cate = []map[string]string{
+ {"name": "全部", "value": ""},
+ {"name": "待付款", "value": "0"},
+ {"name": "待提货", "value": "1"},
+ {"name": "已完成", "value": "2"},
+ {"name": "已取消", "value": "3"},
+ }
+ e.OutSuc(c, cate, nil)
+ return
+}
+func StoreOrderList(c *gin.Context) {
+ var arg map[string]string
+ if err := c.ShouldBindJSON(&arg); err != nil {
+ e.OutErr(c, e.ERR_INVALID_ARGS, err)
+ return
+ }
+ user := GetUser(c)
+ arg["store_uid"] = utils.IntToStr(user.Info.Uid)
+ data := db.GetOrderList(MasterDb(c), arg)
+ var state = []string{"待付款", "待提货", "已完成", "已取消"}
+ list := make([]map[string]interface{}, 0)
+ if data != nil {
+ now := time.Now().Unix()
+ for _, v := range *data {
+ store := db.GetStoreIdEg(MasterDb(c), utils.IntToStr(v.StoreUid))
+ info := db.GetOrderInfoAllEg(MasterDb(c), utils.Int64ToStr(v.Oid))
+ downTime := "0"
+ if v.State == 0 {
+ downTime = utils.IntToStr(int(v.CreateAt.Unix() + 15*60 - now))
+ if now > v.CreateAt.Unix()+15*60 {
+ v.State = 3
+ }
+ if utils.StrToInt(downTime) < 0 {
+ downTime = "0"
+ }
+ }
+ storeName := ""
+ if store != nil {
+ storeName = store.Name
+ }
+ goodsInfo := make([]map[string]string, 0)
+ if info != nil {
+ for _, v1 := range *info {
+ skuData := make([]md.Sku, 0)
+ json.Unmarshal([]byte(v1.SkuInfo), &skuData)
+ skuStr := ""
+ for _, v2 := range skuData {
+ if skuStr != "" {
+ skuStr += ";"
+ }
+ skuStr += v2.Value
+ }
+ if skuStr != "" {
+ skuStr = "(" + skuStr + ")"
+ }
+ tmp := map[string]string{
+ "title": v1.Title + skuStr,
+ "num": utils.IntToStr(v1.Num),
+ "img": v1.Img,
+ }
+ goodsInfo = append(goodsInfo, tmp)
+ }
+ }
+ user1, _ := db.UserFindByID(MasterDb(c), v.Uid)
+ userProfile, _ := db.UserProfileFindByID(MasterDb(c), v.Uid)
+ nickname := ""
+ headImg := ""
+ if userProfile != nil {
+ headImg = userProfile.AvatarUrl
+ }
+ if user1 != nil {
+ if user1.Nickname != user1.Phone {
+ user1.Nickname += " " + user1.Phone
+ }
+ nickname = user1.Nickname
+ }
+ tmp := map[string]interface{}{
+ "goods_info": goodsInfo,
+ "oid": utils.Int64ToStr(v.Oid),
+ "label": "自提",
+ "state": utils.IntToStr(v.State),
+ "state_str": state[v.State],
+ "store_name": storeName,
+ "coupon": v.Coupon,
+ "username": nickname,
+ "head_img": headImg,
+ "amount": v.Amount,
+ "num": utils.IntToStr(v.Num),
+ "timer": "",
+ "down_time": downTime,
+ "create_at": v.CreateAt.Format("2006-01-02 15:04:05"),
+ "pay_at": "",
+ "confirm_at": "",
+ }
+ if v.PayAt.IsZero() == false {
+ tmp["pay_at"] = v.PayAt.Format("2006-01-02 15:04:05")
+ }
+ if v.ConfirmAt.IsZero() == false {
+ tmp["confirm_at"] = v.ConfirmAt.Format("2006-01-02 15:04:05")
+ }
+ if v.Type == 1 {
+ tmp["label"] = "外卖"
+ }
+ if v.IsNow == 1 {
+ tmp["timer"] = "立即提货"
+ } else if v.Timer != "" {
+ tmp["timer"] = v.Timer
+ }
+ list = append(list, tmp)
+ }
+ }
+ e.OutSuc(c, list, nil)
+ return
+}
+func StoreOrderDetail(c *gin.Context) {
+ var arg map[string]string
+ if err := c.ShouldBindJSON(&arg); err != nil {
+ e.OutErr(c, e.ERR_INVALID_ARGS, err)
+ return
+ }
+ data := db.GetOrderEg(MasterDb(c), arg["oid"])
+ var state = []string{"待付款", "待提货", "已完成", "已取消"}
+ now := time.Now().Unix()
+ downTime := "0"
+ if data.State == 0 {
+ downTime = utils.IntToStr(int(data.CreateAt.Unix() + 15*60 - now))
+ if now > data.CreateAt.Unix()+15*60 {
+ data.State = 3
+ }
+ if utils.StrToInt(downTime) < 0 {
+ downTime = "0"
+ }
+ }
+ img := ""
+ title := ""
+ confirmAt := ""
+ if data.ConfirmAt.IsZero() == false {
+ confirmAt = data.ConfirmAt.Format("2006-01-02 15:04:05")
+ }
+ payAt := ""
+ if data.PayAt.IsZero() == false {
+ payAt = data.PayAt.Format("2006-01-02 15:04:05")
+ }
+ timer := ""
+ if data.IsNow == 1 {
+ timer = "立即提货"
+ } else if data.Timer != "" {
+ timer = data.Timer
+ }
+ orderInfo := []map[string]string{
+ {"title": "订单编号", "content": utils.Int64ToStr(data.Oid)},
+ {"title": "提货码", "content": data.Code},
+ {"title": "下单时间", "content": data.CreateAt.Format("2006-01-02 15:04:05")},
+ {"title": "付款时间", "content": payAt},
+ {"title": "预计提货时间", "content": timer},
+ {"title": "提货时间", "content": confirmAt},
+ {"title": "预留电话", "content": data.Phone},
+ {"title": "备注信息", "content": data.Memo},
+ }
+ goodsInfo := make([]map[string]string, 0)
+ info := db.GetOrderInfoAllEg(MasterDb(c), utils.Int64ToStr(data.Oid))
+ if info != nil {
+ for _, v := range *info {
+ tmp := map[string]string{
+ "img": v.Img,
+ "title": v.Title,
+ "price": v.Price,
+ "num": utils.IntToStr(v.Num),
+ "sku_str": "",
+ }
+ skuData := make([]md.Sku, 0)
+ json.Unmarshal([]byte(v.SkuInfo), &skuData)
+ skuStr := ""
+ for _, v1 := range skuData {
+ if skuStr != "" {
+ skuStr += ";"
+ }
+ skuStr += v1.Value
+ }
+ tmp["sku_str"] = skuStr
+ goodsInfo = append(goodsInfo, tmp)
+ }
+ }
+ user1, _ := db.UserFindByID(MasterDb(c), data.Uid)
+ userProfile, _ := db.UserProfileFindByID(MasterDb(c), data.Uid)
+ nickname := ""
+ headImg := ""
+ if userProfile != nil {
+ headImg = userProfile.AvatarUrl
+ }
+ if user1 != nil {
+ if user1.Nickname != user1.Phone {
+ user1.Nickname += " " + user1.Phone
+ }
+ nickname = user1.Nickname
+ }
+ tmp := map[string]interface{}{
+ "oid": utils.Int64ToStr(data.Oid),
+ "label": "自提",
+ "username": nickname,
+ "head_img": headImg,
+ "state": utils.IntToStr(data.State),
+ "state_str": state[data.State],
+ "img": img,
+ "title": title,
+ "amount": data.Amount,
+ "all_amount": utils.Float64ToStr(utils.StrToFloat64(data.Amount) + utils.StrToFloat64(data.Coupon)),
+ "coupon": data.Coupon,
+ "num": utils.IntToStr(data.Num),
+ "code": data.Code,
+ "down_time": downTime,
+ "order_info": orderInfo,
+ "goods_info": goodsInfo,
+ "goods_count": utils.IntToStr(len(goodsInfo)),
+ }
+ if data.Type == 1 {
+ tmp["label"] = "外卖"
+ }
+ e.OutSuc(c, tmp, nil)
+ return
+}
+func StoreOrderConfirm(c *gin.Context) {
+ var arg map[string]string
+ if err := c.ShouldBindJSON(&arg); err != nil {
+ e.OutErr(c, e.ERR_INVALID_ARGS, err)
+ return
+ }
+ // 加锁 防止并发提取
+ mutexKey := fmt.Sprintf("%s:team.StoreOrderConfirm:%s", c.GetString("mid"), arg["oid"])
+ withdrawAvailable, err := cache.Do("SET", mutexKey, 1, "EX", 5, "NX")
+ if err != nil {
+ e.OutErr(c, e.ERR, err)
+ return
+ }
+ if withdrawAvailable != "OK" {
+ e.OutErr(c, e.ERR, e.NewErr(400000, "请求过于频繁,请稍后再试"))
+ return
+ }
+ sess := MasterDb(c).NewSession()
+ defer sess.Close()
+ sess.Begin()
+ order := db.GetOrder(sess, arg["oid"])
+ if order == nil {
+ sess.Rollback()
+ e.OutErr(c, 400, e.NewErr(400, "订单不存在"))
+ return
+ }
+ if order.State != 1 {
+ sess.Rollback()
+ e.OutErr(c, 400, e.NewErr(400, "订单不能确认"))
+ return
+ }
+ order.State = 2
+ order.UpdateAt = time.Now()
+ update, err := sess.Where("id=?", order.Id).Cols("state,update_at").Update(order)
+ if update == 0 || err != nil {
+ e.OutErr(c, 400, e.NewErr(400, "订单确认失败"))
+ return
+ }
+ e.OutSuc(c, "success", nil)
+ return
+}
diff --git a/app/svc/svc_sys_cfg_get.go b/app/svc/svc_sys_cfg_get.go
new file mode 100644
index 0000000..976a7a3
--- /dev/null
+++ b/app/svc/svc_sys_cfg_get.go
@@ -0,0 +1,218 @@
+package svc
+
+import (
+ "applet/app/md"
+ "errors"
+ "github.com/gin-gonic/gin"
+ "strings"
+ "xorm.io/xorm"
+
+ "applet/app/cfg"
+ "applet/app/db"
+
+ "applet/app/utils"
+ "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
+}
+
+// SysCfgGetByMasterId get one config by master id
+func SysCfgGetByMasterId(masterId, key string) string {
+ res := SysCfgFindComm(masterId, key)
+ if _, ok := res[key]; !ok {
+ return ""
+ }
+ return res[key]
+}
+
+// SysCfgFindComm get cfg by master id
+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)
+}
+
+// 多条记录获取
+func SysCfgFindByIds(eg *xorm.Engine, keys ...string) map[string]string {
+ key := utils.Md5(eg.DataSourceName() + md.KEY_SYS_CFG_CACHE)
+ res := map[string]string{}
+ c, ok := cfg.MemCache.Get(key).(map[string]string)
+ if !ok || len(c) == 0 {
+ cfgList, _ := db.DbsSysCfgGetAll(eg)
+ if cfgList == nil {
+ return nil
+ }
+ for _, v := range *cfgList {
+ res[v.Key] = v.Val
+ }
+ cfg.MemCache.Put(key, res, 10)
+ } else {
+ res = c
+ }
+ 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 SysCfgFindByIdsToCache(eg *xorm.Engine, dbName string, keys ...string) map[string]string {
+ key := utils.Md5(dbName + md.KEY_SYS_CFG_CACHE)
+ res := map[string]string{}
+ c, ok := cfg.MemCache.Get(key).(map[string]string)
+ if !ok || len(c) == 0 {
+ cfgList, _ := db.DbsSysCfgGetAll(eg)
+ if cfgList == nil {
+ return nil
+ }
+ for _, v := range *cfgList {
+ res[v.Key] = v.Val
+ }
+ cfg.MemCache.Put(key, res, 1800)
+ } else {
+ res = c
+ }
+ 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 SysCfgFindPayment(c *gin.Context) ([]map[string]string, error) {
+ platform := c.GetHeader("platform")
+ payCfg := SysCfgFind(c, "pay_wx_pay_img", "pay_ali_pay_img", "pay_balance_img", "pay_type")
+ if payCfg["pay_wx_pay_img"] == "" || payCfg["pay_ali_pay_img"] == "" || payCfg["pay_balance_img"] == "" || payCfg["pay_type"] == "" {
+ return nil, errors.New("lack of payment config")
+ }
+ payCfg["pay_wx_pay_img"] = ImageFormat(c, payCfg["pay_wx_pay_img"])
+ payCfg["pay_ali_pay_img"] = ImageFormat(c, payCfg["pay_ali_pay_img"])
+ payCfg["pay_balance_img"] = ImageFormat(c, payCfg["pay_balance_img"])
+
+ var result []map[string]string
+
+ if strings.Contains(payCfg["pay_type"], "aliPay") && platform != md.PLATFORM_WX_APPLET {
+ item := make(map[string]string)
+ item["pay_channel"] = "alipay"
+ item["img"] = payCfg["pay_ali_pay_img"]
+ item["name"] = "支付宝支付"
+ result = append(result, item)
+ }
+
+ if strings.Contains(payCfg["pay_type"], "wxPay") {
+ item := make(map[string]string)
+ item["pay_channel"] = "wx"
+ item["img"] = payCfg["pay_wx_pay_img"]
+ item["name"] = "微信支付"
+ result = append(result, item)
+ }
+
+ if strings.Contains(payCfg["pay_type"], "walletPay") {
+ item := make(map[string]string)
+ item["pay_channel"] = "fin"
+ item["img"] = payCfg["pay_balance_img"]
+ item["name"] = "余额支付"
+ result = append(result, item)
+ }
+
+ return result, nil
+}
diff --git a/app/svc/svc_wx.go b/app/svc/svc_wx.go
new file mode 100644
index 0000000..c551fbf
--- /dev/null
+++ b/app/svc/svc_wx.go
@@ -0,0 +1,103 @@
+package svc
+
+import (
+ "applet/app/db"
+ "applet/app/e"
+ "applet/app/md"
+ "applet/app/utils/logx"
+ "code.fnuoos.com/go_rely_warehouse/zyos_go_pay.git/pay"
+ "github.com/gin-gonic/gin"
+ "strings"
+)
+
+// 微信支付回调处理
+func wxPayCallback(c *gin.Context) (string, error) {
+ data, ok := c.Get("callback")
+ if data == nil || !ok {
+ return "", e.NewErrCode(e.ERR_INVALID_ARGS)
+ }
+ args := data.(*md.WxPayCallback)
+ _, ok = db.DBs[args.MasterID]
+ if !ok {
+ return "", logx.Warn("wxpay Failed : master_id not found")
+ }
+ c.Set("mid", args.MasterID)
+ //回调交易状态失败
+ if args.ResultCode != "SUCCESS" || args.ReturnCode != "SUCCESS" {
+ return "", logx.Warn("wxpay Failed : trade status failed")
+ }
+ return args.OutTradeNo, nil
+}
+func CommPayData(c *gin.Context, params map[string]string) (interface{}, error) {
+ platform := c.GetHeader("Platform")
+ browser := c.GetHeader("browser")
+ var r interface{}
+ var err error
+ switch platform {
+ case md.PLATFORM_WX_APPLET:
+ params = WxMiniProgPayConfig(c, params)
+ r, err = pay.WxMiniProgPay(params)
+ case md.PLATFORM_WAP:
+ if strings.Contains(browser, "wx_pay_browser") {
+ params = WxJsApiConfig(c, params)
+ r, err = pay.WxAppJSAPIPay(params)
+ } else {
+ params = WxH5PayConfig(c, params)
+ r, err = pay.WxH5Pay(params)
+ }
+ case md.PLATFORM_ANDROID, md.PLATFORM_IOS, md.PLATFORM_JSAPI:
+ params = WxAPPConfig(c, params)
+ r, err = pay.WxAppPay(params)
+ default:
+ return nil, e.NewErrCode(e.ERR_PLATFORM)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ return r, nil
+}
+func WxH5PayConfig(c *gin.Context, params map[string]string) map[string]string {
+ params["pay_wx_appid"] = SysCfgGet(c, "wx_official_account_app_id")
+ return params
+}
+func WxAPPConfig(c *gin.Context, params map[string]string) map[string]string {
+ params["pay_wx_appid"] = SysCfgGet(c, "pay_wx_appid")
+ return params
+}
+
+// 小程序v2
+func WxMiniProgPayConfig(c *gin.Context, params map[string]string) map[string]string {
+ //读取小程序设置的
+ wxAppletCfg := db.GetAppletKey(c, MasterDb(c))
+ params["pay_wx_appid"] = wxAppletCfg["app_id"]
+ // 兼容未登录支付 api/v1/unlogin/pay/:payMethod/:orderType(因为该路由未经过jwt-auth中间件)
+ user, err := CheckUser(c)
+ if user == nil || err != nil {
+ return params
+ }
+ if c.GetHeader("openid") != "" { //前端会传过来
+ user.Profile.ThirdPartyWechatMiniOpenid = c.GetHeader("openid")
+ }
+ if user.Profile.ThirdPartyWechatMiniOpenid == "" {
+ return params
+ }
+ params["third_party_wechat_openid"] = user.Profile.ThirdPartyWechatMiniOpenid
+ return params
+}
+func WxJsApiConfig(c *gin.Context, params map[string]string) map[string]string {
+ params["pay_wx_appid"] = SysCfgGet(c, "wx_official_account_app_id")
+ // 兼容未登录支付 api/v1/unlogin/pay/:payMethod/:orderType(因为该路由未经过jwt-auth中间件)
+ user, err := CheckUser(c)
+ if user == nil || err != nil {
+ return params
+ }
+ if c.GetHeader("openid") != "" { //前端会传过来
+ user.Profile.ThirdPartyWechatH5Openid = c.GetHeader("openid")
+ }
+ if user.Profile.ThirdPartyWechatH5Openid == "" {
+ return params
+ }
+ params["third_party_wechat_openid"] = user.Profile.ThirdPartyWechatH5Openid
+ return params
+}
diff --git a/app/task/init.go b/app/task/init.go
new file mode 100644
index 0000000..c35f775
--- /dev/null
+++ b/app/task/init.go
@@ -0,0 +1,300 @@
+package task
+
+import (
+ "github.com/robfig/cron/v3"
+ "time"
+
+ "applet/app/cfg"
+ "applet/app/db"
+ "applet/app/db/model"
+ "applet/app/md"
+ "applet/app/utils"
+ "applet/app/utils/logx"
+ "xorm.io/xorm"
+)
+
+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, 50)
+ workerNum = 50 // 智盟跟单并发数量
+ orderStatWorkerNum = 50 // 智盟跟单并发数量
+ tbagoworkerNum = 50 // 智盟跟单并发数量
+ tbsettleworkerNum = 50 // 智盟跟单并发数量
+ pddOrderWorkerNum = 50 // 拼多多跟单并发数量
+ orderSuccessWorkerNum = 50 //
+ tbOrderWorkerNum = 50 // 淘宝跟单并发数量
+ jdOrderWorkerNum = 50 // 京东跟单并发数量
+ wphOrderWorkerNum = 50 // 唯品会跟单并发数量
+ cardWorkerNum = 20 // 权益卡并发数量
+ tbRelationWorkerNum = 50 // 淘宝并发数量
+ hw365WorkerNum = 50 // 海威并发数量
+ hw365TourismWorkerNum = 50 // 海威并发数量
+ tbpubWorkerNum = 50 // 海威并发数量
+ liveWorkerNum = 50 // 海威并发数量
+ tikTokOwnWorkerNum = 50 // 海威并发数量
+ cardUpdateWorkerNum = 50 // 海威并发数量
+ lifeWorkerNum = 50 //生活服务跟单
+ pddWorkerNum = 50 //
+ oilWorkerNum = 50 //
+ otherWorkerNum = 50 // 淘宝, 苏宁, 考拉并发量
+ jdWorkerNum = 50 //
+ tikTokWorkerNum = 50 //
+ teamGoodsWorkerNum = 50
+ jdCh = make(chan int, 50)
+ jdWorkerNum1 = 50 //
+ orderStatCh = make(chan int, 50)
+ jdCh1 = make(chan int, 50)
+ oilCh = make(chan int, 50)
+ otherCh = make(chan int, 50)
+ otherTourismCh = make(chan int, 50)
+ liveOtherCh = make(chan int, 50)
+ teamGoodsCh = make(chan int, 50)
+ tikTokOwnOtherCh = make(chan int, 50)
+ cardUpdateCh = make(chan int, 50)
+ tbpubCh = make(chan int, 50)
+ cardCh = make(chan int, 20)
+ pddCh = make(chan int, 50)
+ tikTokCh = make(chan int, 50)
+ tbRefundCh = make(chan int, 50)
+ tbagodCh = make(chan int, 50)
+ tbsettleCh = make(chan int, 50)
+ pddFailCh = make(chan int, 50)
+ orderSuccessCh = make(chan int, 50)
+ tbRelationCh = make(chan int, 50)
+)
+
+func Init() {
+ // 初始化任务列表
+ initTasks()
+ var err error
+ timer = cron.New()
+ if baseEntryId, err = timer.AddFunc("@every 20m", 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() {
+ //v2
+ //jobs[md.KEY_CFG_CRON_BUCKLE] = taskOrderBuckle //
+ //jobs[md.KEY_CFG_CRON_CHECK_BUCKLE_ORDER] = taskCheckBuckleOrder //
+ //jobs[md.KEY_CFG_CRON_USER_RELATE] = taskUserRelate //
+
+ //v3
+ //jobs[md.KEY_CFG_CRON_TB12] = taskOrderTaobao12 //淘宝抓单
+ //jobs[md.KEY_CFG_CRON_TBBYAGOTIME] = taskAgoOrderTB //用于恢复个别时间丢单的
+ //jobs[md.KEY_CFG_CRON_TB_PUNISH_REFUND] = taskTbPunishOrderRefund //淘宝退款订单处理
+ //jobs[md.KEY_CFG_CRON_TBREFUND] = taskTbOrderRefund //淘宝退款订单处理
+ //jobs[md.KEY_CFG_CRON_PUBLISHER_RELATION] = taskTaobaoPublisherRelation //获取渠道信息
+ //jobs[md.KEY_CFG_CRON_PUBLISHER_RELATION_NEW] = taskTaobaoPublisherRelationNew //获取渠道信息
+ //jobs[md.KEY_CFG_CRON_TBSETTLEORDER] = taskOrderTaobaoSettleOrder //淘宝抓单结算订单
+
+ //v4
+ //jobs[md.KEY_CFG_CRON_PDD_SUCC] = taskOrderPddSucc
+ //jobs[md.KEY_CFG_CRON_PDDBYSTATUS] = taskOrderPddStatus
+ //jobs[md.KEY_CFG_CRON_PDDBYSTATUSSUCCESS] = taskOrderPddStatusSuccess
+ //jobs[md.KEY_CFG_CRON_PDDBYSTATUSFAIL] = taskOrderPddStatusFail
+ //jobs[md.KEY_CFG_CRON_PDDBYLOOPTIME] = taskLoopOrderPdd //拼多多创建时间循环当天
+ //jobs[md.KEY_CFG_CRON_PDDREFUND] = taskPddOrderRefund //拼多多退款订单处理
+ //jobs[md.KEY_CFG_CRON_PDDBYAGOTIME] = taskAgoOrderPdd //用于恢复个别时间丢单的
+ //jobs[md.KEY_CFG_CRON_PDDBYCREATETIME] = taskOrderPddByCreateTime //拼多多创建时间跟踪订单
+ //jobs[md.KEY_CFG_CRON_PDD] = taskOrderPdd
+
+ //v5-guide-settle
+ //jobs[md.KEY_CFG_CRON_SETTLE] = taskOrderSettle // 结算
+ //v5
+ //jobs[md.KEY_CFG_CRON_FREE_SETTLE] = taskOrderFreeSettle // 结算
+ //jobs[md.KEY_CFG_CRON_SECOND_FREE_SETTLE] = taskOrderSecondFreeSettle // 结算
+ //jobs[md.KEY_CFG_CRON_THIRD_FREE_SETTLE] = taskOrderMoreFreeSettle // 结算
+ //jobs[md.KEY_CFG_CRON_AGGREGATION_RECHARGE_SETTLE] = taskAggregationRechargeSettle //
+ //jobs[md.KEY_CFG_CRON_PLAYLET_SETTLE] = taskAggregationPlaylet //
+ //jobs[md.KEY_CFG_CRON_DUOYOUORD_SETTLE] = taskDuoYouSettle //
+ //jobs[md.KEY_CFG_CRON_LIANLIAN_SETTLE] = taskLianlianSettle //
+ //jobs[md.KEY_CFG_CRON_SWIPE_SETTLE] = taskSwipeSettle //
+ //jobs[md.KEY_CFG_CRON_USER_LV_UP_SETTLE] = taskUserLvUpOrderSettle // 会员费订单结算
+ //jobs[md.KEY_CFG_CRON_PRIVILEGE_CARD_SETTLE] = taskPrivilegeCardOrderSettle // 权益卡订单结算
+ //jobs[md.KEY_CFG_CRON_ACQUISITION_CONDITION] = taskAcquisitionCondition
+ //jobs[md.KEY_CFG_CRON_ACQUISITION_CONDITION_BY_LV] = taskAcquisitionConditionByLv
+ //jobs[md.KEY_CFG_CRON_ACQUISITION_REWARD] = taskAcquisitionReward
+ //jobs[md.KEY_CFG_CRON_NEW_ACQUISTION_SETTLE] = taskNewAcquisition // 拉新
+ //jobs[md.KEY_CFG_CRON_ACQUISTION_SETTLE] = taskAcquisition // 拉新
+ //jobs[md.KEY_CFG_VERIFY] = taskVerify //团长
+
+ //v7
+ //jobs[md.KEY_CFG_CRON_JD] = taskOrderJd
+ //jobs[md.KEY_CFG_CRON_JDFAILBYCREATETIME] = taskOrderJDFailByCreateTime //拼多多创建时间跟踪订单
+ //jobs[md.KEY_CFG_CRON_JDBYCREATETIME] = taskOrderJDByCreateTime //拼多多创建时间跟踪订单
+ //jobs[md.KEY_CFG_CRON_JDBYSUCCESS] = taskOrderJDBySuccess //拼多多创建时间跟踪订单
+ //jobs[md.KEY_CFG_CRON_ORDER_SUCCESS_CHECK] = taskOrderSuccessCheck //
+ //jobs[md.KEY_CFG_CRON_JDBYSTATUS] = taskOrderJdStatus
+ //jobs[md.KEY_CFG_CRON_JDREFUND] = taskJdOrderRefund //京东退款订单处理
+
+ //v6
+ jobs[md.KEY_CFG_CRON_TIKTOKLIFE] = taskOrderTikTokLife //抖音本地生活
+ jobs[md.KEY_CFG_KUAISHOU_AUTH] = taskKuaishouAuth //团长
+ jobs[md.KEY_CFG_TIK_TOK_TEAM_ORDER_PAY] = taskTikTokTeamOrder //团长
+ jobs[md.KEY_CFG_TIK_TOK_TEAM_ORDER_UPDATE] = taskTikTokTeamOrderUpdate //团长
+ jobs[md.KEY_CFG_TIK_TOK_TEAM_USER_BIND_BUYINID] = taskTikTokTeamUserBindBuyinid //达人buyin_id
+ jobs[md.KEY_CFG_KUAISHOU_TEAM_ORDER_PAY] = taskKuaishouTeamOrder //团长
+ jobs[md.KEY_CFG_KUAISHOU_TEAM_ORDER_UPDATE] = taskKuaishouTeamOrderUpdate //团长
+ jobs[md.KEY_CFG_CRON_AUTO_ADD_TIKTOK_GOODS] = taskAutoAddTikTokGoods //
+ jobs[md.KEY_CFG_CRON_KuaishouOwn] = taskOrderKuaishouOwn //
+ jobs[md.KEY_CFG_CRON_KuaishouOwnCreate] = taskOrderKuaishouOwnCreate //
+ jobs[md.KEY_CFG_CRON_KUAISHOU] = taskOrderKuaishou //
+ jobs[md.KEY_CFG_CRON_KUAISHOULIVE] = taskOrderKuaishouLive //
+ jobs[md.KEY_CFG_CRON_TIKTOKCsjp] = taskOrderTIKTokCsjp //
+ jobs[md.KEY_CFG_CRON_TIKTOKCsjpLive] = taskOrderTIKTokCsjpLive //
+ jobs[md.KEY_CFG_CRON_TIKTOKOwnCsjp] = taskOrderTIKTokOwnCsjp //
+ jobs[md.KEY_CFG_CRON_TIKTOKOwnCsjpLive] = taskOrderTIKTokOwnCsjpLive //
+ jobs[md.KEY_CFG_CRON_TIKTOKOwnCsjpActivity] = taskOrderTIKTokOwnCsjpActivity //
+ jobs[md.KEY_CFG_CRON_KUAISHOUOFFICIAL] = taskOrderKuaishouOfficial //
+ jobs[md.KEY_CFG_CRON_KUAISHOUOFFICIALLive] = taskOrderKuaishouOfficialLive //
+
+ //v8
+ //jobs[md.KEY_CFG_CRON_MEITUANLM_START] = taskOrderMeituanLmStart //智盟返回的美团联盟
+ //jobs[md.KEY_CFG_CRON_MEITUAN_START] = taskOrderMeituanStart //智盟返回的美团联盟
+ //jobs[md.KEY_CFG_CRON_MEITUAN] = taskOrderMeituan
+ //jobs[md.KEY_CFG_CRON_MEITUANLM] = taskOrderMeituanLm //智盟返回的美团联盟
+ //jobs[md.KEY_CFG_CRON_STATIONMEITUANLM] = taskOrderStationMeituanLm //站长自己美团联盟
+ //jobs[md.KEY_CFG_CRON_MEITUANOFFICIAL] = taskOrderMeituanOfficial //站长自己美团联盟
+
+ //v9
+ //jobs[md.KEY_CFG_CRON_ELM] = taskOrderElm //
+ //jobs[md.KEY_CFG_CRON_HEYTEA] = taskOrderHeytea //海威365喜茶
+ //jobs[md.KEY_CFG_CRON_PIZZA] = taskOrderPizza //海威365
+ //jobs[md.KEY_CFG_CRON_WALLACE] = taskOrderWallace //海威365
+ //jobs[md.KEY_CFG_CRON_TOURISM] = taskOrderTourism //海威365
+ //jobs[md.KEY_CFG_CRON_FLOWERCAKE] = taskOrderFlowerCake //海威365
+ //jobs[md.KEY_CFG_CRON_DELIVERY] = taskOrderDelivery //海威365
+ //jobs[md.KEY_CFG_CRON_BURGERKING] = taskOrderBurgerKing //海威365汉堡王
+ //jobs[md.KEY_CFG_CRON_STARBUCKS] = taskOrderStarbucks //海威365星巴克
+ //jobs[md.KEY_CFG_CRON_MCDONALD] = taskOrderMcdonald //海威365麦当劳
+ //jobs[md.KEY_CFG_CRON_HWMOVIE] = taskOrderHwMovie //海威365电影票
+ //jobs[md.KEY_CFG_CRON_NAYUKI] = taskOrderNayuki //海威365奈雪
+ //jobs[md.KEY_CFG_CRON_TO_KFC] = taskOrderToKfc //海威365
+ //jobs[md.KEY_CFG_CRON_PAGODA] = taskOrderPagoda //海威365
+ //jobs[md.KEY_CFG_CRON_LUCKIN] = taskOrderLuckin //海威365
+
+ //v10
+
+ ////jobs[md.KEY_CFG_CRON_WPHREFUND] = taskWphOrderRefund //唯品会退款订单处理
+ ////jobs[md.KEY_CFG_CRON_VIP] = taskOrderVip
+ //jobs[md.KEY_CFG_CRON_KFC] = taskOrderKfc
+ //jobs[md.KEY_CFG_CRON_CINEMA] = taskOrderCinema
+ //jobs[md.KEY_CFG_CRON_DUOMAI] = taskOrderDuomai //多麦跟单
+ //jobs[md.KEY_CFG_CRON_PLAYLET_ORDER] = taskPlayletOrder
+ //jobs[md.KEY_CFG_CRON_DIDI_ENERGY] = taskOrderDidiEnergy //滴滴加油
+ //jobs[md.KEY_CFG_CRON_T3_CAR] = taskOrderT3Car //T3打车
+ //jobs[md.KEY_CFG_CRON_DIDI_ONLINE_CAR] = taskOrderDidiOnlineCar //滴滴网约车
+ //jobs[md.KEY_CFG_CRON_KING_FLOWER] = taskOrderKingFlower //滴滴网约车
+ //jobs[md.KEY_CFG_CRON_DIDI_FREIGHT] = taskOrderDidiFreight //滴滴货运
+ //jobs[md.KEY_CFG_CRON_DIDI_CHAUFFEUR] = taskOrderDidiChauffeur //滴滴代驾
+ //jobs[md.KEY_CFG_CRON_OILSTATION] = taskOrderOilstation
+ //jobs[md.KEY_CFG_CRON_BRIGHTOILSTATION] = taskOrderBrightOilstation
+ //jobs[md.KEY_CFG_CRON_SN] = taskOrderSuning
+ //jobs[md.KEY_CFG_CRON_KL] = taskOrderKaola
+
+ ////原来的
+ //jobs[md.KEY_CFG_CRON_PlayLet_Total] = taskPlayletTotal //
+ //jobs[md.KEY_CFG_CRON_CHECK_GUIDE_STORE_ORDER] = taskCheckGuideStoreOrder //
+ //jobs[md.KEY_CFG_CRON_FAST_REFUND] = taskOrderFastRefund //
+ //jobs[md.KEY_CFG_CRON_FAST_SUCCESS] = taskOrderFastSuccess //
+ //jobs[md.KEY_CFG_CRON_DUOYOUORD] = taskOrderDuoYouOrd //
+ //jobs[md.KEY_CFG_CRON_TASKBOX] = taskOrderTaskBoxOrd //
+ //jobs[md.KEY_CFG_CRON_TASKBOXSECOND] = taskOrderTaskSecondOrd //
+ //jobs[md.KEY_CFG_CRON_CARD_CHECK_UPDATE] = taskCardCheckUpdate //权益卡退款
+ //jobs[md.KEY_CFG_CRON_CARD_UPDATE] = taskCardUpdate // 权益卡更新
+ //jobs[md.KEY_CFG_CRON_ORDER_STAT] = taskOrderStat // 订单统计
+ //jobs[md.KEY_CFG_CRON_GOODS_SHELF] = taskGoodsShelf //站内商品上下架
+ //jobs[md.KEY_CFG_CRON_CARD_RETURN] = taskCardReturn //权益卡退款
+ //jobs[md.KEY_CFG_CRON_DTKBRAND] = taskTaoKeBrandInfo // 大淘客品牌信息
+ //jobs[md.KEY_CFG_CRON_PUBLISHER] = taskTaobaoPublisherInfo // 淘宝备案信息绑定
+ //jobs[md.KEY_CFG_CRON_AUTO_UN_FREEZE] = taskAutoUnFreeze // 定时解冻
+
+ //先不用
+ //jobs[md.ZhimengCronPlayletVideoOrder] = taskPlayletVideoOrder //
+ //jobs[md.ZhimengCronPlayletVideoOrderYesterDay] = taskPlayletVideoOrderYesterday //
+ //jobs[md.ZhimengCronPlayletVideoOrderMonth] = taskPlayletVideoOrderMonth //
+ //jobs[md.ZhimengCronPlayletAdvOrderMonth] = taskPlayletAdvOrderMonth //
+ //jobs[md.ZhimengCronPlayletAdvOrder] = taskPlayletAdvOrder //
+ //jobs[md.ZhimengCronPlayletAdvOrderYesterDay] = taskPlayletAdvOrderYesterday //
+ //jobs[md.ZhimengCronPlayletAdvOrderYesterDayToMoney] = taskPlayletAdvOrderYesterdayToMoney //
+
+}
diff --git a/app/utils/aes.go b/app/utils/aes.go
new file mode 100644
index 0000000..5a39181
--- /dev/null
+++ b/app/utils/aes.go
@@ -0,0 +1,172 @@
+package utils
+
+import (
+ "applet/app/cfg"
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "encoding/base64"
+ "encoding/json"
+ "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)]
+}
+
+// AesAdminCurlPOST is 与后台接口加密交互
+func AesAdminCurlPOST(aesData string, url string, uid int) ([]byte, error) {
+ adminKey := cfg.Admin.AesKey
+ adminVI := cfg.Admin.AesIV
+ crypto := AesCrypt{
+ Key: []byte(adminKey),
+ Iv: []byte(adminVI),
+ }
+
+ encrypt, err := crypto.Encrypt([]byte(aesData))
+ if err != nil {
+ return nil, err
+ }
+
+ // 发送请求到后台
+ postData := map[string]string{
+ "postData": base64.StdEncoding.EncodeToString(encrypt),
+ }
+ fmt.Println(adminKey)
+ fmt.Println(adminVI)
+ fmt.Println("=======ADMIN请求=====")
+ fmt.Println(postData)
+ postDataByte, _ := json.Marshal(postData)
+ rdata, err := CurlPost(url, postDataByte, nil)
+ fmt.Println(">>>>>>>>>>>>>>>>>>>>>>>>>", err)
+
+ if err != nil {
+ return nil, err
+ }
+
+ FilePutContents("cash_out", fmt.Sprintf("curl Result返回:%s uid:%d, >>>>>>>>>>>>>>>>>>>>", string(rdata), uid))
+ pass, err := base64.StdEncoding.DecodeString(string(rdata))
+ if err != nil {
+ return nil, err
+ }
+ fmt.Println(pass)
+
+ decrypt, err := crypto.Decrypt(pass)
+ fmt.Println(err)
+
+ if err != nil {
+ return nil, err
+ }
+ return decrypt, nil
+}
diff --git a/app/utils/auth.go b/app/utils/auth.go
new file mode 100644
index 0000000..d7bd9ae
--- /dev/null
+++ b/app/utils/auth.go
@@ -0,0 +1,46 @@
+package utils
+
+import (
+ "errors"
+ "time"
+
+ "applet/app/lib/auth"
+
+ "github.com/dgrijalva/jwt-go"
+)
+
+// GenToken 生成JWT
+func GenToken(uid int, username, phone, appname, MiniOpenID, MiniSK string) (string, error) {
+ // 创建一个我们自己的声明
+ c := auth.JWTUser{
+ uid,
+ username,
+ phone,
+ appname,
+ MiniOpenID,
+ MiniSK,
+ jwt.StandardClaims{
+ ExpiresAt: time.Now().Add(auth.TokenExpireDuration).Unix(), // 过期时间
+ Issuer: "zyos", // 签发人
+ },
+ }
+ // 使用指定的签名方法创建签名对象
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
+ // 使用指定的secret签名并获得完整的编码后的字符串token
+ return token.SignedString(auth.Secret)
+}
+
+// ParseToken 解析JWT
+func ParseToken(tokenString string) (*auth.JWTUser, error) {
+ // 解析token
+ token, err := jwt.ParseWithClaims(tokenString, &auth.JWTUser{}, func(token *jwt.Token) (i interface{}, err error) {
+ return auth.Secret, nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ if claims, ok := token.Claims.(*auth.JWTUser); ok && token.Valid { // 校验token
+ return claims, nil
+ }
+ return nil, errors.New("invalid token")
+}
diff --git a/app/utils/base64.go b/app/utils/base64.go
new file mode 100644
index 0000000..661b552
--- /dev/null
+++ b/app/utils/base64.go
@@ -0,0 +1,117 @@
+package utils
+
+import (
+ "encoding/base64"
+ "fmt"
+ "regexp"
+)
+
+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
+ }
+}
+
+func JudgeBase64(str string) bool {
+ pattern := "^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$"
+ matched, err := regexp.MatchString(pattern, str)
+ if err != nil {
+ return false
+ }
+ if !(len(str)%4 == 0 && matched) {
+ return false
+ }
+ unCodeStr, err := base64.StdEncoding.DecodeString(str)
+ if err != nil {
+ return false
+ }
+ tranStr := base64.StdEncoding.EncodeToString(unCodeStr)
+ //return str==base64.StdEncoding.EncodeToString(unCodeStr)
+ if str == tranStr {
+ return true
+ }
+ return false
+}
diff --git a/app/utils/boolean.go b/app/utils/boolean.go
new file mode 100644
index 0000000..fdfd986
--- /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())
+}
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..4e5f047
--- /dev/null
+++ b/app/utils/cache/redis.go
@@ -0,0 +1,403 @@
+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
+}
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/cachesecond/base.go b/app/utils/cachesecond/base.go
new file mode 100644
index 0000000..126c4d3
--- /dev/null
+++ b/app/utils/cachesecond/base.go
@@ -0,0 +1,421 @@
+package cachesecond
+
+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/cachesecond/cache/cache.go b/app/utils/cachesecond/cache/cache.go
new file mode 100644
index 0000000..e43c5f0
--- /dev/null
+++ b/app/utils/cachesecond/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/cachesecond/cache/conv.go b/app/utils/cachesecond/cache/conv.go
new file mode 100644
index 0000000..6b700ae
--- /dev/null
+++ b/app/utils/cachesecond/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/cachesecond/cache/file.go b/app/utils/cachesecond/cache/file.go
new file mode 100644
index 0000000..5c4e366
--- /dev/null
+++ b/app/utils/cachesecond/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/cachesecond/cache/memory.go b/app/utils/cachesecond/cache/memory.go
new file mode 100644
index 0000000..0cc5015
--- /dev/null
+++ b/app/utils/cachesecond/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/cachesecond/redis.go b/app/utils/cachesecond/redis.go
new file mode 100644
index 0000000..99c5247
--- /dev/null
+++ b/app/utils/cachesecond/redis.go
@@ -0,0 +1,406 @@
+package cachesecond
+
+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, pwd 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
+ }
+ if pwd != "" {
+ c.Send("auth", pwd)
+ }
+ 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
+}
diff --git a/app/utils/cachesecond/redis_cluster.go b/app/utils/cachesecond/redis_cluster.go
new file mode 100644
index 0000000..c86bf55
--- /dev/null
+++ b/app/utils/cachesecond/redis_cluster.go
@@ -0,0 +1,622 @@
+package cachesecond
+
+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/cachesecond/redis_pool.go b/app/utils/cachesecond/redis_pool.go
new file mode 100644
index 0000000..501aa27
--- /dev/null
+++ b/app/utils/cachesecond/redis_pool.go
@@ -0,0 +1,324 @@
+package cachesecond
+
+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/cachesecond/redis_pool_cluster.go b/app/utils/cachesecond/redis_pool_cluster.go
new file mode 100644
index 0000000..30ea8ac
--- /dev/null
+++ b/app/utils/cachesecond/redis_pool_cluster.go
@@ -0,0 +1,617 @@
+package cachesecond
+
+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..4b88b99
--- /dev/null
+++ b/app/utils/convert.go
@@ -0,0 +1,434 @@
+package utils
+
+import (
+ "code.fnuoos.com/go_rely_warehouse/zyos_go_coupon.git/utils"
+ "encoding/binary"
+ "encoding/json"
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "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 Float64ToStrPrec4(f float64) string {
+ return strconv.FormatFloat(f, 'f', 4, 64)
+}
+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 Float64ToStrByPrec(f float64, prec int) string {
+ return strconv.FormatFloat(f, 'f', prec, 64)
+}
+func Float32ToStrByPrec(f float32, prec int) string {
+ return Float64ToStrByPrec(float64(f), prec)
+}
+func GetFloatByOne(count, num, mul float64, prec int) string {
+
+ return Float64ToStrByPrec(float64(int(float64(count)/num*mul))/mul, prec)
+}
+
+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 StrToFormatByType(c *gin.Context, s string, types string) string {
+ commPrec := "2"
+ if types == "commission" {
+ commPrec = c.GetString("commission_prec")
+ }
+ if types == "integral" {
+ commPrec = c.GetString("integral_prec")
+ }
+ if s == "" {
+ s = "0"
+ }
+ s = StrToFormat(c, s, utils.StrToInt(commPrec))
+ ex := strings.Split(s, ".")
+ if len(ex) == 2 && c.GetString("is_show_point") != "1" {
+ if utils.StrToFloat64(ex[1]) == 0 {
+ s = ex[0]
+ } else {
+ val := utils.Float64ToStrByPrec(utils.StrToFloat64(ex[1]), 0)
+ keyMax := 0
+ for i := 0; i < len(val); i++ {
+ ch := string(val[i])
+ fmt.Println(utils.StrToInt(ch))
+ if utils.StrToInt(ch) > 0 {
+ keyMax = i
+ }
+ }
+ valNew := val[0 : keyMax+1]
+ s = ex[0] + "." + strings.ReplaceAll(ex[1], val, valNew)
+ }
+ }
+ return s
+}
+func StrToFormat(c *gin.Context, s string, prec int) string {
+ ex := strings.Split(s, ".")
+ if len(ex) == 2 {
+ if StrToFloat64(ex[1]) == 0 && c.GetString("is_show_point") != "1" { //小数点后面为空就是不要小数点了
+ return ex[0]
+ }
+ //看取多少位
+ str := ex[1]
+ str1 := str
+ if prec < len(str) {
+ str1 = str[0:prec]
+ } else {
+ for i := 0; i < prec-len(str); i++ {
+ str1 += "0"
+ }
+ }
+ if prec > 0 {
+ return ex[0] + "." + str1
+ } else {
+ return ex[0]
+ }
+ }
+ return s
+}
+func StrToFormatEg(s string, prec int, is_show_point string) string {
+ ex := strings.Split(s, ".")
+ if len(ex) == 2 {
+ if StrToFloat64(ex[1]) == 0 && is_show_point != "1" { //小数点后面为空就是不要小数点了
+ return ex[0]
+ }
+ //看取多少位
+ str := ex[1]
+ str1 := str
+ if prec < len(str) {
+ str1 = str[0:prec]
+ } else {
+ for i := 0; i < prec-len(str); i++ {
+ str1 += "0"
+ }
+ }
+ if prec > 0 {
+ return ex[0] + "." + str1
+ } else {
+ return ex[0]
+ }
+ }
+ return s
+}
+
+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)
+}
+
+func IntToFloat64(i int) float64 {
+ s := strconv.Itoa(i)
+ res, err := strconv.ParseFloat(s, 64)
+ if err != nil {
+ return 0
+ }
+ return res
+}
+func Int64ToFloat64(i int64) float64 {
+ s := strconv.FormatInt(i, 10)
+ res, err := strconv.ParseFloat(s, 64)
+ if err != nil {
+ return 0
+ }
+ return res
+}
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/distance.go b/app/utils/distance.go
new file mode 100644
index 0000000..2a6923b
--- /dev/null
+++ b/app/utils/distance.go
@@ -0,0 +1,16 @@
+package utils
+
+import "math"
+
+//返回单位为:千米
+func GetDistance(lat1, lat2, lng1, lng2 float64) float64 {
+ radius := 6371000.0 //6378137.0
+ rad := math.Pi / 180.0
+ lat1 = lat1 * rad
+ lng1 = lng1 * rad
+ lat2 = lat2 * rad
+ lng2 = lng2 * rad
+ theta := lng2 - lng1
+ dist := math.Acos(math.Sin(lat1)*math.Sin(lat2) + math.Cos(lat1)*math.Cos(lat2)*math.Cos(theta))
+ return dist * radius / 1000
+}
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..93ed08f
--- /dev/null
+++ b/app/utils/file.go
@@ -0,0 +1,22 @@
+package utils
+
+import (
+ "os"
+ "path"
+ "strings"
+ "time"
+)
+
+// 获取文件后缀
+func FileExt(fname string) string {
+ return strings.ToLower(strings.TrimLeft(path.Ext(fname), "."))
+}
+
+func FilePutContents(fileName string, content string) {
+ fd, _ := os.OpenFile("./tmp/"+fileName+".log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
+ fd_time := time.Now().Format("2006-01-02 15:04:05")
+ fd_content := strings.Join([]string{"[", fd_time, "] ", content, "\n"}, "")
+ buf := []byte(fd_content)
+ fd.Write(buf)
+ fd.Close()
+}
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..55fa4ac
--- /dev/null
+++ b/app/utils/format.go
@@ -0,0 +1,197 @@
+package utils
+
+import (
+ "code.fnuoos.com/go_rely_warehouse/zyos_go_coupon.git/utils"
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "math"
+ "strings"
+)
+
+func CouponFormat(c *gin.Context, data string) string {
+ switch data {
+ case "0.00", "0", "":
+ return ""
+ default:
+ return GetPrec(c, data, "2")
+ }
+}
+func GetPrec(c *gin.Context, sum, commPrec string) string {
+ if sum == "" {
+ sum = "0"
+ }
+ sum = StrToFormat(c, sum, utils.StrToInt(commPrec))
+ ex := strings.Split(sum, ".")
+ if len(ex) == 2 && c.GetString("is_show_point") != "1" {
+ if utils.StrToFloat64(ex[1]) == 0 {
+ sum = ex[0]
+ } else {
+ val := utils.Float64ToStrByPrec(utils.StrToFloat64(ex[1]), 0)
+ keyMax := 0
+ for i := 0; i < len(val); i++ {
+ ch := string(val[i])
+ fmt.Println(utils.StrToInt(ch))
+ if utils.StrToInt(ch) > 0 {
+ keyMax = i
+ }
+ }
+ valNew := val[0 : keyMax+1]
+ sum = ex[0] + "." + strings.ReplaceAll(ex[1], val, valNew)
+ }
+ }
+ return sum
+}
+
+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 {
+ s = strings.ReplaceAll(s, "+", "")
+ if strings.Contains(s, "万") {
+ s = strings.ReplaceAll(s, "万", "")
+ s = Float64ToStrByPrec(StrToFloat64(s)*10000, 0)
+ }
+ ex := strings.Split(s, ".")
+ if len(ex) == 2 && StrToInt(ex[1]) == 0 {
+ s = ex[0]
+ }
+ if utils.StrToInt(s) > 0 {
+ s = Comm(s)
+ return "已售" + s
+ }
+ return "已售0"
+}
+func SaleCountFormat2(s string) string {
+ s = strings.ReplaceAll(s, "+", "")
+ if strings.Contains(s, "万") {
+ s = strings.ReplaceAll(s, "万", "")
+ s = Float64ToStrByPrec(StrToFloat64(s)*10000, 0)
+ }
+ ex := strings.Split(s, ".")
+ if len(ex) == 2 && StrToInt(ex[1]) == 0 {
+ s = ex[0]
+ }
+ if utils.StrToInt(s) > 0 {
+ s = Comm(s)
+
+ return s
+ }
+ return "0"
+}
+func SaleCountFormat1(s string) string {
+ s = strings.ReplaceAll(s, "+", "")
+ if strings.Contains(s, "万") {
+ s = strings.ReplaceAll(s, "万", "")
+ s = Float64ToStrByPrec(StrToFloat64(s)*10000, 0)
+ }
+ ex := strings.Split(s, ".")
+ if len(ex) == 2 && StrToInt(ex[1]) == 0 {
+ s = ex[0]
+ }
+ if utils.StrToInt(s) > 0 {
+ s = Comm(s)
+ return s
+ }
+ return "0"
+}
+func Comm(s string) string {
+ ex := strings.Split(s, ".")
+ if len(ex) == 2 && StrToInt(ex[1]) == 0 {
+ s = ex[0]
+ }
+ if utils.StrToInt(s) >= 10000 {
+ num := FloatFormat(StrToFloat64(s)/10000, 2)
+ numStr := Float64ToStr(num)
+ ex := strings.Split(numStr, ".")
+ if len(ex) == 2 {
+ if utils.StrToFloat64(ex[1]) == 0 {
+ numStr = ex[0]
+ } else {
+ val := utils.Float64ToStrByPrec(utils.StrToFloat64(ex[1]), 0)
+ keyMax := 0
+ for i := 0; i < len(val); i++ {
+ ch := string(val[i])
+ fmt.Println(utils.StrToInt(ch))
+ if utils.StrToInt(ch) > 0 {
+ keyMax = i
+ }
+ }
+ valNew := val[0 : keyMax+1]
+ numStr = ex[0] + "." + strings.ReplaceAll(ex[1], val, valNew)
+ }
+ }
+ s = numStr + "万"
+ }
+ //if StrToInt(s) >= 10000000 {
+ // num := FloatFormat(StrToFloat64(s)/10000000, 2)
+ // numStr := Float64ToStr(num)
+ // ex := strings.Split(numStr, ".")
+ // if len(ex) == 2 {
+ // if utils.StrToFloat64(ex[1]) == 0 {
+ // numStr = ex[0]
+ // } else {
+ // val := utils.Float64ToStrByPrec(utils.StrToFloat64(ex[1]), 0)
+ // keyMax := 0
+ // for i := 0; i < len(val); i++ {
+ // ch := string(val[i])
+ // fmt.Println(utils.StrToInt(ch))
+ // if utils.StrToInt(ch) > 0 {
+ // keyMax = i
+ // }
+ // }
+ // valNew := val[0 : keyMax+1]
+ // numStr = ex[0] + "." + strings.ReplaceAll(ex[1], val, valNew)
+ // }
+ // }
+ // s = numStr + "千万"
+ //}
+ return s
+}
+func CommissionFormat1(numStr string) string {
+ split := strings.Split(numStr, ".")
+ if len(split) == 2 {
+ if split[1] == "00" {
+ numStr = strings.ReplaceAll(numStr, ".00", "")
+ }
+ }
+ return numStr
+}
+
+// 小数格式化
+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/json.go b/app/utils/json.go
new file mode 100644
index 0000000..2905833
--- /dev/null
+++ b/app/utils/json.go
@@ -0,0 +1,37 @@
+package utils
+
+import (
+ "bytes"
+ "encoding/json"
+ "regexp"
+)
+
+func JsonMarshal(interface{}) {
+
+}
+
+// 不科学计数法
+func JsonDecode(data []byte, v interface{}) error {
+ d := json.NewDecoder(bytes.NewReader(data))
+ d.UseNumber()
+ return d.Decode(v)
+}
+
+// json字符串驼峰命名格式 转为 下划线命名格式
+// c :json字符串
+func MarshalJSONCamelCase2JsonSnakeCase(c string) []byte {
+ // Regexp definitions
+ var keyMatchRegex = regexp.MustCompile(`\"(\w+)\":`)
+ var wordBarrierRegex = regexp.MustCompile(`(\w)([A-Z])`)
+ marshalled := []byte(c)
+ converted := keyMatchRegex.ReplaceAllFunc(
+ marshalled,
+ func(match []byte) []byte {
+ return bytes.ToLower(wordBarrierRegex.ReplaceAll(
+ match,
+ []byte(`${1}_${2}`),
+ ))
+ },
+ )
+ return converted
+}
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_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/map_to_map.go b/app/utils/map_to_map.go
new file mode 100644
index 0000000..3a68bad
--- /dev/null
+++ b/app/utils/map_to_map.go
@@ -0,0 +1,112 @@
+package utils
+
+import (
+ "math"
+)
+
+// WGS84坐标系:即地球坐标系,国际上通用的坐标系。
+// GCJ02坐标系:即火星坐标系,WGS84坐标系经加密后的坐标系。Google Maps,高德在用。
+// BD09坐标系:即百度坐标系,GCJ02坐标系经加密后的坐标系。
+
+const (
+ X_PI = math.Pi * 3000.0 / 180.0
+ OFFSET = 0.00669342162296594323
+ AXIS = 6378245.0
+)
+
+//BD09toGCJ02 百度坐标系->火星坐标系
+func BD09toGCJ02(lon, lat float64) (float64, float64) {
+ x := lon - 0.0065
+ y := lat - 0.006
+
+ z := math.Sqrt(x*x+y*y) - 0.00002*math.Sin(y*X_PI)
+ theta := math.Atan2(y, x) - 0.000003*math.Cos(x*X_PI)
+
+ gLon := z * math.Cos(theta)
+ gLat := z * math.Sin(theta)
+
+ return gLon, gLat
+}
+
+//GCJ02toBD09 火星坐标系->百度坐标系
+func GCJ02toBD09(lon, lat float64) (float64, float64) {
+ z := math.Sqrt(lon*lon+lat*lat) + 0.00002*math.Sin(lat*X_PI)
+ theta := math.Atan2(lat, lon) + 0.000003*math.Cos(lon*X_PI)
+
+ bdLon := z*math.Cos(theta) + 0.0065
+ bdLat := z*math.Sin(theta) + 0.006
+
+ return bdLon, bdLat
+}
+
+//WGS84toGCJ02 WGS84坐标系->火星坐标系
+func WGS84toGCJ02(lon, lat float64) (float64, float64) {
+ if isOutOFChina(lon, lat) {
+ return lon, lat
+ }
+
+ mgLon, mgLat := delta(lon, lat)
+
+ return mgLon, mgLat
+}
+
+//GCJ02toWGS84 火星坐标系->WGS84坐标系
+func GCJ02toWGS84(lon, lat float64) (float64, float64) {
+ if isOutOFChina(lon, lat) {
+ return lon, lat
+ }
+
+ mgLon, mgLat := delta(lon, lat)
+
+ return lon*2 - mgLon, lat*2 - mgLat
+}
+
+//BD09toWGS84 百度坐标系->WGS84坐标系
+func BD09toWGS84(lon, lat float64) (float64, float64) {
+ lon, lat = BD09toGCJ02(lon, lat)
+ return GCJ02toWGS84(lon, lat)
+}
+
+//WGS84toBD09 WGS84坐标系->百度坐标系
+func WGS84toBD09(lon, lat float64) (float64, float64) {
+ lon, lat = WGS84toGCJ02(lon, lat)
+ return GCJ02toBD09(lon, lat)
+}
+
+func delta(lon, lat float64) (float64, float64) {
+ dlat := transformlat(lon-105.0, lat-35.0)
+ dlon := transformlng(lon-105.0, lat-35.0)
+
+ radlat := lat / 180.0 * math.Pi
+ magic := math.Sin(radlat)
+ magic = 1 - OFFSET*magic*magic
+ sqrtmagic := math.Sqrt(magic)
+
+ dlat = (dlat * 180.0) / ((AXIS * (1 - OFFSET)) / (magic * sqrtmagic) * math.Pi)
+ dlon = (dlon * 180.0) / (AXIS / sqrtmagic * math.Cos(radlat) * math.Pi)
+
+ mgLat := lat + dlat
+ mgLon := lon + dlon
+
+ return mgLon, mgLat
+}
+
+func transformlat(lon, lat float64) float64 {
+ var ret = -100.0 + 2.0*lon + 3.0*lat + 0.2*lat*lat + 0.1*lon*lat + 0.2*math.Sqrt(math.Abs(lon))
+ ret += (20.0*math.Sin(6.0*lon*math.Pi) + 20.0*math.Sin(2.0*lon*math.Pi)) * 2.0 / 3.0
+ ret += (20.0*math.Sin(lat*math.Pi) + 40.0*math.Sin(lat/3.0*math.Pi)) * 2.0 / 3.0
+ ret += (160.0*math.Sin(lat/12.0*math.Pi) + 320*math.Sin(lat*math.Pi/30.0)) * 2.0 / 3.0
+ return ret
+}
+
+func transformlng(lon, lat float64) float64 {
+ var ret = 300.0 + lon + 2.0*lat + 0.1*lon*lon + 0.1*lon*lat + 0.1*math.Sqrt(math.Abs(lon))
+ ret += (20.0*math.Sin(6.0*lon*math.Pi) + 20.0*math.Sin(2.0*lon*math.Pi)) * 2.0 / 3.0
+ ret += (20.0*math.Sin(lon*math.Pi) + 40.0*math.Sin(lon/3.0*math.Pi)) * 2.0 / 3.0
+ ret += (150.0*math.Sin(lon/12.0*math.Pi) + 300.0*math.Sin(lon/30.0*math.Pi)) * 2.0 / 3.0
+ return ret
+}
+
+func isOutOFChina(lon, lat float64) bool {
+ return !(lon > 73.66 && lon < 135.05 && lat > 3.86 && lat < 53.55)
+}
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/open_platform.go b/app/utils/open_platform.go
new file mode 100644
index 0000000..b4f79da
--- /dev/null
+++ b/app/utils/open_platform.go
@@ -0,0 +1,135 @@
+package utils
+
+import (
+ "applet/app/cfg"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "sort"
+ "time"
+)
+
+// 调用开放平台的封装
+type OpenPlatformReqClient struct {
+ AppKey string
+ AppSecret string
+ Method string
+ Version string
+ Timestamp string
+ Nonce string
+ Sign string
+ BizData map[string]interface{}
+ params map[string]string
+ ReturnData ReturnDataResp
+}
+
+type ReturnDataResp struct {
+ Code int `json:"code"`
+ Msg string `json:"msg"`
+ Data interface{} `json:"data"`
+}
+
+func NewOpenPlatformReqClient(appKey, method, version, appSecret string, bizData map[string]interface{}) (*OpenPlatformReqClient, error) {
+ if appKey == "" || appSecret == "" || method == "" || version == "" {
+ return nil, errors.New("appKey,method,version not allow empty")
+ }
+ nowStr := AnyToString(time.Now().Unix())
+ nonce := UUIDString()
+ return &OpenPlatformReqClient{
+ AppKey: appKey,
+ AppSecret: appSecret,
+ Method: method,
+ Version: version,
+ Nonce: nonce,
+ Timestamp: nowStr,
+ params: map[string]string{"app_key": appKey, "method": method, "version": version, "timestamp": nowStr, "nonce": nonce},
+ BizData: bizData,
+ }, nil
+}
+
+func (client *OpenPlatformReqClient) CurlOpen() error {
+ if client.params == nil {
+ return errors.New("params not allow empty")
+ }
+ url := cfg.ZhiosOpen.URL + "/api/open/gw"
+ fmt.Printf("%#v\n", string(Serialize(client.params)))
+ resp, err := CurlPost(url, Serialize(client.params), nil)
+ if err != nil {
+ return err
+ }
+ err = json.Unmarshal(resp, &client.ReturnData)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+/*func (client *OpenPlatformReqClient) CreateParams() *OpenPlatformReqClient {
+ client.params["timestamp"] = client.Timestamp
+ client.params["nonce"] = client.Nonce
+ //client.params["biz_data"] = SerializeStr(client.BizData)
+ return client
+}*/
+
+func (client *OpenPlatformReqClient) SetBizDataToParams() *OpenPlatformReqClient {
+ client.params["biz_data"] = SerializeStr(client.BizData)
+ return client
+}
+
+func (client *OpenPlatformReqClient) SetParams(key, value string) *OpenPlatformReqClient {
+ client.params[key] = value
+ return client
+}
+
+func (client *OpenPlatformReqClient) GetParams(key string) string {
+ return client.params[key]
+}
+
+func (client *OpenPlatformReqClient) CreateSign() *OpenPlatformReqClient {
+ /*if client.BizData != nil {
+ for key := range client.BizData {
+ client.params[key] = AnyToString2(client.BizData[key])
+ }
+ }*/
+ var keys []string
+ for key := range client.params {
+ keys = append(keys, key)
+ }
+ sort.Strings(keys)
+ str := ""
+ for _, key := range keys {
+ str += fmt.Sprintf("%v=%v&", key, client.params[key])
+ }
+ str = client.AppSecret + str[:len(str)-1] + client.AppSecret
+ fmt.Printf("sign: %s\n", str)
+ client.Sign = Md5(str)
+ client.params["sign"] = client.Sign
+ if client.BizData != nil {
+ for key := range client.BizData {
+ if _, ok := client.params[key]; ok {
+ delete(client.params, key)
+ }
+ }
+ }
+ return client
+}
+
+func (client *OpenPlatformReqClient) VerifySign(sign string) bool {
+ if sign == "" || client.Sign == "" {
+ return false
+ }
+ if client.Sign == sign {
+ return true
+ }
+ return false
+}
+
+func (client *OpenPlatformReqClient) ResetNonce() *OpenPlatformReqClient {
+ client.Nonce = UUIDString()
+ return client
+}
+
+func (client *OpenPlatformReqClient) ResetTimestamp() *OpenPlatformReqClient {
+ client.Timestamp = AnyToString(time.Now().Unix())
+ return client
+}
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..2d0fe75
--- /dev/null
+++ b/app/utils/qrcode/getBase64.go
@@ -0,0 +1,55 @@
+package qrcode
+
+// 生成登录二维码图片, 方便在网页上显示
+
+import (
+ "bytes"
+ "encoding/base64"
+ "image/jpeg"
+ "image/png"
+ "io/ioutil"
+ "net/http"
+
+ "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)
+}
+func GetFileBase64(content string) string {
+ res, err := http.Get(content)
+ if err != nil {
+ return ""
+ }
+ defer res.Body.Close()
+ data, _ := ioutil.ReadAll(res.Body)
+ imageBase64 := base64.StdEncoding.EncodeToString(data)
+ return imageBase64
+}
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..fd4bf25
--- /dev/null
+++ b/app/utils/rand.go
@@ -0,0 +1,77 @@
+package utils
+
+import (
+ crand "crypto/rand"
+ "fmt"
+ "math"
+ "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))
+}
+
+// x的y次方
+func RandPow(l int) string {
+ var i = "1"
+ for j := 0; j < l; j++ {
+ i += "0"
+ }
+ k := StrToInt64(i)
+ n := rand.New(rand.NewSource(time.Now().UnixNano())).Int63n(k)
+ ls := "%0" + IntToStr(l) + "v"
+ str := fmt.Sprintf(ls, n)
+ //min := int(math.Pow10(l - 1))
+ //max := int(math.Pow10(l) - 1)
+ return str
+}
+func RandInt(min, max int) int {
+ if min >= max || min == 0 || max == 0 {
+ if max == 0 {
+ max = min
+ }
+ return max
+ }
+ return rand.Intn(max-min) + min
+}
+func RandInt1(min, max int) int {
+ rand.Seed(time.Now().UnixNano())
+ return rand.Intn(max-min+1) + min
+}
+
+func CalculateDistance(lat1, lng1, lat2, lng2 float64) float64 {
+ radius := 6371.0 // 地球半径(单位:公里)
+ dLat := DegToRad(lat2 - lat1)
+ dLng := DegToRad(lng2 - lng1)
+ a := math.Sin(dLat/2)*math.Sin(dLat/2) + math.Cos(DegToRad(lat1))*math.Cos(DegToRad(lat2))*math.Sin(dLng/2)*math.Sin(dLng/2)
+ c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
+ distance := radius * c
+
+ return distance
+}
+
+// 将角度转换为弧度
+func DegToRad(deg float64) float64 {
+ return deg * (math.Pi / 180)
+
+}
diff --git a/app/utils/redis.go b/app/utils/redis.go
new file mode 100644
index 0000000..6080e6e
--- /dev/null
+++ b/app/utils/redis.go
@@ -0,0 +1,32 @@
+package utils
+
+import (
+ "applet/app/utils/cache"
+ "fmt"
+ "github.com/gin-gonic/gin"
+)
+
+func ClearRedis(c *gin.Context) {
+ var str = []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"}
+ key := fmt.Sprintf("%s:cfg_cache", c.GetString("mid"))
+ cache.Del(key)
+ key2 := fmt.Sprintf("%s:virtual_coin_cfg", c.GetString("mid"))
+ cache.Del(key2)
+ for _, v := range str {
+ key1 := fmt.Sprintf("%s:cfg_cache:%s", c.GetString("mid"), v)
+ cache.Del(key1)
+ }
+
+}
+func ClearRedisDb(dbname string) {
+ var str = []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"}
+ key := fmt.Sprintf("%s:cfg_cache", dbname)
+ cache.Del(key)
+ key2 := fmt.Sprintf("%s:virtual_coin_cfg", dbname)
+ cache.Del(key2)
+ for _, v := range str {
+ key1 := fmt.Sprintf("%s:cfg_cache:%s", dbname, v)
+ cache.Del(key1)
+ }
+
+}
diff --git a/app/utils/rpc_client.go b/app/utils/rpc_client.go
new file mode 100644
index 0000000..4fc392a
--- /dev/null
+++ b/app/utils/rpc_client.go
@@ -0,0 +1,60 @@
+package utils
+
+import (
+ "applet/pkg/pb"
+ "context"
+ "fmt"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/metadata"
+ "strconv"
+ "time"
+)
+
+func GetBusinessIntClient(url, port string) pb.BusinessIntClient {
+ target := fmt.Sprintf("%s:%s", url, port)
+ conn, err := grpc.Dial(target, grpc.WithInsecure())
+ if err != nil {
+ fmt.Println(err)
+ return nil
+ }
+ return pb.NewBusinessIntClient(conn)
+}
+
+func GetBusinessExtClient(url, port string) pb.BusinessExtClient {
+ target := fmt.Sprintf("%s:%s", url, port)
+ conn, err := grpc.Dial(target, grpc.WithInsecure())
+ //defer conn.Close()
+ if err != nil {
+ fmt.Println(err)
+ return nil
+ }
+ return pb.NewBusinessExtClient(conn)
+}
+
+func GetLogicExtClient(url, port string) pb.LogicExtClient {
+ target := fmt.Sprintf("%s:%s", url, port)
+ conn, err := grpc.Dial(target, grpc.WithInsecure())
+ if err != nil {
+ fmt.Println(err)
+ return nil
+ }
+ return pb.NewLogicExtClient(conn)
+}
+
+func GetCtx(token, userId, deviceId, masterId string) context.Context {
+ if userId == "" {
+ userId = "1"
+ }
+ if deviceId == "" {
+ deviceId = "1"
+ }
+ if token == "" {
+ token = "0"
+ }
+ return metadata.NewOutgoingContext(context.TODO(), metadata.Pairs(
+ "user_id", userId,
+ "device_id", deviceId,
+ "token", token,
+ "master_id", masterId,
+ "request_id", strconv.FormatInt(time.Now().UnixNano(), 10)))
+}
diff --git a/app/utils/rsa.go b/app/utils/rsa.go
new file mode 100644
index 0000000..02f0386
--- /dev/null
+++ b/app/utils/rsa.go
@@ -0,0 +1,231 @@
+package utils
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/sha256"
+ "crypto/x509"
+ "encoding/base64"
+ "encoding/pem"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "os"
+ "strings"
+)
+
+// 生成私钥文件 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 SyRsaEncrypt(signStr, signature, privateKeyBytes string) error {
+ h := sha256.New()
+ h.Write([]byte(signStr))
+ d := h.Sum(nil)
+ sign, _ := base64.StdEncoding.DecodeString(signature)
+ pub, err := ParsePKIXPublicKey(privateKeyBytes)
+ err = rsa.VerifyPKCS1v15(pub, crypto.SHA256, d, sign)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+func FormatPublicKey(publicKey string) (pKey string) {
+ var buffer strings.Builder
+ buffer.WriteString("-----BEGIN PUBLIC KEY-----\n")
+ rawLen := 64
+ keyLen := len(publicKey)
+ raws := keyLen / rawLen
+ temp := keyLen % rawLen
+ if temp > 0 {
+ raws++
+ }
+ start := 0
+ end := start + rawLen
+ for i := 0; i < raws; i++ {
+ if i == raws-1 {
+ buffer.WriteString(publicKey[start:])
+ } else {
+ buffer.WriteString(publicKey[start:end])
+ }
+ buffer.WriteByte('\n')
+ start += rawLen
+ end = start + rawLen
+ }
+ buffer.WriteString("-----END PUBLIC KEY-----\n")
+ pKey = buffer.String()
+ return
+}
+
+func ParsePKIXPublicKey(privateKey string) (*rsa.PublicKey, error) {
+
+ privateKey = FormatPublicKey(privateKey)
+ // 2、解码私钥字节,生成加密对象
+ block, _ := pem.Decode([]byte(privateKey))
+ if block == nil {
+
+ return nil, errors.New("私钥信息错误!")
+ }
+ // 3、解析DER编码的私钥,生成私钥对象
+ priKey, err := x509.ParsePKIXPublicKey(block.Bytes)
+ if err != nil {
+
+ return nil, err
+ }
+ return priKey.(*rsa.PublicKey), nil
+}
+
+// 从证书获取公钥
+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..5c80ab1
--- /dev/null
+++ b/app/utils/sign_check.go
@@ -0,0 +1,216 @@
+package utils
+
+import (
+ "applet/app/cfg"
+ "applet/app/utils/logx"
+ "fmt"
+ "github.com/forgoer/openssl"
+ "github.com/gin-gonic/gin"
+ "github.com/syyongx/php2go"
+ "strings"
+ "time"
+)
+
+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")
+ }
+ if StrToInt(apiVersion) == 0 { //没有版本号先不校验
+ apiVersion = c.GetString("apiVersion")
+ }
+ if StrToInt(apiVersion) == 0 {
+ platform := c.GetHeader("platform")
+ if InArr(platform, []string{"ios", "android"}) == false && c.GetString("h5_applet_must_sign") == "1" {
+ apiVersion = "1"
+ }
+ if InArr(platform, []string{"android"}) && c.GetString("android_must_sign") == "1" {
+ apiVersion = "1"
+ }
+ if InArr(platform, []string{"ios"}) && c.GetString("ios_must_sign") == "1" {
+ apiVersion = "1"
+ }
+ }
+ if c.GetString("api_version") == "1" && cfg.Prd {
+ apiVersion = "1"
+ }
+ if (strings.Contains(c.Request.Host, "zhios-app") || strings.Contains(c.Request.Host, "api.zhios.cn")) && apiVersion == "1" {
+ apiVersion = "0"
+ c.Set("api_version", "0")
+ }
+
+ //if InArr(c.GetHeader("platform"), []string{"ios", "android"}) {
+ // apiVersion = "0"
+ //}
+ var uri = c.Request.RequestURI
+ if InArr(c.GetHeader("platform"), []string{"ios", "android", "pc"}) { //不用签名的接口
+ var filterList = []string{
+ "/api/v1/appcheck",
+ "/api/v1/app/guide",
+ "/api/v1/new/config.json",
+ "pub.flutter.web_download_page",
+ }
+ for _, v := range filterList {
+ if strings.Contains(uri, v) {
+ apiVersion = "0"
+ }
+ }
+ }
+ return StrToInt(apiVersion)
+}
+func CheckUri(c *gin.Context) int {
+ apiVersion := "1"
+ //var uri = c.Request.RequestURI
+ if InArr(c.GetHeader("platform"), []string{"ios", "android"}) { //不用签名的接口
+ //var filterList = []string{
+ // "/api/v1/appcheck",
+ // "/api/v1/app/guide",
+ // "/api/v1/new/config.json",
+ // "api/v1/rec",
+ // "api/v1/custom/mod/",
+ // "api/v1/mod/",
+ // "api/v1/s/",
+ //}
+ //for _, v := range filterList {
+ // if strings.Contains(uri, v) {
+ // apiVersion = "0"
+ // }
+ //}
+ apiVersion = "0"
+ }
+ 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
+ }
+
+ if StrToInt64(query["timestamp"])/1000 < time.Now().Unix()-300 {
+ fmt.Println("============" + query["timestamp"])
+ return false
+ }
+ //if query["nonce"] != "" {
+ // //TODO s
+ // getString, err := cache.GetString(query["nonce"])
+ // if err != nil {
+ // fmt.Println("nonce", err)
+ // }
+ // if getString != "" {
+ // fmt.Println("nonce", "============"+getString)
+ // return false
+ // } else {
+ // cache.SetEx(query["nonce"], "1", 300)
+ // }
+ //}
+ 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 ""
+ }
+ fmt.Println("============aes============")
+ fmt.Println(string(aes))
+ fmt.Println(string(raw))
+ 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")
+ if key == "" {
+ key = c.GetHeader("Key")
+ }
+ fmt.Println("验签", key)
+ base, _ := php2go.Base64Decode(key)
+ aes, err := RsaDecrypt([]byte(base), privateKey)
+ if err != nil {
+ logx.Info(err)
+ return ""
+ }
+ raw = strings.ReplaceAll(raw, "\"", "")
+ fmt.Println(raw)
+ value1, _ := php2go.Base64Decode(raw)
+ if value1 == "" {
+ return ""
+ }
+ str1, _ := openssl.AesECBDecrypt([]byte(value1), aes, openssl.PKCS7_PADDING)
+ fmt.Println("==========解码=========")
+ fmt.Println(string(str1))
+ return string(str1)
+}
diff --git a/app/utils/slice.go b/app/utils/slice.go
new file mode 100644
index 0000000..fd86081
--- /dev/null
+++ b/app/utils/slice.go
@@ -0,0 +1,13 @@
+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
+}
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..adae62e
--- /dev/null
+++ b/app/utils/string.go
@@ -0,0 +1,223 @@
+package utils
+
+import (
+ "code.fnuoos.com/go_rely_warehouse/zyos_go_order_relate_rule.git/lib/comm_plan"
+ "fmt"
+ "github.com/syyongx/php2go"
+ "math/rand"
+ "reflect"
+ "regexp"
+ "sort"
+ "strings"
+ "unicode"
+)
+
+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 RremoveChinese(s string) string {
+ // 正则表达式匹配中文字符
+ re := regexp.MustCompile("[\u4e00-\u9fa5]")
+ // 将所有中文字符替换为空字符串
+ s = re.ReplaceAllString(s, "")
+ return s
+}
+
+// 字符串是否在数组里
+func InArr(target string, strArray []string) bool {
+ for _, element := range strArray {
+ if target == element {
+ return true
+ }
+ }
+ return false
+}
+func ContainsDigitOrLetter(s string) bool {
+ reg, err := regexp.Compile("[0-9a-zA-Z]")
+ if err != nil {
+ // 处理正则表达式编译错误
+ return false
+ }
+ return reg.MatchString(s)
+}
+
+// 生成指定长度的字符串
+func RandStringBytes(n int) string {
+ const letterBytes = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ b := make([]byte, n)
+ for i := range b {
+ b[i] = letterBytes[rand.Intn(len(letterBytes))]
+ }
+ return string(b)
+}
+
+// 把数组的值放到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 IsChineseChar(str string) bool {
+ for _, r := range str {
+ if unicode.Is(unicode.Scripts["Han"], r) || (regexp.MustCompile("[\u3002\uff1b\uff0c\uff1a\u201c\u201d\uff08\uff09\u3001\uff1f\u300a\u300b]").MatchString(string(r))) {
+ return true
+ }
+ }
+ return false
+}
+
+// 清空前面是0的
+func LeadZeros(str string) string {
+ bytes := []byte(str)
+ var index int
+ for i, b := range bytes {
+ if b != byte(48) {
+ index = i
+ break
+ }
+ }
+ i := bytes[index:len(bytes)]
+ return string(i)
+}
+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)
+}
+
+// 手机号中间4位替换为*号
+func FormatMobileStar(mobile string) string {
+ if len(mobile) <= 10 {
+ return mobile
+ }
+ return mobile[:3] + "****" + mobile[7:]
+}
+func ConvertList2Map(a []*comm_plan.VirtualCoinCommission) (b map[string]float64) {
+ b = make(map[string]float64)
+ for _, i := range a {
+ b[i.Cid] = i.Val
+ }
+ return b
+}
diff --git a/app/utils/struct2UrlParams.go b/app/utils/struct2UrlParams.go
new file mode 100644
index 0000000..7f8ac05
--- /dev/null
+++ b/app/utils/struct2UrlParams.go
@@ -0,0 +1,43 @@
+package utils
+
+import "sort"
+
+func Struct2UrlParams(obj interface{}) string {
+ var str = ""
+ mapVal := Struct2Map(obj)
+ var keys []string
+ for key := range mapVal {
+ keys = append(keys, key)
+ }
+ sort.Strings(keys)
+ for _, key := range keys {
+ str += key + "=" + AnyToString(mapVal[key]) + "&"
+ }
+ /*t := reflect.TypeOf(origin)
+ v := reflect.ValueOf(origin)
+
+ for i := 0; i < t.NumField(); i++ {
+ tag := strings.ToLower(t.Field(i).Tag.Get("json"))
+ if tag != "sign" {
+ switch v.Field(i).Kind(){
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ str += tag + "=" + utils.Int64ToStr(v.Field(i).Int()) + "&"
+ break
+ case reflect.String:
+ str += tag + "=" + v.Field(i).String() + "&"
+ break
+ case reflect.Bool:
+ str += tag + "=" + utils.BoolToStr(v.Field(i).Bool()) + "&"
+ break
+ case reflect.Float32, reflect.Float64:
+ str += tag + "=" + utils.Float64ToStr(v.Field(i).Float())
+ break
+ case reflect.Array, reflect.Struct, reflect.Slice:
+ str += ToUrlKeyValue(v.Field(i).Interface())
+ default:
+ break
+ }
+ }
+ }*/
+ return str
+}
diff --git a/app/utils/time.go b/app/utils/time.go
new file mode 100644
index 0000000..9c0f6f8
--- /dev/null
+++ b/app/utils/time.go
@@ -0,0 +1,239 @@
+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 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":
+ // 明天 0点
+ etime = time.Date(t.Year(), t.Month(), t.Day()+1, 0, 0, 0, 0, t.Location())
+ stime = time.Unix(etime.Unix()-7*86400, 0)
+
+ case "within_fifteen_days":
+ // 明天 0点
+ etime = time.Date(t.Year(), t.Month(), t.Day()+1, 0, 0, 0, 0, t.Location())
+ // 前14天0点
+ stime = time.Unix(etime.Unix()-15*86400, 0)
+ case "current_month":
+ stime = GetFirstDateOfMonth(t)
+ etime = time.Now()
+ case "last_month":
+ etime = GetFirstDateOfMonth(t)
+ monthTimes := TimeStdParseUnix(etime.Format("2006-01-02 15:04:05")) - 86400
+ times, _ := UnixToTime(Int64ToStr(monthTimes))
+ stime = GetFirstDateOfMonth(times)
+ }
+
+ return map[string]int64{
+ "start": stime.Unix(),
+ "end": etime.Unix(),
+ }
+}
+
+/**
+获取本周周一的日期
+*/
+func GetFirstDateOfWeek() (weekMonday string) {
+ now := time.Now()
+
+ offset := int(time.Monday - now.Weekday())
+ if offset > 0 {
+ offset = -6
+ }
+
+ weekStartDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local).AddDate(0, 0, offset)
+ weekMonday = weekStartDate.Format("2006-01-02")
+ return
+}
+
+/**
+获取上周的周一日期
+*/
+func GetLastWeekFirstDate() (weekMonday string) {
+ thisWeekMonday := GetFirstDateOfWeek()
+ TimeMonday, _ := time.Parse("2006-01-02", thisWeekMonday)
+ lastWeekMonday := TimeMonday.AddDate(0, 0, -7)
+ weekMonday = lastWeekMonday.Format("2006-01-02")
+ return
+}
+
+//时间戳转时间格式
+func UnixToTime(e string) (datatime time.Time, err error) {
+ data, err := strconv.ParseInt(e, 10, 64)
+ datatime = time.Unix(data, 0)
+ return
+}
+
+//获取传入的时间所在月份的第一天,即某月第一天的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())
+}
+
+//获取当月某天的某个时间的时间
+func GetDayToTime(day, timeStr string) string {
+ if timeStr == "" {
+ timeStr = "00:00:00"
+ }
+ year := time.Now().Year()
+ month := time.Now().Format("01")
+ times := fmt.Sprintf("%s-%s-%s %s", IntToStr(year), month, day, timeStr)
+ return times
+}
diff --git a/app/utils/translate.go b/app/utils/translate.go
new file mode 100644
index 0000000..dbcb00d
--- /dev/null
+++ b/app/utils/translate.go
@@ -0,0 +1,110 @@
+package utils
+
+import (
+ "applet/app/cfg"
+ "applet/app/utils/cache"
+ "encoding/json"
+ "github.com/gin-gonic/gin"
+ "github.com/go-creed/sat"
+ "strings"
+)
+
+func ReadReverse(c *gin.Context, str string) string {
+ if c.GetString("translate_open") == "zh_Hant_" { //繁体先不改
+ sat.InitDefaultDict(sat.SetPath(cfg.WxappletFilepath.URL + "/" + "sat.txt")) //使用自定义词库
+ sat := sat.DefaultDict()
+ res := sat.ReadReverse(str)
+ list := strings.Split(res, "http")
+ imgList := []string{".png", ".jpg", ".jpeg", ".gif"}
+ for _, v := range list {
+ for _, v1 := range imgList {
+ if strings.Contains(v, v1) { //判断是不是有图片 有图片就截取 替换简繁体
+ strs := strings.Split(v, v1)
+ if len(strs) > 0 {
+ oldStr := strs[0]
+ newStr := sat.Read(oldStr)
+ res = strings.ReplaceAll(res, oldStr, newStr)
+ }
+ }
+ }
+ }
+ return res
+ }
+ if c.GetString("translate_open") != "zh_Hant_" { //除了繁体,其他都走这里
+ //简体---其他语言
+ cTouString, err := cache.GetString("multi_language_c_to_" + c.GetString("translate_open"))
+ if err != nil {
+ return str
+ }
+ var cTou = make(map[string]string)
+ json.Unmarshal([]byte(cTouString), &cTou)
+ if len(cTou) == 0 {
+ return str
+ }
+ //其他语言--简体
+ getString1, err1 := cache.GetString("multi_language_" + c.GetString("translate_open") + "_to_c")
+ if err1 != nil {
+ return str
+ }
+ var uToc = make(map[string]string)
+ json.Unmarshal([]byte(getString1), &uToc)
+ if len(uToc) == 0 {
+ return str
+ }
+ res := str
+ for k, v := range cTou {
+ res = strings.ReplaceAll(res, k, v)
+ }
+ list := strings.Split(res, "http")
+ imgList := []string{".png", ".jpg", ".jpeg", ".gif"}
+ for _, v := range list {
+ for _, v1 := range imgList {
+ if strings.Contains(v, v1) { //判断是不是有图片 有图片就截取 替换简繁体
+ strs := strings.Split(v, v1)
+ if len(strs) > 0 {
+ oldStr := strs[0]
+ newStr := oldStr
+ for k2, v2 := range uToc {
+ newStr = strings.ReplaceAll(oldStr, k2, v2)
+ }
+ res = strings.ReplaceAll(res, oldStr, newStr)
+ }
+ }
+ }
+ }
+ return res
+ }
+ return str
+
+}
+
+func ReadReverse1(str, types string) string {
+ res := map[string]map[string]string{}
+ err := cache.GetJson("multi_language", &res)
+ if err != nil {
+ return str
+ }
+ for k, v := range res {
+ str = strings.ReplaceAll(str, k, v[types])
+ }
+ resStr := str
+ list := strings.Split(resStr, "http")
+ imgList := []string{".png", ".jpg", ".jpeg", ".gif"}
+ for _, v := range list {
+ for _, v1 := range imgList {
+ if strings.Contains(v, v1) { //判断是不是有图片 有图片就截取 替换简繁体
+ strs := strings.Split(v, v1)
+ if len(strs) > 0 {
+ oldStr := strs[0]
+ for k2, v2 := range res {
+ if v2[types] == oldStr {
+ resStr = strings.ReplaceAll(resStr, oldStr, k2)
+ }
+ }
+ //res = strings.ReplaceAll(res, oldStr, newStr)
+ }
+ }
+ }
+ }
+ return resStr
+}
diff --git a/app/utils/trim_html.go b/app/utils/trim_html.go
new file mode 100644
index 0000000..f796f32
--- /dev/null
+++ b/app/utils/trim_html.go
@@ -0,0 +1,38 @@
+package utils
+
+import (
+ "regexp"
+ "strings"
+)
+
+func TrimHtml(src string) string {
+ re, _ := regexp.Compile("<[\\S\\s]+?>")
+ src = re.ReplaceAllStringFunc(src, strings.ToLower)
+
+ re, _ = regexp.Compile("