├── .gitignore ├── LICENSE ├── README.md ├── api ├── api.go └── v1 │ ├── currency.go │ ├── kLine.go │ ├── market.go │ ├── order.go │ ├── orderBook.go │ ├── ticker.go │ ├── trade.go │ └── user.go ├── build.sh ├── config ├── amqp.go ├── amqp.yml.example ├── aws_s3.yml.example ├── database.yml.example ├── env.go ├── env.yml.example ├── interfaces.yml.example ├── qiniu.yml.example ├── redis.yml.example ├── workers.go └── workers.yml.example ├── go.mod ├── go.sum ├── initializers ├── auth.go ├── cacheData.go ├── filters.go ├── i18n.go ├── initializeWorkers.go ├── latestKLine.go ├── locale │ └── parse.go ├── locales │ ├── error_code.en.yml │ ├── error_code.zh-CN.yml │ ├── wallet.en.yml │ └── wallet.zh-CN.yml ├── rabbitmq.go └── ticker.go ├── models ├── account.go ├── accountVersion.go ├── accountVersionCheckPoint.go ├── apiToken.go ├── common.go ├── currency.go ├── device.go ├── identity.go ├── k.go ├── market.go ├── order.go ├── ticker.go ├── token.go ├── trade.go └── user.go ├── order ├── cancel.go └── cancel │ ├── base.go │ └── worker.go ├── restart.sh ├── routes └── v1.go ├── schedules ├── backup │ └── tasks │ │ ├── account_version.go │ │ ├── logs.go │ │ └── tokens.go ├── kLine │ └── create.go ├── order │ └── waitingCheck.go └── schedule.go ├── start.sh ├── stop.sh ├── trade ├── matching.go ├── matching │ ├── base.go │ ├── depth.go │ ├── trade.go │ └── worker.go ├── treat.go └── treat │ ├── base.go │ └── worker.go ├── utils ├── config.go ├── gorm.go ├── helper.go ├── randString.go ├── redis.go ├── response.go ├── uploadToQiniu.go └── uploadToS3.go └── workers ├── sneakerWorkers ├── accountVersionWorker.go ├── kLineWorker.go ├── rebuildkLineToRedisWorker.go └── tickerWorker.go └── workers.go /.gitignore: -------------------------------------------------------------------------------- 1 | bin/* 2 | config/*.yml 3 | pems/*.pem 4 | logs/* 5 | vendor/ 6 | Godeps/ 7 | pids/ 8 | public/web_assets/* 9 | test*.go 10 | docker/* 11 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Leon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go(Golang)版 数字货币交易所 2 | 3 | ``` 4 | _______ ______ ________ 5 | | \ / \ | \ 6 | ______ ______ | $$$$$$$\| $$$$$$\| $$$$$$$$ 7 | / \ / \ | $$ | $$| $$ \$$| $$__ 8 | | $$$$$$\| $$$$$$\| $$ | $$| $$ | $$ \ 9 | | $$ | $$| $$ | $$| $$ | $$| $$ __ | $$$$$ 10 | | $$__| $$| $$__/ $$| $$__/ $$| $$__/ \| $$_____ 11 | \$$ $$ \$$ $$| $$ $$ \$$ $$| $$ \ 12 | _\$$$$$$$ \$$$$$$ \$$$$$$$ \$$$$$$ \$$$$$$$$ 13 | | \__| $$ 14 | \$$ $$ 15 | \$$$$$$ 16 | 17 | ``` 18 | 19 | ## Dependencies 20 | * MySql 21 | * RabbitMQ 22 | * Redis 23 | 24 | # Install Golang 25 | ``` 26 | wget https://dl.google.com/go/go1.13.linux-amd64.tar.gz 27 | 28 | sudo tar zxvf go1.13.linux-amd64.tar.gz -C /usr/local 29 | 30 | vim ~/.bashrc 31 | 添加 32 | export GOROOT=/usr/local/go 33 | export PATH=$PATH:$GOPATH:/usr/local/go/bin 34 | 35 | source ~/.bashrc 36 | 37 | ``` 38 | 39 | # 获取代码 40 | ``` 41 | git clone https://github.com/oldfritter/goDCE 42 | ``` 43 | # 单独启动api 44 | ``` 45 | cd goDCE 46 | go run api/api.go // 此时将自动安装依赖 47 | ``` 48 | # 导入前端代码 49 | 将前端代码导入`goDCE/public/assets`目录下即可 50 | # 编译 51 | ``` 52 | ./build.sh 53 | ``` 54 | # 启动 55 | ``` 56 | ./start.sh 57 | ``` 58 | # 停止 59 | ``` 60 | ./stop.sh 61 | ``` 62 | # 重启 63 | ``` 64 | ./restart.sh 65 | ``` 66 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "strconv" 11 | "time" 12 | 13 | newrelic "github.com/dafiti/echo-middleware" 14 | "github.com/labstack/echo" 15 | "github.com/labstack/echo/middleware" 16 | 17 | envConfig "github.com/oldfritter/goDCE/config" 18 | "github.com/oldfritter/goDCE/initializers" 19 | "github.com/oldfritter/goDCE/models" 20 | "github.com/oldfritter/goDCE/routes" 21 | "github.com/oldfritter/goDCE/utils" 22 | ) 23 | 24 | func main() { 25 | initialize() 26 | e := echo.New() 27 | 28 | e.File("/web", "public/assets/index.html") 29 | e.File("/web/*", "public/assets/index.html") 30 | e.Static("/assets", "public/assets") 31 | 32 | if envConfig.CurrentEnv.Newrelic.AppName != "" && envConfig.CurrentEnv.Newrelic.LicenseKey != "" { 33 | e.Use(newrelic.NewRelic(envConfig.CurrentEnv.Newrelic.AppName, envConfig.CurrentEnv.Newrelic.LicenseKey)) 34 | } 35 | e.Use(middleware.Logger()) 36 | e.Use(middleware.Recover()) 37 | e.Use(initializers.Auth) 38 | routes.SetV1Interfaces(e) 39 | e.HTTPErrorHandler = customHTTPErrorHandler 40 | e.HideBanner = true 41 | go func() { 42 | if err := e.Start(":9990"); err != nil { 43 | fmt.Println("start close echo") 44 | time.Sleep(500 * time.Millisecond) 45 | closeResource() 46 | fmt.Println("shutting down the server") 47 | } 48 | }() 49 | 50 | quit := make(chan os.Signal) 51 | signal.Notify(quit, os.Interrupt) 52 | <-quit 53 | fmt.Println("accepted signal") 54 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 55 | 56 | defer cancel() 57 | if err := e.Shutdown(ctx); err != nil { 58 | fmt.Println("shutting down failed, err:" + err.Error()) 59 | e.Logger.Fatal(err) 60 | } 61 | } 62 | 63 | func customHTTPErrorHandler(err error, context echo.Context) { 64 | language := context.Get("language").(string) 65 | if response, ok := err.(utils.Response); ok { 66 | response.Head["msg"] = fmt.Sprint(initializers.I18n.T(language, "error_code."+response.Head["code"])) 67 | context.JSON(http.StatusBadRequest, response) 68 | } else { 69 | panic(err) 70 | } 71 | } 72 | 73 | func initialize() { 74 | envConfig.InitEnv() 75 | utils.InitMainDB() 76 | utils.InitBackupDB() 77 | models.AutoMigrations() 78 | utils.InitRedisPools() 79 | initializers.InitializeAmqpConfig() 80 | 81 | initializers.LoadInterfaces() 82 | initializers.InitI18n() 83 | initializers.LoadCacheData() 84 | initializers.LoadLatestKLines() 85 | initializers.LoadLatestTickers() 86 | 87 | err := ioutil.WriteFile("pids/api.pid", []byte(strconv.Itoa(os.Getpid())), 0644) 88 | if err != nil { 89 | fmt.Println(err) 90 | } 91 | } 92 | 93 | func closeResource() { 94 | initializers.CloseAmqpConnection() 95 | utils.CloseRedisPools() 96 | utils.CloseMainDB() 97 | } 98 | -------------------------------------------------------------------------------- /api/v1/currency.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/labstack/echo" 7 | . "github.com/oldfritter/goDCE/models" 8 | "github.com/oldfritter/goDCE/utils" 9 | ) 10 | 11 | func V1GetCurrencies(context echo.Context) error { 12 | mainDB := utils.MainDbBegin() 13 | defer mainDB.DbRollback() 14 | 15 | var currencies []Currency 16 | mainDB.Find(¤cies) 17 | 18 | response := utils.SuccessResponse 19 | response.Body = currencies 20 | return context.JSON(http.StatusOK, response) 21 | } 22 | -------------------------------------------------------------------------------- /api/v1/kLine.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/gomodule/redigo/redis" 10 | "github.com/labstack/echo" 11 | . "github.com/oldfritter/goDCE/models" 12 | "github.com/oldfritter/goDCE/utils" 13 | ) 14 | 15 | type chart struct { 16 | K1 []interface{} `json:"k1"` 17 | K5 []interface{} `json:"k5"` 18 | K15 []interface{} `json:"k15"` 19 | K30 []interface{} `json:"k30"` 20 | K60 []interface{} `json:"k60"` 21 | K120 []interface{} `json:"k120"` 22 | K240 []interface{} `json:"k240"` 23 | K360 []interface{} `json:"k360"` 24 | K720 []interface{} `json:"k720"` 25 | K1440 []interface{} `json:"k1440"` 26 | K4320 []interface{} `json:"k4320"` 27 | K10080 []interface{} `json:"k10080"` 28 | } 29 | 30 | func V1GetK(context echo.Context) error { 31 | market, err := FindMarketByCode(context.QueryParam("market")) 32 | if err != nil { 33 | return utils.BuildError("1021") 34 | } 35 | limit := 100 36 | if context.QueryParam("limit") != "" { 37 | limit, _ = strconv.Atoi(context.QueryParam("limit")) 38 | if limit > 10000 { 39 | limit = 10000 40 | } 41 | } 42 | period, _ := strconv.ParseInt(context.QueryParam("period"), 10, 64) 43 | if period == 0 { 44 | return utils.BuildError("1053") 45 | } 46 | var timestamp int64 47 | if context.QueryParam("timestamp") == "" { 48 | timestamp = time.Now().Unix() 49 | } else { 50 | timestamp, _ = strconv.ParseInt(context.QueryParam("timestamp"), 10, 64) 51 | } 52 | kRedis := utils.GetRedisConn("data") 53 | defer kRedis.Close() 54 | values, _ := redis.Values(kRedis.Do("ZREVRANGEBYSCORE", market.KLineRedisKey(period), timestamp, 0, "limit", 0, limit, "withscores")) 55 | var line []KLine 56 | var k KLine 57 | for i, value := range values { 58 | if i%2 == 0 { 59 | json.Unmarshal(value.([]byte), &k) 60 | } else { 61 | k.Timestamp, _ = strconv.ParseInt(string(value.([]byte)), 10, 64) 62 | line = append(line, k) 63 | } 64 | } 65 | response := utils.SuccessResponse 66 | response.Body = line 67 | return context.JSON(http.StatusOK, response) 68 | } 69 | 70 | func V1GetChart(context echo.Context) error { 71 | market, err := FindMarketByCode(context.QueryParam("market")) 72 | if err != nil { 73 | return utils.BuildError("1021") 74 | } 75 | kRedis := utils.GetRedisConn("k") 76 | defer kRedis.Close() 77 | limit := 100 78 | timestamp := time.Now().Unix() 79 | 80 | var c chart 81 | periods := []int64{1, 5, 15, 30, 60, 120, 240, 360, 720, 1440, 4320, 10080} 82 | for _, period := range periods { 83 | 84 | values, _ := redis.Values( 85 | kRedis.Do( 86 | "ZREVRANGEBYSCORE", 87 | market.KLineRedisKey(period), 88 | timestamp, 89 | 0, 90 | "limit", 91 | 0, 92 | limit, 93 | ), 94 | ) 95 | var items []interface{} 96 | for _, v := range values { 97 | var item [6]string 98 | json.Unmarshal(v.([]byte), &item) 99 | items = append(items, item) 100 | } 101 | switch period { 102 | case 1: 103 | c.K1 = items 104 | case 5: 105 | c.K5 = items 106 | case 15: 107 | c.K15 = items 108 | case 30: 109 | c.K30 = items 110 | case 60: 111 | c.K60 = items 112 | case 120: 113 | c.K120 = items 114 | case 240: 115 | c.K240 = items 116 | case 360: 117 | c.K360 = items 118 | case 720: 119 | c.K720 = items 120 | case 1440: 121 | c.K1440 = items 122 | case 4320: 123 | c.K4320 = items 124 | case 10080: 125 | c.K10080 = items 126 | } 127 | } 128 | response := utils.SuccessResponse 129 | response.Body = c 130 | return context.JSON(http.StatusOK, response) 131 | } 132 | -------------------------------------------------------------------------------- /api/v1/market.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/labstack/echo" 7 | . "github.com/oldfritter/goDCE/models" 8 | "github.com/oldfritter/goDCE/utils" 9 | ) 10 | 11 | func V1GetMarkets(context echo.Context) error { 12 | mainDB := utils.MainDbBegin() 13 | defer mainDB.DbRollback() 14 | 15 | var markets []Market 16 | mainDB.Find(&markets) 17 | 18 | response := utils.SuccessResponse 19 | response.Body = markets 20 | return context.JSON(http.StatusOK, response) 21 | } 22 | -------------------------------------------------------------------------------- /api/v1/order.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/labstack/echo" 10 | "github.com/shopspring/decimal" 11 | "github.com/streadway/amqp" 12 | 13 | envConfig "github.com/oldfritter/goDCE/config" 14 | . "github.com/oldfritter/goDCE/models" 15 | "github.com/oldfritter/goDCE/utils" 16 | ) 17 | 18 | func V1GetOrder(context echo.Context) error { 19 | user := context.Get("current_user").(User) 20 | 21 | mainDB := utils.MainDbBegin() 22 | defer mainDB.DbRollback() 23 | var order Order 24 | if mainDB.Where("id = ? AND user_id = ?", context.QueryParam("id"), user.Id). 25 | First(&order).RecordNotFound() { 26 | return utils.BuildError("1020") 27 | } 28 | response := utils.SuccessResponse 29 | response.Body = order 30 | return context.JSON(http.StatusOK, response) 31 | } 32 | 33 | func V1GetOrders(context echo.Context) error { 34 | user := context.Get("current_user").(User) 35 | 36 | var market Market 37 | mainDB := utils.MainDbBegin() 38 | defer mainDB.DbRollback() 39 | if mainDB.Where("name = ?", context.QueryParam("market")).First(&market).RecordNotFound() { 40 | return utils.BuildError("1021") 41 | } 42 | state := 100 43 | if context.QueryParam("state") != "" { 44 | if context.QueryParam("state") == "done" { 45 | state = 200 46 | } else if context.QueryParam("state") == "cancel" { 47 | state = 0 48 | } 49 | } 50 | limit := 30 51 | if context.QueryParam("limit") != "" { 52 | limit, _ = strconv.Atoi(context.QueryParam("limit")) 53 | if limit > 2000 { 54 | limit = 30 55 | } 56 | } 57 | page := 1 58 | if context.QueryParam("page") != "" { 59 | page, _ = strconv.Atoi(context.QueryParam("page")) 60 | if page < 1 { 61 | page = 1 62 | } 63 | } 64 | day := "" 65 | if context.QueryParam("day") != "" { 66 | day = context.QueryParam("day") 67 | } 68 | orderParam := "id DESC" 69 | if context.QueryParam("order_by") == "asc" { 70 | orderParam = "id ASC" 71 | } 72 | var orders []Order 73 | var count int 74 | if day == "" { 75 | mainDB.Model(&Order{}).Where("market_id = ? AND user_id = ? AND state = ? ", market.Id, user.Id, state).Count(&count) 76 | if mainDB.Order(orderParam). 77 | Where("market_id = ? AND user_id = ? AND state = ? ", market.Id, user.Id, state). 78 | Offset(limit * (page - 1)).Limit(limit).Find(&orders).RecordNotFound() { 79 | return utils.BuildError("1020") 80 | } 81 | } else { 82 | mainDB.Model(&Order{}).Where("market_id = ? AND user_id = ? AND state = ? AND date(created_at) = ?", market.Id, user.Id, state, day).Count(&count) 83 | if mainDB.Order(orderParam). 84 | Where("market_id = ? AND user_id = ? AND state = ? AND date(created_at) = ?", market.Id, user.Id, state, day). 85 | Offset(limit * (page - 1)).Limit(limit).Find(&orders).RecordNotFound() { 86 | return utils.BuildError("1020") 87 | } 88 | } 89 | response := utils.ArrayResponse 90 | response.Init(orders, page, count, limit) 91 | return context.JSON(http.StatusOK, response) 92 | } 93 | 94 | func V1PostOrders(context echo.Context) error { 95 | params := context.Get("params").(map[string]string) 96 | if params["price"] == "" { 97 | return utils.BuildError("1024") 98 | } 99 | if params["volume"] == "" { 100 | return utils.BuildError("1023") 101 | } 102 | user := context.Get("current_user").(User) 103 | var market Market 104 | mainDB := utils.MainDbBegin() 105 | defer mainDB.DbRollback() 106 | if mainDB.Where("name = ?", params["market"]).First(&market).RecordNotFound() { 107 | return utils.BuildError("1021") 108 | } 109 | side := context.QueryParam("side") 110 | price, _ := decimal.NewFromString(params["price"]) 111 | volume, _ := decimal.NewFromString(params["volume"]) 112 | price, volume = price.Truncate(int32(market.BidFixed)), volume.Truncate(int32(market.AskFixed)) 113 | if price.LessThanOrEqual(decimal.Zero) { 114 | return utils.BuildError("1024") 115 | } 116 | if volume.LessThanOrEqual(decimal.Zero) { 117 | return utils.BuildError("1023") 118 | } 119 | var orderType string 120 | locked := volume 121 | if side == "buy" { 122 | locked = volume.Mul(price) 123 | orderType = "OrderBid" 124 | } else if side == "sell" { 125 | orderType = "OrderAsk" 126 | } else { 127 | return utils.BuildError("1022") 128 | } 129 | order := Order{ 130 | Source: context.Param("platform") + "-APIv1", 131 | State: WAIT, 132 | UserId: user.Id, 133 | MarketId: market.Id, 134 | Volume: volume, 135 | OriginVolume: volume, 136 | Price: price, 137 | OrderType: "limit", 138 | Type: orderType, 139 | Locked: locked, 140 | OriginLocked: locked, 141 | } 142 | response := utils.SuccessResponse 143 | err := tryToChangeAccount(context, &order, &market, side, user.Id, 2) 144 | if err == nil { 145 | pushMessageToMatching(&order, &market, "submit") 146 | response := utils.SuccessResponse 147 | response.Body = order 148 | } else { 149 | response = utils.BuildError("3022") 150 | response.Body = order 151 | return response 152 | } 153 | return context.JSON(http.StatusOK, response) 154 | 155 | } 156 | 157 | func V1PostOrderDelete(context echo.Context) error { 158 | params := context.Get("params").(map[string]string) 159 | user := context.Get("current_user").(User) 160 | mainDB := utils.MainDbBegin() 161 | defer mainDB.DbRollback() 162 | var order Order 163 | if mainDB.Where("state = 100 AND id = ? AND user_id = ?", params["id"], user.Id).First(&order).RecordNotFound() { 164 | return utils.BuildError("2004") 165 | } 166 | pushMessageToMatching(&order, &order.Market, "cancel") 167 | response := utils.SuccessResponse 168 | response.Body = order 169 | return context.JSON(http.StatusOK, response) 170 | } 171 | 172 | func V1PostOrdersClear(context echo.Context) error { 173 | params := context.Get("params").(map[string]string) 174 | user := context.Get("current_user").(User) 175 | var market Market 176 | mainDB := utils.MainDbBegin() 177 | defer mainDB.DbRollback() 178 | if mainDB.Where("name = ?", params["market"]).First(&market).RecordNotFound() { 179 | return utils.BuildError("1021") 180 | } 181 | var orders []Order 182 | if market.Id == 0 { 183 | mainDB.Where("state = 100 AND user_id = ?", user.Id).Find(&orders) 184 | for _, order := range orders { 185 | pushMessageToMatching(&order, &market, "cancel") 186 | } 187 | } else { 188 | mainDB.Where("state = 100 AND user_id = ? AND market_id = ?", user.Id, market.Id).Find(&orders) 189 | for _, order := range orders { 190 | pushMessageToMatching(&order, &market, "cancel") 191 | } 192 | } 193 | response := utils.SuccessResponse 194 | response.Body = orders 195 | return context.JSON(http.StatusOK, response) 196 | } 197 | 198 | func pushMessageToMatching(order *Order, market *Market, option string) { 199 | payload := MatchingPayload{ 200 | Action: option, 201 | Order: OrderJson{ 202 | Id: (*order).Id, 203 | MarketId: (*order).MarketId, 204 | Type: (*order).OType(), 205 | OrderType: (*order).OrderType, 206 | Volume: (*order).Volume, 207 | Price: (*order).Price, 208 | Locked: (*order).Locked, 209 | Timestamp: (*order).CreatedAt.Unix(), 210 | }, 211 | } 212 | b, err := json.Marshal(payload) 213 | if err != nil { 214 | fmt.Println("error:", err) 215 | } 216 | 217 | err = envConfig.RabbitMqConnect.PublishMessageWithRouteKey( 218 | envConfig.AmqpGlobalConfig.Exchange["matching"]["key"], 219 | market.Code, "text/plain", 220 | false, 221 | false, 222 | &b, 223 | amqp.Table{}, 224 | amqp.Persistent, 225 | "", 226 | ) 227 | if err != nil { 228 | fmt.Println("{ error:", err, "}") 229 | panic(err) 230 | } 231 | } 232 | 233 | func tryToChangeAccount(context echo.Context, order *Order, market *Market, side string, user_id, times int) error { 234 | mainDB := utils.MainDbBegin() 235 | defer mainDB.DbRollback() 236 | var account Account 237 | if side == "buy" { 238 | if mainDB.Where("user_id = ?", user_id).Where("currency = ?", (*market).BidCurrencyId). 239 | First(&account).RecordNotFound() { 240 | return utils.BuildError("1026") 241 | } 242 | } else if side == "sell" { 243 | if mainDB.Where("user_id = ?", user_id).Where("currency = ?", (*market).AskCurrencyId). 244 | First(&account).RecordNotFound() { 245 | return utils.BuildError("1026") 246 | } 247 | } 248 | if account.Balance.Sub((*order).Locked).IsNegative() { 249 | return utils.BuildError("1041") 250 | } 251 | result := mainDB.Create(order) 252 | if result.Error != nil { 253 | mainDB.DbRollback() 254 | if times > 0 { 255 | (*order).Id = 0 256 | return tryToChangeAccount(context, order, market, side, user_id, times-1) 257 | } 258 | } 259 | err := account.LockFunds(mainDB, (*order).Locked, ORDER_SUBMIT, (*order).Id, "Order") 260 | if err == nil { 261 | mainDB.DbCommit() 262 | return nil 263 | } 264 | 265 | mainDB.DbRollback() 266 | if times > 0 { 267 | (*order).Id = 0 268 | return tryToChangeAccount(context, order, market, side, user_id, times-1) 269 | } 270 | return utils.BuildError("2002") 271 | } 272 | -------------------------------------------------------------------------------- /api/v1/orderBook.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/gomodule/redigo/redis" 10 | "github.com/labstack/echo" 11 | . "github.com/oldfritter/goDCE/models" 12 | "github.com/oldfritter/goDCE/utils" 13 | ) 14 | 15 | type Depth struct { 16 | Timestamp int64 `json:"timestamp"` 17 | Asks interface{} `json:"asks"` 18 | Bids interface{} `json:"bids"` 19 | PriceGroupFixed string `json:"price_group_fixed"` 20 | } 21 | 22 | func V1Getdepth(context echo.Context) error { 23 | var market Market 24 | mainDB := utils.MainDbBegin() 25 | defer mainDB.DbRollback() 26 | dataRedis := utils.GetRedisConn("data") 27 | defer dataRedis.Close() 28 | if mainDB.Where("name = ?", context.QueryParam("market")).First(&market).RecordNotFound() { 29 | return utils.BuildError("1021") 30 | } 31 | limit := 300 32 | if context.QueryParam("limit") != "" { 33 | limit, _ = strconv.Atoi(context.QueryParam("limit")) 34 | if limit > 1000 { 35 | limit = 300 36 | } 37 | } 38 | vAsk, _ := redis.String(dataRedis.Do("GET", market.AskRedisKey())) 39 | vBid, _ := redis.String(dataRedis.Do("GET", market.BidRedisKey())) 40 | if vAsk == "" || vBid == "" { 41 | return utils.BuildError("1026") 42 | } 43 | var asksOrigin, bidsOrigin interface{} 44 | var asks, bids []interface{} 45 | json.Unmarshal([]byte(vAsk), &asksOrigin) 46 | json.Unmarshal([]byte(vBid), &bidsOrigin) 47 | for i, askOrigin := range asksOrigin.([]interface{}) { 48 | if i < limit { 49 | asks = append(asks, askOrigin) 50 | } 51 | } 52 | for i, bidOrigin := range bidsOrigin.([]interface{}) { 53 | if i < limit { 54 | bids = append(bids, bidOrigin) 55 | } 56 | } 57 | var depth Depth 58 | depth.PriceGroupFixed = strconv.Itoa(market.PriceGroupFixed) 59 | depth.Timestamp = time.Now().Unix() 60 | depth.Asks = asks 61 | depth.Bids = bids 62 | return context.JSON(http.StatusOK, depth) 63 | } 64 | -------------------------------------------------------------------------------- /api/v1/ticker.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/labstack/echo" 8 | . "github.com/oldfritter/goDCE/models" 9 | "github.com/oldfritter/goDCE/utils" 10 | ) 11 | 12 | func V1GetTickersMarket(context echo.Context) error { 13 | market, err := FindMarketByCode(context.Param("market")) 14 | if err != nil { 15 | return utils.BuildError("1021") 16 | } 17 | ticker := Ticker{MarketId: market.Id, TickerAspect: market.Ticker} 18 | response := utils.SuccessResponse 19 | response.Body = ticker 20 | return context.JSON(http.StatusOK, response) 21 | } 22 | 23 | func V1GetTickers(context echo.Context) error { 24 | var tickers []Ticker 25 | for _, market := range AllMarkets { 26 | tickers = append(tickers, Ticker{ 27 | MarketId: market.Id, 28 | At: time.Now().Unix(), 29 | Name: market.Name, 30 | TickerAspect: market.Ticker, 31 | }) 32 | } 33 | 34 | response := utils.SuccessResponse 35 | response.Body = tickers 36 | return context.JSON(http.StatusOK, response) 37 | } 38 | -------------------------------------------------------------------------------- /api/v1/trade.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/gomodule/redigo/redis" 9 | "github.com/labstack/echo" 10 | . "github.com/oldfritter/goDCE/models" 11 | "github.com/oldfritter/goDCE/utils" 12 | ) 13 | 14 | type TradeCache struct { 15 | Amount string `json:"amount"` 16 | Date int `json:"date"` 17 | OwnerSn string `json:"owner_sn"` 18 | Price string `json:"price"` 19 | Tid int `json:"tid"` 20 | TradeType string `json:"type"` 21 | } 22 | 23 | func V1GetTrades(context echo.Context) error { 24 | if context.QueryParam("market") == "" { 25 | return utils.BuildError("1021") 26 | } 27 | limit := 30 28 | if context.QueryParam("limit") != "" { 29 | limit, _ = strconv.Atoi(context.QueryParam("limit")) 30 | } 31 | tickerRedis := utils.GetRedisConn("ticker") 32 | defer tickerRedis.Close() 33 | var all, result []TradeCache 34 | key := "hex:redismodels::latesttrades:" + context.QueryParam("market") 35 | re, _ := redis.String(tickerRedis.Do("GET", key)) 36 | if re == "" { 37 | return utils.BuildError("1026") 38 | } 39 | json.Unmarshal([]byte(re), &all) 40 | for i := 0; i < limit && i < len(all); i++ { 41 | result = append(result, all[i]) 42 | } 43 | response := utils.SuccessResponse 44 | response.Body = result 45 | return context.JSON(http.StatusOK, response) 46 | } 47 | 48 | func V1GetTradesMy(context echo.Context) error { 49 | user := context.Get("current_user").(User) 50 | db := utils.MainDbBegin() 51 | defer db.DbRollback() 52 | limit := 30 53 | if context.QueryParam("limit") != "" { 54 | limit, _ = strconv.Atoi(context.QueryParam("limit")) 55 | } 56 | if context.QueryParam("market") == "" { 57 | return utils.BuildError("1021") 58 | } 59 | var market Market 60 | if db.Where("name = ?", context.QueryParam("market")).First(&market).RecordNotFound() { 61 | return utils.BuildError("1021") 62 | } 63 | page := 1 64 | if context.QueryParam("page") != "" { 65 | page, _ = strconv.Atoi(context.QueryParam("page")) 66 | if page < 1 { 67 | page = 1 68 | } 69 | } 70 | orderParam := "id DESC" 71 | if context.QueryParam("order") == "asc" { 72 | orderParam = "id ASC" 73 | } 74 | var trades []Trade 75 | db.Order(orderParam).Where("currency = ? AND (ask_user_id = ? OR bid_user_id = ?)", market.Code, user.Id, user.Id). 76 | Offset(limit * (page - 1)).Limit(limit).Find(&trades) 77 | 78 | for i, trade := range trades { 79 | if trade.BidUserId == user.Id && trade.AskUserId == user.Id { 80 | trades[i].Side = "self" 81 | } else if trade.BidUserId == user.Id { 82 | trades[i].Side = "buy" 83 | } else if trade.AskUserId == user.Id { 84 | trades[i].Side = "sell" 85 | } 86 | } 87 | response := utils.SuccessResponse 88 | response.Body = trades 89 | return context.JSON(http.StatusOK, response) 90 | } 91 | -------------------------------------------------------------------------------- /api/v1/user.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | "time" 7 | 8 | "github.com/labstack/echo" 9 | . "github.com/oldfritter/goDCE/models" 10 | "github.com/oldfritter/goDCE/utils" 11 | ) 12 | 13 | func V1GetUsersMe(context echo.Context) error { 14 | response := utils.SuccessResponse 15 | response.Body = context.Get("current_user") 16 | return context.JSON(http.StatusOK, response) 17 | } 18 | 19 | func V1GetUsersAccounts(context echo.Context) error { 20 | user := context.Get("current_user").(User) 21 | mainDB := utils.MainDbBegin() 22 | defer mainDB.DbRollback() 23 | var accounts []Account 24 | mainDB.Joins("INNER JOIN (currencies) ON (accounts.currency_id = currencies.id)"). 25 | Where("user_id = ?", user.Id).Where("currencies.visible is true").Find(&accounts) 26 | 27 | response := utils.SuccessResponse 28 | response.Body = accounts 29 | return context.JSON(http.StatusOK, response) 30 | } 31 | 32 | func V1GetUsersAccountsCurrency(context echo.Context) error { 33 | user := context.Get("current_user").(User) 34 | mainDB := utils.MainDbBegin() 35 | defer mainDB.DbRollback() 36 | var currency Currency 37 | if mainDB.Where("code = ?", strings.ToLower(context.Param("currency"))).First(¤cy).RecordNotFound() { 38 | return utils.BuildError("3027") 39 | } 40 | var account Account 41 | mainDB.Where("user_id = ? AND currency= ?", user.Id, currency.Id).First(&account) 42 | 43 | response := utils.SuccessResponse 44 | response.Body = account 45 | return context.JSON(http.StatusOK, response) 46 | } 47 | 48 | func V1PostUsersAccountsCurrency(context echo.Context) error { 49 | user := context.Get("current_user").(User) 50 | mainDB := utils.MainDbBegin() 51 | defer mainDB.DbRollback() 52 | var currency Currency 53 | if mainDB.Where("key = ?", strings.ToLower(context.Param("currency"))).First(¤cy).RecordNotFound() { 54 | return utils.BuildError("1027") 55 | } 56 | var account Account 57 | if mainDB.Where("user_id = ? AND currency_id = ?", user.Id, currency.Id).First(&account).RecordNotFound() { 58 | account.UserId = user.Id 59 | account.CurrencyId = currency.Id 60 | now := time.Now() 61 | account.CreatedAt = now 62 | account.UpdatedAt = now 63 | mainDB.Save(&account) 64 | mainDB.DbCommit() 65 | } 66 | 67 | response := utils.SuccessResponse 68 | response.Body = account 69 | return context.JSON(http.StatusOK, response) 70 | } 71 | 72 | func V1PostUsersLogin(context echo.Context) error { 73 | mainDB := utils.MainDbBegin() 74 | defer mainDB.DbRollback() 75 | var user User 76 | if mainDB.Joins("INNER JOIN (identities) ON (identities.user_id users.id)"). 77 | Where("identities.source = ?", context.FormValue("source")). 78 | Where("identities.symbol = ?", context.FormValue("symbol")).First(&user).RecordNotFound() { 79 | return utils.BuildError("2026") 80 | } 81 | user.Password = context.FormValue("password") 82 | if user.CompareHashAndPassword() { 83 | context.Set("current_user", user) 84 | } else { 85 | return utils.BuildError("2026") 86 | } 87 | 88 | var token, inToken Token 89 | token.UserId = user.Id 90 | token.InitializeLoginToken() 91 | if !mainDB.Where("token = ?", token.Token).First(&inToken).RecordNotFound() { 92 | token.InitializeLoginToken() 93 | } 94 | mainDB.Create(&token) 95 | mainDB.Where("user_id = ?", user.Id).Where("expire_at < ?", time.Now()).First(&user.Tokens) 96 | mainDB.DbCommit() 97 | 98 | response := utils.SuccessResponse 99 | response.Body = user 100 | return context.JSON(http.StatusOK, response) 101 | } 102 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | go build -o ./cmd/api api/api.go 3 | go build -o ./cmd/matching trade/matching.go 4 | go build -o ./cmd/cancel order/cancel.go 5 | go build -o ./cmd/treat trade/treat.go 6 | go build -o ./cmd/workers workers/workers.go 7 | -------------------------------------------------------------------------------- /config/amqp.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "path/filepath" 7 | 8 | "github.com/oldfritter/sneaker-go/v3" 9 | "gopkg.in/yaml.v2" 10 | ) 11 | 12 | var ( 13 | RabbitMqConnect sneaker.RabbitMqConnect 14 | ) 15 | 16 | var AmqpGlobalConfig struct { 17 | Connect struct { 18 | Host string `yaml:"host"` 19 | Port string `yaml:"port"` 20 | Username string `yaml:"username"` 21 | Password string `yaml:"password"` 22 | Vhost string `yaml:"vhost"` 23 | } `yaml:"connect"` 24 | 25 | Exchange map[string]map[string]string `yaml:"exchange"` 26 | Queue map[string]map[string]string `yaml:"queue"` 27 | } 28 | 29 | func InitAmqpConfig() { 30 | pathStr, _ := filepath.Abs("config/amqp.yml") 31 | content, err := ioutil.ReadFile(pathStr) 32 | if err != nil { 33 | log.Fatal(err) 34 | return 35 | } 36 | err = yaml.Unmarshal(content, &AmqpGlobalConfig) 37 | if err != nil { 38 | log.Fatal(err) 39 | return 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /config/amqp.yml.example: -------------------------------------------------------------------------------- 1 | connect: 2 | host: 127.0.0.1 3 | port: 5672 4 | username: guest 5 | password: guest 6 | vhost: / 7 | 8 | exchange: 9 | default: 10 | key: goDCE.default 11 | type: direct 12 | matching: 13 | key: goDCE.trade.matching 14 | type: direct 15 | trade: 16 | key: goDCE.trade.treat 17 | type: direct 18 | cancel: 19 | key: goDCE.order.cancel 20 | type: direct 21 | fanout: 22 | default: goDCE.fanout 23 | k: goDCE.fanout.notify.k 24 | ticker: goDCE.fanout.notify.ticker 25 | 26 | queue: 27 | matching: 28 | reload: goDCE.reload.trade.matching 29 | trade: 30 | reload: goDCE.reload.trade.treat 31 | cancel: 32 | reload: goDCE.reload.order.cancel 33 | -------------------------------------------------------------------------------- /config/aws_s3.yml.example: -------------------------------------------------------------------------------- 1 | S3_BACKUP_BUCKET: 2 | AWS_ACCESS_KEY_ID: 3 | AWS_SECRET_ACCESS_KEY: 4 | AWS_REGION: 5 | -------------------------------------------------------------------------------- /config/database.yml.example: -------------------------------------------------------------------------------- 1 | production: 2 | main: 3 | database: goDCE 4 | username: root 5 | password: 6 | host: 127.0.0.1 7 | port: 3306 8 | dbargs: charset=utf8&parseTime=True&loc=Local 9 | pool: 5 10 | backup: 11 | database: goDCE_backup 12 | username: root 13 | password: 14 | host: 127.0.0.1 15 | port: 3306 16 | dbargs: charset=utf8&parseTime=True&loc=Local 17 | pool: 5 18 | 19 | development: 20 | main: 21 | database: goDCE 22 | username: root 23 | password: 24 | host: 127.0.0.1 25 | port: 3306 26 | dbargs: charset=utf8&parseTime=True&loc=Local 27 | pool: 5 28 | backup: 29 | database: goDCE_backup 30 | username: root 31 | password: 32 | host: 127.0.0.1 33 | port: 3306 34 | dbargs: charset=utf8&parseTime=True&loc=Local 35 | pool: 5 36 | -------------------------------------------------------------------------------- /config/env.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "path/filepath" 7 | 8 | "gopkg.in/yaml.v2" 9 | ) 10 | 11 | type Env struct { 12 | Model string `yaml:"model"` 13 | Node string `yaml:"node"` 14 | 15 | Newrelic struct { 16 | AppName string `yaml:"app_name"` 17 | LicenseKey string `yaml:"license_key"` 18 | } `yaml:"newrelic"` 19 | 20 | Schedules []string `yaml:"schedules"` 21 | } 22 | 23 | var CurrentEnv Env 24 | 25 | func InitEnv() { 26 | path_str, _ := filepath.Abs("config/env.yml") 27 | content, err := ioutil.ReadFile(path_str) 28 | if err != nil { 29 | log.Fatal(err) 30 | return 31 | } 32 | err = yaml.Unmarshal(content, &CurrentEnv) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /config/env.yml.example: -------------------------------------------------------------------------------- 1 | model: production 2 | node: a 3 | newrelic: 4 | app_name: 5 | license_key: 6 | -------------------------------------------------------------------------------- /config/interfaces.yml.example: -------------------------------------------------------------------------------- 1 | --- 2 | - path: /web 3 | method: GET 4 | - path: /web/* 5 | method: GET 6 | 7 | # V1 -------------------------------------------------------------------------------------------------------------------------------------------------- 8 | - path: /api/:platform/v1/currencies 9 | method: GET 10 | 11 | - path: /api/:platform/v1/k 12 | method: GET 13 | - path: /api/:platform/v1/chart 14 | method: GET 15 | 16 | - path: /api/:platform/v1/markets 17 | method: GET 18 | 19 | - path: /api/:platform/v1/users/login 20 | method: POST 21 | - path: /api/:platform/v1/users/me 22 | auth: true 23 | method: GET 24 | - path: /api/:platform/v1/users/accounts/:currency 25 | auth: true 26 | method: POST 27 | - path: /api/:platform/v1/users/accounts 28 | auth: true 29 | sign: true 30 | method: GET 31 | 32 | - path: /api/:platform/v1/depth 33 | method: GET 34 | 35 | - path: /api/:platform/v1/order 36 | auth: true 37 | method: GET 38 | - path: /api/:platform/v1/orders 39 | auth: true 40 | method: GET 41 | - path: /api/:platform/v1/orders 42 | auth: true 43 | sign: true 44 | method: POST 45 | reloading_matching_check: true 46 | market_can_trade: true 47 | is_rabbitmq_connected: true 48 | - path: /api/:platform/v1/orders/multi2 49 | method: POST 50 | auth: true 51 | sign: true 52 | reloading_matching_check: true 53 | market_can_trade: true 54 | is_rabbitmq_connected: true 55 | - path: /api/:platform/v1/order/delete 56 | auth: true 57 | sign: true 58 | method: POST 59 | - path: /api/:platform/v1/orders/delete 60 | auth: true 61 | sign: true 62 | method: POST 63 | - path: /api/:platform/v1/orders/clear 64 | auth: true 65 | sign: true 66 | method: POST 67 | 68 | - path: /api/:platform/v1/tickers 69 | method: GET 70 | - path: /api/:platform/v1/tickers/:market 71 | method: GET 72 | 73 | - path: /api/:platform/v1/trades 74 | method: GET 75 | - path: /api/:platform/v1/trades/my 76 | auth: true 77 | method: GET 78 | 79 | -------------------------------------------------------------------------------- /config/qiniu.yml.example: -------------------------------------------------------------------------------- 1 | access_key: P4i11d2V8IpsBIkvQPlmUWbt-72UY-fLdY9WaWHa 2 | secret_key: wgU5keLT1nyNQ4cIUIn9XDwtOP2oZvOMJyxhkYS- 3 | backup_bucket: backup 4 | -------------------------------------------------------------------------------- /config/redis.yml.example: -------------------------------------------------------------------------------- 1 | matching: 2 | server: 127.0.0.1:6379 3 | db: 0 4 | pool: 5 5 | data: 6 | server: 127.0.0.1:6379 7 | db: 3 8 | pool: 5 9 | k: 10 | server: 127.0.0.1:6379 11 | db: 1 12 | pool: 5 13 | ticker: 14 | server: 127.0.0.1:6379 15 | db: 3 16 | pool: 5 17 | limit: 18 | server: 127.0.0.1:6379 19 | db: 4 20 | pool: 5 21 | -------------------------------------------------------------------------------- /config/workers.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | sneaker "github.com/oldfritter/sneaker-go/v3" 5 | ) 6 | 7 | var ( 8 | AllWorkers []sneaker.Worker 9 | AllWorkerIs []sneaker.WorkerI 10 | ) 11 | -------------------------------------------------------------------------------- /config/workers.yml.example: -------------------------------------------------------------------------------- 1 | --- 2 | - name: KLineWorker 3 | exchange: goDCE.default 4 | routing_key: goDCE.k 5 | queue: goDCE.k 6 | durable: true 7 | ack: true 8 | threads: 4 9 | 10 | - name: TickerWorker 11 | exchange: goDCE.default 12 | routing_key: goDCE.ticker 13 | queue: goDCE.ticker 14 | durable: true 15 | ack: true 16 | threads: 4 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/oldfritter/goDCE 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/andybalholm/cascadia v1.2.0 // indirect 7 | github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 // indirect 8 | github.com/aws/aws-sdk-go v1.29.30 9 | github.com/dafiti/echo-middleware v0.0.0-20180423194757-e57a87d075ea 10 | github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect 11 | github.com/disintegration/imaging v1.6.2 // indirect 12 | github.com/emirpasic/gods v1.12.0 // indirect 13 | github.com/fatih/color v1.9.0 // indirect 14 | github.com/go-sql-driver/mysql v1.5.0 15 | github.com/gomodule/redigo v2.0.0+incompatible 16 | github.com/gorilla/context v1.1.1 // indirect 17 | github.com/gorilla/sessions v1.2.0 // indirect 18 | github.com/gosimple/slug v1.9.0 // indirect 19 | github.com/jinzhu/configor v1.2.0 // indirect 20 | github.com/jinzhu/gorm v1.9.12 21 | github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28 22 | github.com/labstack/echo v3.3.10+incompatible 23 | github.com/labstack/gommon v0.3.0 // indirect 24 | github.com/microcosm-cc/bluemonday v1.0.2 // indirect 25 | github.com/newrelic/go-agent v3.4.0+incompatible // indirect 26 | github.com/oldfritter/matching v0.0.0-20190827024937-efdf83ea2ab0 27 | github.com/oldfritter/sneaker-go/v3 v3.0.2 28 | github.com/qiniu/api.v7/v7 v7.5.0 29 | github.com/qor/admin v0.0.0-20200315024928-877b98a68a6f // indirect 30 | github.com/qor/assetfs v0.0.0-20170713023933-ff57fdc13a14 // indirect 31 | github.com/qor/cache v0.0.0-20171031031927-c9d48d1f13ba // indirect 32 | github.com/qor/i18n v0.0.0-20181014061908-f7206d223bcd 33 | github.com/qor/media v0.0.0-20200714084904-de0fa0e627d7 // indirect 34 | github.com/qor/middlewares v0.0.0-20170822143614-781378b69454 // indirect 35 | github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76 // indirect 36 | github.com/qor/qor v0.0.0-20200224122013-457d2e3f50e1 // indirect 37 | github.com/qor/responder v0.0.0-20171031032654-b6def473574f // indirect 38 | github.com/qor/roles v0.0.0-20171127035124-d6375609fe3e // indirect 39 | github.com/qor/serializable_meta v0.0.0-20180510060738-5fd8542db417 // indirect 40 | github.com/qor/session v0.0.0-20170907035918-8206b0adab70 // indirect 41 | github.com/qor/validations v0.0.0-20171228122639-f364bca61b46 // indirect 42 | github.com/robfig/cron v1.2.0 43 | github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc 44 | github.com/sirupsen/logrus v1.5.0 // indirect 45 | github.com/streadway/amqp v0.0.0-20200108173154-1c71cc93ed71 46 | github.com/theplant/cldr v0.0.0-20190423050709-9f76f7ce4ee8 // indirect 47 | github.com/theplant/htmltestingutils v0.0.0-20190423050759-0e06de7b6967 // indirect 48 | github.com/theplant/testingutils v0.0.0-20190603093022-26d8b4d95c61 // indirect 49 | github.com/valyala/fasttemplate v1.1.0 // indirect 50 | github.com/yosssi/gohtml v0.0.0-20200519115854-476f5b4b8047 // indirect 51 | golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 52 | gopkg.in/yaml.v2 v2.2.8 53 | ) 54 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY= 3 | github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0= 4 | github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= 5 | github.com/aws/aws-sdk-go v1.29.30 h1:HEEb7p5H850+hKVLRif2fWpaoJe5ZkZ5WUJwJ+CKWV4= 6 | github.com/aws/aws-sdk-go v1.29.30/go.mod h1:1KvfttTE3SPKMpo8g2c6jL3ZKfXtFvKscTgahTma5Xg= 7 | github.com/dafiti/echo-middleware v0.0.0-20180423194757-e57a87d075ea h1:v4z3/022CYpUTXPePGjVsqr1rx3dEEpdEWcTqUtXbv8= 8 | github.com/dafiti/echo-middleware v0.0.0-20180423194757-e57a87d075ea/go.mod h1:essEU8/iGS5w4Ih9lxX073dNv9nxoWTlHX1Zi4I4q6g= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 12 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 13 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 14 | github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= 15 | github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= 16 | github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 17 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 18 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 19 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 20 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= 21 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 22 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 23 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 24 | github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= 25 | github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= 26 | github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= 27 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 28 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 29 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 30 | github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ= 31 | github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 32 | github.com/gosimple/slug v1.9.0 h1:r5vDcYrFz9BmfIAMC829un9hq7hKM4cHUrsv36LbEqs= 33 | github.com/gosimple/slug v1.9.0/go.mod h1:AMZ+sOVe65uByN3kgEyf9WEBKBCSS+dJjMX9x4vDJbg= 34 | github.com/jinzhu/configor v1.2.0/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc= 35 | github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q= 36 | github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs= 37 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 38 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 39 | github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= 40 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 41 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= 42 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 43 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 44 | github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28 h1:mkl3tvPHIuPaWsLtmHTybJeoVEW7cbePK73Ir8VtruA= 45 | github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28/go.mod h1:T/T7jsxVqf9k/zYOqbgNAsANsjxTd1Yq3htjDhQ1H0c= 46 | github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= 47 | github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= 48 | github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= 49 | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= 50 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 51 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 52 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= 53 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 54 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 55 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 56 | github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= 57 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 58 | github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 59 | github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s= 60 | github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= 61 | github.com/newrelic/go-agent v3.4.0+incompatible h1:GhUhNLDdR3ETfUVJAN/czXlqRTcgbPs6U02jYhf15rg= 62 | github.com/newrelic/go-agent v3.4.0+incompatible/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ= 63 | github.com/oldfritter/matching v0.0.0-20190827024937-efdf83ea2ab0 h1:IojUXXuzJ+K7qisHlNqgB07U+E7j+moRnGU1F6WYors= 64 | github.com/oldfritter/matching v0.0.0-20190827024937-efdf83ea2ab0/go.mod h1:qtjPJJjsQnSacfr0tDIkqP91I69DFJKwY/0EBlwVumA= 65 | github.com/oldfritter/sneaker-go v1.1.0 h1:J5bXkAx73eVylAgqM+4x8oNdoHKmAHK9IRmTyNJtRnU= 66 | github.com/oldfritter/sneaker-go/v2 v2.0.8 h1:GAQ1QyI/XDg3SK6hfhRoY684zsVzVGldgc4gPFah2fk= 67 | github.com/oldfritter/sneaker-go/v2 v2.0.8/go.mod h1:bryvrz4puhkiLZVVZFVC/VcPmZbPHC5mq9o7UKTJoj4= 68 | github.com/oldfritter/sneaker-go/v2 v2.1.1 h1:o130Ma1FUUpqgc9ti/Ks2wea1w5oB1iXpAFgWBNu96A= 69 | github.com/oldfritter/sneaker-go/v3 v3.0.2 h1:gxSMDuUxGFNzoV3SE65I1OlUaS5NRZS3jJDiT3bQZLA= 70 | github.com/oldfritter/sneaker-go/v3 v3.0.2/go.mod h1:I0YXd0M0D/xc1M4LsDriRf0oBwuD7TAvAH2w9RuvHqc= 71 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 72 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 73 | github.com/qiniu/api.v7/v7 v7.5.0 h1:DY6NrIp6FZ1GP4Roc9hRnO2m+OLzASYNnvz5Mbgw1rk= 74 | github.com/qiniu/api.v7/v7 v7.5.0/go.mod h1:VE5oC5rkE1xul0u1S2N0b2Uxq9/6hZzhyqjgK25XDcM= 75 | github.com/qor/admin v0.0.0-20200315024928-877b98a68a6f h1:U5CYGBUdxvfhmaPIqnFPaTbNza3ihKo/Oy9YlvwHxE4= 76 | github.com/qor/admin v0.0.0-20200315024928-877b98a68a6f/go.mod h1:Sm5kX+Hkq1LKiFyqZJLnncUg8dWM/2roOEiy98NOUzA= 77 | github.com/qor/assetfs v0.0.0-20170713023933-ff57fdc13a14 h1:JRpyNNSRAkwNHd4WgyPcalTAhxOCh3eFNMoQkxWhjSw= 78 | github.com/qor/assetfs v0.0.0-20170713023933-ff57fdc13a14/go.mod h1:GZSCP3jIneuPsav3pXmpmJwz9ES+Fuq4ZPOUC3wwckQ= 79 | github.com/qor/cache v0.0.0-20171031031927-c9d48d1f13ba h1:XBEqqX1xirq3pYMGw8Jr0urCfhPXbJEh1KPwKLd24MM= 80 | github.com/qor/cache v0.0.0-20171031031927-c9d48d1f13ba/go.mod h1:DvjFc9BNYPuokuaZVaXvVukLqqkYDlToNC+Gszv3oRs= 81 | github.com/qor/i18n v0.0.0-20181014061908-f7206d223bcd h1:vMNFvjn0dHz7Y+RedFPwvDore+i+DvjsEN33rsfUC+M= 82 | github.com/qor/i18n v0.0.0-20181014061908-f7206d223bcd/go.mod h1:J1gDK288kgSz1oIV9MgV0JmL6pxtXxL/tN7MvCJB3V4= 83 | github.com/qor/media v0.0.0-20200714084904-de0fa0e627d7/go.mod h1:hzx9cTt5MpROMVIhwNhRJETc7e3a1J4bLNYflkcQ1wk= 84 | github.com/qor/middlewares v0.0.0-20170822143614-781378b69454 h1:+WCc1IigwWpWBxMFsmLUsIF230TakGHstDajd8aKDAc= 85 | github.com/qor/middlewares v0.0.0-20170822143614-781378b69454/go.mod h1:PejEyg3hS+Toh5m0AKRv2jK5br8qIoHLqmHrpg0WJYg= 86 | github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76/go.mod h1:JhtPzUhP5KGtCB2yksmxuYAD4hEWw4qGQJpucjsm3U0= 87 | github.com/qor/qor v0.0.0-20200224122013-457d2e3f50e1 h1:oxV/UtQmFJiWvh18ngprVhnyxRmSZaK/NcH7REjfEkk= 88 | github.com/qor/qor v0.0.0-20200224122013-457d2e3f50e1/go.mod h1:oG+LgDEnsI9avcFFdczoZnBe3rw42s4cG433w6XpEig= 89 | github.com/qor/responder v0.0.0-20171031032654-b6def473574f h1:sKELSAyL+z5BRHFe97Bx71z197cBEobVJ6rASKTMSqU= 90 | github.com/qor/responder v0.0.0-20171031032654-b6def473574f/go.mod h1:0TL2G+qGDYhHJ6XIJ6UcqkZVN+jp8AGqGU2wDnv2qas= 91 | github.com/qor/roles v0.0.0-20171127035124-d6375609fe3e h1:F0BNcPJKfubM/+IIILu/GbrH9v2vPZWQ5/StSRKUfK4= 92 | github.com/qor/roles v0.0.0-20171127035124-d6375609fe3e/go.mod h1:++RicL9Ia/BrQHppwAsMc5CA7mAjnBLNniB46MzUteA= 93 | github.com/qor/serializable_meta v0.0.0-20180510060738-5fd8542db417/go.mod h1:ZhRkze6qCgCNNqisHEFTAkndoBtY/XJbA03LJLJrzDY= 94 | github.com/qor/session v0.0.0-20170907035918-8206b0adab70 h1:8l21EEdlZ9R0AA3FbeUAANc5NAx8Y3tn1VKbyAgjYlI= 95 | github.com/qor/session v0.0.0-20170907035918-8206b0adab70/go.mod h1:ldgcaca0ZGx6tFtd/w0ELq5jHD/KLJ1Lbdn8qhr/pQ0= 96 | github.com/qor/validations v0.0.0-20171228122639-f364bca61b46 h1:dRlsVUhwD1pwrasuVbNooGQITYjKzmXK5eYoEEvBGQI= 97 | github.com/qor/validations v0.0.0-20171228122639-f364bca61b46/go.mod h1:UJsA0AuvrKNaWtrb1UzKai10mN3ZBbQkPjUHpxwahTc= 98 | github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ= 99 | github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q= 100 | github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= 101 | github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= 102 | github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY= 103 | github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 104 | github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q= 105 | github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= 106 | github.com/streadway/amqp v0.0.0-20200108173154-1c71cc93ed71 h1:2MR0pKUzlP3SGgj5NYJe/zRYDwOu9ku6YHy+Iw7l5DM= 107 | github.com/streadway/amqp v0.0.0-20200108173154-1c71cc93ed71/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= 108 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 109 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 110 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 111 | github.com/theplant/cldr v0.0.0-20190423050709-9f76f7ce4ee8 h1:di0cR5qqo2DllBMwmP75kZpUX6dAXhsn1O2dshQfMaA= 112 | github.com/theplant/cldr v0.0.0-20190423050709-9f76f7ce4ee8/go.mod h1:MIL7SmF8wRAYDn+JexczVRUiJXTCi4VbQavsCKWKwXI= 113 | github.com/theplant/htmltestingutils v0.0.0-20190423050759-0e06de7b6967/go.mod h1:86iN4EAYaQbx1VTW5uPslTIviRkYH8CzslMC//g+BgY= 114 | github.com/theplant/testingutils v0.0.0-20190603093022-26d8b4d95c61/go.mod h1:p22Q3Bg5ML+hdI3QSQkB/pZ2+CjfOnGugoQIoyE2Ub8= 115 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 116 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 117 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 118 | github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4= 119 | github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 120 | github.com/yosssi/gohtml v0.0.0-20200519115854-476f5b4b8047/go.mod h1:+ccdNT0xMY1dtc5XBxumbYfOUhmduiGudqaDgD2rVRE= 121 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 122 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 123 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 124 | golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM= 125 | golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 126 | golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 127 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 128 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 129 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 130 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 131 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= 132 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 133 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 134 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 135 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 136 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 137 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 138 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= 139 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 140 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 141 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 142 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 143 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 144 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 145 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 146 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 147 | -------------------------------------------------------------------------------- /initializers/auth.go: -------------------------------------------------------------------------------- 1 | package initializers 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "path/filepath" 7 | "time" 8 | 9 | "github.com/labstack/echo" 10 | . "github.com/oldfritter/goDCE/models" 11 | "github.com/oldfritter/goDCE/utils" 12 | "gopkg.in/yaml.v2" 13 | ) 14 | 15 | type ApiInterface struct { 16 | Method string `yaml:"method"` 17 | Path string `yaml:"path"` 18 | Auth bool `yaml:"auth"` 19 | Sign bool `yaml:"sign"` 20 | CheckTimestamp bool `yaml:"check_timestamp"` 21 | LimitTrafficWithIp bool `yaml:"limit_traffic_with_ip"` 22 | LimitTrafficWithEmail bool `yaml:"limit_traffic_with_email"` 23 | IsRabbitMqConnected bool `yaml:"is_rabbitmq_connected"` 24 | } 25 | 26 | var GlobalApiInterfaces []ApiInterface 27 | 28 | func LoadInterfaces() { 29 | path_str, _ := filepath.Abs("config/interfaces.yml") 30 | content, err := ioutil.ReadFile(path_str) 31 | if err != nil { 32 | log.Fatal(err) 33 | return 34 | } 35 | err = yaml.Unmarshal(content, &GlobalApiInterfaces) 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | } 40 | 41 | func Auth(next echo.HandlerFunc) echo.HandlerFunc { 42 | return func(context echo.Context) error { 43 | 44 | treatLanguage(context) 45 | 46 | params := make(map[string]string) 47 | 48 | for k, v := range context.QueryParams() { 49 | params[k] = v[0] 50 | } 51 | values, _ := context.FormParams() 52 | for k, v := range values { 53 | params[k] = v[0] 54 | } 55 | context.Set("params", params) 56 | var currentApiInterface ApiInterface 57 | for _, apiInterface := range GlobalApiInterfaces { 58 | if context.Path() == apiInterface.Path && context.Request().Method == apiInterface.Method { 59 | currentApiInterface = apiInterface 60 | // limit_traffic_with_email 61 | if currentApiInterface.LimitTrafficWithEmail && LimitTrafficWithEmail(context) != true { 62 | return utils.BuildError("3043") 63 | } 64 | // limit_traffic_with_ip 65 | if currentApiInterface.LimitTrafficWithIp && LimitTrafficWithIp(context) != true { 66 | return utils.BuildError("3043") 67 | } 68 | if apiInterface.Auth != true { 69 | return next(context) 70 | } 71 | } 72 | } 73 | 74 | if currentApiInterface.Path == "" { 75 | return utils.BuildError("3025") 76 | } 77 | if context.Request().Header.Get("Authorization") == "" { 78 | return utils.BuildError("4001") 79 | } 80 | if currentApiInterface.CheckTimestamp && checkTimestamp(context, ¶ms) == false { 81 | return utils.BuildError("3050") 82 | } 83 | if currentApiInterface.IsRabbitMqConnected && !IsRabbitMqConnected() { 84 | return utils.BuildError("2046") 85 | } 86 | 87 | db := utils.MainDbBegin() 88 | defer db.DbRollback() 89 | 90 | var user User 91 | var token Token 92 | var apiToken ApiToken 93 | var device Device 94 | var err error 95 | if context.Param("platform") == "client" { 96 | user, apiToken, err = robotAuth(context, ¶ms, db) 97 | if currentApiInterface.Sign && checkSign(context, apiToken.SecretKey, ¶ms) == false { 98 | return utils.BuildError("4005") 99 | } 100 | } else if context.Param("platform") == "mobile" { 101 | user, device, err = mobileAuth(context, ¶ms, db) 102 | } else if context.Param("platform") == "web" { 103 | user, token, err = webAuth(context, ¶ms, db) 104 | } 105 | if err != nil { 106 | return err 107 | } 108 | 109 | db.DbCommit() 110 | context.Set("current_user", user) 111 | context.Set("current_token", token.Token) 112 | context.Set("current_user_id", user.Id) 113 | context.Set("current_device", device.Token) 114 | context.Set("current_api_token", apiToken.AccessKey) 115 | return next(context) 116 | } 117 | } 118 | 119 | func robotAuth(context echo.Context, params *map[string]string, db *utils.GormDB) (user User, apiToken ApiToken, err error) { 120 | if db.Where("access_key = ?", (*params)["access_key"]).First(&apiToken).RecordNotFound() { 121 | return user, apiToken, utils.BuildError("2008") 122 | } 123 | if db.Where("id = ?", apiToken.UserId).First(&user).RecordNotFound() { 124 | return user, apiToken, utils.BuildError("2016") 125 | } 126 | return 127 | } 128 | 129 | func mobileAuth(context echo.Context, params *map[string]string, db *utils.GormDB) (user User, device Device, err error) { 130 | if db.Where("token = ?", context.Request().Header.Get("Authorization")).First(&device).RecordNotFound() { 131 | return user, device, utils.BuildError("4016") 132 | } 133 | if db.Where("id = ?", device.UserId).First(&user).RecordNotFound() { 134 | return user, device, utils.BuildError("4016") 135 | } 136 | return 137 | } 138 | 139 | func webAuth(context echo.Context, params *map[string]string, db *utils.GormDB) (user User, token Token, err error) { 140 | if db.Where("token = ? AND ? < expire_at", context.Request().Header.Get("Authorization"), time.Now()).First(&token).RecordNotFound() { 141 | return user, token, utils.BuildError("4016") 142 | } 143 | if db.Where("id = ?", token.UserId).First(&user).RecordNotFound() { 144 | return user, token, utils.BuildError("4016") 145 | } 146 | return 147 | } 148 | -------------------------------------------------------------------------------- /initializers/cacheData.go: -------------------------------------------------------------------------------- 1 | package initializers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "reflect" 8 | 9 | "github.com/oldfritter/goDCE/config" 10 | . "github.com/oldfritter/goDCE/models" 11 | "github.com/oldfritter/goDCE/utils" 12 | ) 13 | 14 | type Payload struct { 15 | Update string `json:"update"` 16 | Symbol int `json:"symbol"` 17 | } 18 | 19 | func InitCacheData() { 20 | db := utils.MainDbBegin() 21 | defer db.DbRollback() 22 | InitAllCurrencies(db) 23 | InitAllMarkets(db) 24 | } 25 | 26 | func LoadCacheData() { 27 | InitCacheData() 28 | go func() { 29 | channel, err := config.RabbitMqConnect.Channel() 30 | if err != nil { 31 | log.Println(fmt.Errorf("Channel: %s", err)) 32 | } 33 | channel.ExchangeDeclare(config.AmqpGlobalConfig.Exchange["fanout"]["default"], "fanout", true, false, false, false, nil) 34 | queue, err := channel.QueueDeclare("", true, true, false, false, nil) 35 | if err != nil { 36 | return 37 | } 38 | channel.QueueBind(queue.Name, queue.Name, config.AmqpGlobalConfig.Exchange["fanout"]["default"], false, nil) 39 | msgs, _ := channel.Consume(queue.Name, "", true, false, false, false, nil) 40 | for d := range msgs { 41 | var payload Payload 42 | err := json.Unmarshal(d.Body, &payload) 43 | if err == nil { 44 | reflect.ValueOf(&payload).MethodByName(payload.Update).Call([]reflect.Value{reflect.ValueOf(payload.Symbol)}) 45 | } else { 46 | log.Println(fmt.Sprintf("{error: %v}", err)) 47 | } 48 | } 49 | return 50 | }() 51 | } 52 | 53 | func (payload *Payload) ReloadCurrencies() { 54 | db := utils.MainDbBegin() 55 | defer db.DbRollback() 56 | InitAllCurrencies(db) 57 | } 58 | 59 | func (payload *Payload) ReloadMarkets() { 60 | db := utils.MainDbBegin() 61 | defer db.DbRollback() 62 | InitAllMarkets(db) 63 | } 64 | -------------------------------------------------------------------------------- /initializers/filters.go: -------------------------------------------------------------------------------- 1 | package initializers 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "fmt" 7 | "sort" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/gomodule/redigo/redis" 12 | "github.com/labstack/echo" 13 | 14 | "github.com/oldfritter/goDCE/config" 15 | "github.com/oldfritter/goDCE/initializers/locale" 16 | "github.com/oldfritter/goDCE/utils" 17 | ) 18 | 19 | func checkSign(context echo.Context, secretKey string, params *map[string]string) bool { 20 | sign := (*params)["signature"] 21 | targetStr := context.Request().Method + "|" + context.Path() + "|" 22 | 23 | keys := make([]string, len(*params)-1) 24 | i := 0 25 | for k, _ := range *params { 26 | if k == "signature" { 27 | continue 28 | } 29 | keys[i] = k 30 | i++ 31 | } 32 | sort.Strings(keys) 33 | for i, key := range keys { 34 | if i > 0 { 35 | targetStr += "&" 36 | } 37 | targetStr += key + "=" + (*params)[key] 38 | } 39 | mac := hmac.New(sha256.New, []byte(secretKey)) 40 | mac.Write([]byte(targetStr)) 41 | return sign == fmt.Sprintf("%x", mac.Sum(nil)) 42 | } 43 | 44 | func LimitTrafficWithIp(context echo.Context) bool { 45 | dataRedis := utils.GetRedisConn("data") 46 | defer dataRedis.Close() 47 | key := "limit-traffic-with-ip:" + context.Path() + ":" + context.RealIP() 48 | timesStr, _ := redis.String(dataRedis.Do("GET", key)) 49 | if timesStr == "" { 50 | dataRedis.Do("SETEX", key, 1, 60) 51 | } else { 52 | times, _ := strconv.Atoi(timesStr) 53 | if times > 10 { 54 | return false 55 | } else { 56 | dataRedis.Do("INCR", key) 57 | } 58 | } 59 | return true 60 | } 61 | 62 | func LimitTrafficWithEmail(context echo.Context) bool { 63 | dataRedis := utils.GetRedisConn("data") 64 | defer dataRedis.Close() 65 | key := "limit-traffic-with-email-" + context.FormValue("email") 66 | timesStr, _ := redis.String(dataRedis.Do("GET", key)) 67 | if timesStr == "" { 68 | dataRedis.Do("Set", key, 1) 69 | dataRedis.Do("EXPIRE", key, 600) 70 | } else { 71 | times, _ := strconv.Atoi(timesStr) 72 | if times > 10 { 73 | return false 74 | } else { 75 | dataRedis.Do("INCR", key) 76 | } 77 | } 78 | return true 79 | } 80 | 81 | func treatLanguage(context echo.Context) { 82 | var language string 83 | var lqs []locale.LangQ 84 | if context.QueryParam("lang") != "" { 85 | lqs = locale.ParseAcceptLanguage(context.QueryParam("lang")) 86 | } else { 87 | lqs = locale.ParseAcceptLanguage(context.Request().Header.Get("Accept-Language")) 88 | } 89 | if lqs[0].Lang == "en" { 90 | language = "en" 91 | } else if lqs[0].Lang == "ja" { 92 | language = "ja" 93 | } else if lqs[0].Lang == "ko" { 94 | language = "ko" 95 | } else { 96 | language = "zh-CN" 97 | } 98 | context.Set("language", language) 99 | } 100 | 101 | func checkTimestamp(context echo.Context, params *map[string]string) bool { 102 | timestamp, _ := strconv.Atoi((*params)["timestamp"]) 103 | now := time.Now() 104 | if int(now.Add(-time.Second*60*5).Unix()) <= timestamp && timestamp <= int(now.Add(time.Second*60*5).Unix()) { 105 | return true 106 | } 107 | return false 108 | } 109 | 110 | func IsRabbitMqConnected() bool { 111 | c := config.RabbitMqConnect 112 | ok := true 113 | if c.IsClosed() { 114 | fmt.Println("Connection state: closed") 115 | ok = false 116 | } 117 | return ok 118 | } 119 | -------------------------------------------------------------------------------- /initializers/i18n.go: -------------------------------------------------------------------------------- 1 | package initializers 2 | 3 | import ( 4 | i18n "github.com/qor/i18n" 5 | "github.com/qor/i18n/backends/yaml" 6 | ) 7 | 8 | var I18n *i18n.I18n 9 | 10 | func InitI18n() { 11 | I18n = i18n.New(yaml.New("initializers/locales")) 12 | } 13 | -------------------------------------------------------------------------------- /initializers/initializeWorkers.go: -------------------------------------------------------------------------------- 1 | package initializers 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "path/filepath" 7 | 8 | "gopkg.in/yaml.v2" 9 | 10 | "github.com/oldfritter/goDCE/config" 11 | "github.com/oldfritter/goDCE/workers/sneakerWorkers" 12 | ) 13 | 14 | func InitWorkers() { 15 | pathStr, _ := filepath.Abs("config/workers.yml") 16 | content, err := ioutil.ReadFile(pathStr) 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | yaml.Unmarshal(content, &config.AllWorkers) 21 | sneakerWorkers.InitializeKLineWorker() 22 | sneakerWorkers.InitializeTickerWorker() 23 | sneakerWorkers.InitializeRebuildKLineToRedisWorker() 24 | sneakerWorkers.InitializeAccountVersionCheckPointWorker() 25 | } 26 | -------------------------------------------------------------------------------- /initializers/latestKLine.go: -------------------------------------------------------------------------------- 1 | package initializers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/oldfritter/goDCE/config" 8 | . "github.com/oldfritter/goDCE/models" 9 | ) 10 | 11 | func LoadLatestKLines() { 12 | go func() { 13 | channel, err := config.RabbitMqConnect.Channel() 14 | if err != nil { 15 | fmt.Errorf("Channel: %s", err) 16 | } 17 | channel.ExchangeDeclare(config.AmqpGlobalConfig.Exchange["fanout"]["k"], "fanout", true, false, false, false, nil) 18 | queue, err := channel.QueueDeclare("", true, true, false, false, nil) 19 | if err != nil { 20 | return 21 | } 22 | channel.QueueBind(queue.Name, queue.Name, config.AmqpGlobalConfig.Exchange["fanout"]["k"], false, nil) 23 | msgs, _ := channel.Consume(queue.Name, "", true, false, false, false, nil) 24 | for d := range msgs { 25 | var notifyKLine KLine 26 | json.Unmarshal(d.Body, ¬ifyKLine) 27 | for i, _ := range AllMarkets { 28 | if AllMarkets[i].Id == notifyKLine.MarketId { 29 | AllMarkets[i].LatestKLines[notifyKLine.Period] = notifyKLine 30 | } 31 | } 32 | } 33 | return 34 | }() 35 | } 36 | -------------------------------------------------------------------------------- /initializers/locale/parse.go: -------------------------------------------------------------------------------- 1 | package locale 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | type LangQ struct { 9 | Lang string 10 | Q float64 11 | } 12 | 13 | func ParseAcceptLanguage(acptLang string) []LangQ { 14 | var lqs []LangQ 15 | 16 | langQStrs := strings.Split(acptLang, ",") 17 | for _, langQStr := range langQStrs { 18 | trimedLangQStr := strings.Trim(langQStr, " ") 19 | 20 | langQ := strings.Split(trimedLangQStr, ";") 21 | if len(langQ) == 1 { 22 | lq := LangQ{langQ[0], 1} 23 | lqs = append(lqs, lq) 24 | } else { 25 | qp := strings.Split(langQ[1], "=") 26 | q, err := strconv.ParseFloat(qp[1], 64) 27 | if err != nil { 28 | panic(err) 29 | } 30 | lq := LangQ{langQ[0], q} 31 | lqs = append(lqs, lq) 32 | } 33 | } 34 | return lqs 35 | } 36 | -------------------------------------------------------------------------------- /initializers/locales/error_code.en.yml: -------------------------------------------------------------------------------- 1 | 'en': 2 | success_code: 3 | '1000': Success. 4 | error_code: 5 | '1021': 没有匹配的market。 6 | 7 | '1053': 无效的 period. 8 | 9 | '3020': No record can be found. 10 | '3021': 暂不开放用户注册。 11 | '3022': 下单失败。 12 | '3023': 部分下单失败。 13 | 14 | '3025': No this interface. 15 | '3026': No data can be found. 16 | '3027': No currency can be found. 17 | '3028': 数量错误。 18 | '3029': 币种错误。 19 | '3030': Need parameter name. 20 | '3031': Need parameter email. 21 | '3032': Need parameter password. 22 | '3033': Need parameter password confirmation. 23 | 24 | '3035': Email was Wrong. 25 | '3036': Email code was wrong. 26 | '3037': No app can be found. 27 | '3038': Failed to create transfer. 28 | '3039': No transfer can be found. 29 | '3040': No this email code. 30 | '3041': No enough coin. 31 | '3042': 请刷新页面重试。 32 | '3043': The interface call is too frequent, please try again later. 33 | '3044': Failed at test. 34 | '3045': Transfer already exist。 35 | '3046': Transfer amount is too less. 36 | '3047': No right. 37 | '3048': 此app不支持资产划拨。 38 | '3049': 该转账不存在。 39 | '3050': Initiation time exception. 40 | 41 | '3054': 重复的转账。 42 | '3055': 已绑定手机号。 43 | 44 | '4001': Authorization failed. 45 | 46 | '4005': Signature is incorrect. 47 | '4006': The tonce has already been used. 48 | 49 | '4013': TwoFactor failed. 50 | 51 | '4016': The user does not exist. 52 | '4017': The user does not activated. 53 | '4018': The user has no id_document. 54 | 55 | '4020': The user has no OTP. 56 | 57 | '4023': This currency can not withdraw. 58 | 59 | '4026': Wrong email or password. 60 | '4027': Invalid OTP code. 61 | '4028': Inconsistent passwords. 62 | '4029': User exist. 63 | 64 | '4031': Failed. 65 | 66 | '4040': 图片资料不全。 67 | 68 | '4043': 请使用私钥签名,并指定sign_type为private_key。 69 | 70 | '4045': It is forbidden to transfer to enterprise account. 71 | 72 | '4053': Withdraw failed. 73 | 74 | '4055': 实名认证通过24小时内不允许操作此接口 75 | '4056': 修改两步验证24小时内不允许操作此接口 76 | 77 | '5001': Verification code request frequency is too fast, please try again later. 78 | '5002': Invalid authentication code. 79 | '5003': Verification code does not exist. 80 | '5004': Email or Mobile number is required. 81 | '5005': 以达到创建上限。 82 | -------------------------------------------------------------------------------- /initializers/locales/error_code.zh-CN.yml: -------------------------------------------------------------------------------- 1 | 'zh-CN': 2 | success_code: 3 | '1000': 成功。 4 | error_code: 5 | '1021': 没有匹配的market。 6 | 7 | '1053': 无效的 period. 8 | 9 | '3020': 找不到记录。 10 | '3021': 暂不开放用户注册。 11 | '3022': 下单失败。 12 | '3023': 部分下单失败。 13 | 14 | '3025': 没有这个接口。 15 | '3026': 没有数据 16 | '3027': 没有找到此币种。 17 | '3028': 数量错误。 18 | '3029': 币种错误。 19 | '3030': 需要参数 name。 20 | '3031': 需要参数 email。 21 | '3032': 需要参数 password。 22 | '3033': 需要参数 password confirmation。 23 | '3035': Email 错误。 24 | '3036': Email code 错误。 25 | '3037': 没有该应用。 26 | '3038': 转账失败。 27 | '3039': 没有该转账记录。 28 | '3040': 没有该 Email code。 29 | '3041': 帐号余额不足。 30 | '3042': 请刷新页面重试。 31 | '3043': 接口调用过于频繁,请稍后再试。 32 | '3044': 验证失败。 33 | '3045': 该转账已存在。 34 | '3046': 转账数额太少。 35 | '3047': 无权限。 36 | '3048': 此app不支持资产划拨。 37 | '3049': 该转账不存在。 38 | '3050': 发起时间异常。 39 | 40 | '3054': 重复的转账。 41 | '3055': 已绑定手机号。 42 | 43 | '4001': 授权token无效。 44 | 45 | '4005': 签名无效。 46 | '4006': tonce已使用。 47 | 48 | '4013': 两步验证失败。 49 | 50 | '4016': 用户不存在。 51 | '4017': 用户尚未邮件激活。 52 | '4018': 用户尚未实名认证。 53 | 54 | '4020': 用户未绑定两步验证。 55 | 56 | '4023': 该币不可提币。 57 | 58 | '4026': email或密码错误。 59 | '4027': 无效的OTP代码。 60 | '4028': 密码不一致。 61 | '4029': 用户存在。 62 | 63 | '4031': 失败。 64 | 65 | '4040': 图片资料不全。 66 | 67 | '4043': 请使用私钥签名,并指定sign_type为private_key。 68 | 69 | '4045': 禁止转入到企业账号。 70 | 71 | '4053': 提取确认失败 72 | 73 | '4055': 实名认证通过24小时内不允许操作此接口 74 | '4056': 修改两步验证24小时内不允许操作此接口 75 | 76 | '5001': 验证码请求频率过快,请稍候重试。 77 | '5002': 无效验证码。 78 | '5003': 验证码不存在。 79 | '5004': 请输入邮箱或手机号。 80 | '5005': 以达到创建上限。 81 | -------------------------------------------------------------------------------- /initializers/locales/wallet.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | wallet: 3 | email_code_verified: 邮件验证码 4 | app_reset_password: 重置密码鉴权 5 | set_fund_password: 设置资金密码鉴权 6 | -------------------------------------------------------------------------------- /initializers/locales/wallet.zh-CN.yml: -------------------------------------------------------------------------------- 1 | zh-CN: 2 | wallet: 3 | email_code_verified: 邮件验证码 4 | app_reset_password: 重置密码鉴权 5 | set_fund_password: 设置资金密码鉴权 6 | -------------------------------------------------------------------------------- /initializers/rabbitmq.go: -------------------------------------------------------------------------------- 1 | package initializers 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "path/filepath" 7 | "time" 8 | 9 | "github.com/oldfritter/sneaker-go/v3" 10 | "github.com/streadway/amqp" 11 | "gopkg.in/yaml.v2" 12 | 13 | "github.com/oldfritter/goDCE/config" 14 | ) 15 | 16 | func InitializeAmqpConfig() { 17 | path_str, _ := filepath.Abs("config/amqp.yml") 18 | content, err := ioutil.ReadFile(path_str) 19 | if err != nil { 20 | log.Fatal(err) 21 | return 22 | } 23 | err = yaml.Unmarshal(content, &config.AmqpGlobalConfig) 24 | if err != nil { 25 | log.Fatal(err) 26 | return 27 | } 28 | InitializeAmqpConnection() 29 | } 30 | 31 | func InitializeAmqpConnection() { 32 | var err error 33 | conn, err := amqp.Dial("amqp://" + config.AmqpGlobalConfig.Connect.Username + ":" + config.AmqpGlobalConfig.Connect.Password + "@" + config.AmqpGlobalConfig.Connect.Host + ":" + config.AmqpGlobalConfig.Connect.Port + "/" + config.AmqpGlobalConfig.Connect.Vhost) 34 | config.RabbitMqConnect = sneaker.RabbitMqConnect{conn} 35 | if err != nil { 36 | time.Sleep(5000) 37 | InitializeAmqpConnection() 38 | return 39 | } 40 | go func() { 41 | <-config.RabbitMqConnect.NotifyClose(make(chan *amqp.Error)) 42 | InitializeAmqpConnection() 43 | }() 44 | } 45 | 46 | func CloseAmqpConnection() { 47 | config.RabbitMqConnect.Close() 48 | } 49 | 50 | func GetRabbitMqConnect() sneaker.RabbitMqConnect { 51 | return config.RabbitMqConnect 52 | } 53 | -------------------------------------------------------------------------------- /initializers/ticker.go: -------------------------------------------------------------------------------- 1 | package initializers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/gomodule/redigo/redis" 8 | 9 | "github.com/oldfritter/goDCE/config" 10 | . "github.com/oldfritter/goDCE/models" 11 | "github.com/oldfritter/goDCE/utils" 12 | ) 13 | 14 | func LoadLatestTickers() { 15 | go func() { 16 | channel, err := config.RabbitMqConnect.Channel() 17 | if err != nil { 18 | fmt.Errorf("Channel: %s", err) 19 | } 20 | channel.ExchangeDeclare(config.AmqpGlobalConfig.Exchange["fanout"]["ticker"], "fanout", true, false, false, false, nil) 21 | queue, err := channel.QueueDeclare("", true, true, false, false, nil) 22 | if err != nil { 23 | return 24 | } 25 | channel.QueueBind(queue.Name, queue.Name, config.AmqpGlobalConfig.Exchange["fanout"]["ticker"], false, nil) 26 | msgs, _ := channel.Consume(queue.Name, "", true, false, false, false, nil) 27 | for d := range msgs { 28 | var ticker Ticker 29 | json.Unmarshal(d.Body, &ticker) 30 | for i, _ := range AllMarkets { 31 | if AllMarkets[i].Id == ticker.MarketId { 32 | AllMarkets[i].Ticker = ticker.TickerAspect 33 | } 34 | } 35 | } 36 | return 37 | }() 38 | 39 | go func() { 40 | tickerRedis := utils.GetRedisConn("ticker") 41 | defer tickerRedis.Close() 42 | for i, market := range AllMarkets { 43 | jsonStr, err := redis.Bytes(tickerRedis.Do("GET", market.TickerRedisKey())) 44 | if err != nil { 45 | continue 46 | } 47 | var ticker Ticker 48 | json.Unmarshal(jsonStr, &ticker) 49 | if AllMarkets[i].Ticker == nil { 50 | AllMarkets[i].Ticker = ticker.TickerAspect 51 | } 52 | } 53 | }() 54 | } 55 | -------------------------------------------------------------------------------- /models/account.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/jinzhu/gorm" 9 | "github.com/oldfritter/goDCE/utils" 10 | "github.com/shopspring/decimal" 11 | ) 12 | 13 | var ( 14 | UNKNOWN = 0 15 | FIX = 1 16 | STRIKE_FEE = 100 17 | STRIKE_ADD = 110 18 | STRIKE_SUB = 120 19 | STRIKE_UNLOCK = 130 20 | ORDER_SUBMIT = 600 21 | ORDER_CANCEL = 610 22 | ORDER_FULLFILLED = 620 23 | TRANSFER = 700 24 | TRANSFER_BACK = 710 25 | WALLET_TRANSFER = 720 26 | WALLET_TRANSFER_BACK = 730 27 | WALLET_TRANSFER_LOCK = 735 28 | WALLET_TRANSFER_UNLOCK = 736 29 | WITHDRAW_LOCK = 800 30 | WITHDRAW_UNLOCK = 810 31 | DEPOSIT = 1000 32 | WITHDRAW = 2000 33 | AWARD = 3000 34 | OTC_ORDER_SUBMIT = 900 35 | OTC_ORDER_FINISHED = 910 36 | OTC_ORDER_FINISHED_2 = 911 37 | OTC_ORDER_CANCELLED = 920 38 | OTC_ORDER_CANCELLED_2 = 921 39 | OPTION_DEPOSIT = 1100 40 | OPTION_UNLOCK = 1200 41 | OPTION_CANCEL = 1500 42 | PROFIT_LOCK = 1300 43 | PROFIT_UNLOCK = 1400 44 | RECYCLE_TRANS_FEE = 1600 45 | 46 | FUNS = map[string]int{ 47 | "UnlockFunds": 1, 48 | "LockFunds": 2, 49 | "PlusFunds": 3, 50 | "SubFunds": 4, 51 | "UnlockedAndSubFunds": 5, 52 | } 53 | ) 54 | 55 | type Account struct { 56 | CommonModel 57 | UserId int `json:"user_id"` 58 | CurrencyId int `json:"currency_id"` 59 | Balance decimal.Decimal `gorm:"type:decimal(32,16);default:null;" json:"balance"` 60 | Locked decimal.Decimal `gorm:"type:decimal(32,16);default:null;" json:"locked"` 61 | } 62 | 63 | func (account *Account) AfterSave(db *gorm.DB) {} 64 | 65 | func (account *Account) Amount() (amount decimal.Decimal) { 66 | amount = account.Balance.Add(account.Locked) 67 | return 68 | } 69 | 70 | func (account *Account) PlusFunds(db *utils.GormDB, amount, fee decimal.Decimal, reason, modifiableId int, modifiableType string) (err error) { 71 | if amount.LessThan(decimal.Zero) || fee.GreaterThan(amount) { 72 | err = fmt.Errorf("cannot add funds (amount: %v)", amount) 73 | return 74 | } 75 | err = account.changeBalanceAndLocked(db, amount, decimal.Zero) 76 | if err != nil { 77 | return 78 | } 79 | opts := map[string]string{ 80 | "fee": fee.String(), 81 | "reason": strconv.Itoa(reason), 82 | "modifiable_id": strconv.Itoa(modifiableId), 83 | "modifiable_type": modifiableType, 84 | } 85 | err = account.after(db, FUNS["PlusFunds"], amount, opts) 86 | return 87 | } 88 | 89 | func (account *Account) SubFunds(db *utils.GormDB, amount, fee decimal.Decimal, reason, modifiableId int, modifiableType string) (err error) { 90 | if amount.LessThan(decimal.Zero) || fee.GreaterThan(amount) { 91 | err = fmt.Errorf("cannot add funds (amount: %v)", amount) 92 | return 93 | } 94 | err = account.changeBalanceAndLocked(db, amount.Neg(), decimal.Zero) 95 | if err != nil { 96 | return 97 | } 98 | opts := map[string]string{ 99 | "fee": fee.String(), 100 | "reason": strconv.Itoa(reason), 101 | "modifiable_id": strconv.Itoa(modifiableId), 102 | "modifiable_type": modifiableType, 103 | } 104 | err = account.after(db, FUNS["SubFunds"], amount, opts) 105 | return 106 | } 107 | 108 | func (account *Account) LockFunds(db *utils.GormDB, amount decimal.Decimal, reason, modifiableId int, modifiableType string) (err error) { 109 | if amount.LessThanOrEqual(decimal.Zero) || amount.GreaterThan(account.Balance) { 110 | err = fmt.Errorf("cannot lock funds (amount: %v)", amount) 111 | return 112 | } 113 | 114 | err = account.changeBalanceAndLocked(db, amount.Neg(), amount) 115 | if err != nil { 116 | return 117 | } 118 | opts := map[string]string{ 119 | "reason": strconv.Itoa(reason), 120 | "modifiable_id": strconv.Itoa(modifiableId), 121 | "modifiable_type": modifiableType, 122 | } 123 | err = account.after(db, FUNS["LockFunds"], amount, opts) 124 | return 125 | } 126 | 127 | func (account *Account) UnlockFunds(db *utils.GormDB, amount decimal.Decimal, reason, modifiableId int, modifiableType string) (err error) { 128 | if amount.LessThanOrEqual(decimal.Zero) || amount.GreaterThan(account.Locked) { 129 | err = fmt.Errorf("cannot unlock funds (amount: %v)", amount) 130 | return 131 | } 132 | 133 | err = account.changeBalanceAndLocked(db, amount, amount.Neg()) 134 | if err != nil { 135 | return 136 | } 137 | opts := map[string]string{ 138 | "reason": strconv.Itoa(reason), 139 | "modifiable_id": strconv.Itoa(modifiableId), 140 | "modifiable_type": modifiableType, 141 | } 142 | err = account.after(db, FUNS["UnlockFunds"], amount, opts) 143 | return 144 | } 145 | 146 | func (account *Account) UnlockedAndSubFunds(db *utils.GormDB, amount, locked, fee decimal.Decimal, reason, modifiableId int, modifiableType string) (err error) { 147 | if amount.LessThanOrEqual(decimal.Zero) || amount.GreaterThan(locked) { 148 | err = fmt.Errorf("cannot unlock and subtract funds (amount: %v)", amount) 149 | return 150 | } 151 | if locked.LessThanOrEqual(decimal.Zero) { 152 | err = fmt.Errorf("invalid lock amount") 153 | return 154 | } 155 | if locked.GreaterThan(account.Locked) { 156 | err = fmt.Errorf("Account# %v invalid lock amount (amount: %v, locked: %v, self.locked: %v)", account.Id, amount, locked, account.Locked) 157 | return 158 | } 159 | err = account.changeBalanceAndLocked(db, locked.Sub(amount), locked.Neg()) 160 | if err != nil { 161 | return 162 | } 163 | opts := map[string]string{ 164 | "fee": fee.String(), 165 | "locked": locked.String(), 166 | "reason": strconv.Itoa(reason), 167 | "modifiable_id": strconv.Itoa(modifiableId), 168 | "modifiable_type": modifiableType, 169 | } 170 | err = account.after(db, FUNS["UnlockedAndSubFunds"], amount, opts) 171 | return 172 | } 173 | 174 | func (account *Account) after(db *utils.GormDB, fun int, amount decimal.Decimal, opts map[string]string) (err error) { 175 | var fee decimal.Decimal 176 | if opts["fee"] != "" { 177 | fee, _ = decimal.NewFromString(opts["fee"]) 178 | } 179 | var reason int 180 | if opts["reason"] == "" { 181 | reason = UNKNOWN 182 | } 183 | attributes := map[string]string{ 184 | "fun": strconv.Itoa(fun), 185 | "fee": fee.String(), 186 | "reason": strconv.Itoa(reason), 187 | "amount": account.Amount().String(), 188 | "currency_id": strconv.Itoa(account.CurrencyId), 189 | "user_id": strconv.Itoa(account.UserId), 190 | "account_id": strconv.Itoa(account.Id), 191 | "modifiable_id": opts["modifiable_id"], 192 | "modifiable_type": opts["modifiable_type"], 193 | } 194 | attributes["locked"], attributes["balance"], err = computeLockedAndBalance(fun, amount, opts) 195 | if err != nil { 196 | return 197 | } 198 | err = optimisticallyLockAccountAndCreate(db, account.Balance, account.Locked, attributes) 199 | return 200 | } 201 | 202 | func (account *Account) changeBalanceAndLocked(db *utils.GormDB, deltaB, deltaL decimal.Decimal) (err error) { 203 | db.Set("gorm:query_option", "FOR UPDATE").First(&account, account.Id) 204 | account.Balance = account.Balance.Add(deltaB) 205 | account.Locked = account.Locked.Add(deltaL) 206 | updateSql := fmt.Sprintf("UPDATE accounts SET balance = balance + %v, locked = locked + %v WHERE accounts.id = %v ", deltaB, deltaL, account.Id) 207 | accountresult := db.Exec(updateSql) 208 | if accountresult.RowsAffected != 1 { 209 | err = fmt.Errorf("Insert row failed.") 210 | } 211 | return 212 | } 213 | 214 | func computeLockedAndBalance(fun int, amount decimal.Decimal, opts map[string]string) (locked, balance string, err error) { 215 | switch fun { 216 | case 1: 217 | locked = amount.Neg().String() 218 | balance = amount.String() 219 | case 2: 220 | locked = amount.String() 221 | balance = amount.Neg().String() 222 | case 3: 223 | locked = "0" 224 | balance = amount.String() 225 | case 4: 226 | locked = "0" 227 | balance = amount.Neg().String() 228 | case 5: 229 | l, _ := decimal.NewFromString(opts["locked"]) 230 | locked = l.Neg().String() 231 | balance = l.Sub(amount).String() 232 | default: 233 | err = fmt.Errorf("forbidden account operation") 234 | } 235 | return 236 | } 237 | 238 | func optimisticallyLockAccountAndCreate(db *utils.GormDB, balance, locked decimal.Decimal, attrs map[string]string) (err error) { 239 | if attrs["account_id"] == "" { 240 | err = fmt.Errorf("account must be specified") 241 | } 242 | attrs["created_at"] = time.Now().Format("2006-01-02 15:04:05") 243 | attrs["updated_at"] = attrs["created_at"] 244 | 245 | sql := `INSERT INTO account_versions (user_id, account_id, reason, balance, locked, fee, amount, modifiable_id, modifiable_type, currency_id, fun, created_at, updated_at) SELECT ?,?,?,?,?,?,?,?,?,?,?,?,? FROM accounts WHERE accounts.balance = ? AND accounts.locked = ? AND accounts.id = ?` 246 | result := db.Exec(sql, attrs["user_id"], attrs["account_id"], attrs["reason"], attrs["balance"], attrs["locked"], attrs["fee"], attrs["amount"], attrs["modifiable_id"], attrs["modifiable_type"], attrs["currency_id"], attrs["fun"], attrs["created_at"], attrs["updated_at"], balance, locked, attrs["account_id"]) 247 | if result.RowsAffected != 1 { 248 | err = fmt.Errorf("Insert row failed.") 249 | } 250 | return 251 | } 252 | -------------------------------------------------------------------------------- /models/accountVersion.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/shopspring/decimal" 5 | ) 6 | 7 | type AccountVersion struct { 8 | CommonModel 9 | UserId int `json:"user_id"` 10 | AccountId int `json:"account_id"` 11 | Reason int `json:"reason"` 12 | Balance decimal.Decimal `gorm:"type:decimal(32,16);default:null;" json:"balance"` 13 | Locked decimal.Decimal `gorm:"type:decimal(32,16);default:null;" json:"locked"` 14 | Fee decimal.Decimal `gorm:"type:decimal(32,16);default:null;" json:"fee"` 15 | Amount decimal.Decimal `gorm:"type:decimal(32,16);default:null;" json:"amount"` 16 | ModifiableId int `json:"modifiable_id"` 17 | ModifiableType string `json:"modifiable_type"` 18 | CurrencyId int `json:"currency_id"` 19 | Fun int `json:"fun"` 20 | } 21 | -------------------------------------------------------------------------------- /models/accountVersionCheckPoint.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/shopspring/decimal" 5 | ) 6 | 7 | type AccountVersionCheckPoint struct { 8 | CommonModel 9 | AccountVersionId int `json:"account_version_id"` 10 | UserId int `json:"user_id"` 11 | AccountId int `json:"account_id"` 12 | Fixed string `json:"fixed"` 13 | FixedNum decimal.Decimal `json:"fixed_num" gorm:"type:decimal(32,16)"` 14 | Balance decimal.Decimal `json:"balance" gorm:"type:decimal(32,16)"` 15 | } 16 | -------------------------------------------------------------------------------- /models/apiToken.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type ApiToken struct { 8 | CommonModel 9 | UserId int `json:"user_id"` 10 | AccessKey string `gorm:"type:varchar(64)" json:"access_key"` 11 | SecretKey string `gorm:"type:varchar(64)" json:"secret_key"` 12 | Label string `gorm:"type:varchar(32)" json:"label"` 13 | Scopes string `gorm:"type:varchar(32)" json:"scopes"` 14 | ExpireAt time.Time `json:"expire_at" gorm:"default:null"` 15 | DeletedAt time.Time `json:"-" gorm:"default:null"` 16 | } 17 | -------------------------------------------------------------------------------- /models/common.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/oldfritter/goDCE/utils" 7 | ) 8 | 9 | type CommonModel struct { 10 | Id int `json:"id" gorm:"primary_key"` 11 | CreatedAt time.Time `json:"-"` 12 | UpdatedAt time.Time `json:"-"` 13 | } 14 | 15 | func AutoMigrations() { 16 | mainDB := utils.MainDbBegin() 17 | defer mainDB.DbRollback() 18 | backupDB := utils.BackupDbBegin() 19 | defer backupDB.DbRollback() 20 | 21 | // account_version_check_point 22 | mainDB.AutoMigrate(&AccountVersionCheckPoint{}) 23 | mainDB.Model(&AccountVersionCheckPoint{}).AddIndex("index_account_version_check_points_on_account_id", "account_id") 24 | 25 | // account_version 26 | mainDB.AutoMigrate(&AccountVersion{}) 27 | mainDB.Model(&AccountVersion{}).AddIndex("index_account_versions_on_user_id_and_reason", "user_id", "reason") 28 | mainDB.Model(&AccountVersion{}).AddIndex("index_account_versions_on_account_id_and_reason", "account_id", "reason") 29 | mainDB.Model(&AccountVersion{}).AddIndex("index_account_versions_on_modifiable_id_and_modifiable_type", "modifiable_id", "modifiable_type") 30 | mainDB.Model(&AccountVersion{}).AddIndex("index_account_versions_on_currency_id_and_created_at", "currency_id", "created_at") 31 | 32 | backupDB.AutoMigrate(&AccountVersion{}) 33 | backupDB.Model(&AccountVersion{}).AddIndex("index_account_versions_on_user_id_and_reason", "user_id", "reason") 34 | backupDB.Model(&AccountVersion{}).AddIndex("index_account_versions_on_account_id_and_reason", "account_id", "reason") 35 | backupDB.Model(&AccountVersion{}).AddIndex("index_account_versions_on_modifiable_id_and_modifiable_type", "modifiable_id", "modifiable_type") 36 | backupDB.Model(&AccountVersion{}).AddIndex("index_account_versions_on_currency_id_and_created_at", "currency_id", "created_at") 37 | 38 | // account 39 | mainDB.AutoMigrate(&Account{}) 40 | 41 | // api_token 42 | mainDB.AutoMigrate(&ApiToken{}) 43 | mainDB.Model(&ApiToken{}).AddIndex("api_tokens_idx0", "access_key", "deleted_at") 44 | 45 | // currency 46 | mainDB.AutoMigrate(&Currency{}) 47 | 48 | // device 49 | mainDB.AutoMigrate(&Device{}) 50 | 51 | // identity 52 | mainDB.AutoMigrate(&Identity{}) 53 | mainDB.Model(&Identity{}).AddIndex("identity_idx0", "source", "symbol") 54 | 55 | // k 56 | backupDB.AutoMigrate(&KLine{}) 57 | backupDB.Model(&KLine{}).AddUniqueIndex("k_line_idx0", "market_id", "period", "timestamp") 58 | 59 | // market 60 | mainDB.AutoMigrate(&Market{}) 61 | mainDB.Model(&Market{}).AddUniqueIndex("markets_idx0", "code") 62 | 63 | // order 64 | mainDB.AutoMigrate(&Order{}) 65 | backupDB.AutoMigrate(&Order{}) 66 | 67 | // token 68 | mainDB.AutoMigrate(&Token{}) 69 | backupDB.AutoMigrate(&Token{}) 70 | 71 | // trade 72 | mainDB.AutoMigrate(&Trade{}) 73 | backupDB.AutoMigrate(&Trade{}) 74 | 75 | // user 76 | mainDB.AutoMigrate(&User{}) 77 | 78 | } 79 | -------------------------------------------------------------------------------- /models/currency.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | // "regexp" 5 | "github.com/oldfritter/goDCE/utils" 6 | ) 7 | 8 | type Currency struct { 9 | CommonModel 10 | Key string `json:"key"` 11 | Code string `json:"code"` 12 | Symbol string `json:"-"` 13 | Coin bool `json:"coin"` 14 | Precision int `json:"precision"` 15 | Erc20 bool `json:"erc20"` 16 | Erc23 bool `json:"erc23"` 17 | Visible bool `json:"visible"` 18 | Tradable bool `json:"tradable"` 19 | Depositable bool `json:"depositable"` 20 | } 21 | 22 | var AllCurrencies []Currency 23 | 24 | func InitAllCurrencies(db *utils.GormDB) { 25 | db.Where("visible = ?", true).Find(&AllCurrencies) 26 | } 27 | func (currency *Currency) IsEthereum() (result bool) { 28 | if currency.Code == "eth" || currency.Erc20 || currency.Erc23 { 29 | result = true 30 | } 31 | return 32 | } 33 | -------------------------------------------------------------------------------- /models/device.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/oldfritter/goDCE/utils" 5 | ) 6 | 7 | type Device struct { 8 | CommonModel 9 | UserId int `json:"user_id"` 10 | IsUsed bool `json:"is_used"` 11 | Token string `json:"token" gorm:"type:varchar(64)"` 12 | PublicKey string `json:"-"` 13 | } 14 | 15 | func (device *Device) InitializeToken() { 16 | device.Token = utils.RandStringRunes(64) 17 | } 18 | -------------------------------------------------------------------------------- /models/identity.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Identity struct { 4 | CommonModel 5 | UserId int `json:"user_id"` 6 | Source string `json:"source" gorm:"type:varchar(32)"` // Email or Phone, 7 | Symbol string `json:"symbol" gorm:"type:varchar(64)"` // Email address or Phone number 8 | } 9 | -------------------------------------------------------------------------------- /models/k.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/shopspring/decimal" 8 | ) 9 | 10 | type KLine struct { 11 | CommonModel 12 | MarketId int `json:"market_id"` 13 | Period int `json:"period"` 14 | 15 | Timestamp int64 `json:"timestamp"` // 时间戳 16 | Open decimal.Decimal `gorm:"type:decimal(32,16);default:null;" json:"open"` // 开盘价 17 | High decimal.Decimal `gorm:"type:decimal(32,16);default:null;" json:"high"` // 最高价 18 | Low decimal.Decimal `gorm:"type:decimal(32,16);default:null;" json:"low"` // 最低价 19 | Close decimal.Decimal `gorm:"type:decimal(32,16);default:null;" json:"close"` // 收盘价 20 | Volume decimal.Decimal `gorm:"type:decimal(32,16);default:null;" json:"volume"` // 交易量 21 | } 22 | 23 | func (k *KLine) Data() (data [5]string) { 24 | data[0] = strconv.FormatInt(k.Timestamp, 10) 25 | data[1] = k.Open.String() 26 | data[2] = k.High.String() 27 | data[3] = k.Low.String() 28 | data[4] = k.Close.String() 29 | return 30 | } 31 | 32 | func (k *KLine) RedisKey() string { 33 | return fmt.Sprintln("goDCE:k:%v:%v", k.MarketId, k.Period) 34 | } 35 | -------------------------------------------------------------------------------- /models/market.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/jinzhu/gorm" 8 | "github.com/shopspring/decimal" 9 | 10 | "github.com/oldfritter/goDCE/utils" 11 | ) 12 | 13 | type OrderCurrency struct { 14 | Fee decimal.Decimal `json:"fee"` 15 | Currency string `json:"currency"` 16 | CurrencyId int `json:"currency_id"` 17 | Fixed int `json:"fixed"` 18 | } 19 | 20 | type Market struct { 21 | CommonModel 22 | Name string `json:"name"" gorm:"type:varchar(16)"` 23 | Code string `json:"code" gorm:"type:varchar(16)"` 24 | PriceGroupFixed int `json:"price_group_fixed"` 25 | SortOrder int `json:"sort_order"` 26 | AskCurrencyId int `json:"ask_currency_id"` 27 | BidCurrencyId int `json:"bid_currency_id"` 28 | AskFee decimal.Decimal `json:"ask_fee" gorm:"type:decimal(32,16);default:null;"` 29 | BidFee decimal.Decimal `json:"bid_fee" gorm:"type:decimal(32,16);default:null;"` 30 | AskFixed int `json:"ask_fixed"` 31 | BidFixed int `json:"bid_fixed"` 32 | Visible bool `json:"visible"` 33 | Tradable bool `json:"tradable"` 34 | 35 | // 暂存数据 36 | Ticker *TickerAspect `sql:"-" json:"ticker"` 37 | LatestKLines map[int]KLine `sql:"-" json:"-"` 38 | 39 | // 撮合相关属性 40 | Ack bool `json:"-"` 41 | Durable bool `json:"-"` 42 | MatchingAble bool `json:"-"` 43 | MatchingNode string `json:"-" gorm:"default:'a'; type:varchar(11)"` 44 | TradeTreatNode string `json:"-" gorm:"default:'a'; type:varchar(11)"` 45 | OrderCancelNode string `json:"-" gorm:"default:'a'; type:varchar(11)"` 46 | Running bool `json:"-" sql:"-"` 47 | 48 | Matching string `json:"-"` 49 | TradeTreat string `json:"-"` 50 | OrderCancel string `json:"-"` 51 | } 52 | 53 | var AllMarkets []Market 54 | 55 | func InitAllMarkets(db *utils.GormDB) { 56 | db.Where("visible = ?", true).Find(&AllMarkets) 57 | } 58 | 59 | func FindAllMarket() []Market { 60 | return AllMarkets 61 | } 62 | 63 | func FindMarketById(id int) (Market, error) { 64 | for _, market := range AllMarkets { 65 | if market.Id == id { 66 | return market, nil 67 | } 68 | } 69 | return Market{}, fmt.Errorf("No market can be found.") 70 | } 71 | 72 | func FindMarketByCode(code string) (Market, error) { 73 | for _, market := range AllMarkets { 74 | if market.Code == code { 75 | return market, nil 76 | } 77 | } 78 | return Market{}, fmt.Errorf("No market can be found.") 79 | } 80 | 81 | func (market *Market) AfterCreate(db *gorm.DB) { 82 | tickerRedis := utils.GetRedisConn("ticker") 83 | defer tickerRedis.Close() 84 | ticker := Ticker{MarketId: market.Id, Name: market.Name} 85 | b, _ := json.Marshal(ticker) 86 | if _, err := tickerRedis.Do("HSET", TickersRedisKey, market.Id, string(b)); err != nil { 87 | fmt.Println("{ error: ", err, "}") 88 | return 89 | } 90 | } 91 | 92 | func (market *Market) AfterFind(db *gorm.DB) { 93 | market.LatestKLines = make(map[int]KLine) 94 | } 95 | 96 | // Exchange 97 | func (assignment *Market) MatchingExchange() string { 98 | return assignment.Matching 99 | } 100 | func (assignment *Market) TradeTreatExchange() string { 101 | return assignment.TradeTreat 102 | } 103 | func (assignment *Market) OrderCancelExchange() string { 104 | return assignment.OrderCancel 105 | } 106 | 107 | // Queue 108 | func (assignment *Market) MatchingQueue() string { 109 | return assignment.MatchingExchange() + "." + assignment.Code 110 | } 111 | func (assignment *Market) TradeTreatQueue() string { 112 | return assignment.TradeTreatExchange() + "." + assignment.Code 113 | } 114 | func (assignment *Market) OrderCancelQueue() string { 115 | return assignment.OrderCancelExchange() + "." + assignment.Code 116 | } 117 | 118 | func (market *Market) LatestTradesRedisKey() string { 119 | return fmt.Sprintf("goDCE:latestTrades:%v", market.Code) 120 | } 121 | func (market *Market) TickerRedisKey() string { 122 | return "goDCE:ticker:" + market.Code 123 | } 124 | func (market *Market) KLineRedisKey(period int64) string { 125 | return fmt.Sprintf("goDCE:k:%v:%v", market.Id, period) 126 | } 127 | 128 | func (market *Market) AskRedisKey() string { 129 | return fmt.Sprintf("goDCE:depth:%v:ask", market.Id) 130 | } 131 | func (market *Market) BidRedisKey() string { 132 | return fmt.Sprintf("goDCE:depth:%v:bid", market.Id) 133 | } 134 | 135 | // Notify 136 | func (market *Market) KLineNotify(period int64) string { 137 | return "market:kLine:notify" 138 | } 139 | func (market *Market) TickerNotify() string { 140 | return fmt.Sprintf("market:ticker:notify:%v", market.Id) 141 | } 142 | -------------------------------------------------------------------------------- /models/order.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/jinzhu/gorm" 7 | "github.com/oldfritter/goDCE/utils" 8 | "github.com/shopspring/decimal" 9 | ) 10 | 11 | var ( 12 | CANCEL = 0 13 | WAIT = 100 14 | DONE = 200 15 | ) 16 | 17 | type Order struct { 18 | CommonModel 19 | UserId int `json:"user_id"` 20 | AskAccountId int `json:"ask_account_id" gorm:"default:null"` 21 | BidAccountId int `json:"bid_account_id" gorm:"default:null"` 22 | MarketId int `json:"market_id"` 23 | State int `json:"-"` 24 | Type string `gorm:"type:varchar(16)" json:"type"` 25 | Sn string `gorm:"type:varchar(16)" json:"sn" gorm:"default:null"` 26 | Source string `gorm:"type:varchar(16)" json:"source"` 27 | OrderType string `gorm:"type:varchar(16)" json:"order_type"` 28 | Price decimal.Decimal `gorm:"type:decimal(32,16);default:null;" json:"price"` 29 | Volume decimal.Decimal `gorm:"type:decimal(32,16);default:null;" json:"volume"` 30 | OriginVolume decimal.Decimal `gorm:"type:decimal(32,16);default:null;" json:"origin_volume"` 31 | Locked decimal.Decimal `gorm:"type:decimal(32,16);default:null;" json:"locked"` 32 | OriginLocked decimal.Decimal `gorm:"type:decimal(32,16);default:null;" json:"origin_locked"` 33 | FundsReceived decimal.Decimal `gorm:"type:decimal(32,16);default:null;" json:"funds_received"` 34 | TradesCount int `json:"trades_count"` 35 | 36 | holdAccount Account `sql:"-" json:"-"` 37 | expectAccount Account `sql:"-" json:"-"` 38 | Market Market `sql:"-" json:"-"` 39 | StateStr string `sql:"-" json:"state"` 40 | AvgPrice decimal.Decimal `sql:"-" json:"avg_price"` 41 | } 42 | 43 | type OrderJson struct { 44 | Id int `json:"id"` 45 | MarketId int `json:"market_id"` 46 | Type string `json:"type"` 47 | OrderType string `json:"order_type"` 48 | Volume decimal.Decimal `json:"volume"` 49 | Price decimal.Decimal `json:"price"` 50 | Locked decimal.Decimal `json:"locked"` 51 | Timestamp int64 `json:"timestamp"` 52 | } 53 | 54 | type MatchingPayload struct { 55 | Action string `json:"action"` 56 | Order OrderJson `json:"order"` 57 | } 58 | 59 | func (mp *MatchingPayload) OrderAttrs() (attrs map[string]string) { 60 | attrs["id"] = strconv.Itoa(mp.Order.Id) 61 | attrs["market_id"] = strconv.Itoa(mp.Order.MarketId) 62 | attrs["type"] = mp.Order.Type 63 | attrs["order_type"] = mp.Order.OrderType 64 | attrs["volume"] = mp.Order.Volume.String() 65 | attrs["price"] = mp.Order.Price.String() 66 | attrs["locked"] = mp.Order.Locked.String() 67 | attrs["timestamp"] = strconv.FormatInt(mp.Order.Timestamp, 10) 68 | return 69 | } 70 | 71 | func (order *Order) AfterFind(db *gorm.DB) { 72 | db.Where("id = ?", order.MarketId).First(&order.Market) 73 | if order.Type == "OrderAsk" { 74 | db.Where("id = ?", order.AskAccountId).First(&order.holdAccount) 75 | db.Where("id = ?", order.BidAccountId).First(&order.expectAccount) 76 | } else { 77 | db.Where("id = ?", order.AskAccountId).First(&order.expectAccount) 78 | db.Where("id = ?", order.BidAccountId).First(&order.holdAccount) 79 | } 80 | } 81 | 82 | func (order *Order) OrderAttrs() (attrs map[string]string) { 83 | attrs["id"] = strconv.Itoa(order.Id) 84 | attrs["market_id"] = strconv.Itoa(order.MarketId) 85 | attrs["type"] = order.Type 86 | attrs["order_type"] = order.OrderType 87 | attrs["volume"] = order.Volume.String() 88 | attrs["price"] = order.Price.String() 89 | attrs["locked"] = order.Locked.String() 90 | attrs["timestamp"] = strconv.FormatInt(order.CreatedAt.Unix(), 10) 91 | return 92 | } 93 | 94 | func (order *Order) OType() string { 95 | if order.Type == "OrderBid" { 96 | return "bid" 97 | } else if order.Type == "OrderAsk" { 98 | return "ask" 99 | } 100 | return "" 101 | } 102 | 103 | func (order *Order) InitStateStr() { 104 | switch order.State { 105 | case 200: 106 | order.StateStr = "done" 107 | case 100: 108 | order.StateStr = "wait" 109 | case 0: 110 | order.StateStr = "cancel" 111 | } 112 | } 113 | 114 | func (order *Order) CalculationAvgPrice() { 115 | funds_used := order.OriginLocked.Sub(order.Locked) 116 | zero, _ := decimal.NewFromString("0") 117 | if order.Type == "OrderBid" { 118 | if order.FundsReceived.Equal(zero) { 119 | order.AvgPrice = zero 120 | } else { 121 | order.AvgPrice = funds_used.Div(order.FundsReceived) 122 | } 123 | } else if order.Type == "OrderAsk" { 124 | if funds_used.Equal(zero) { 125 | order.AvgPrice = zero 126 | } else { 127 | order.AvgPrice = order.FundsReceived.Div(funds_used) 128 | } 129 | } 130 | } 131 | 132 | func (order *Order) Fee() (fee decimal.Decimal) { 133 | if order.Type == "OrderAsk" { 134 | fee = order.Market.BidFee 135 | } else { 136 | fee = order.Market.AskFee 137 | } 138 | return 139 | } 140 | 141 | func (order *Order) getAccountChanges(trade Trade) (a, b decimal.Decimal) { 142 | if order.Type == "OrderAsk" { 143 | a = trade.Volume 144 | b = trade.Funds 145 | } else { 146 | b = trade.Volume 147 | a = trade.Funds 148 | } 149 | return 150 | } 151 | 152 | func (order *Order) Strike(db *utils.GormDB, trade Trade) (err error) { 153 | realSub, add := order.getAccountChanges(trade) 154 | realFee := add.Mul(order.Fee()) 155 | realAdd := add.Sub(realFee) 156 | err = order.holdAccount.UnlockedAndSubFunds(db, realSub, realSub, decimal.Zero, STRIKE_SUB, trade.Id, "Trade") 157 | if err != nil { 158 | return 159 | } 160 | err = order.expectAccount.PlusFunds(db, realAdd, realFee, STRIKE_ADD, trade.Id, "Trade") 161 | if err != nil { 162 | return 163 | } 164 | 165 | order.Volume = order.Volume.Sub(trade.Volume) 166 | order.Locked = order.Locked.Sub(realSub) 167 | order.FundsReceived = order.FundsReceived.Add(add) 168 | order.TradesCount += 1 169 | 170 | if order.Volume.Equal(decimal.Zero) { 171 | order.State = DONE 172 | // unlock not used funds 173 | if order.Locked.GreaterThan(decimal.Zero) { 174 | err = order.holdAccount.UnlockFunds(db, order.Locked, ORDER_FULLFILLED, trade.Id, "Trade") 175 | if err != nil { 176 | return 177 | } 178 | } 179 | } else if order.OrderType == "market" && order.Locked.Equal(decimal.Zero) { 180 | order.State = CANCEL 181 | } 182 | return 183 | } 184 | -------------------------------------------------------------------------------- /models/ticker.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/shopspring/decimal" 5 | ) 6 | 7 | var ( 8 | TickersRedisKey = "goDCE:tickers" 9 | ) 10 | 11 | type TickerAspect struct { 12 | Buy decimal.Decimal `json:"buy"` 13 | Sell decimal.Decimal `json:"sell"` 14 | Low decimal.Decimal `json:"low"` 15 | High decimal.Decimal `json:"high"` 16 | Last decimal.Decimal `json:"last"` 17 | Open decimal.Decimal `json:"open"` 18 | Volume decimal.Decimal `json:"volume"` 19 | } 20 | 21 | type Ticker struct { 22 | MarketId int `json:"market_id"` 23 | At int64 `json:"at"` 24 | Name string `json:"name"` 25 | TickerAspect *TickerAspect `json:"ticker"` 26 | } 27 | -------------------------------------------------------------------------------- /models/token.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/oldfritter/goDCE/utils" 7 | ) 8 | 9 | type Token struct { 10 | CommonModel 11 | Token string `gorm:"type:varchar(64)" json:"token"` 12 | UserId int `json:"user_id"` 13 | IsUsed bool `json:"is_used"` 14 | ExpireAt time.Time `gorm:"default:null" json:"expire_at"` 15 | LastVerifyAt time.Time `gorm:"default:null" json:"last_verify_at"` 16 | } 17 | 18 | func (token *Token) InitializeLoginToken() { 19 | token.Token = utils.RandStringRunes(64) 20 | secondsEastOfUTC := int((8 * time.Hour).Seconds()) 21 | beijing := time.FixedZone("Beijing Time", secondsEastOfUTC) 22 | now := time.Now() 23 | token.ExpireAt = time.Date(now.Year(), now.Month(), now.Day()+7, now.Hour(), now.Minute(), now.Second(), 0, beijing) 24 | } 25 | -------------------------------------------------------------------------------- /models/trade.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/shopspring/decimal" 6 | ) 7 | 8 | type Fee struct { 9 | CurrencyId int `json:"currency_id"` 10 | CurrencyCode string `json:"currency_code" gorm:"type:varchar(16)"` 11 | Amount decimal.Decimal `json:"amount"` 12 | } 13 | 14 | type Trade struct { 15 | CommonModel 16 | Trend int `json:"trend"` 17 | MarketId int `json:"market_id"` 18 | AskId int `json:"ask_id"` 19 | BidId int `json:"bid_id"` 20 | Price decimal.Decimal `gorm:"type:decimal(32,16);default:null;" json:"price"` 21 | Volume decimal.Decimal `gorm:"type:decimal(32,16);default:null;" json:"volume"` 22 | Funds decimal.Decimal `gorm:"type:decimal(32,16);default:null;" json:"funds"` 23 | 24 | Side string `json:"side" sql:"-" gorm:"type:varchar(4)"` 25 | Market Market `json:"market" sql:"-"` 26 | AskUserId int `json:"ask_user_id"` 27 | BidUserId int `json:"bid_user_id"` 28 | AskFee Fee `json:"ask_fee" sql:"-"` 29 | BidFee Fee `json:"bid_fee" sql:"-"` 30 | } 31 | type Attrs struct { 32 | Tid int `json:"tid"` 33 | Amount decimal.Decimal `json:"amount"` 34 | Price decimal.Decimal `json:"price"` 35 | Date int64 `json:"date"` 36 | } 37 | 38 | func (trade *Trade) AfterFind(db *gorm.DB) { 39 | } 40 | 41 | func (trade *Trade) SimpleAttrs() (attrs Attrs) { 42 | attrs.Tid = trade.Id 43 | attrs.Amount = trade.Volume 44 | attrs.Price = trade.Price 45 | attrs.Date = trade.CreatedAt.Unix() 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/oldfritter/goDCE/utils" 6 | "golang.org/x/crypto/bcrypt" 7 | ) 8 | 9 | type User struct { 10 | CommonModel 11 | Sn string `gorm:"type:varchar(32);default:null" json:"sn"` 12 | PasswordDigest string `gorm:"type:varchar(64);default:null"` 13 | Nickname string `gorm:"type:varchar(32);default:null" json:"nickname"` 14 | State int `gorm:"default:null" json:"state"` 15 | Activated bool `gorm:"default:null" json:"activated"` 16 | Disabled bool `json:"disabled"` 17 | ApiDisabled bool `json:"api_disabled"` 18 | 19 | Password string `sql:"-"` 20 | Tokens []Token `sql:"-" json:"tokens"` 21 | Accounts []Account `sql:"-" json:"accounts"` 22 | } 23 | 24 | func (user *User) GenerateSn() { 25 | user.Sn = "PEA" + utils.RandStringRunes(8) + "TIO" 26 | } 27 | 28 | func (user *User) AfterSave(db *gorm.DB) { 29 | } 30 | 31 | func (user *User) CompareHashAndPassword() bool { 32 | err := bcrypt.CompareHashAndPassword([]byte(user.PasswordDigest), []byte(user.Password)) 33 | if err == nil { 34 | return true 35 | } 36 | return false 37 | } 38 | 39 | func (user *User) SetPasswordDigest() { 40 | b, _ := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) 41 | user.PasswordDigest = string(b[:]) 42 | } 43 | -------------------------------------------------------------------------------- /order/cancel.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "os/signal" 8 | "strconv" 9 | 10 | envConfig "github.com/oldfritter/goDCE/config" 11 | "github.com/oldfritter/goDCE/initializers" 12 | "github.com/oldfritter/goDCE/models" 13 | "github.com/oldfritter/goDCE/order/cancel" 14 | "github.com/oldfritter/goDCE/utils" 15 | ) 16 | 17 | func main() { 18 | initialize() 19 | initAssignments() 20 | 21 | quit := make(chan os.Signal) 22 | signal.Notify(quit, os.Interrupt) 23 | <-quit 24 | closeResource() 25 | } 26 | 27 | func initialize() { 28 | envConfig.InitEnv() 29 | utils.InitMainDB() 30 | utils.InitBackupDB() 31 | models.AutoMigrations() 32 | utils.InitRedisPools() 33 | initializers.InitializeAmqpConfig() 34 | initializers.LoadCacheData() 35 | 36 | err := ioutil.WriteFile("pids/cancel.pid", []byte(strconv.Itoa(os.Getpid())), 0644) 37 | if err != nil { 38 | fmt.Println(err) 39 | } 40 | } 41 | 42 | func closeResource() { 43 | initializers.CloseAmqpConnection() 44 | utils.CloseRedisPools() 45 | utils.CloseMainDB() 46 | } 47 | 48 | func initAssignments() { 49 | cancel.InitAssignments() 50 | cancel.SubscribeReload() 51 | 52 | go func() { 53 | channel, err := envConfig.RabbitMqConnect.Channel() 54 | if err != nil { 55 | fmt.Errorf("Channel: %s", err) 56 | } 57 | queueName := envConfig.AmqpGlobalConfig.Queue["cancel"]["reload"] 58 | queue, err := channel.QueueDeclare(queueName, true, false, false, false, nil) 59 | if err != nil { 60 | return 61 | } 62 | msgs, err := channel.Consume(queue.Name, "", false, false, false, false, nil) 63 | for _ = range msgs { 64 | cancel.InitAssignments() 65 | } 66 | return 67 | }() 68 | } 69 | -------------------------------------------------------------------------------- /order/cancel/base.go: -------------------------------------------------------------------------------- 1 | package cancel 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/streadway/amqp" 7 | 8 | envConfig "github.com/oldfritter/goDCE/config" 9 | . "github.com/oldfritter/goDCE/models" 10 | "github.com/oldfritter/goDCE/utils" 11 | ) 12 | 13 | var ( 14 | Assignments = make(map[int]Market) 15 | ) 16 | 17 | func InitAssignments() { 18 | 19 | mainDB := utils.MainDbBegin() 20 | defer mainDB.DbRollback() 21 | var markets []Market 22 | mainDB.Where("order_cancel_node = ?", envConfig.CurrentEnv.Node).Find(&markets) 23 | for _, market := range markets { 24 | market.Running = Assignments[market.Id].Running 25 | if market.MatchingAble && !market.Running { 26 | Assignments[market.Id] = market 27 | } else if !market.MatchingAble { 28 | delete(Assignments, market.Id) 29 | } 30 | } 31 | mainDB.DbRollback() 32 | for id, assignment := range Assignments { 33 | if assignment.MatchingAble && !assignment.Running { 34 | go func(id int) { 35 | a := Assignments[id] 36 | subscribeMessageByQueue(&a, amqp.Table{}) 37 | }(id) 38 | assignment.Running = true 39 | Assignments[id] = assignment 40 | } 41 | } 42 | } 43 | 44 | func subscribeMessageByQueue(assignment *Market, arguments amqp.Table) error { 45 | channel, err := envConfig.RabbitMqConnect.Channel() 46 | if err != nil { 47 | fmt.Errorf("Channel: %s", err) 48 | } 49 | 50 | channel.ExchangeDeclare((*assignment).OrderCancelExchange(), "topic", (*assignment).Durable, false, false, false, nil) 51 | channel.QueueBind((*assignment).OrderCancelQueue(), (*assignment).Code, (*assignment).OrderCancelExchange(), false, nil) 52 | 53 | go func(id int) { 54 | channel, err := envConfig.RabbitMqConnect.Channel() 55 | if err != nil { 56 | fmt.Errorf("Channel: %s", err) 57 | } 58 | a := Assignments[id] 59 | msgs, err := channel.Consume( 60 | a.OrderCancelQueue(), // queue 61 | "", // consumer 62 | false, // auto-ack 63 | false, // exclusive 64 | false, // no-local 65 | false, // no-wait 66 | nil, // args 67 | ) 68 | for d := range msgs { 69 | Cancel(&d.Body) 70 | d.Ack(a.Ack) 71 | } 72 | return 73 | }(assignment.Id) 74 | 75 | return nil 76 | } 77 | 78 | func SubscribeReload() (err error) { 79 | channel, err := envConfig.RabbitMqConnect.Channel() 80 | if err != nil { 81 | fmt.Errorf("Channel: %s", err) 82 | return 83 | } 84 | channel.ExchangeDeclare(envConfig.AmqpGlobalConfig.Exchange["default"]["key"], "topic", true, false, false, false, nil) 85 | channel.QueueBind(envConfig.AmqpGlobalConfig.Queue["cancel"]["reload"], envConfig.AmqpGlobalConfig.Queue["cancel"]["reload"], envConfig.AmqpGlobalConfig.Exchange["default"]["key"], false, nil) 86 | return 87 | } 88 | -------------------------------------------------------------------------------- /order/cancel/worker.go: -------------------------------------------------------------------------------- 1 | package cancel 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | . "github.com/oldfritter/goDCE/models" 7 | "github.com/oldfritter/goDCE/utils" 8 | ) 9 | 10 | func Cancel(payloadJson *[]byte) { 11 | var payload struct { 12 | Id int `json:"id"` 13 | } 14 | json.Unmarshal([]byte(*payloadJson), &payload) 15 | 16 | db := utils.MainDbBegin() 17 | defer db.DbRollback() 18 | var order Order 19 | if db.Where("id = ?", payload.Id).First(&order).RecordNotFound() { 20 | return 21 | } 22 | order.State = 0 23 | db.Save(&order) 24 | var account Account 25 | db.Where("market_id = ?", order.MarketId).Where("user_id = ?", order.UserId).First(&account) 26 | account.UnlockFunds(db, order.Locked, ORDER_CANCEL, order.Id, "Order") 27 | db.DbCommit() 28 | return 29 | } 30 | -------------------------------------------------------------------------------- /restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | sh stop.sh 3 | sh start.sh 4 | -------------------------------------------------------------------------------- /routes/v1.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/labstack/echo" 5 | . "github.com/oldfritter/goDCE/api/v1" 6 | ) 7 | 8 | func SetV1Interfaces(e *echo.Echo) { 9 | 10 | e.GET("/api/:platform/v1/currencies", V1GetCurrencies) 11 | e.GET("/api/:platform/v1/k", V1GetK) 12 | e.GET("/api/:platform/v1/chart", V1GetChart) 13 | e.GET("/api/:platform/v1/markets", V1GetMarkets) 14 | e.POST("/api/:platform/v1/users/login", V1PostUsersLogin) 15 | e.GET("/api/:platform/v1/users/me", V1GetUsersMe) 16 | e.GET("/api/:platform/v1/users/accounts/:currency", V1GetUsersAccountsCurrency) 17 | e.GET("/api/:platform/v1/users/accounts", V1GetUsersAccounts) 18 | e.GET("/api/:platform/v1/depth", V1Getdepth) 19 | e.GET("/api/:platform/v1/order", V1GetOrder) 20 | e.GET("/api/:platform/v1/orders", V1GetOrders) 21 | e.POST("/api/:platform/v1/orders", V1PostOrders) 22 | e.POST("/api/:platform/v1/order/delete", V1PostOrderDelete) 23 | e.POST("/api/:platform/v1/orders/clear", V1PostOrdersClear) 24 | e.GET("/api/:platform/v1/tickers", V1GetTickers) 25 | e.GET("/api/:platform/v1/tickers/:market", V1GetTickersMarket) 26 | e.GET("/api/:platform/v1/trades", V1GetTrades) 27 | e.GET("/api/:platform/v1/trades/my", V1GetTradesMy) 28 | 29 | } 30 | -------------------------------------------------------------------------------- /schedules/backup/tasks/account_version.go: -------------------------------------------------------------------------------- 1 | package tasks 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | . "github.com/oldfritter/goDCE/models" 8 | "github.com/oldfritter/goDCE/utils" 9 | ) 10 | 11 | func BackupAccountVersions() { 12 | mainDB := utils.MainDbBegin() 13 | defer mainDB.DbRollback() 14 | var first, last AccountVersion 15 | mainDB.Where("created_at < ?", time.Now().Add(-time.Hour*24*30)).First(&first) 16 | mainDB.Where("created_at < ?", time.Now().Add(-time.Hour*24*30)).Last(&last) 17 | limit := 1000 18 | maxThreads := 16 19 | 20 | c := make(chan int) 21 | quit := make(chan int) 22 | i := 0 23 | for i < maxThreads { 24 | go func(j int) { 25 | if (first.Id + j*limit) < last.Id { 26 | backupRowsAccountVersions(first.Id+j*limit, last.Id, limit) 27 | c <- 1 28 | } 29 | }(i) 30 | if (first.Id + i*limit) > last.Id { 31 | break 32 | } 33 | i += 1 34 | } 35 | 36 | for _ = range c { 37 | go func(j int) { 38 | if (first.Id + j*limit) > last.Id { 39 | time.Sleep(10 * time.Second) 40 | quit <- 1 41 | } else { 42 | backupRowsAccountVersions(first.Id+j*limit, last.Id, limit) 43 | c <- 1 44 | } 45 | }(i) 46 | if (first.Id + i*limit) > last.Id { 47 | break 48 | } 49 | i += 1 50 | } 51 | 52 | <-quit 53 | fmt.Println("quiting...") 54 | time.Sleep(10 * time.Second) 55 | return 56 | 57 | } 58 | 59 | func backupRowsAccountVersions(begin, lastId, limit int) { 60 | fmt.Println(begin) 61 | end := begin + limit - 1 62 | if end > lastId { 63 | end = lastId 64 | } 65 | mainDB := utils.MainDbBegin() 66 | defer mainDB.DbRollback() 67 | var avs []AccountVersion 68 | mainDB.Where("id between ? AND ?", begin, end).Find(&avs) 69 | for _, av := range avs { 70 | insertRowToBackup(&av, 9) 71 | } 72 | 73 | for _, av := range avs { 74 | mainDB.Delete(&av) 75 | } 76 | mainDB.DbCommit() 77 | return 78 | } 79 | 80 | func insertRowToBackup(av *AccountVersion, times int) { 81 | backupDB := utils.BackupDbBegin() 82 | defer backupDB.DbRollback() 83 | var avBackup AccountVersion 84 | if backupDB.Where("id = ?", av.Id).First(&avBackup).RecordNotFound() { 85 | sql := fmt.Sprintf("INSERT INTO account_versions (id, created_at, updated_at, user_id, account_id, reason, balance, locked, fee, amount, modifiable_id, modifiable_type, currency_id, fun) VALUES (%v, '%v', '%v', %v, %v, %v, %v, %v, %v, %v, %v, '%v', %v, %v)", 86 | (*av).Id, 87 | (*av).CreatedAt.Format("2006-01-02 15:04:05"), 88 | (*av).UpdatedAt.Format("2006-01-02 15:04:05"), 89 | (*av).UserId, 90 | (*av).AccountId, 91 | (*av).Reason, 92 | (*av).Balance, 93 | (*av).Locked, 94 | (*av).Fee, 95 | (*av).Amount, 96 | (*av).ModifiableId, 97 | (*av).ModifiableType, 98 | (*av).CurrencyId, 99 | (*av).Fun, 100 | ) 101 | result := backupDB.Exec(sql) 102 | if result.RowsAffected == 1 { 103 | backupDB.DbCommit() 104 | return 105 | } 106 | backupDB.DbRollback() 107 | if times > 0 { 108 | insertRowToBackup(av, times-1) 109 | } 110 | } 111 | return 112 | } 113 | -------------------------------------------------------------------------------- /schedules/backup/tasks/logs.go: -------------------------------------------------------------------------------- 1 | package tasks 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "path/filepath" 7 | "time" 8 | 9 | "github.com/oldfritter/goDCE/utils" 10 | ) 11 | 12 | type MyPutRet struct { 13 | Key string 14 | Hash string 15 | Fsize int 16 | Bucket string 17 | Name string 18 | } 19 | 20 | var ( 21 | logNames = []string{"api", "workers", "schedule"} 22 | ) 23 | 24 | func UploadLogFileToQiniu() { 25 | UploadLogFileToQiniuByDay(time.Now().Add(-time.Hour * 24)) 26 | UploadLogFileToQiniuByDay(time.Now()) 27 | } 28 | 29 | func UploadLogFileToS3() { 30 | UploadLogFileToS3ByDay(time.Now().Add(-time.Hour * 24)) 31 | UploadLogFileToS3ByDay(time.Now()) 32 | } 33 | 34 | func UploadLogFileToQiniuByDay(day time.Time) { 35 | utils.InitQiniuConfig() 36 | for _, logName := range logNames { 37 | gzFile := "/tmp/panama/" + logName + day.Format("2006-01-02") + ".tar.gz" 38 | 39 | name, _ := exec.Command("sh", "-c", "hostname").Output() 40 | hostname := string(name) 41 | 42 | exec.Command("sh", "-c", "mkdir -p /tmp/panama/").Output() 43 | exec.Command("sh", "-c", "tar -czvf "+gzFile+" "+"logs/"+logName+day.Format("2006-01-02")+".log").Output() 44 | 45 | key := "logs/panama/" + day.Format("01/02") + "/" + logName + "/" + hostname + ".tar.gz" 46 | 47 | err := utils.UploadFileToQiniu(utils.QiniuConfig["backup_bucket"], key, gzFile) 48 | if err != nil { 49 | fmt.Println("err: ", err) 50 | } 51 | exec.Command("sh", "-c", "rm -rf "+gzFile).Output() 52 | 53 | } 54 | } 55 | 56 | func UploadLogFileToS3ByDay(day time.Time) { 57 | utils.InitAwsS3Config() 58 | for _, logName := range logNames { 59 | gzFile := "/tmp/panama/" + logName + day.Format("2006-01-02") + ".tar.gz" 60 | 61 | name, _ := exec.Command("sh", "-c", "hostname").Output() 62 | hostname := string(name) 63 | 64 | exec.Command("sh", "-c", "mkdir -p /tmp/panama/").Output() 65 | exec.Command("sh", "-c", "tar -czvf "+gzFile+" "+"logs/"+logName+day.Format("2006-01-02")+".log").Output() 66 | 67 | key := "logs/panama/" + day.Format("01/02") + "/" + logName + "/" + hostname + ".tar.gz" 68 | 69 | err := utils.UploadFileToS3(utils.S3Config["S3_BACKUP_BUCKET"], key, gzFile) 70 | if err != nil { 71 | fmt.Println("err: ", err) 72 | } 73 | exec.Command("sh", "-c", "rm -rf "+gzFile).Output() 74 | 75 | } 76 | } 77 | func BackupLogFiles() { 78 | for _, logName := range logNames { 79 | a, _ := filepath.Abs("logs/" + logName + ".log") 80 | b, _ := filepath.Abs("logs/" + logName + time.Now().Format("2006-01-02") + ".log") 81 | exec.Command("sh", "-c", "cat "+fmt.Sprintf(a)+" >> "+fmt.Sprintf(b)).Output() 82 | exec.Command("sh", "-c", "echo '\n' > "+fmt.Sprintf(a)).Output() 83 | } 84 | } 85 | 86 | func CleanLogs() { 87 | for _, logName := range logNames { 88 | day := 2 89 | for day < 10 { 90 | str := time.Now().Add(-time.Hour * 24 * time.Duration(day)).Format("2006-01-02") 91 | b, _ := filepath.Abs("logs/" + logName + str + ".log") 92 | exec.Command("sh", "-c", "rm -rf "+fmt.Sprintf(b)).Output() 93 | day += 1 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /schedules/backup/tasks/tokens.go: -------------------------------------------------------------------------------- 1 | package tasks 2 | 3 | import ( 4 | "time" 5 | 6 | . "github.com/oldfritter/goDCE/models" 7 | "github.com/oldfritter/goDCE/utils" 8 | ) 9 | 10 | func CleanTokens() { 11 | utils.InitMainDB() 12 | db := utils.MainDbBegin() 13 | defer db.DbRollback() 14 | 15 | db.Where("expire_at < ?", time.Now().Add(-time.Hour*8)).Delete(Token{}) 16 | db.DbCommit() 17 | utils.CloseMainDB() 18 | } 19 | -------------------------------------------------------------------------------- /schedules/kLine/create.go: -------------------------------------------------------------------------------- 1 | package kLine 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/streadway/amqp" 9 | 10 | "github.com/oldfritter/goDCE/config" 11 | . "github.com/oldfritter/goDCE/models" 12 | ) 13 | 14 | func CreateLatestKLine() { 15 | 16 | markets := FindAllMarket() 17 | for _, market := range markets { 18 | periods := []int64{1, 5, 15, 30, 60, 120, 240, 360, 720, 1440, 4320, 10080} 19 | for _, period := range periods { 20 | payload := struct { 21 | MarketId int `json:"market_id"` 22 | Timestamp int64 `json:"timestamp"` 23 | Period int64 `json:"period"` 24 | DataSource string `json:"data_source"` 25 | }{ 26 | MarketId: market.Id, 27 | Timestamp: time.Now().Unix(), 28 | Period: period, 29 | DataSource: "db", 30 | } 31 | b, err := json.Marshal(payload) 32 | if err != nil { 33 | fmt.Println("error:", err) 34 | } 35 | err = config.RabbitMqConnect.PublishMessageWithRouteKey("goDCE.default", "goDCE.k", "text/plain", false, false, &b, amqp.Table{}, amqp.Persistent, "") 36 | if err != nil { 37 | fmt.Println("{ error:", err, "}") 38 | panic(err) 39 | } 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /schedules/order/waitingCheck.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/gomodule/redigo/redis" 9 | . "github.com/oldfritter/goDCE/models" 10 | "github.com/oldfritter/goDCE/utils" 11 | ) 12 | 13 | func WaitingOrderCheck() { 14 | db := utils.MainDbBegin() 15 | defer db.DbRollback() 16 | 17 | for _, market := range AllMarkets { 18 | ordersPerMarket(db, &market) 19 | } 20 | db.DbCommit() 21 | } 22 | 23 | func ordersPerMarket(db *utils.GormDB, market *Market) { 24 | dataRedis := utils.GetRedisConn("data") 25 | defer dataRedis.Close() 26 | 27 | var orderBook struct { 28 | AskIds []int `json:"ask_ids"` 29 | BidIds []int `json:"bid_ids"` 30 | } 31 | key := "goDCE:order_book:" + (*market).Code 32 | values, _ := redis.String(dataRedis.Do("GET", key)) 33 | json.Unmarshal([]byte(values), &orderBook) 34 | 35 | var orders []Order 36 | if !db.Where("id not in (?)", orderBook.AskIds). 37 | Where("type = ?", "OrderAsk"). 38 | Where("market_id = ?", (*market).Code). 39 | Where("state = ?", 100). 40 | Where("created_at < ?", time.Now().Add(-time.Second*10)). 41 | Find(&orders).RecordNotFound() { 42 | var ids []int 43 | for _, order := range orders { 44 | ids = append(ids, order.Id) 45 | } 46 | fmt.Println(time.Now().Format("2006-01-02 15:04:05"), "--WaitingOrderCheck orders: ", ids) 47 | } 48 | if !db.Where("id not in (?)", orderBook.BidIds). 49 | Where("type = ?", "OrderBid"). 50 | Where("market_id = ?", (*market).Code). 51 | Where("state = ?", 100). 52 | Where("created_at < ?", time.Now().Add(-time.Second*10)). 53 | Find(&orders).RecordNotFound() { 54 | var ids []int 55 | for _, order := range orders { 56 | ids = append(ids, order.Id) 57 | } 58 | fmt.Println(time.Now().Format("2006-01-02 15:04:05"), "--WaitingOrderCheck orders: ", ids) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /schedules/schedule.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "os/signal" 8 | "strconv" 9 | 10 | envConfig "github.com/oldfritter/goDCE/config" 11 | "github.com/oldfritter/goDCE/initializers" 12 | "github.com/oldfritter/goDCE/schedules/backup/tasks" 13 | "github.com/oldfritter/goDCE/schedules/kLine" 14 | "github.com/oldfritter/goDCE/schedules/order" 15 | "github.com/oldfritter/goDCE/utils" 16 | "github.com/robfig/cron" 17 | ) 18 | 19 | var QueueName string 20 | 21 | func main() { 22 | envConfig.InitEnv() 23 | utils.InitMainDB() 24 | utils.InitBackupDB() 25 | utils.InitRedisPools() 26 | initializers.InitializeAmqpConfig() 27 | 28 | initializers.LoadCacheData() 29 | 30 | InitSchedule() 31 | 32 | err := ioutil.WriteFile("pids/schedule.pid", []byte(strconv.Itoa(os.Getpid())), 0644) 33 | if err != nil { 34 | fmt.Println(err) 35 | } 36 | 37 | quit := make(chan os.Signal) 38 | signal.Notify(quit, os.Interrupt) 39 | <-quit 40 | closeResource() 41 | } 42 | 43 | func closeResource() { 44 | initializers.CloseAmqpConnection() 45 | utils.CloseRedisPools() 46 | utils.CloseMainDB() 47 | utils.CloseBackupDB() 48 | } 49 | 50 | func InitSchedule() { 51 | c := cron.New() 52 | // 日志备份 53 | c.AddFunc("0 55 23 * * *", tasks.BackupLogFiles) 54 | c.AddFunc("0 56 23 * * *", tasks.UploadLogFileToS3) 55 | c.AddFunc("0 59 23 * * *", tasks.CleanLogs) 56 | 57 | for _, schedule := range envConfig.CurrentEnv.Schedules { 58 | if schedule == "CleanTokens" { 59 | // 清理tokens 60 | c.AddFunc("0 57 23 * * *", tasks.CleanTokens) 61 | } else if schedule == "CreateLatestKLine" { 62 | // 生成存储于数据库的K线 63 | c.AddFunc("*/5 * * * * *", kLine.CreateLatestKLine) 64 | } else if schedule == "WaitingOrderCheck" { 65 | // 二十秒检查一次待成交订单 66 | c.AddFunc("*/20 * * * * *", order.WaitingOrderCheck) 67 | } 68 | } 69 | 70 | c.Start() 71 | } 72 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | nohup ./cmd/api >> logs/api.log & 3 | nohup ./cmd/matching >> logs/matching.log & 4 | nohup ./cmd/cancel >> logs/cancel.log & 5 | nohup ./cmd/treat >> logs/treat.log & 6 | nohup ./cmd/workers >> logs/workers.log & 7 | -------------------------------------------------------------------------------- /stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cat pids/matching.pid | xargs kill -INT 4 | 5 | cat pids/cancel.pid | xargs kill -INT 6 | 7 | cat pids/treat.pid | xargs kill -INT 8 | 9 | cat pids/workers.pid | xargs kill -INT 10 | 11 | cat pids/api.pid | xargs kill -INT 12 | -------------------------------------------------------------------------------- /trade/matching.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "os/signal" 8 | "strconv" 9 | 10 | envConfig "github.com/oldfritter/goDCE/config" 11 | "github.com/oldfritter/goDCE/initializers" 12 | "github.com/oldfritter/goDCE/models" 13 | "github.com/oldfritter/goDCE/trade/matching" 14 | "github.com/oldfritter/goDCE/utils" 15 | ) 16 | 17 | func main() { 18 | initialize() 19 | initAssignments() 20 | 21 | quit := make(chan os.Signal) 22 | signal.Notify(quit, os.Interrupt) 23 | <-quit 24 | closeResource() 25 | } 26 | 27 | func initialize() { 28 | envConfig.InitEnv() 29 | utils.InitMainDB() 30 | utils.InitBackupDB() 31 | models.AutoMigrations() 32 | utils.InitRedisPools() 33 | initializers.InitializeAmqpConfig() 34 | initializers.LoadCacheData() 35 | 36 | err := ioutil.WriteFile("pids/matching.pid", []byte(strconv.Itoa(os.Getpid())), 0644) 37 | if err != nil { 38 | fmt.Println(err) 39 | } 40 | } 41 | 42 | func closeResource() { 43 | initializers.CloseAmqpConnection() 44 | utils.CloseRedisPools() 45 | utils.CloseMainDB() 46 | } 47 | 48 | func initAssignments() { 49 | matching.InitAssignments() 50 | matching.SubscribeReload() 51 | 52 | go func() { 53 | channel, err := envConfig.RabbitMqConnect.Channel() 54 | if err != nil { 55 | fmt.Errorf("Channel: %s", err) 56 | } 57 | queueName := envConfig.AmqpGlobalConfig.Queue["matching"]["reload"] 58 | queue, err := channel.QueueDeclare(queueName, true, false, false, false, nil) 59 | if err != nil { 60 | return 61 | } 62 | msgs, err := channel.Consume(queue.Name, "", false, false, false, false, nil) 63 | for _ = range msgs { 64 | matching.InitAssignments() 65 | } 66 | return 67 | }() 68 | } 69 | -------------------------------------------------------------------------------- /trade/matching/base.go: -------------------------------------------------------------------------------- 1 | package matching 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "runtime" 7 | 8 | envConfig "github.com/oldfritter/goDCE/config" 9 | . "github.com/oldfritter/goDCE/models" 10 | "github.com/oldfritter/goDCE/utils" 11 | "github.com/oldfritter/matching" 12 | "github.com/streadway/amqp" 13 | ) 14 | 15 | var ( 16 | Assignments = make(map[int]Market) 17 | Engines = make(map[int]matching.Engine) 18 | ) 19 | 20 | func InitAssignments() { 21 | 22 | mainDB := utils.MainDbBegin() 23 | defer mainDB.DbRollback() 24 | var markets []Market 25 | mainDB.Where("matching_node = ?", envConfig.CurrentEnv.Node).Find(&markets) 26 | for _, market := range markets { 27 | market.Running = Assignments[market.Id].Running 28 | if market.MatchingAble && !market.Running { 29 | Assignments[market.Id] = market 30 | engine := matching.InitializeEngine(market.Id, matching.Options{}) 31 | Engines[market.Id] = engine 32 | // 加载过往订单 33 | var orders []Order 34 | if !mainDB.Where("state = ?", 100).Where("market_id = ?", market.Id).Find(&orders).RecordNotFound() { 35 | for _, o := range orders { 36 | order, _ := matching.InitializeOrder(o.OrderAttrs()) 37 | engine.Submit(order) 38 | } 39 | } 40 | } else if !market.MatchingAble { 41 | delete(Assignments, market.Id) 42 | delete(Engines, market.Id) 43 | } 44 | } 45 | mainDB.DbRollback() 46 | for id, assignment := range Assignments { 47 | if assignment.MatchingAble && !assignment.Running { 48 | go func(id int) { 49 | a := Assignments[id] 50 | subscribeMessageByQueue(&a, amqp.Table{}) 51 | }(id) 52 | assignment.Running = true 53 | Assignments[id] = assignment 54 | } 55 | } 56 | } 57 | 58 | func subscribeMessageByQueue(assignment *Market, arguments amqp.Table) error { 59 | channel, err := envConfig.RabbitMqConnect.Channel() 60 | if err != nil { 61 | fmt.Errorf("Channel: %s", err) 62 | } 63 | channel.ExchangeDeclare((*assignment).MatchingExchange(), "topic", (*assignment).Durable, false, false, false, nil) 64 | channel.QueueBind((*assignment).MatchingQueue(), (*assignment).Code, (*assignment).MatchingExchange(), false, nil) 65 | 66 | go func(id int) { 67 | a := Assignments[id] 68 | channel, err := envConfig.RabbitMqConnect.Channel() 69 | if err != nil { 70 | fmt.Errorf("Channel: %s", err) 71 | } 72 | msgs, _ := channel.Consume( 73 | a.MatchingQueue(), // queue 74 | "", // consumer 75 | false, // auto-ack 76 | false, // exclusive 77 | false, // no-local 78 | false, // no-wait 79 | nil, // args 80 | ) 81 | 82 | for d := range msgs { 83 | if !a.MatchingAble { 84 | d.Reject(true) 85 | d.Nack(false, false) 86 | runtime.Goexit() 87 | } 88 | doMatching(&d.Body) 89 | d.Ack(a.Ack) 90 | } 91 | return 92 | }((*assignment).Id) 93 | 94 | engine := Engines[(*assignment).Id] 95 | buildDepth(assignment) 96 | 97 | // trade 98 | go func() { 99 | for offer := range engine.Traded { 100 | b, err := json.Marshal(offer) 101 | if err != nil { 102 | fmt.Println("error:", err) 103 | } 104 | err = envConfig.RabbitMqConnect.PublishMessageWithRouteKey((*assignment).TradeTreatExchange(), (*assignment).Code, "text/plain", false, false, &b, amqp.Table{}, amqp.Persistent, "") 105 | if err != nil { 106 | fmt.Println("{ error:", err, "}") 107 | } else { 108 | buildDepth(assignment) 109 | buildLatestTrades(assignment) 110 | } 111 | } 112 | }() 113 | 114 | // cancel 115 | go func() { 116 | for offer := range engine.Canceled { 117 | b, err := json.Marshal(offer) 118 | if err != nil { 119 | fmt.Println("error:", err) 120 | } 121 | err = envConfig.RabbitMqConnect.PublishMessageWithRouteKey((*assignment).OrderCancelExchange(), (*assignment).Code, "text/plain", false, false, &b, amqp.Table{}, amqp.Persistent, "") 122 | if err != nil { 123 | fmt.Println("{ error:", err, "}") 124 | } else { 125 | buildDepth(assignment) 126 | } 127 | } 128 | }() 129 | 130 | return nil 131 | } 132 | 133 | func SubscribeReload() (err error) { 134 | channel, err := envConfig.RabbitMqConnect.Channel() 135 | if err != nil { 136 | fmt.Errorf("Channel: %s", err) 137 | return 138 | } 139 | channel.ExchangeDeclare(envConfig.AmqpGlobalConfig.Exchange["default"]["key"], "topic", true, false, false, false, nil) 140 | channel.QueueBind(envConfig.AmqpGlobalConfig.Queue["matching"]["reload"], envConfig.AmqpGlobalConfig.Queue["matching"]["reload"], envConfig.AmqpGlobalConfig.Exchange["default"]["key"], false, nil) 141 | return 142 | } 143 | -------------------------------------------------------------------------------- /trade/matching/depth.go: -------------------------------------------------------------------------------- 1 | package matching 2 | 3 | import ( 4 | . "github.com/oldfritter/goDCE/models" 5 | "github.com/oldfritter/goDCE/utils" 6 | "github.com/shopspring/decimal" 7 | ) 8 | 9 | type Depth struct { 10 | MarketId int `json:"market_id"` 11 | AskOrders map[string]decimal.Decimal `json:"asks"` 12 | BidOrders map[string]decimal.Decimal `json:"bids"` 13 | } 14 | 15 | func InitializeDepth(marketId int) (depth Depth) { 16 | depth.MarketId = marketId 17 | return 18 | } 19 | 20 | func buildDepth(market *Market) { 21 | engine := Engines[(*market).Id] 22 | depth := InitializeDepth((*market).Id) 23 | 24 | askOrderBook := engine.AskOrderBook() 25 | askDepth := askOrderBook.LimitOrdersMap() 26 | for price, orders := range askDepth { 27 | var volume decimal.Decimal 28 | for _, order := range orders { 29 | volume = volume.Add(order.Volume) 30 | } 31 | depth.AskOrders[price] = volume 32 | } 33 | bidOrderBook := engine.BidOrderBook() 34 | bidDepth := bidOrderBook.LimitOrdersMap() 35 | for price, orders := range bidDepth { 36 | var volume decimal.Decimal 37 | for _, order := range orders { 38 | volume = volume.Add(order.Volume) 39 | } 40 | depth.BidOrders[price] = volume 41 | } 42 | 43 | dataRedis := utils.GetRedisConn("data") 44 | defer dataRedis.Close() 45 | 46 | dataRedis.Do("SET", (*market).AskRedisKey(), depth.AskOrders) 47 | dataRedis.Do("SET", (*market).BidRedisKey(), depth.BidOrders) 48 | } 49 | -------------------------------------------------------------------------------- /trade/matching/trade.go: -------------------------------------------------------------------------------- 1 | package matching 2 | 3 | import ( 4 | . "github.com/oldfritter/goDCE/models" 5 | "github.com/oldfritter/goDCE/utils" 6 | ) 7 | 8 | func buildLatestTrades(market *Market) { 9 | mainDB := utils.MainDbBegin() 10 | defer mainDB.DbRollback() 11 | 12 | var trades []Trade 13 | var results []Attrs 14 | mainDB.Where("market_id = ?", (*market).Id).Order("id DESC").Limit(80).Find(&trades) 15 | for _, trade := range trades { 16 | results = append(results, trade.SimpleAttrs()) 17 | } 18 | 19 | dataRedis := utils.GetRedisConn("data") 20 | defer dataRedis.Close() 21 | 22 | dataRedis.Do("SET", (*market).LatestTradesRedisKey(), results) 23 | } 24 | -------------------------------------------------------------------------------- /trade/matching/worker.go: -------------------------------------------------------------------------------- 1 | package matching 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | . "github.com/oldfritter/goDCE/models" 7 | "github.com/oldfritter/matching" 8 | ) 9 | 10 | func doMatching(payloadJson *[]byte) { 11 | var payload MatchingPayload 12 | json.Unmarshal([]byte(*payloadJson), &payload) 13 | order, err := matching.InitializeOrder(payload.OrderAttrs()) 14 | if err != nil { 15 | return 16 | } 17 | engine := Engines[order.MarketId] 18 | if payload.Action == "submit" { 19 | engine.Submit(order) 20 | } else if payload.Action == "cancel" { 21 | engine.Cancel(order) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /trade/treat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "os/signal" 8 | "strconv" 9 | 10 | envConfig "github.com/oldfritter/goDCE/config" 11 | "github.com/oldfritter/goDCE/initializers" 12 | "github.com/oldfritter/goDCE/models" 13 | "github.com/oldfritter/goDCE/trade/treat" 14 | "github.com/oldfritter/goDCE/utils" 15 | ) 16 | 17 | func main() { 18 | initialize() 19 | initAssignments() 20 | 21 | quit := make(chan os.Signal) 22 | signal.Notify(quit, os.Interrupt) 23 | <-quit 24 | closeResource() 25 | } 26 | 27 | func initialize() { 28 | envConfig.InitEnv() 29 | utils.InitMainDB() 30 | utils.InitBackupDB() 31 | models.AutoMigrations() 32 | utils.InitRedisPools() 33 | initializers.InitializeAmqpConfig() 34 | initializers.InitWorkers() 35 | initializers.LoadCacheData() 36 | 37 | err := ioutil.WriteFile("pids/treat.pid", []byte(strconv.Itoa(os.Getpid())), 0644) 38 | if err != nil { 39 | fmt.Println(err) 40 | } 41 | } 42 | 43 | func closeResource() { 44 | initializers.CloseAmqpConnection() 45 | utils.CloseRedisPools() 46 | utils.CloseMainDB() 47 | } 48 | 49 | func initAssignments() { 50 | treat.InitAssignments() 51 | treat.SubscribeReload() 52 | 53 | go func() { 54 | channel, err := envConfig.RabbitMqConnect.Channel() 55 | if err != nil { 56 | fmt.Errorf("Channel: %s", err) 57 | } 58 | queueName := envConfig.AmqpGlobalConfig.Queue["trade"]["reload"] 59 | queue, err := channel.QueueDeclare(queueName, true, false, false, false, nil) 60 | if err != nil { 61 | return 62 | } 63 | msgs, err := channel.Consume(queue.Name, "", false, false, false, false, nil) 64 | for _ = range msgs { 65 | treat.InitAssignments() 66 | } 67 | return 68 | }() 69 | } 70 | -------------------------------------------------------------------------------- /trade/treat/base.go: -------------------------------------------------------------------------------- 1 | package treat 2 | 3 | import ( 4 | "fmt" 5 | 6 | envConfig "github.com/oldfritter/goDCE/config" 7 | . "github.com/oldfritter/goDCE/models" 8 | "github.com/oldfritter/goDCE/utils" 9 | "github.com/streadway/amqp" 10 | ) 11 | 12 | var ( 13 | Assignments = make(map[int]Market) 14 | ) 15 | 16 | func InitAssignments() { 17 | 18 | mainDB := utils.MainDbBegin() 19 | defer mainDB.DbRollback() 20 | var markets []Market 21 | mainDB.Where("trade_treat_node = ?", envConfig.CurrentEnv.Node).Find(&markets) 22 | for _, market := range markets { 23 | market.Running = Assignments[market.Id].Running 24 | if market.MatchingAble && !market.Running { 25 | Assignments[market.Id] = market 26 | } else if !market.MatchingAble { 27 | delete(Assignments, market.Id) 28 | } 29 | } 30 | mainDB.DbRollback() 31 | for id, assignment := range Assignments { 32 | if assignment.MatchingAble && !assignment.Running { 33 | go func(id int) { 34 | a := Assignments[id] 35 | subscribeMessageByQueue(&a, amqp.Table{}) 36 | }(id) 37 | assignment.Running = true 38 | Assignments[id] = assignment 39 | } 40 | } 41 | } 42 | 43 | func subscribeMessageByQueue(assignment *Market, arguments amqp.Table) error { 44 | channel, err := envConfig.RabbitMqConnect.Channel() 45 | if err != nil { 46 | fmt.Errorf("Channel: %s", err) 47 | } 48 | 49 | channel.ExchangeDeclare((*assignment).TradeTreatExchange(), "topic", (*assignment).Durable, false, false, false, nil) 50 | channel.QueueBind((*assignment).TradeTreatQueue(), (*assignment).Code, (*assignment).TradeTreatExchange(), false, nil) 51 | 52 | go func(id int) { 53 | a := Assignments[id] 54 | channel, err := envConfig.RabbitMqConnect.Channel() 55 | if err != nil { 56 | fmt.Errorf("Channel: %s", err) 57 | } 58 | msgs, err := channel.Consume( 59 | a.TradeTreatQueue(), 60 | "", 61 | false, 62 | false, 63 | false, 64 | false, 65 | nil, 66 | ) 67 | for d := range msgs { 68 | Treat(&d.Body) 69 | d.Ack(a.Ack) 70 | } 71 | return 72 | }(assignment.Id) 73 | 74 | return nil 75 | } 76 | 77 | func SubscribeReload() (err error) { 78 | channel, err := envConfig.RabbitMqConnect.Channel() 79 | if err != nil { 80 | fmt.Errorf("Channel: %s", err) 81 | return 82 | } 83 | channel.ExchangeDeclare(envConfig.AmqpGlobalConfig.Exchange["default"]["key"], "topic", true, false, false, false, nil) 84 | channel.QueueBind(envConfig.AmqpGlobalConfig.Queue["trade"]["reload"], envConfig.AmqpGlobalConfig.Queue["trade"]["reload"], envConfig.AmqpGlobalConfig.Exchange["default"]["key"], false, nil) 85 | return 86 | } 87 | -------------------------------------------------------------------------------- /trade/treat/worker.go: -------------------------------------------------------------------------------- 1 | package treat 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/streadway/amqp" 9 | 10 | "github.com/oldfritter/goDCE/config" 11 | . "github.com/oldfritter/goDCE/models" 12 | "github.com/oldfritter/goDCE/utils" 13 | "github.com/oldfritter/matching" 14 | ) 15 | 16 | func Treat(payloadJson *[]byte) { 17 | var offer matching.Offer 18 | json.Unmarshal([]byte(*payloadJson), &offer) 19 | tryCreateTrade(&offer, 2) 20 | return 21 | } 22 | 23 | func tryCreateTrade(offer *matching.Offer, times int) { 24 | mainDB := utils.MainDbBegin() 25 | defer mainDB.DbRollback() 26 | var ask, bid Order 27 | if mainDB.Where("id = ?", offer.AskId).First(&ask).RecordNotFound() { 28 | return 29 | } 30 | if mainDB.Where("id = ?", offer.BidId).First(&bid).RecordNotFound() { 31 | return 32 | } 33 | 34 | trade := Trade{ 35 | MarketId: (*offer).MarketId, 36 | AskId: (*offer).AskId, 37 | BidId: (*offer).BidId, 38 | Price: (*offer).StrikePrice, 39 | Volume: (*offer).Volume, 40 | Funds: (*offer).Funds, 41 | AskUserId: ask.UserId, 42 | BidUserId: bid.UserId, 43 | } 44 | result := mainDB.Create(&trade) 45 | if result.Error != nil { 46 | mainDB.DbRollback() 47 | if times > 0 { 48 | trade.Id = 0 49 | tryCreateTrade(offer, times-1) 50 | return 51 | } 52 | } 53 | 54 | errAsk := ask.Strike(mainDB, trade) 55 | errBid := bid.Strike(mainDB, trade) 56 | if errAsk == nil && errBid == nil { 57 | mainDB.DbCommit() 58 | pushMessageToRefreshTicker((*offer).MarketId) 59 | pushMessageToRefreshKLine((*offer).MarketId) 60 | return 61 | } 62 | mainDB.DbRollback() 63 | if times > 0 { 64 | trade.Id = 0 65 | tryCreateTrade(offer, times-1) 66 | } 67 | return 68 | } 69 | 70 | func pushMessageToRefreshTicker(marketId int) { 71 | var payload struct { 72 | MarketId int `json:"market_id"` 73 | } 74 | payload.MarketId = marketId 75 | b, err := json.Marshal(payload) 76 | if err != nil { 77 | fmt.Println("error:", err) 78 | } 79 | 80 | var exchange, routingKey string 81 | for _, w := range config.AllWorkers { 82 | if w.Name == "TickerWorker" { 83 | exchange = w.Exchange 84 | routingKey = w.RoutingKey 85 | } 86 | } 87 | err = config.RabbitMqConnect.PublishMessageWithRouteKey(exchange, routingKey, "text/plain", false, false, &b, amqp.Table{}, amqp.Persistent, "") 88 | if err != nil { 89 | fmt.Println("{ error:", err, "}") 90 | panic(err) 91 | } 92 | return 93 | } 94 | 95 | func pushMessageToRefreshKLine(marketId int) { 96 | var payload struct { 97 | MarketId int `json:"market_id"` 98 | Period int64 `json:"period"` 99 | Timestamp int64 `json:"timestamp"` 100 | } 101 | payload.MarketId = marketId 102 | payload.Timestamp = time.Now().Unix() 103 | var exchange, routingKey string 104 | for _, w := range config.AllWorkers { 105 | if w.Name == "KLineWorker" { 106 | exchange = w.Exchange 107 | routingKey = w.RoutingKey 108 | } 109 | } 110 | for _, period := range []int64{1, 5, 15, 30, 60, 120, 240, 360, 720, 1440, 4320, 10080} { 111 | payload.Period = period 112 | b, err := json.Marshal(payload) 113 | if err != nil { 114 | fmt.Println("error:", err) 115 | } 116 | err = config.RabbitMqConnect.PublishMessageWithRouteKey(exchange, routingKey, "text/plain", false, false, &b, amqp.Table{}, amqp.Persistent, "") 117 | if err != nil { 118 | fmt.Println("{ error:", err, "}") 119 | return 120 | } 121 | } 122 | return 123 | } 124 | -------------------------------------------------------------------------------- /utils/config.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "github.com/kylelemons/go-gypsy/yaml" 6 | "log" 7 | "strconv" 8 | "time" 9 | ) 10 | 11 | type ConfigEnv struct { 12 | configFile *yaml.File 13 | } 14 | 15 | func getDatabaseConfig() *ConfigEnv { 16 | return getConfig("database") 17 | } 18 | func getRedisConfig() *ConfigEnv { 19 | return getConfig("redis") 20 | } 21 | 22 | func getConfig(name string) *ConfigEnv { 23 | filePath := fmt.Sprintf("config/%s.yml", name) 24 | return NewEnv(filePath) 25 | } 26 | 27 | func NewEnv(configFile string) *ConfigEnv { 28 | env := &ConfigEnv{ 29 | configFile: yaml.ConfigFile(configFile), 30 | } 31 | if env.configFile == nil { 32 | panic("go-configenv failed to open configFile: " + configFile) 33 | } 34 | return env 35 | } 36 | 37 | func (env *ConfigEnv) Get(spec, defaultValue string) string { 38 | value, err := env.configFile.Get(spec) 39 | if err != nil { 40 | value = defaultValue 41 | } 42 | return value 43 | } 44 | 45 | func (env *ConfigEnv) GetInt(spec string, defaultValue int) int { 46 | str := env.Get(spec, "") 47 | if str == "" { 48 | return defaultValue 49 | } 50 | val, err := strconv.Atoi(str) 51 | if err != nil { 52 | log.Panic("go-configenv GetInt failed Atoi", spec, str) 53 | } 54 | return val 55 | } 56 | 57 | func (env *ConfigEnv) GetDuration(spec string, defaultValue string) time.Duration { 58 | str := env.Get(spec, "") 59 | if str == "" { 60 | str = defaultValue 61 | } 62 | duration, err := time.ParseDuration(str) 63 | if err != nil { 64 | log.Panic("go-configenv GetDuration failed ParseDuration", spec, str) 65 | } 66 | return duration 67 | } 68 | -------------------------------------------------------------------------------- /utils/gorm.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | _ "github.com/go-sql-driver/mysql" 10 | "github.com/jinzhu/gorm" 11 | envConfig "github.com/oldfritter/goDCE/config" 12 | ) 13 | 14 | var ( 15 | MainDb, BackupDb *gorm.DB 16 | ) 17 | 18 | type GormDB struct { 19 | *gorm.DB 20 | gdbDone bool 21 | } 22 | 23 | func (c *GormDB) DbCommit() { 24 | if c.gdbDone { 25 | return 26 | } 27 | tx := c.Commit() 28 | c.gdbDone = true 29 | if err := tx.Error; err != nil && err != sql.ErrTxDone { 30 | panic(err) 31 | } 32 | } 33 | 34 | func (c *GormDB) DbRollback() { 35 | if c.gdbDone { 36 | return 37 | } 38 | tx := c.Rollback() 39 | c.gdbDone = true 40 | if err := tx.Error; err != nil && err != sql.ErrTxDone { 41 | panic(err) 42 | } 43 | } 44 | 45 | func getConnectionString(config *ConfigEnv, name string) string { 46 | host := config.Get(envConfig.CurrentEnv.Model+"."+name+".host", "") 47 | port := config.Get(envConfig.CurrentEnv.Model+"."+name+".port", "3306") 48 | user := config.Get(envConfig.CurrentEnv.Model+"."+name+".username", "") 49 | pass := config.Get(envConfig.CurrentEnv.Model+"."+name+".password", "") 50 | dbname := config.Get(envConfig.CurrentEnv.Model+"."+name+".database", "") 51 | protocol := config.Get(envConfig.CurrentEnv.Model+"."+name+".protocol", "tcp") 52 | dbargs := config.Get(envConfig.CurrentEnv.Model+"."+name+".dbargs", " ") 53 | if strings.Trim(dbargs, " ") != "" { 54 | dbargs = "?" + dbargs 55 | } else { 56 | dbargs = "" 57 | } 58 | return fmt.Sprintf("%s:%s@%s([%s]:%s)/%s%s", user, pass, protocol, host, port, dbname, dbargs) 59 | } 60 | 61 | func InitBackupDB() { 62 | config := getDatabaseConfig() 63 | var connstring string 64 | connstring = getConnectionString(config, "backup") 65 | db, err := gorm.Open("mysql", connstring) 66 | if err != nil { 67 | panic(err) 68 | } 69 | db.DB().SetMaxIdleConns(config.GetInt(envConfig.CurrentEnv.Model+".backup.pool", 5)) 70 | db.DB().SetMaxOpenConns(config.GetInt(envConfig.CurrentEnv.Model+".backup.maxopen", 0)) 71 | du, _ := time.ParseDuration(config.Get(envConfig.CurrentEnv.Model+".backup.timeout", "3600") + "s") 72 | db.DB().SetConnMaxLifetime(du) 73 | db.Exec("set transaction isolation level repeatable read") 74 | BackupDb = db 75 | } 76 | 77 | func CloseBackupDB() { 78 | BackupDb.Close() 79 | } 80 | 81 | func BackupDbBegin() *GormDB { 82 | txn := BackupDb.Begin() 83 | if txn.Error != nil { 84 | panic(txn.Error) 85 | } 86 | return &GormDB{txn, false} 87 | } 88 | 89 | func InitMainDB() { 90 | config := getDatabaseConfig() 91 | var connstring string 92 | connstring = getConnectionString(config, "main") 93 | db, err := gorm.Open("mysql", connstring) 94 | if err != nil { 95 | panic(err) 96 | } 97 | db.DB().SetMaxIdleConns(config.GetInt(envConfig.CurrentEnv.Model+".main.pool", 5)) 98 | db.DB().SetMaxOpenConns(config.GetInt(envConfig.CurrentEnv.Model+".main.maxopen", 0)) 99 | du, _ := time.ParseDuration(config.Get(envConfig.CurrentEnv.Model+".main.timeout", "3600") + "s") 100 | db.DB().SetConnMaxLifetime(du) 101 | db.Exec("set transaction isolation level repeatable read") 102 | MainDb = db 103 | } 104 | 105 | func CloseMainDB() { 106 | MainDb.Close() 107 | } 108 | 109 | func MainDbBegin() *GormDB { 110 | txn := MainDb.Begin() 111 | if txn.Error != nil { 112 | panic(txn.Error) 113 | } 114 | return &GormDB{txn, false} 115 | } 116 | -------------------------------------------------------------------------------- /utils/helper.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/labstack/echo" 5 | "strings" 6 | ) 7 | 8 | func GetRealIp(context echo.Context)(ip string) { 9 | ips := context.RealIP() 10 | //some forwarded_ips like 223.104.64.228,183.240.52.39, 172.68.254.56, sduppid operators 11 | rips := strings.Split(ips, ",") 12 | ip = rips[0] 13 | return 14 | } -------------------------------------------------------------------------------- /utils/randString.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/rand" 5 | "math/big" 6 | ) 7 | 8 | func RandStringRunes(n int) string { 9 | var letterRunes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890") 10 | b := make([]rune, n) 11 | for i := range b { 12 | index, _ := rand.Int(rand.Reader, big.NewInt(51)) 13 | b[i] = letterRunes[index.Int64()] 14 | } 15 | return string(b) 16 | } 17 | -------------------------------------------------------------------------------- /utils/redis.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/gomodule/redigo/redis" 5 | "time" 6 | "fmt" 7 | ) 8 | 9 | var ( 10 | RailsCachePool *redis.Pool 11 | DatePool *redis.Pool 12 | TickerPool *redis.Pool 13 | KLinePool *redis.Pool 14 | LimitPool *redis.Pool 15 | ) 16 | 17 | func InitRedisPools() { 18 | RailsCachePool = newRedisPool("cache") 19 | DatePool = newRedisPool("data") 20 | TickerPool = newRedisPool("ticker") 21 | KLinePool = newRedisPool("k") 22 | LimitPool = newRedisPool("limit") 23 | } 24 | 25 | func CloseRedisPools() { 26 | RailsCachePool.Close() 27 | DatePool.Close() 28 | TickerPool.Close() 29 | KLinePool.Close() 30 | LimitPool.Close() 31 | } 32 | 33 | func GetRedisConn(redisName string) redis.Conn { 34 | if redisName == "cache" { 35 | return RailsCachePool.Get() 36 | } else if redisName == "data" { 37 | return DatePool.Get() 38 | } else if redisName == "ticker" { 39 | return TickerPool.Get() 40 | } else if redisName == "k" { 41 | return KLinePool.Get() 42 | } else if redisName == "limit" { 43 | return LimitPool.Get() 44 | } 45 | return nil 46 | } 47 | 48 | func newRedisPool(redisName string) *redis.Pool { 49 | config := getRedisConfig() 50 | capacity := config.GetInt(redisName+".pool", 10) 51 | maxCapacity := config.GetInt(redisName+".maxopen", 0) 52 | idleTimout := config.GetDuration(redisName+".timeout", "4m") 53 | maxConnLifetime := config.GetDuration(redisName+".life_time", "2m") 54 | network := config.Get(redisName+".network", "tcp") 55 | server := config.Get(redisName+".server", "localhost:6379") 56 | db := config.Get(redisName+".db", "") 57 | password := config.Get(redisName+".password", "") 58 | 59 | return &redis.Pool{ 60 | MaxIdle: capacity, 61 | MaxActive: maxCapacity, 62 | IdleTimeout: idleTimout, 63 | MaxConnLifetime: maxConnLifetime, 64 | Wait: true, 65 | Dial: func() (redis.Conn, error) { 66 | conn, err := redis.Dial(network, server) 67 | if err != nil { 68 | fmt.Println("redis can't dial:" + err.Error()) 69 | return nil, err 70 | } 71 | 72 | if password != "" { 73 | _, err := conn.Do("AUTH", password) 74 | if err != nil { 75 | fmt.Println("redis can't AUTH:" + err.Error()) 76 | conn.Close() 77 | return nil, err 78 | } 79 | } 80 | 81 | if db != "" { 82 | _, err := conn.Do("SELECT", db) 83 | if err != nil { 84 | fmt.Println("redis can't SELECT:" + err.Error()) 85 | conn.Close() 86 | return nil, err 87 | } 88 | } 89 | return conn, err 90 | }, 91 | TestOnBorrow: func(c redis.Conn, t time.Time) error { 92 | _, err := c.Do("PING") 93 | if err != nil { 94 | fmt.Println("redis can't ping, err:" + err.Error()) 95 | } 96 | return err 97 | }, 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /utils/response.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Response struct { 8 | Head map[string]string `json:"head"` 9 | Body interface{} `json:"body"` 10 | } 11 | 12 | type ArrayBodyStruct struct { 13 | CurrentPage int `json:"current_page"` 14 | TotalPages int `json:"total_pages"` 15 | PerPage int `json:"per_page"` 16 | NextPage int `json:"next_page"` 17 | PreviousPage int `json:"previous_page"` 18 | Data interface{} `json:"data"` 19 | } 20 | 21 | type ArrayDataResponse struct { 22 | Head map[string]string `json:"head"` 23 | Body interface{} `json:"body"` 24 | } 25 | 26 | var ( 27 | SuccessResponse = Response{Head: map[string]string{"code": "1000", "msg": "Success."}} 28 | ArrayResponse = ArrayDataResponse{Head: map[string]string{"code": "1000", "msg": "Success."}} 29 | ) 30 | 31 | func BuildError(code string) Response { 32 | return Response{Head: map[string]string{"code": code}} 33 | } 34 | 35 | func (errorResponse Response) Error() string { 36 | return fmt.Sprintf("code: %s; msg: %s", errorResponse.Head["code"], errorResponse.Head["msg"]) 37 | } 38 | 39 | func (arrayResponse *ArrayDataResponse) Init(data interface{}, page, count, per_page int) { 40 | total_page := count / per_page 41 | if (count % per_page) != 0 { 42 | total_page += 1 43 | } 44 | 45 | nextPage := page + 1 46 | if nextPage > total_page { 47 | nextPage = total_page 48 | } 49 | previousPage := page - 1 50 | if previousPage < 1 { 51 | previousPage = 1 52 | } 53 | 54 | body := ArrayBodyStruct{} 55 | 56 | body.Data = data 57 | body.CurrentPage = page 58 | body.TotalPages = total_page 59 | body.PerPage = per_page 60 | body.NextPage = nextPage 61 | body.PreviousPage = previousPage 62 | 63 | arrayResponse.Body = body 64 | } 65 | -------------------------------------------------------------------------------- /utils/uploadToQiniu.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "path/filepath" 8 | 9 | "github.com/qiniu/api.v7/v7/auth/qbox" 10 | "github.com/qiniu/api.v7/v7/storage" 11 | "gopkg.in/yaml.v2" 12 | ) 13 | 14 | var QiniuConfig map[string]string 15 | 16 | type MyPutRet struct { 17 | Key string 18 | Hash string 19 | Fsize int 20 | Bucket string 21 | Name string 22 | } 23 | 24 | func InitQiniuConfig() { 25 | path_str, _ := filepath.Abs("config/qiniu.yml") 26 | content, err := ioutil.ReadFile(path_str) 27 | if err != nil { 28 | fmt.Printf("error (%v)", err) 29 | return 30 | } 31 | yaml.Unmarshal(content, &QiniuConfig) 32 | } 33 | 34 | func UploadFileToQiniu(bucket, key, filePath string) error { 35 | putPolicy := storage.PutPolicy{ 36 | Scope: fmt.Sprintf("%s:%s", bucket, key), 37 | ReturnBody: `{"key":"$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)","name":"$(x:name)"}`, 38 | } 39 | mac := qbox.NewMac(QiniuConfig["access_key"], QiniuConfig["secret_key"]) 40 | upToken := putPolicy.UploadToken(mac) 41 | cfg := storage.Config{} 42 | formUploader := storage.NewFormUploader(&cfg) 43 | ret := MyPutRet{} 44 | putExtra := storage.PutExtra{ 45 | Params: map[string]string{ 46 | "x:name": "panama logo", 47 | }, 48 | } 49 | err := formUploader.PutFile(context.Background(), &ret, upToken, key, filePath, &putExtra) 50 | if err != nil { 51 | return err 52 | } 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /utils/uploadToS3.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/aws/aws-sdk-go/aws" 12 | "github.com/aws/aws-sdk-go/aws/credentials" 13 | "github.com/aws/aws-sdk-go/aws/session" 14 | "github.com/aws/aws-sdk-go/service/s3" 15 | "gopkg.in/yaml.v2" 16 | ) 17 | 18 | var S3Config map[string]string 19 | 20 | func InitAwsS3Config() { 21 | path_str, _ := filepath.Abs("config/aws_s3.yml") 22 | content, err := ioutil.ReadFile(path_str) 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | yaml.Unmarshal(content, &S3Config) 27 | } 28 | 29 | // AddFileToS3 will upload a single file to S3, it will require a pre-built aws session 30 | // and will set file info like content type and encryption on the uploaded file. 31 | func AddFileToS3(s *session.Session, bucket, key, filePath string) error { 32 | 33 | // Open the file for use 34 | file, err := os.Open(filePath) 35 | if err != nil { 36 | return err 37 | } 38 | defer file.Close() 39 | 40 | // Get file size and read the file content into a buffer 41 | fileInfo, _ := file.Stat() 42 | var size int64 = fileInfo.Size() 43 | buffer := make([]byte, size) 44 | file.Read(buffer) 45 | 46 | // Config settings: this is where you choose the bucket, filename, content-type etc. 47 | // of the file you're uploading. 48 | _, err = s3.New(s).PutObject(&s3.PutObjectInput{ 49 | Bucket: aws.String(bucket), 50 | Key: aws.String(key), 51 | ACL: aws.String("public-read"), 52 | Body: bytes.NewReader(buffer), 53 | ContentLength: aws.Int64(size), 54 | ContentType: aws.String(http.DetectContentType(buffer)), 55 | // ContentDisposition: aws.String("attachment"), 56 | // ServerSideEncryption: aws.String("AES256"), 57 | }) 58 | if err != nil { 59 | return err 60 | } else { 61 | return nil 62 | } 63 | } 64 | 65 | func UploadFileToS3(bucket, key, filePath string) error { 66 | s, err := session.NewSession(&aws.Config{ 67 | Region: aws.String(S3Config["AWS_REGION"]), 68 | Credentials: credentials.NewStaticCredentials( 69 | S3Config["AWS_ACCESS_KEY_ID"], // id 70 | S3Config["AWS_SECRET_ACCESS_KEY"], // secret 71 | ""), // token can be left blank for now 72 | }) 73 | if err != nil { 74 | return err 75 | } 76 | return AddFileToS3(s, bucket, key, filePath) 77 | } 78 | -------------------------------------------------------------------------------- /workers/sneakerWorkers/accountVersionWorker.go: -------------------------------------------------------------------------------- 1 | package sneakerWorkers 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/oldfritter/goDCE/utils" 7 | sneaker "github.com/oldfritter/sneaker-go/v3" 8 | "github.com/shopspring/decimal" 9 | 10 | "github.com/oldfritter/goDCE/config" 11 | . "github.com/oldfritter/goDCE/models" 12 | ) 13 | 14 | func InitializeAccountVersionCheckPointWorker() { 15 | for _, w := range config.AllWorkers { 16 | if w.Name == "AccountVersionCheckPointWorker" { 17 | config.AllWorkerIs = append(config.AllWorkerIs, &AccountVersionCheckPointWorker{w}) 18 | return 19 | } 20 | } 21 | } 22 | 23 | type AccountVersionCheckPointWorker struct { 24 | sneaker.Worker 25 | } 26 | 27 | func (worker *AccountVersionCheckPointWorker) Work(payloadJson *[]byte) (err error) { 28 | var payload struct { 29 | AccountId string `json:"account_id"` 30 | } 31 | json.Unmarshal([]byte(*payloadJson), &payload) 32 | 33 | db := utils.MainDb 34 | var account Account 35 | if db.Where("id = ?", payload.AccountId).First(&account).RecordNotFound() { 36 | return 37 | } 38 | fixAccountVersions(account.Id, 200) 39 | return 40 | } 41 | 42 | func fixAccountVersions(accountId, limit int) { 43 | db := utils.MainDbBegin() 44 | defer db.DbRollback() 45 | var point AccountVersionCheckPoint 46 | version := 0 47 | sum, _ := decimal.NewFromString("0") 48 | if !db.Where("account_id = ?", accountId).First(&point).RecordNotFound() { 49 | version = point.AccountVersionId 50 | sum = point.Balance 51 | } 52 | var accountVersions []AccountVersion 53 | if db.Order("id ASC").Where("id > ?", version).Where("account_id = ?", accountId).Limit(limit).Find(&accountVersions).RecordNotFound() { 54 | return 55 | } 56 | for _, av := range accountVersions { 57 | point.AccountVersionId = av.Id 58 | sum = sum.Add(av.Balance).Add(av.Locked) 59 | if sum != av.Amount { 60 | point.Fixed = "unfixed" 61 | point.FixedNum = point.FixedNum.Add(av.Amount.Sub(sum)) 62 | db.Save(&point) 63 | db.DbCommit() 64 | return 65 | } 66 | } 67 | if point.Fixed == "" { 68 | point.Fixed = "nomal" 69 | } 70 | db.Save(&point) 71 | db.DbCommit() 72 | if len(accountVersions) == limit { 73 | fixAccountVersions(accountId, limit) 74 | } 75 | return 76 | } 77 | -------------------------------------------------------------------------------- /workers/sneakerWorkers/kLineWorker.go: -------------------------------------------------------------------------------- 1 | package sneakerWorkers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/gomodule/redigo/redis" 9 | sneaker "github.com/oldfritter/sneaker-go/v3" 10 | "github.com/shopspring/decimal" 11 | 12 | "github.com/oldfritter/goDCE/config" 13 | . "github.com/oldfritter/goDCE/models" 14 | "github.com/oldfritter/goDCE/utils" 15 | ) 16 | 17 | func InitializeKLineWorker() { 18 | for _, w := range config.AllWorkers { 19 | if w.Name == "KLineWorker" { 20 | config.AllWorkerIs = append(config.AllWorkerIs, &KLineWorker{w}) 21 | return 22 | } 23 | } 24 | } 25 | 26 | type KLineWorker struct { 27 | sneaker.Worker 28 | } 29 | 30 | func (worker *KLineWorker) Work(payloadJson *[]byte) (err error) { 31 | start := time.Now().UnixNano() 32 | var payload struct { 33 | MarketId int `json:"market_id"` 34 | Timestamp int64 `json:"timestamp"` 35 | Period int64 `json:"period"` 36 | DataSource string `json:"data_source"` 37 | } 38 | json.Unmarshal([]byte(*payloadJson), &payload) 39 | if payload.Period == 0 { 40 | fmt.Println("INFO--KLineWorker payload: ", payload) 41 | return 42 | } 43 | timestamp := payload.Timestamp 44 | begin := timestamp / (payload.Period * 60) * (payload.Period * 60) 45 | end := timestamp/(payload.Period*60)*(payload.Period*60) + payload.Period*60 46 | createPoint(payload.MarketId, payload.Period, begin, end, payload.DataSource) 47 | fmt.Println("INFO--KLineWorker payload: ", payload, ", time:", (time.Now().UnixNano()-start)/1000000, " ms") 48 | return 49 | } 50 | 51 | func createPoint(marketId int, period, begin, end int64, dataSource string) { 52 | if dataSource == "redis" { 53 | synFromRedis(marketId, period, begin, end) 54 | } else { 55 | if period == 1 { 56 | calculateInDB(marketId, period, begin, end) 57 | } else { 58 | calculateInBackupDB(marketId, period, begin, end) 59 | } 60 | } 61 | } 62 | 63 | func calculateInDB(marketId int, period, begin, end int64) (k KLine) { 64 | mainDB := utils.MainDbBegin() 65 | defer mainDB.DbRollback() 66 | 67 | backupDB := utils.BackupDbBegin() 68 | defer backupDB.DbRollback() 69 | if backupDB.Where("market_id = ?", marketId).Where("period = ?", period).Where("timestamp = ?", begin).First(&k).RecordNotFound() { 70 | k.MarketId = marketId 71 | k.Period = int(period) 72 | k.Timestamp = begin 73 | } 74 | 75 | mainDB.Model(Trade{}).Where("market_id = ?", marketId).Where("? <= created_at AND created_at < ?", time.Unix(begin, 0), time.Unix(end, 0)).Limit(1).Select("price as open").Scan(&k) 76 | if k.Open.Equal(decimal.Zero) { 77 | mainDB.Model(Trade{}).Order("created_at DESC").Where("market_id = ?", marketId).Where("created_at < ?", time.Unix(begin, 0)).Limit(1).Select("price as open").Scan(&k) 78 | var latestK KLine 79 | backupDB.Order("timestamp DESC").Where("market_id = ?", marketId).Where("period = ?", period).First(&latestK) 80 | k.High = latestK.High 81 | k.Low = latestK.Low 82 | k.Close = latestK.Close 83 | k.Volume = decimal.Zero 84 | } else { 85 | mainDB.Model(Trade{}).Where("market_id = ?", marketId).Where("? <= created_at AND created_at < ?", time.Unix(begin, 0), time.Unix(end, 0)).Select("max(price) as high").Scan(&k) 86 | mainDB.Model(Trade{}).Where("market_id = ?", marketId).Where("? <= created_at AND created_at < ?", time.Unix(begin, 0), time.Unix(end, 0)).Select("min(price) as low").Scan(&k) 87 | mainDB.Model(Trade{}).Order("created_at DESC").Where("market_id = ?", marketId).Where("? <= created_at AND created_at < ?", time.Unix(begin, 0), time.Unix(end, 0)).Limit(1).Select("price as close").Scan(&k) 88 | mainDB.Model(Trade{}).Where("market_id = ?", marketId).Where("? <= created_at AND created_at < ?", time.Unix(begin, 0), time.Unix(end, 0)).Select("sum(volume) as volume").Scan(&k) 89 | } 90 | backupDB.Save(&k) 91 | backupDB.DbCommit() 92 | synToRedis(&k) 93 | return 94 | } 95 | 96 | func calculateInBackupDB(marketId int, period, begin, end int64) (k KLine) { 97 | backupDB := utils.BackupDbBegin() 98 | defer backupDB.DbRollback() 99 | if backupDB.Where("market_id = ?", marketId).Where("period = ?", period).Where("timestamp = ?", begin).First(&k).RecordNotFound() { 100 | k.MarketId = marketId 101 | k.Period = int(period) 102 | k.Timestamp = begin 103 | } 104 | periods := []int64{1, 5, 15, 30, 60, 120, 240, 360, 720, 1440, 4320, 10080} 105 | lastPeriod := periods[0] 106 | for _, p := range periods { 107 | lastPeriod = p 108 | if p >= period || lastPeriod == 120 { 109 | break 110 | } 111 | } 112 | backupDB.Model(KLine{}).Where("market_id = ?", marketId).Where("period = ?", lastPeriod).Where("? <= timestamp AND timestamp < ?", begin, end).Limit(1).Select("open as open").Scan(&k) 113 | if k.Open.Equal(decimal.Zero) { 114 | backupDB.Model(KLine{}).Where("market_id = ?", marketId).Where("timestamp < ?", begin).Limit(1).Select("close as open").Scan(&k) 115 | } 116 | backupDB.Model(KLine{}).Where("market_id = ?", marketId).Where("period = ?", lastPeriod).Where("? <= timestamp AND timestamp < ?", begin, end).Select("max(high) as high").Scan(&k) 117 | backupDB.Model(KLine{}).Where("market_id = ?", marketId).Where("period = ?", lastPeriod).Where("? <= timestamp AND timestamp < ?", begin, end).Select("min(low) as low").Scan(&k) 118 | backupDB.Model(KLine{}).Where("market_id = ?", marketId).Where("period = ?", lastPeriod).Where("? <= timestamp AND timestamp < ?", begin, end).Select("sum(volume) as volume").Scan(&k) 119 | backupDB.Model(KLine{}).Where("market_id = ?", marketId).Where("period = ?", lastPeriod).Where("? <= timestamp AND timestamp < ?", begin, end).Order("timestamp DESC").Limit(1).Select("close as close").Scan(&k) 120 | backupDB.Save(&k) 121 | backupDB.DbCommit() 122 | synToRedis(&k) 123 | return 124 | } 125 | 126 | func synFromRedis(marketId int, period, begin, end int64) (k KLine) { 127 | market, _ := FindMarketById(marketId) 128 | backupDB := utils.BackupDbBegin() 129 | defer backupDB.DbRollback() 130 | if backupDB.Where("market_id = ?", marketId).Where("period = ?", period).Where("timestamp = ?", begin).First(&k).RecordNotFound() { 131 | k.MarketId = marketId 132 | k.Period = int(period) 133 | k.Timestamp = begin 134 | } 135 | kRedis := utils.GetRedisConn("k") 136 | defer kRedis.Close() 137 | key := fmt.Sprintf("peatio:%v:k:%v", market.Id, period) 138 | value, _ := redis.String(kRedis.Do("LINDEX", key, 0)) 139 | var item [6]decimal.Decimal 140 | json.Unmarshal([]byte(value), &item) 141 | offset := (begin - item[0].IntPart()) / 60 / int64(period) 142 | if offset < 0 { 143 | offset = 0 144 | } 145 | values, err := redis.Values(kRedis.Do("LRANGE", key, offset, offset+2-1)) 146 | if err != nil { 147 | fmt.Println("lrange err", err.Error()) 148 | return 149 | } 150 | for _, v := range values { 151 | json.Unmarshal(v.([]byte), &item) 152 | if item[0].IntPart() == begin { 153 | k.Timestamp = begin 154 | k.Open = item[1] 155 | k.High = item[2] 156 | k.Low = item[3] 157 | k.Close = item[4] 158 | k.Volume = item[5] 159 | } 160 | } 161 | if k.Open.IsZero() && k.High.IsZero() && k.Low.IsZero() && k.Close.IsZero() && k.Volume.IsZero() { 162 | backupDB.DbRollback() 163 | } else { 164 | backupDB.Save(&k) 165 | backupDB.DbCommit() 166 | } 167 | return 168 | } 169 | 170 | func synToRedis(k *KLine) { 171 | kRedis := utils.GetRedisConn("k") 172 | defer kRedis.Close() 173 | 174 | b, _ := json.Marshal((*k).Data()) 175 | kRedis.Send("ZREMRANGEBYSCORE", (*k).RedisKey(), (*k).Timestamp) 176 | kRedis.Do("ZADD", k.RedisKey(), (*k).Timestamp, string(b)) 177 | 178 | } 179 | -------------------------------------------------------------------------------- /workers/sneakerWorkers/rebuildkLineToRedisWorker.go: -------------------------------------------------------------------------------- 1 | package sneakerWorkers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | sneaker "github.com/oldfritter/sneaker-go/v3" 8 | 9 | "github.com/oldfritter/goDCE/config" 10 | . "github.com/oldfritter/goDCE/models" 11 | "github.com/oldfritter/goDCE/utils" 12 | ) 13 | 14 | func InitializeRebuildKLineToRedisWorker() { 15 | for _, w := range config.AllWorkers { 16 | if w.Name == "RebuildKLineToRedisWorker" { 17 | config.AllWorkerIs = append(config.AllWorkerIs, &RebuildKLineToRedisWorker{w}) 18 | return 19 | } 20 | } 21 | } 22 | 23 | type RebuildKLineToRedisWorker struct { 24 | sneaker.Worker 25 | } 26 | 27 | func (worker *RebuildKLineToRedisWorker) Work(payloadJson *[]byte) (err error) { 28 | var payload struct { 29 | MarketId int `json:"market_id"` 30 | Period int `json:"period"` 31 | } 32 | json.Unmarshal([]byte(*payloadJson), &payload) 33 | mainDB := utils.MainDbBegin() 34 | defer mainDB.DbRollback() 35 | var ks []KLine 36 | if mainDB.Where("market_id = ?", payload.MarketId).Where("period = ?", payload.Period).Find(&ks).RecordNotFound() { 37 | return 38 | } 39 | mainDB.DbRollback() 40 | kRedis := utils.GetRedisConn("k") 41 | defer kRedis.Close() 42 | for i, k := range ks { 43 | b, _ := json.Marshal(k.Data()) 44 | kRedis.Send("ZREMRANGEBYSCORE", k.RedisKey(), k.Timestamp) 45 | kRedis.Send("ZADD", k.RedisKey(), k.Timestamp, string(b)) 46 | if i%10 == 9 { 47 | if _, err := kRedis.Do(""); err != nil { 48 | fmt.Println(err) 49 | } 50 | } 51 | } 52 | if _, err := kRedis.Do(""); err != nil { 53 | fmt.Println(err) 54 | } 55 | return 56 | } 57 | -------------------------------------------------------------------------------- /workers/sneakerWorkers/tickerWorker.go: -------------------------------------------------------------------------------- 1 | package sneakerWorkers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/gomodule/redigo/redis" 9 | sneaker "github.com/oldfritter/sneaker-go/v3" 10 | "github.com/streadway/amqp" 11 | 12 | "github.com/oldfritter/goDCE/config" 13 | . "github.com/oldfritter/goDCE/models" 14 | "github.com/oldfritter/goDCE/utils" 15 | ) 16 | 17 | func InitializeTickerWorker() { 18 | for _, w := range config.AllWorkers { 19 | if w.Name == "TickerWorker" { 20 | config.AllWorkerIs = append(config.AllWorkerIs, &TickerWorker{w}) 21 | return 22 | } 23 | } 24 | } 25 | 26 | type TickerWorker struct { 27 | sneaker.Worker 28 | } 29 | 30 | func (worker *TickerWorker) Work(payloadJson *[]byte) (err error) { 31 | var payload struct { 32 | MarketId int `json:"market_id"` 33 | } 34 | json.Unmarshal([]byte(*payloadJson), &payload) 35 | buildTicker(payload.MarketId) 36 | return 37 | } 38 | 39 | func buildTicker(marketId int) { 40 | market, err := FindMarketById(marketId) 41 | if err != nil { 42 | fmt.Println("error:", err) 43 | } 44 | ticker := refreshTicker(&market) 45 | t, err := json.Marshal(ticker) 46 | if err != nil { 47 | fmt.Println("error:", err) 48 | } 49 | err = config.RabbitMqConnect.PublishMessageWithRouteKey(config.AmqpGlobalConfig.Exchange["fanout"]["ticker"], "#", "text/plain", false, false, &t, amqp.Table{}, amqp.Persistent, "") 50 | if err != nil { 51 | fmt.Println("{ error:", err, "}") 52 | } 53 | tickerRedis := utils.GetRedisConn("ticker") 54 | defer tickerRedis.Close() 55 | tickerRedis.Do("SET", market.TickerRedisKey(), string(t)) 56 | } 57 | 58 | func refreshTicker(market *Market) (ticker Ticker) { 59 | klineRedis := utils.GetRedisConn("kline") 60 | defer klineRedis.Close() 61 | now := time.Now() 62 | ticker.MarketId = (*market).Id 63 | ticker.At = now.Unix() 64 | ticker.Name = (*market).Name 65 | kJsons, _ := redis.Values(klineRedis.Do("ZRANGEBYSCORE", (*market).KLineRedisKey(1), now.Add(-time.Hour*24).Unix(), now.Unix())) 66 | var k KLine 67 | for i, kJson := range kJsons { 68 | json.Unmarshal(kJson.([]byte), &k) 69 | if i == 0 { 70 | ticker.TickerAspect.Open = k.Open 71 | } 72 | ticker.TickerAspect.Last = k.Close 73 | if ticker.TickerAspect.Low.IsZero() || ticker.TickerAspect.Low.GreaterThan(k.Low) { 74 | ticker.TickerAspect.Low = k.Low 75 | } 76 | if ticker.TickerAspect.High.LessThan(k.High) { 77 | ticker.TickerAspect.High = k.High 78 | } 79 | ticker.TickerAspect.Volume = ticker.TickerAspect.Volume.Add(k.Volume) 80 | } 81 | return 82 | } 83 | -------------------------------------------------------------------------------- /workers/workers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "strconv" 9 | 10 | sneaker "github.com/oldfritter/sneaker-go/v3" 11 | 12 | envConfig "github.com/oldfritter/goDCE/config" 13 | "github.com/oldfritter/goDCE/initializers" 14 | "github.com/oldfritter/goDCE/models" 15 | "github.com/oldfritter/goDCE/utils" 16 | ) 17 | 18 | func main() { 19 | initialize() 20 | initWorkers() 21 | 22 | StartAllWorkers() 23 | 24 | quit := make(chan os.Signal) 25 | signal.Notify(quit, os.Interrupt) 26 | <-quit 27 | closeResource() 28 | } 29 | 30 | func initialize() { 31 | envConfig.InitEnv() 32 | utils.InitMainDB() 33 | utils.InitBackupDB() 34 | models.AutoMigrations() 35 | utils.InitRedisPools() 36 | initializers.InitializeAmqpConfig() 37 | initializers.LoadCacheData() 38 | initializers.InitializeAmqpConfig() 39 | 40 | setLog() 41 | err := ioutil.WriteFile("pids/workers.pid", []byte(strconv.Itoa(os.Getpid())), 0644) 42 | if err != nil { 43 | log.Println(err) 44 | } 45 | } 46 | 47 | func closeResource() { 48 | initializers.CloseAmqpConnection() 49 | utils.CloseRedisPools() 50 | utils.CloseMainDB() 51 | utils.CloseBackupDB() 52 | } 53 | 54 | func initWorkers() { 55 | initializers.InitWorkers() 56 | } 57 | 58 | func StartAllWorkers() { 59 | for _, w := range envConfig.AllWorkerIs { 60 | for i := 0; i < w.GetThreads(); i++ { 61 | go func(w sneaker.WorkerI) { 62 | w.InitLogger() 63 | sneaker.SubscribeMessageByQueue(envConfig.RabbitMqConnect.Connection, w, nil) 64 | }(w) 65 | } 66 | } 67 | } 68 | 69 | func setLog() { 70 | err := os.Mkdir("logs", 0755) 71 | if err != nil { 72 | if !os.IsExist(err) { 73 | log.Fatalf("create folder error: %v", err) 74 | } 75 | } 76 | 77 | file, err := os.OpenFile("logs/workers.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) 78 | if err != nil { 79 | log.Fatalf("open file error: %v", err) 80 | } 81 | log.SetOutput(file) 82 | 83 | } 84 | --------------------------------------------------------------------------------