├── .air.toml ├── .gitignore ├── .vscode ├── remote.launch.json └── settings.json ├── Dockerfile.alpine.base ├── Dockerfile.base ├── Dockerfile.dev ├── Dockerfile.prod ├── LICENSE ├── README.md ├── build ├── build.sh ├── deploy.sh └── initenv.sh ├── configs ├── config-mac.toml └── config.toml ├── db ├── club.go ├── common.go ├── const.go ├── consume.go ├── desk.go ├── history.go ├── logger.go ├── model.go ├── model │ ├── enum.go │ └── struct.go ├── online.go ├── order.go ├── third_account.go ├── trade.go ├── types.go ├── user.go └── views.go ├── docker-compose.dev.yaml ├── docker-compose.mysql.5.7.yaml ├── docker-compose.mysql.yaml ├── docker.md ├── docs ├── README.md ├── SQL.md └── images │ ├── 1.png │ ├── 10.png │ ├── 11.png │ ├── 12.png │ ├── 13.png │ ├── 14.png │ ├── 15.png │ ├── 16.png │ ├── 17.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ ├── 9.png │ ├── argocd.png │ ├── banner.png │ ├── devops1.png │ ├── devops2.png │ └── gitops.png ├── go.mod ├── go.sum ├── internal ├── game │ ├── club_manager.go │ ├── constants.go │ ├── crypto.go │ ├── crypto_test.go │ ├── desk.go │ ├── desk_manager.go │ ├── dice.go │ ├── dissolve_context.go │ ├── game.go │ ├── helper.go │ ├── history │ │ └── history.go │ ├── mahjong │ │ ├── README.md │ │ ├── algorithm.go │ │ ├── algorithm_test.go │ │ ├── base.go │ │ ├── base_test.go │ │ ├── heler.go │ │ ├── indexes.go │ │ ├── indexes_test.go │ │ ├── mahjong.go │ │ ├── mahjong_test.go │ │ ├── meta.go │ │ └── tile.go │ ├── manager.go │ ├── player.go │ ├── prepare_context.go │ ├── rest_api.go │ └── types.go └── web │ ├── api │ ├── desk.go │ ├── history.go │ ├── login.go │ ├── order.go │ └── provider │ │ └── wechat.go │ ├── gm.go │ ├── static │ └── update │ │ ├── v1.9.3.kl.2.patch │ │ ├── v1.9.3.kl.6.patch │ │ └── version.json │ ├── stats.go │ └── web.go ├── k8s-devops ├── README.md └── nanoserver │ ├── ingressroute-tcp.yaml │ ├── nanoserver-config.yaml │ ├── nanoserver │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ │ ├── NOTES.txt │ │ ├── _helpers.tpl │ │ ├── deployment.yaml │ │ ├── ingress.yaml │ │ ├── service.yaml │ │ ├── serviceaccount.yaml │ │ └── tests │ │ │ └── test-connection.yaml │ └── values.yaml │ └── values.yaml ├── mahjong.apk ├── main.go ├── media ├── wechat-group.jpg ├── wechat.jpg └── wechat.png ├── pkg ├── algoutil │ ├── algoutil.go │ ├── algoutil_test.go │ ├── crypt.go │ ├── params.go │ ├── params_test.go │ ├── password.go │ └── password_test.go ├── async │ └── async.go ├── constant │ └── const.go ├── crypto │ └── crypto.go ├── errutil │ ├── code.go │ └── errutil.go ├── room │ ├── room.go │ └── room_test.go ├── security │ ├── sql.go │ ├── validity.go │ └── validity_test.go ├── set │ └── set.go └── whitelist │ ├── white_list.go │ └── white_list_test.go ├── protocol ├── agent.go ├── apps.go ├── club.go ├── common.go ├── const.go ├── desk.go ├── history.go ├── login.go ├── order.go ├── req.go ├── route.go ├── stats.go ├── test.go ├── users.go └── web.go ├── screenshot ├── 1.png ├── 10.png ├── 11.png ├── 12.png ├── 13.png ├── 14.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png └── 9.png ├── tools ├── README.md └── sql2struct │ ├── dbr.exe │ ├── sql2struct.bat │ └── struct.xorm.kwx.tpl └── wx-bot.png /.air.toml: -------------------------------------------------------------------------------- 1 | # Config file for [Air](https://github.com/cosmtrek/air) in TOML format 2 | 3 | # Working directory 4 | # . or absolute path, please note that the directories following must be under root. 5 | root = "." 6 | tmp_dir = "tmp" 7 | 8 | [build] 9 | # Just plain old shell command. You could use `make` as well. 10 | cmd = "go build -o ./tmp/main ." 11 | # Binary file yields from `cmd`. 12 | bin = "tmp/main" 13 | # Customize binary. 14 | full_bin = "APP_ENV=dev APP_USER=air ./tmp/main" 15 | # Watch these filename extensions. 16 | include_ext = ["go", "tpl", "tmpl", "html"] 17 | # Ignore these filename extensions or directories. 18 | exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules"] 19 | # Watch these directories if you specified. 20 | include_dir = [] 21 | # Exclude files. 22 | exclude_file = [] 23 | # This log file places in your tmp_dir. 24 | log = "air.log" 25 | # It's not necessary to trigger build each time file changes if it's too frequent. 26 | delay = 1000 # ms 27 | # Stop running old binary when build errors occur. 28 | stop_on_error = true 29 | # Send Interrupt signal before killing process (windows does not support this feature) 30 | send_interrupt = false 31 | # Delay after sending Interrupt signal 32 | kill_delay = 500 # ms 33 | 34 | [log] 35 | # Show log time 36 | time = false 37 | 38 | [color] 39 | # Customize each part's color. If no color found, use the raw app log. 40 | main = "magenta" 41 | watcher = "cyan" 42 | build = "yellow" 43 | runner = "green" 44 | 45 | [misc] 46 | # Delete tmp directory on exit 47 | clean_on_exit = true 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*.iml 2 | .idea 3 | dist/ 4 | cmd/kwxd/configs/config.local.toml 5 | tmp/** 6 | .DS_Store -------------------------------------------------------------------------------- /.vscode/remote.launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "nanoserver", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "remote", 12 | "remotePath":"/workspace/app", 13 | "port": 2345, 14 | "program": "${workspaceFolder}", 15 | "env": { 16 | "GO111MODULE":"on" 17 | }, 18 | "args": [], 19 | "trace": "log", 20 | "showLog": true 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.formatTool": "goformat" 3 | } 4 | -------------------------------------------------------------------------------- /Dockerfile.alpine.base: -------------------------------------------------------------------------------- 1 | FROM alpine:3.12 2 | 3 | RUN addgroup -S app \ 4 | && adduser -S -g app app \ 5 | && apk --no-cache add \ 6 | ca-certificates curl netcat-openbsd 7 | 8 | RUN apk --update add tzdata \ 9 | && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 10 | && echo "Asia/Shanghai" > /etc/timezone \ 11 | && apk del tzdata -------------------------------------------------------------------------------- /Dockerfile.base: -------------------------------------------------------------------------------- 1 | FROM golang:1.14-alpine 2 | 3 | RUN go env -w GO111MODULE=on 4 | RUN go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct 5 | RUN mkdir -p /nanoserver/ 6 | 7 | WORKDIR /nanoserver 8 | COPY go.mod go.mod 9 | RUN go mod download -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM golang:1.14 2 | 3 | WORKDIR /workspace 4 | 5 | # 阿里云 6 | RUN go env -w GO111MODULE=on 7 | RUN go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct 8 | 9 | # debug 10 | RUN go get github.com/go-delve/delve/cmd/dlv 11 | 12 | # live reload 13 | RUN go get -u github.com/cosmtrek/air 14 | 15 | # copy modules manifests 16 | COPY go.mod go.mod 17 | COPY go.sum go.sum 18 | 19 | # cache modules 20 | RUN go mod download -------------------------------------------------------------------------------- /Dockerfile.prod: -------------------------------------------------------------------------------- 1 | ### nanoserver:base 2 | FROM hackerlinner/nanoserver:base as builder 3 | 4 | WORKDIR /nanoserver 5 | COPY . . 6 | RUN CGO_ENABLED=0 go build -a -o bin/nanoserver 7 | 8 | FROM hackerlinner/nanoserver-alpine:base 9 | 10 | LABEL maintainer="为少" 11 | WORKDIR /home/app 12 | COPY --from=builder /nanoserver/bin/nanoserver . 13 | COPY ./configs ./configs 14 | RUN chown -R app:app ./ 15 | USER app 16 | CMD ["./nanoserver"] 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Chris Lonng 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 | # 云原生项目实践DevOps(GitOps)+K8S+BPF+SRE,从0到1使用Golang开发生产级麻将游戏服务器 2 | 3 | ## 系列文章 4 | 1. [云原生项目实践 DevOps(GitOps)+K8S+BPF+SRE,从 0 到 1 使用 Golang 开发生产级麻将游戏服务器—第 1 篇](https://mp.weixin.qq.com/s/Jyq_A1vehrnMwv6AdOtQ1w) 5 | 2. [云原生项目实践 DevOps(GitOps)+K8S+BPF+SRE,从 0 到 1 使用 Golang 开发生产级麻将游戏服务器—第 2 篇](https://mp.weixin.qq.com/s/jnQaz08fAzQ3J7tZBdil4Q) 6 | 7 | 8 | ![](wx-bot.png) 9 | 10 | ### 本地开发 11 | 12 | ☁️ Live reload for Go apps 13 | 14 | ```sh 15 | go get -u github.com/cosmtrek/air 16 | 17 | air 18 | ``` 19 | 20 | ### 本地调试(Mac OS) 21 | 22 | VSCode-Go Debugging 23 | * [https://github.com/golang/vscode-go/blob/master/docs/debugging.md](https://github.com/golang/vscode-go/blob/master/docs/debugging.md) 24 | 25 | 注意看教程,非常简单。 -------------------------------------------------------------------------------- /build/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export GOPROXY=https://goproxy.io 4 | export GOOS=linux 5 | export GOARCH=amd64 6 | 7 | echo "=============================" 8 | echo "==== building" 9 | echo "=============================" 10 | go build -o mahjong 11 | 12 | if [[ $? -ne 0 ]] 13 | then 14 | echo "build failed" 15 | exit -1 16 | fi 17 | 18 | echo "=============================" 19 | echo "==== packaging" 20 | echo "=============================" 21 | tar -czf mahjong.tar.gz mahjong configs 22 | 23 | rm -rf dist 24 | mkdir -p dist 25 | mv mahjong.tar.gz dist/ 26 | 27 | echo "=============================" 28 | echo "==== clean" 29 | echo "=============================" 30 | rm -rf "mahjong" 31 | -------------------------------------------------------------------------------- /build/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | BASE=$(dirname $0)/.. 4 | 5 | $BASE/build/build.sh 6 | 7 | if [ $? -ne 0 ] 8 | then 9 | echo "build failed" 10 | exit -1 11 | fi 12 | 13 | REMOTE=YOUR_REMOTE_SERVER_IP 14 | 15 | echo "=============================" 16 | echo "==== deploy to remote server" 17 | echo "=============================" 18 | scp dist/mahjong.tar.gz root@$REMOTE:/opt/mahjong 19 | 20 | ssh root@$REMOTE <> install mysql" 10 | yum install -y mysql-server mysql 11 | service mysqld start 12 | 13 | echo "==>> install supervisord" 14 | yum install -y python-setuptools 15 | easy_install supervisor 16 | 17 | echo "==>> init supervisord config" 18 | mkdir -p /etc/supervisor.d/ 19 | echo_supervisord_conf > /etc/supervisord.conf 20 | echo "[include]" >> /etc/supervisord.conf 21 | echo "files = /etc/supervisor.d/*.conf" >> /etc/supervisord.conf 22 | 23 | echo "==>> prepare game logic server env" 24 | mkdir -p /opt/triple/ 25 | mkdir -p /opt/triple/logs 26 | 27 | echo "==>> supervisor configuration" 28 | echo "[program:triple]" >> /etc/supervisor.d/triple.conf 29 | echo "command=/opt/triple/tripled -c configs/config.prod.toml" >> /etc/supervisor.d/triple.conf 30 | echo "directory=/opt/triple" >> /etc/supervisor.d/triple.conf 31 | echo "stdout_logfile=/opt/triple/logs/triple.log" >> /etc/supervisor.d/triple.conf 32 | echo "stderr_logfile=/opt/triple/logs/triple.log" >> /etc/supervisor.d/triple.conf 33 | echo "stopsignal=INT" >> /etc/supervisor.d/triple.conf 34 | EOF 35 | 36 | echo "done" -------------------------------------------------------------------------------- /configs/config-mac.toml: -------------------------------------------------------------------------------- 1 | [core] 2 | # enable debug mode 3 | debug = true 4 | heartbeat = 30 5 | consume = "4/2,8/3,16/4" #房卡消耗, 使用逗号隔开, 局数/房卡数, 例如4局消耗1张, 8局消耗1张, 16局消耗2张, 则为: 4/1,8/1,16/2 6 | 7 | #WEB服务器设置 8 | [webserver] 9 | addr = "0.0.0.0:12307" #监听地址 10 | enable_ssl = false #是否使用https, 如果为true, 则必须配置cert和key的路径 11 | static_dir = "web/static" 12 | 13 | #证书设置 14 | [webserver.certificates] 15 | cert = "configs/****.crt" #证书路径 16 | key = "configs/****.key" #Key路径 17 | 18 | [game-server] 19 | host = "127.0.0.1" 20 | port = 33251 21 | 22 | # Redis server config 23 | [redis] 24 | host = "127.0.0.1" 25 | port = 6357 26 | 27 | # Mysql server config 28 | [database] 29 | host = "127.0.0.1" 30 | port = 3306 31 | dbname = "scmj" 32 | password = "123456" 33 | username = "root" 34 | args = "charset=utf8" 35 | buf_size = 10 36 | max_idle_conns = 20 37 | max_open_conns = 15 38 | show_sql = true 39 | 40 | # 微信 41 | [wechat] 42 | appid = "YOUR_WX_APPID" 43 | appsecret = "YOUR_APP_SECRET" 44 | callback_url = "YOUR_CALLBACK" 45 | mer_id = "YOUR_MER_ID" 46 | unify_order_url = "https://api.mch.weixin.qq.com/pay/unifiedorder" 47 | 48 | #Token设置 49 | [token] 50 | expires = 21600 #token过期时间 51 | 52 | #白名单设置 53 | [whitelist] 54 | ip = ["10.10.*", "127.0.0.1", ".*"] #白名单地址, 支持golang正则表达式语法 55 | 56 | #分享信息 57 | [share] 58 | title = "血战到底" 59 | desc = "纯正四川玩法,快捷便利的掌上血战,轻松组局,随时随地尽情游戏" 60 | 61 | #更新设置 62 | [update] 63 | force = true #是否强制更新 64 | version = "1.9.3" 65 | android = "https://fir.im/tand" 66 | ios = "https://fir.im/tios" 67 | 68 | #联系设置 69 | [contact] 70 | daili1 = "kefuweixin01" 71 | daili2 = "kefuweixin01" 72 | kefu1 = "kefuweixin01" 73 | 74 | #语音账号http://gcloud.qq.com/product/6 75 | [voice] 76 | appid = "xxx" 77 | appkey = "xxx" 78 | 79 | #广播消息 80 | [broadcast] 81 | message = ["系统消息:健康游戏,禁止赌博", "欢迎进入游戏"] 82 | 83 | #登陆相关 84 | [login] 85 | guest = true 86 | lists = ["test"] -------------------------------------------------------------------------------- /configs/config.toml: -------------------------------------------------------------------------------- 1 | [core] 2 | # enable debug mode 3 | debug = true 4 | heartbeat = 30 5 | consume = "4/2,8/3,16/4" #房卡消耗, 使用逗号隔开, 局数/房卡数, 例如4局消耗1张, 8局消耗1张, 16局消耗2张, 则为: 4/1,8/1,16/2 6 | 7 | #WEB服务器设置 8 | [webserver] 9 | addr = "0.0.0.0:12307" #监听地址 10 | enable_ssl = false #是否使用https, 如果为true, 则必须配置cert和key的路径 11 | static_dir = "web/static" 12 | 13 | #证书设置 14 | [webserver.certificates] 15 | cert = "configs/****.crt" #证书路径 16 | key = "configs/****.key" #Key路径 17 | 18 | [game-server] 19 | # host = "192.168.31.125" 20 | host = "10.10.19.204" 21 | port = 33251 22 | 23 | # Redis server config 24 | [redis] 25 | host = "127.0.0.1" 26 | port = 6357 27 | 28 | # Mysql server config 29 | [database] 30 | host = "127.0.0.1" 31 | port = 3306 32 | dbname = "scmj" 33 | password = "123456" 34 | username = "root" 35 | args = "charset=utf8mb4" 36 | buf_size = 10 37 | max_idle_conns = 20 38 | max_open_conns = 15 39 | show_sql = true 40 | 41 | # 微信 42 | [wechat] 43 | appid = "YOUR_WX_APPID" 44 | appsecret = "YOUR_APP_SECRET" 45 | callback_url = "YOUR_CALLBACK" 46 | mer_id = "YOUR_MER_ID" 47 | unify_order_url = "https://api.mch.weixin.qq.com/pay/unifiedorder" 48 | 49 | #Token设置 50 | [token] 51 | expires = 21600 #token过期时间 52 | 53 | #白名单设置 54 | [whitelist] 55 | ip = ["10.10.*", "127.0.0.1", ".*"] #白名单地址, 支持golang正则表达式语法 56 | 57 | #分享信息 58 | [share] 59 | title = "血战到底" 60 | desc = "纯正四川玩法,快捷便利的掌上血战,轻松组局,随时随地尽情游戏" 61 | 62 | #更新设置 63 | [update] 64 | force = true #是否强制更新 65 | version = "1.9.3" 66 | android = "https://fir.im/tand" 67 | ios = "https://fir.im/tios" 68 | 69 | #联系设置 70 | [contact] 71 | daili1 = "kefuweixin01" 72 | daili2 = "kefuweixin01" 73 | kefu1 = "kefuweixin01" 74 | 75 | #语音账号http://gcloud.qq.com/product/6 76 | [voice] 77 | appid = "xxx" 78 | appkey = "xxx" 79 | 80 | #广播消息 81 | [broadcast] 82 | message = ["系统消息:健康游戏,禁止赌博", "欢迎进入游戏"] 83 | 84 | #登陆相关 85 | [login] 86 | guest = true 87 | lists = ["test", "konglai"] 88 | -------------------------------------------------------------------------------- /db/club.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/lonng/nanoserver/db/model" 9 | ) 10 | 11 | func IsClubMember(clubId, uid int64) bool { 12 | uc := model.UserClub{ 13 | Uid: uid, 14 | ClubId: clubId, 15 | Status: model.UserClubStatusAgree, 16 | } 17 | 18 | has, err := database.Get(&uc) 19 | if err != nil { 20 | return false 21 | } 22 | return has 23 | } 24 | 25 | func IsBalanceEnough(clubId int64) bool { 26 | c := model.Club{ClubId: clubId} 27 | has, err := database.Get(&c) 28 | if err != nil { 29 | return false 30 | } 31 | if has == false { 32 | return false 33 | } 34 | return c.Balance > -100 35 | } 36 | 37 | func ApplyClub(uid, clubId int64) error { 38 | if clubId < 100000 || clubId >= 1000000 { 39 | return fmt.Errorf("俱乐部ID%d错误,请输入正确的俱乐部ID", clubId) 40 | } 41 | 42 | c := &model.Club{ClubId: clubId} 43 | ok, err := database.Get(c) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | if !ok { 49 | return fmt.Errorf("ID为%d的俱乐部不存在,请检查是否输入错误", clubId) 50 | } 51 | 52 | uc := &model.UserClub{ 53 | Uid: uid, 54 | ClubId: clubId, 55 | CreatedAt: time.Now().Unix(), 56 | } 57 | 58 | ok, err = database.Get(uc) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | if ok { 64 | if uc.Status == model.UserClubStatusAgree { 65 | return errors.New("你已加入该俱乐部,无需申请") 66 | } 67 | 68 | if uc.Status == model.UserClubStatusApply { 69 | return errors.New("你已申请加入该俱乐部,等待部长同意") 70 | } 71 | } 72 | 73 | uc.Status = model.UserClubStatusApply 74 | _, err = database.Insert(uc) 75 | return err 76 | } 77 | 78 | func ClubList(uid int64) ([]model.Club, error) { 79 | bean := &model.UserClub{ 80 | Uid: uid, 81 | Status: model.UserClubStatusAgree, 82 | } 83 | 84 | list := []model.UserClub{} 85 | if err := database.Find(&list, bean); err != nil { 86 | return nil, err 87 | } 88 | 89 | if len(list) < 1 { 90 | return []model.Club{}, nil 91 | } 92 | 93 | ids := []int64{} 94 | for i := range list { 95 | ids = append(ids, list[i].ClubId) 96 | } 97 | 98 | ret := []model.Club{} 99 | database.In("club_id", ids).Find(&ret) 100 | 101 | return ret, nil 102 | } 103 | 104 | func ClubLoseBalance(clubId, balance int64, consume *model.CardConsume) error { 105 | session := database.NewSession() 106 | defer session.Close() 107 | 108 | if err := session.Begin(); err != nil { 109 | return err 110 | } 111 | 112 | c := &model.Club{ClubId: clubId} 113 | has, err := session.Get(c) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | if !has { 119 | return fmt.Errorf("俱乐部不存在,ID=%d", clubId) 120 | } 121 | 122 | c.Balance -= balance 123 | 124 | //FIXED: 用户剩余1的时候, 扣除不成功 125 | if _, err := session.Cols("balance").Where("club_id=?", clubId).Update(c); err != nil { 126 | session.Rollback() 127 | return err 128 | } 129 | 130 | if _, err := session.Insert(consume); err != nil { 131 | session.Rollback() 132 | return err 133 | } 134 | 135 | return session.Commit() 136 | } 137 | -------------------------------------------------------------------------------- /db/common.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Option func(setting *Setting) 9 | type Closer func() 10 | 11 | type Setting struct { 12 | ShowSQL bool 13 | MaxOpenConns int 14 | MaxIdleConns int 15 | } 16 | 17 | func MaxIdleConnOption(i int) Option { 18 | return func(s *Setting) { 19 | s.MaxIdleConns = i 20 | } 21 | } 22 | 23 | func MaxOpenConnOption(i int) Option { 24 | return func(s *Setting) { 25 | s.MaxOpenConns = i 26 | } 27 | } 28 | 29 | func ShowSQLOption(show bool) Option { 30 | return func(s *Setting) { 31 | s.ShowSQL = show 32 | } 33 | } 34 | 35 | // Build data source name 36 | func BuildDSN(host string, port int, username, password, dbname, args string) string { 37 | return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?%s", username, password, host, port, dbname, args) 38 | } 39 | 40 | //// 根据一个大区信息, 返回大区的SQL条件语句 41 | //// 例如: area: {AreaID: 100, ServerIDs: []int{1, 2, 3, 5}} 42 | //// 返回: (`area_id`=100 AND `server_id` IN(1,2,3,5)) 43 | //func AreaCondition(area *protocol.Area) string { 44 | // if len(area.ServerIDs) > 0 { 45 | // servers := []string{} 46 | // for _, id := range area.ServerIDs { 47 | // servers = append(servers, strconv.Itoa(id)) 48 | // } 49 | // return fmt.Sprintf("(`area_id`=%d AND `server_id` IN(%s))", area.AreaID, strings.Join(servers, ",")) 50 | // } else { 51 | // return fmt.Sprintf("`area_id`=%d", area.AreaID) 52 | // } 53 | //} 54 | // 55 | //func AreaVectorCondition(areas []*protocol.Area) string { 56 | // conditions := []string{} 57 | // for _, area := range areas { 58 | // conditions = append(conditions, AreaCondition(area)) 59 | // } 60 | // return fmt.Sprintf("(%s)", strings.Join(conditions, " OR ")) 61 | //} 62 | 63 | // 给定列, 返回起始时间条件SQL语句, [begin, end) 64 | func RangeCondition(column string, begin, end int64) string { 65 | return fmt.Sprintf("(`%s` BETWEEN %d AND %d)", column, begin, end) 66 | } 67 | 68 | func ChannelCondition(c []string) string { 69 | return fmt.Sprintf("`channel` IN('%s')", strings.Join(c, "','")) 70 | } 71 | 72 | func EqIntCondition(col string, v int) string { 73 | return fmt.Sprintf("`%s`=%d", col, v) 74 | } 75 | 76 | func EqInt64Condition(col string, v int64) string { 77 | return fmt.Sprintf("`%s`=%d", col, v) 78 | } 79 | 80 | func LtInt64Condition(col string, v int64) string { 81 | return fmt.Sprintf("`%s`<%d", col, v) 82 | } 83 | 84 | func Combined(cond ...string) string { 85 | return strings.Join(cond, " AND ") 86 | } 87 | 88 | func Insert(bean interface{}) error { 89 | _, err := database.Insert(bean) 90 | return err 91 | } 92 | -------------------------------------------------------------------------------- /db/const.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | const ( 4 | KWX = "broker" 5 | ) 6 | 7 | const ( 8 | defaultMaxConns = 10 9 | ) 10 | 11 | // Users表中role字段的取值 12 | const ( 13 | RoleTypeAdmin = 1 //管理员账号 14 | RoleTypeThird = 2 //三方平台账号 15 | ) 16 | 17 | const ( 18 | OpActionRegister = 1 //注册 19 | OpActionFreezen = 2 //冻结账号 20 | OpActionUnFreezen = 3 //账号解冻 21 | OpActionDelete = 4 //账号删除 22 | ) 23 | 24 | const ( 25 | UserOffline = 1 //离线 26 | UserOnline = 2 //在线 27 | ) 28 | 29 | const ( 30 | StatusNormal = 1 //正常 31 | StatusDeleted = 2 //删除 32 | StatusFreezed = 3 //冻结 33 | StatusBound = 4 //绑定 34 | ) 35 | 36 | // 订单状态 37 | const ( 38 | OrderStatusCreated = 1 //创建 39 | OrderStatusPayed = 2 //完成 40 | OrderStatusNotified = 3 //已确认订单 41 | ) 42 | 43 | const ( 44 | OrderTypeUnknown = iota 45 | OrderTypeBuyToken //购买令牌 46 | OrderTypeConsumeToken //消费代币(eg:使用令牌购买游戏中的道具,比如房卡) 47 | OrderTypeConsume3rd //第三方支付平台消费(eg:直接使用alipay, wechat等购买游戏中的道具) 48 | OrderTypeTest //支付测试 49 | ) 50 | 51 | const ( 52 | NotifyResultSuccess = 1 //通知成功 53 | NotifyResultFailed = 2 //通知失败 54 | ) 55 | 56 | const ( 57 | dayInSecond = 24 * 60 * 60 58 | 59 | day1 = dayInSecond 60 | day2 = day1 * 2 61 | day3 = day1 * 3 62 | day7 = day1 * 7 63 | day14 = day1 * 14 64 | day30 = day1 * 30 65 | ) 66 | 67 | const ( 68 | RankingNormal = 1 69 | RankingDesc = 2 70 | ) 71 | 72 | const ( 73 | DefaultTopN = 10 74 | ) 75 | -------------------------------------------------------------------------------- /db/consume.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/lonng/nanoserver/db/model" 5 | "github.com/lonng/nanoserver/protocol" 6 | log "github.com/sirupsen/logrus" 7 | "strconv" 8 | "time" 9 | ) 10 | 11 | func InsertConsume(entity *model.CardConsume) error { 12 | _, err := database.Insert(entity) 13 | if err != nil { 14 | log.Error(err) 15 | } 16 | 17 | return err 18 | } 19 | 20 | //消耗统计 21 | func ConsumeStats(from, to int64) ([]*protocol.CardConsume, error) { 22 | fn := func(from, to int64) *protocol.CardConsume { 23 | mQuery, err := database.Query("SELECT SUM(card_count) AS cards FROM card_consume WHERE consume_at BETWEEN ? AND ?; ", 24 | from, 25 | to) 26 | 27 | cc := &protocol.CardConsume{ 28 | Date: from, 29 | } 30 | 31 | if len(mQuery) < 1 || err != nil { 32 | return cc 33 | } 34 | 35 | temp := string(mQuery[0]["cards"]) 36 | if temp != "" { 37 | cc.Value, err = strconv.ParseInt(temp, 10, 0) 38 | } 39 | 40 | return cc 41 | 42 | } 43 | begin := time.Unix(from, 0) 44 | 45 | var ret []*protocol.CardConsume 46 | 47 | t := time.Date(begin.Year(), begin.Month(), begin.Day(), 0, 0, 0, 0, time.Local) 48 | 49 | for i := t.Unix(); i < to; i += dayInSecond { 50 | cc := fn(i, i+dayInSecond-1) 51 | 52 | ret = append(ret, cc) 53 | } 54 | return ret, nil 55 | } 56 | -------------------------------------------------------------------------------- /db/desk.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/lonng/nanoserver/db/model" 5 | "github.com/lonng/nanoserver/pkg/errutil" 6 | ) 7 | 8 | func InsertDesk(h *model.Desk) error { 9 | if h == nil { 10 | return errutil.ErrInvalidParameter 11 | } 12 | _, err := database.Insert(h) 13 | if err != nil { 14 | return err 15 | } 16 | return nil 17 | } 18 | 19 | func UpdateDesk(d *model.Desk) error { 20 | _, err := database.Exec("UPDATE `desk` SET `score_change0` = ?, `score_change1` = ?, `score_change2` = ?, `score_change3` = ?, `round` = ? WHERE `id`= ? ", 21 | d.ScoreChange0, 22 | d.ScoreChange1, 23 | d.ScoreChange2, 24 | d.ScoreChange3, 25 | d.Round, 26 | d.Id) 27 | if err != nil { 28 | return err 29 | } 30 | return nil 31 | } 32 | 33 | func QueryDesk(id int64) (*model.Desk, error) { 34 | h := &model.Desk{Id: id} 35 | has, err := database.Get(h) 36 | if err != nil { 37 | return nil, err 38 | } 39 | if !has { 40 | return nil, errutil.ErrDeskNotFound 41 | } 42 | return h, nil 43 | } 44 | 45 | //指定的桌子是否存在 46 | func DeskNumberExists(no string) bool { 47 | d := &model.Desk{ 48 | DeskNo: no, 49 | } 50 | 51 | has, err := database.Get(d) 52 | if err != nil { 53 | return true 54 | } 55 | return has 56 | } 57 | 58 | func DeleteDesk(id int64) error { 59 | _, err := database.Delete(&model.Desk{Id: id}) 60 | return err 61 | } 62 | 63 | func DeskList(player int64) ([]model.Desk, int, error) { 64 | const ( 65 | limit = 15 66 | ) 67 | result := make([]model.Desk, 0) 68 | err := database.Where("(player0 = ? OR player1 = ? OR player2 = ? OR player3 = ? ) AND round > 0", 69 | player, player, player, player).Desc("created_at").Limit(limit, 0).Find(&result) 70 | 71 | if err != nil { 72 | return nil, 0, errutil.ErrDBOperation 73 | } 74 | return result, len(result), nil 75 | } 76 | -------------------------------------------------------------------------------- /db/history.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/lonng/nanoserver/pkg/errutil" 5 | log "github.com/sirupsen/logrus" 6 | 7 | "github.com/lonng/nanoserver/db/model" 8 | ) 9 | 10 | func InsertHistory(h *model.History) error { 11 | if h == nil { 12 | return errutil.ErrInvalidParameter 13 | } 14 | _, err := database.Insert(h) 15 | if err != nil { 16 | return errutil.ErrDBOperation 17 | } 18 | return nil 19 | } 20 | 21 | func QueryHistory(id int64) (*model.History, error) { 22 | h := &model.History{Id: id} 23 | has, err := database.Get(h) 24 | if err != nil { 25 | log.Error(err) 26 | return nil, err 27 | } 28 | if !has { 29 | return nil, errutil.ErrOrderNotFound 30 | } 31 | return h, nil 32 | } 33 | 34 | func DeleteHistory(id int64) error { 35 | _, err := database.Delete(&model.History{Id: id}) 36 | return err 37 | } 38 | 39 | func DeleteHistoriesByDeskID(deskId int64) error { 40 | _, err := database.Delete(&model.History{DeskId: deskId}) 41 | return err 42 | } 43 | 44 | func QueryHistoriesByDeskID(deskID int64) ([]model.History, int, error) { 45 | result := make([]model.History, 0) 46 | err := database.Where("desk_id=?", deskID).Asc("begin_at").Find(&result) 47 | if err != nil { 48 | log.Error(err) 49 | return nil, 0, errutil.ErrDBOperation 50 | } 51 | return result, len(result), nil 52 | } 53 | -------------------------------------------------------------------------------- /db/logger.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/go-xorm/core" 5 | log "github.com/sirupsen/logrus" 6 | ) 7 | 8 | type Logger struct { 9 | *log.Entry 10 | level core.LogLevel 11 | } 12 | 13 | func (l *Logger) SetLevel(level core.LogLevel) { 14 | l.level = level 15 | } 16 | 17 | func (l *Logger) Level() core.LogLevel { 18 | return l.level 19 | } 20 | 21 | func (l *Logger) ShowSQL(show ...bool) {} 22 | func (l *Logger) IsShowSQL() bool { return false } 23 | -------------------------------------------------------------------------------- /db/model.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/lonng/nanoserver/db/model" 7 | 8 | _ "github.com/go-sql-driver/mysql" 9 | "github.com/go-xorm/xorm" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | const asyncTaskBacklog = 128 14 | 15 | var ( 16 | database *xorm.Engine 17 | logger *log.Entry 18 | chWrite chan interface{} // async write channel 19 | chUpdate chan interface{} // async update channel 20 | ) 21 | 22 | type options struct { 23 | showSQL bool 24 | maxOpenConns int 25 | maxIdleConns int 26 | } 27 | 28 | // ModelOption specifies an option for dialing a xordefaultModel. 29 | type ModelOption func(*options) 30 | 31 | // MaxIdleConns specifies the max idle connect numbers. 32 | func MaxIdleConns(i int) ModelOption { 33 | return func(opts *options) { 34 | opts.maxIdleConns = i 35 | } 36 | } 37 | 38 | // MaxOpenConns specifies the max open connect numbers. 39 | func MaxOpenConns(i int) ModelOption { 40 | return func(opts *options) { 41 | opts.maxOpenConns = i 42 | } 43 | } 44 | 45 | // ShowSQL specifies the buffer size. 46 | func ShowSQL(show bool) ModelOption { 47 | return func(opts *options) { 48 | opts.showSQL = show 49 | } 50 | } 51 | 52 | func envInit() { 53 | // async task 54 | go func() { 55 | for { 56 | select { 57 | case t, ok := <-chWrite: 58 | if !ok { 59 | return 60 | } 61 | 62 | if _, err := database.Insert(t); err != nil { 63 | logger.Error(err) 64 | } 65 | 66 | case t, ok := <-chUpdate: 67 | if !ok { 68 | return 69 | } 70 | 71 | if _, err := database.Update(t); err != nil { 72 | logger.Error(err) 73 | } 74 | } 75 | } 76 | }() 77 | 78 | // 定时ping数据库, 保持连接池连接 79 | go func() { 80 | ticker := time.NewTicker(time.Minute * 5) 81 | for { 82 | select { 83 | case <-ticker.C: 84 | database.Ping() 85 | } 86 | } 87 | }() 88 | } 89 | 90 | //New create the database's connection 91 | func MustStartup(dsn string, opts ...ModelOption) func() { 92 | logger = log.WithField("component", "model") 93 | settings := &options{ 94 | maxIdleConns: defaultMaxConns, 95 | maxOpenConns: defaultMaxConns, 96 | showSQL: true, 97 | } 98 | 99 | // options handle 100 | for _, opt := range opts { 101 | opt(settings) 102 | } 103 | 104 | logger.Infof("DSN=%s ShowSQL=%t MaxIdleConn=%v MaxOpenConn=%v", dsn, settings.showSQL, settings.maxIdleConns, settings.maxOpenConns) 105 | 106 | // create database instance 107 | if db, err := xorm.NewEngine("mysql", dsn); err != nil { 108 | panic(err) 109 | } else { 110 | database = db 111 | } 112 | 113 | // 设置日志相关 114 | database.SetLogger(&Logger{Entry: logger.WithField("orm", "xorm")}) 115 | 116 | chWrite = make(chan interface{}, asyncTaskBacklog) 117 | chUpdate = make(chan interface{}, asyncTaskBacklog) 118 | 119 | // options 120 | database.SetMaxIdleConns(settings.maxIdleConns) 121 | database.SetMaxOpenConns(settings.maxOpenConns) 122 | database.ShowSQL(settings.showSQL) 123 | 124 | syncSchema() 125 | envInit() 126 | 127 | closer := func() { 128 | close(chWrite) 129 | close(chUpdate) 130 | database.Close() 131 | logger.Info("stopped") 132 | } 133 | 134 | return closer 135 | } 136 | 137 | func syncSchema() { 138 | database.StoreEngine("InnoDB").Sync2( 139 | new(model.Agent), 140 | new(model.CardConsume), 141 | new(model.Desk), 142 | new(model.History), 143 | new(model.Login), 144 | new(model.Online), 145 | new(model.Order), 146 | new(model.Recharge), 147 | new(model.Register), 148 | new(model.ThirdAccount), 149 | new(model.Trade), 150 | new(model.User), 151 | new(model.Uuid), 152 | new(model.Club), 153 | new(model.UserClub), 154 | ) 155 | } 156 | -------------------------------------------------------------------------------- /db/model/enum.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | const ( 4 | UserClubStatusApply = 1 5 | UserClubStatusAgree = 2 6 | ) 7 | -------------------------------------------------------------------------------- /db/online.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | "time" 6 | 7 | "github.com/lonng/nanoserver/db/model" 8 | "github.com/lonng/nanoserver/pkg/errutil" 9 | ) 10 | 11 | func InsertOnline(count int, deskCount int) { 12 | o := model.Online{ 13 | Time: time.Now().Unix(), 14 | UserCount: count, 15 | DeskCount: deskCount, 16 | } 17 | 18 | _, err := database.Insert(o) 19 | if err != nil { 20 | log.Errorf("统计在线人数失败: %s", err.Error()) 21 | } 22 | } 23 | 24 | func OnlineStats(begin, end int64) ([]model.Online, error) { 25 | if begin > end { 26 | return nil, errutil.ErrIllegalParameter 27 | } 28 | 29 | list := []model.Online{} 30 | 31 | return list, database.Where("`time` BETWEEN ? AND ?", begin, end).Find(&list) 32 | } 33 | -------------------------------------------------------------------------------- /db/order.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/lonng/nanoserver/db/model" 7 | "github.com/lonng/nanoserver/pkg/algoutil" 8 | "github.com/lonng/nanoserver/pkg/errutil" 9 | ) 10 | 11 | const ( 12 | noLimitFlag = -1 //如果count == -1则表示返回所有数据 13 | noTimeFilter = -1 //如果start/end == -1则表示无时间筛选 14 | ) 15 | 16 | func QueryOrder(orderID string) (*model.Order, error) { 17 | order := &model.Order{OrderId: orderID} 18 | has, err := database.Get(order) 19 | if err != nil { 20 | return nil, err 21 | } 22 | if !has { 23 | return nil, errutil.ErrOrderNotFound 24 | } 25 | return order, nil 26 | } 27 | 28 | func InsertOrder(order *model.Order) error { 29 | if order == nil { 30 | return errutil.ErrInvalidParameter 31 | } 32 | _, err := database.Insert(order) 33 | if err != nil { 34 | return errutil.ErrDBOperation 35 | } 36 | return nil 37 | } 38 | 39 | func YXPayOrderList(uid int64, appid, channelID, orderID string, start, end int64, typ, offset, count int) ([]model.Order, int, error) { 40 | 41 | order := &model.Order{ 42 | AppId: appid, 43 | ChannelId: channelID, 44 | Uid: uid, 45 | OrderId: orderID, 46 | Type: typ, 47 | } 48 | 49 | start, end = algoutil.TimeRange(start, end) 50 | 51 | //println("uid", uid, "appid", appid, "channelid", channelID, "start", start, "end", end, "offset", offset, "count", count) 52 | 53 | total, err := database.Where("created_at BETWEEN ? AND ?", start, end).Count(order) 54 | if err != nil { 55 | logger.Error(err) 56 | return nil, 0, errutil.ErrDBOperation 57 | } 58 | 59 | result := make([]model.Order, 0) 60 | if count == noLimitFlag { 61 | err = database.Where("created_at BETWEEN ? AND ?", start, end). 62 | Desc("id").Find(&result, order) 63 | } else { 64 | err = database.Where("created_at BETWEEN ? AND ?", start, end). 65 | Desc("id").Limit(count, offset).Find(&result, order) 66 | } 67 | 68 | if err != nil { 69 | logger.Error(err) 70 | return nil, 0, errutil.ErrDBOperation 71 | } 72 | 73 | return result, int(total), nil 74 | } 75 | 76 | func OrderList(uid int64, appid, channelID, orderID, payBy string, start, end int64, status, offset, count int) ([]model.Order, int, error) { 77 | order := &model.Order{ 78 | AppId: appid, 79 | ChannelId: channelID, 80 | Uid: uid, 81 | OrderId: orderID, 82 | PayPlatform: payBy, 83 | Status: status, 84 | } 85 | 86 | start, end = algoutil.TimeRange(start, end) 87 | 88 | //println("uid", uid, "appid", appid, "channelid", channelID, "start", start, "end", end, "offset", offset, "count", count) 89 | 90 | total, err := database.Where("created_at BETWEEN ? AND ?", start, end).Count(order) 91 | if err != nil { 92 | logger.Error(err) 93 | return nil, 0, errutil.ErrDBOperation 94 | } 95 | 96 | result := make([]model.Order, 0) 97 | if count == noLimitFlag { 98 | err = database.Where("created_at BETWEEN ? AND ?", start, end). 99 | Desc("id").Find(&result, order) 100 | } else { 101 | err = database.Where("created_at BETWEEN ? AND ?", start, end). 102 | Desc("id").Limit(count, offset).Find(&result, order) 103 | } 104 | 105 | if err != nil { 106 | logger.Error(err) 107 | return nil, 0, errutil.ErrDBOperation 108 | } 109 | 110 | return result, int(total), nil 111 | } 112 | 113 | func BalanceList(uids []string) (map[string]string, error) { 114 | if uids == nil { 115 | return nil, errutil.ErrIllegalParameter 116 | } 117 | 118 | sql := "SELECT uid, coin from `user` WHERE uid IN ( " + strings.Join(uids, ",") + ")" 119 | results, err := database.Query(sql) 120 | if err != nil { 121 | logger.Error(err) 122 | return nil, errutil.ErrDBOperation 123 | } 124 | 125 | m := make(map[string]string) 126 | 127 | for _, result := range results { 128 | m[string(result["uid"])] = string(result["coin"]) 129 | } 130 | return m, nil 131 | 132 | } 133 | -------------------------------------------------------------------------------- /db/third_account.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/lonng/nanoserver/db/model" 5 | "github.com/lonng/nanoserver/pkg/errutil" 6 | ) 7 | 8 | func QueryThirdAccount(account, platform string) (*model.ThirdAccount, error) { 9 | t := &model.ThirdAccount{ThirdAccount: account, Platform: platform} 10 | has, err := database.Get(t) 11 | if err != nil { 12 | return nil, err 13 | } 14 | 15 | if !has { 16 | return nil, errutil.ErrThirdAccountNotFound 17 | } 18 | 19 | return t, nil 20 | } 21 | 22 | func InsertThirdAccount(account *model.ThirdAccount, u *model.User) error { 23 | session := database.NewSession() 24 | if err := session.Begin(); err != nil { 25 | return err 26 | } 27 | defer session.Close() 28 | 29 | if _, err := session.Insert(u); err != nil { 30 | session.Rollback() 31 | return err 32 | } 33 | 34 | // update uid 35 | account.Uid = u.Id 36 | 37 | if _, err := session.Insert(account); err != nil { 38 | session.Rollback() 39 | return err 40 | } 41 | 42 | return session.Commit() 43 | } 44 | 45 | func UpdateThirdAccount(account *model.ThirdAccount) error { 46 | if account == nil { 47 | return errutil.ErrInvalidParameter 48 | } 49 | _, err := database.Where("id=?", account.Id).Update(account) 50 | return err 51 | } 52 | -------------------------------------------------------------------------------- /db/trade.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/lonng/nanoserver/db/model" 5 | "github.com/lonng/nanoserver/pkg/algoutil" 6 | "github.com/lonng/nanoserver/pkg/errutil" 7 | ) 8 | 9 | func InsertTrade(t *model.Trade) error { 10 | logger.Info("insert trade, order id: " + t.OrderId) 11 | 12 | trade := &model.Trade{OrderId: t.OrderId} 13 | has, err := database.Get(trade) 14 | if err != nil { 15 | return err 16 | } 17 | if has { 18 | return errutil.ErrTradeExisted 19 | } 20 | order, err := QueryOrder(t.OrderId) 21 | if err != nil { 22 | return err 23 | } 24 | if order.Type == OrderTypeBuyToken { 25 | order.Status = OrderStatusNotified 26 | } else { 27 | order.Status = OrderStatusPayed 28 | } 29 | sess := database.NewSession() 30 | 31 | // 开始事务 32 | sess.Begin() 33 | defer sess.Close() 34 | if _, err := sess.Insert(t); err != nil { 35 | println(err.Error()) 36 | sess.Rollback() 37 | return err 38 | } 39 | 40 | if _, err := sess.Where("order_id = ?", order.OrderId).Update(order); err != nil { 41 | println(err.Error()) 42 | sess.Rollback() 43 | return err 44 | } 45 | 46 | u := &model.User{} 47 | sess.Where("uid = ?", order.Uid).Get(u) 48 | 49 | //添加首充时间 50 | if u.FirstRechargeAt == 0 { 51 | u.FirstRechargeAt = order.CreatedAt 52 | if _, err = sess.Id(u.Id).Update(u); err != nil { 53 | sess.Rollback() 54 | return err 55 | } 56 | } 57 | 58 | return sess.Commit() 59 | } 60 | 61 | func TradeList(appid, channelID, orderID string, start, end int64, offset, count int) ([]ViewTrade, int, error) { 62 | start, end = algoutil.TimeRange(start, end) 63 | 64 | trade := &ViewTrade{ 65 | AppId: appid, 66 | ChannelId: channelID, 67 | OrderId: orderID, 68 | } 69 | total, err := database.Where("pay_at BETWEEN ? AND ?", start, end).Count(trade) 70 | if err != nil { 71 | logger.Error(err) 72 | return nil, 0, errutil.ErrDBOperation 73 | } 74 | 75 | result := make([]ViewTrade, 0) 76 | if count == noLimitFlag { 77 | err = database.Where("pay_at BETWEEN ? AND ?", start, end). 78 | Desc("id").Find(&result, trade) 79 | } else { 80 | err = database.Where("pay_at BETWEEN ? AND ?", start, end). 81 | Desc("id").Limit(count, offset).Find(&result, trade) 82 | } 83 | 84 | if err != nil { 85 | logger.Error(err) 86 | return nil, 0, errutil.ErrDBOperation 87 | } 88 | 89 | return result, int(total), nil 90 | } 91 | -------------------------------------------------------------------------------- /db/types.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | //KeyPair rsa's public & private key pair 4 | type KeyPair struct { 5 | PrivateKey string 6 | PublicKey string 7 | } 8 | 9 | type retentionStats struct { 10 | date int64 11 | register int64 //注册人数 12 | loginDay1 int64 //1,2,3,7,14,30登录 13 | loginDay2 int64 14 | loginDay3 int64 15 | loginDay7 int64 16 | loginDay14 int64 17 | loginDay30 int64 18 | } 19 | -------------------------------------------------------------------------------- /db/views.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | //trade & order => views 4 | type ViewTrade struct { 5 | PayAt int64 6 | Uid int64 7 | Id int64 8 | 9 | Type int 10 | Money int 11 | RealMoney int 12 | ProductCount int 13 | Status int 14 | 15 | OrderId string 16 | ComsumerId string 17 | AppId string 18 | ChannelId string 19 | OrderPlatform string 20 | ChannelOrderId string 21 | Currency string 22 | RoleId string 23 | ServerName string 24 | ProductId string 25 | ProductName string 26 | RoleName string 27 | PayPlatform string 28 | } 29 | 30 | func (v *ViewTrade) TableName() string { 31 | return "view_trade" 32 | } 33 | 34 | // trade & register & user => views 35 | type ViewChannelApp struct { 36 | Id int64 37 | Type byte 38 | Status byte 39 | 40 | Uid int64 41 | CreatedAt int64 42 | RegisterAt int64 43 | FirstRechargeAt int64 44 | 45 | RealMoney int 46 | RegisterType int 47 | 48 | Os string 49 | Imei string 50 | OrderId string 51 | AppId string 52 | Model string 53 | ServerId string 54 | ProductId string 55 | OrderPlatform string 56 | PayPlatform string 57 | PassportChannelId string 58 | PaymentChannelId string 59 | } 60 | 61 | func (v *ViewChannelApp) TableName() string { 62 | return "view_channel_app" 63 | } 64 | -------------------------------------------------------------------------------- /docker-compose.dev.yaml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | services: 3 | 4 | scmj: 5 | image: scmj-server:dev 6 | command: > 7 | bash -c "cp ./go.mod ./go.sum app/ 8 | && cd app/ 9 | && ls -la 10 | && air -c .air.toml -d" 11 | volumes: 12 | - ./:/workspace/app 13 | networks: 14 | - app_network 15 | ports: 16 | - 12307:12307 17 | - 33251:33251 18 | 19 | scmj-debug: 20 | image: scmj-server:dev 21 | command: > 22 | bash -c "cp ./go.mod ./go.sum app/ 23 | && cd app/ 24 | && ls -la 25 | && dlv debug main.go --headless --log -l 0.0.0.0:2345 --api-version=2" 26 | volumes: 27 | - ./:/workspace/app 28 | networks: 29 | - app_network 30 | ports: 31 | - 12307:12307 32 | - 33251:33251 33 | - 2345:2345 34 | security_opt: 35 | - "seccomp:unconfined" 36 | 37 | networks: 38 | app_network: 39 | driver: "bridge" -------------------------------------------------------------------------------- /docker-compose.mysql.5.7.yaml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | 5 | db: 6 | image: bitnami/mysql:5.7-debian-10 7 | restart: always 8 | networks: 9 | - db_network 10 | ports: 11 | - "3306:3306" 12 | volumes: 13 | - 'db_data:/bitnami/mysql/data' 14 | environment: 15 | MYSQL_DATABASE: scmj 16 | MYSQL_ROOT_PASSWORD: 123456 17 | MYSQL_CHARACTER_SET: 'utf8mb4' 18 | MYSQL_COLLATE: 'utf8mb4_unicode_ci' 19 | healthcheck: 20 | test: ['CMD', '/opt/bitnami/scripts/mysql/healthcheck.sh'] 21 | interval: 15s 22 | timeout: 5s 23 | retries: 6 24 | 25 | adminer: 26 | image: adminer 27 | restart: always 28 | networks: 29 | - db_network 30 | ports: 31 | - 8080:8080 32 | 33 | volumes: 34 | db_data: 35 | driver: local 36 | 37 | networks: 38 | db_network: 39 | driver: "bridge" -------------------------------------------------------------------------------- /docker-compose.mysql.yaml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | 5 | db: 6 | image: mysql 7 | command: 8 | - --default-authentication-plugin=mysql_native_password 9 | - --character-set-server=utf8mb4 10 | - --collation-server=utf8mb4_unicode_ci 11 | restart: always 12 | networks: 13 | - db_network 14 | ports: 15 | - "3306:3306" 16 | volumes: 17 | - 'db_data:/bitnami/mysql/data' 18 | environment: 19 | MYSQL_DATABASE: scmj 20 | MYSQL_ROOT_PASSWORD: 123456 21 | healthcheck: 22 | test: ['CMD', '/opt/bitnami/scripts/mysql/healthcheck.sh'] 23 | interval: 15s 24 | timeout: 5s 25 | retries: 6 26 | 27 | adminer: 28 | image: adminer 29 | restart: always 30 | networks: 31 | - db_network 32 | ports: 33 | - 8080:8080 34 | 35 | volumes: 36 | db_data: 37 | driver: local 38 | 39 | networks: 40 | db_network: 41 | driver: "bridge" -------------------------------------------------------------------------------- /docker.md: -------------------------------------------------------------------------------- 1 | ### DEV 2 | 3 | ```sh 4 | ## mysql & adminer 5 | docker-compose -f docker-compose.mysql.yaml up -d 6 | 7 | docker build -f Dockerfile.dev -t scmj-server:dev . 8 | 9 | docker-compose -f docker-compose.dev.yaml up scmj 10 | ``` -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/docs/README.md -------------------------------------------------------------------------------- /docs/SQL.md: -------------------------------------------------------------------------------- 1 | # 按照时间统计局数 2 | 3 | ``` 4 | select uid as `ID`, name as `昵称`, count(*) as `局数` from rank where `record_at` > 1505480400 and uid > 0 group by uid order by `局数` desc; 5 | ``` -------------------------------------------------------------------------------- /docs/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/docs/images/1.png -------------------------------------------------------------------------------- /docs/images/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/docs/images/10.png -------------------------------------------------------------------------------- /docs/images/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/docs/images/11.png -------------------------------------------------------------------------------- /docs/images/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/docs/images/12.png -------------------------------------------------------------------------------- /docs/images/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/docs/images/13.png -------------------------------------------------------------------------------- /docs/images/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/docs/images/14.png -------------------------------------------------------------------------------- /docs/images/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/docs/images/15.png -------------------------------------------------------------------------------- /docs/images/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/docs/images/16.png -------------------------------------------------------------------------------- /docs/images/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/docs/images/17.png -------------------------------------------------------------------------------- /docs/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/docs/images/2.png -------------------------------------------------------------------------------- /docs/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/docs/images/3.png -------------------------------------------------------------------------------- /docs/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/docs/images/4.png -------------------------------------------------------------------------------- /docs/images/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/docs/images/5.png -------------------------------------------------------------------------------- /docs/images/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/docs/images/6.png -------------------------------------------------------------------------------- /docs/images/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/docs/images/7.png -------------------------------------------------------------------------------- /docs/images/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/docs/images/8.png -------------------------------------------------------------------------------- /docs/images/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/docs/images/9.png -------------------------------------------------------------------------------- /docs/images/argocd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/docs/images/argocd.png -------------------------------------------------------------------------------- /docs/images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/docs/images/banner.png -------------------------------------------------------------------------------- /docs/images/devops1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/docs/images/devops1.png -------------------------------------------------------------------------------- /docs/images/devops2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/docs/images/devops2.png -------------------------------------------------------------------------------- /docs/images/gitops.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/docs/images/gitops.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lonng/nanoserver 2 | 3 | go 1.14 4 | 5 | replace ( 6 | golang.org/x/crypto => github.com/golang/crypto v0.0.0-20181001203147-e3636079e1a4 7 | golang.org/x/net => github.com/golang/net v0.0.0-20180926154720-4dfa2610cdf3 8 | golang.org/x/sys => github.com/golang/sys v0.0.0-20180928133829-e4b3c5e90611 9 | golang.org/x/text => github.com/golang/text v0.3.0 10 | ) 11 | 12 | require ( 13 | github.com/chanxuehong/rand v0.0.0-20180830053958-4b3aff17f488 // indirect 14 | github.com/go-sql-driver/mysql v1.4.0 15 | github.com/go-xorm/core v0.6.0 16 | github.com/go-xorm/xorm v0.7.0 17 | github.com/gorilla/mux v1.6.2 18 | github.com/lonng/nano v0.4.1-0.20190704005402-15209d995681 19 | github.com/lonng/nex v1.4.1 20 | github.com/pborman/uuid v1.2.0 21 | github.com/pkg/errors v0.8.0 22 | github.com/sirupsen/logrus v1.1.0 23 | github.com/spf13/viper v1.2.1 24 | github.com/urfave/cli v1.20.1-0.20190203184040-693af58b4d51 25 | github.com/xxtea/xxtea-go v0.0.0-20170828040851-35c4b17eecf6 26 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 27 | golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 28 | golang.org/x/text v0.3.2 29 | gopkg.in/chanxuehong/wechat.v2 v2.0.0-20180924084534-7e0579cb5377 30 | ) 31 | -------------------------------------------------------------------------------- /internal/game/club_manager.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "github.com/lonng/nanoserver/protocol" 5 | 6 | "github.com/lonng/nanoserver/db" 7 | "github.com/lonng/nanoserver/pkg/async" 8 | 9 | "github.com/lonng/nano/component" 10 | "github.com/lonng/nano/session" 11 | ) 12 | 13 | type ClubManager struct { 14 | component.Base 15 | } 16 | 17 | func (c *ClubManager) ApplyClub(s *session.Session, payload *protocol.ApplyClubRequest) error { 18 | mid := s.LastMid() 19 | logger.Debugf("玩家申请加入俱乐部,UID=%d,俱乐部ID=%d", s.UID(), payload.ClubId) 20 | async.Run(func() { 21 | if err := db.ApplyClub(s.UID(), payload.ClubId); err != nil { 22 | s.ResponseMID(mid, &protocol.ErrorResponse{ 23 | Code: -1, 24 | Error: err.Error(), 25 | }) 26 | } else { 27 | s.ResponseMID(mid, &protocol.SuccessResponse) 28 | } 29 | }) 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /internal/game/constants.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | type ScoreChangeType byte 4 | 5 | const ( 6 | ScoreChangeTypeAnGang ScoreChangeType = iota + 1 7 | ScoreChangeTypeBaGang 8 | ScoreChangeTypeHu 9 | ) 10 | 11 | var scoreChangeTypeDesc = [...]string{ 12 | ScoreChangeTypeAnGang: "下雨", 13 | ScoreChangeTypeBaGang: "刮风", 14 | ScoreChangeTypeHu: "胡", 15 | } 16 | 17 | const ( 18 | turnUnknown = 255 //最多可能只有4个方位 19 | ) 20 | 21 | const ( 22 | kCurPlayer = "player" 23 | ) 24 | 25 | func (s ScoreChangeType) String() string { 26 | return scoreChangeTypeDesc[s] 27 | } 28 | -------------------------------------------------------------------------------- /internal/game/crypto.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | 7 | "github.com/lonng/nano/pipeline" 8 | "github.com/lonng/nano/session" 9 | "github.com/xxtea/xxtea-go/xxtea" 10 | ) 11 | 12 | var xxteaKey = []byte("7AEC4MA152BQE9HWQ7KB") 13 | 14 | type Crypto struct { 15 | key []byte 16 | } 17 | 18 | func newCrypto() *Crypto { 19 | return &Crypto{xxteaKey} 20 | } 21 | 22 | func (c *Crypto) inbound(s *session.Session, msg *pipeline.Message) error { 23 | out, err := base64.StdEncoding.DecodeString(string(msg.Data)) 24 | if err != nil { 25 | logger.Errorf("Inbound Error=%s, In=%s", err.Error(), string(msg.Data)) 26 | return err 27 | } 28 | 29 | out = xxtea.Decrypt(out, c.key) 30 | if out == nil { 31 | return fmt.Errorf("decrypt error=%s", err.Error()) 32 | } 33 | msg.Data = out 34 | return nil 35 | } 36 | 37 | func (c *Crypto) outbound(s *session.Session, msg *pipeline.Message) error { 38 | out := xxtea.Encrypt(msg.Data, c.key) 39 | msg.Data = []byte(base64.StdEncoding.EncodeToString(out)) 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/game/crypto_test.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import "testing" 4 | 5 | func BenchmarkCrypto_Inbound(b *testing.B) { 6 | c := &crypto{[]byte("hKKJdfskj997sdSk")} 7 | payload := []byte(`[{"name":"test","length":1.06666672229767,"segments":[{"t":0.233333334326744,"v":4.44000005722046},{"t":0.200000002980232,"v":2.62499976158142},{"t":0.266666650772095,"v":0.686249911785126},{"t":0.166666686534882,"v":1.34915959835052},{"t":0.200000047683716,"v":2.28395414352417}]}]`) 8 | test := c.outbound(nil, payload) 9 | for i := 0; i < b.N; i++ { 10 | c.inbound(nil, test) 11 | } 12 | } 13 | 14 | func BenchmarkCrypto_Outbound(b *testing.B) { 15 | c := &crypto{[]byte("hKKJdfskj997sdSk")} 16 | payload := []byte(`[{"name":"test","length":1.06666672229767,"segments":[{"t":0.233333334326744,"v":4.44000005722046},{"t":0.200000002980232,"v":2.62499976158142},{"t":0.266666650772095,"v":0.686249911785126},{"t":0.166666686534882,"v":1.34915959835052},{"t":0.200000047683716,"v":2.28395414352417}]}]`) 17 | for i := 0; i < b.N; i++ { 18 | c.outbound(nil, payload) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /internal/game/dice.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import "math/rand" 4 | 5 | type dice struct { 6 | dice1 int 7 | dice2 int 8 | } 9 | 10 | func newDice() *dice { 11 | return &dice{} 12 | } 13 | 14 | func (d *dice) random() { 15 | d.dice1, d.dice2 = rand.Intn(6)+1, rand.Intn(6)+1 16 | } 17 | -------------------------------------------------------------------------------- /internal/game/dissolve_context.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/lonng/nano/scheduler" 7 | "github.com/lonng/nanoserver/pkg/constant" 8 | "github.com/lonng/nanoserver/protocol" 9 | ) 10 | 11 | // 房间解散统计 12 | type dissolveContext struct { 13 | desk *Desk // 牌桌 14 | status map[int64]bool //解散统计 15 | desc map[int64]string //解散描述 16 | restTime int32 //解散剩余时间 17 | timer *scheduler.Timer //取消解散房间 18 | pause map[int64]bool //离线状态 19 | } 20 | 21 | func newDissolveContext(desk *Desk) *dissolveContext { 22 | return &dissolveContext{ 23 | desk: desk, 24 | status: map[int64]bool{}, 25 | desc: map[int64]string{}, 26 | pause: map[int64]bool{}, 27 | } 28 | } 29 | 30 | func (d *dissolveContext) reset() { 31 | d.status = map[int64]bool{} 32 | d.desc = map[int64]string{} 33 | } 34 | 35 | func (d *dissolveContext) stop() { 36 | if d.timer != nil { 37 | d.desk.logger.Info("关闭解散倒计时定时器") 38 | d.timer.Stop() 39 | d.timer = nil 40 | } 41 | } 42 | 43 | func (d *dissolveContext) start(restTime int32) { 44 | d.desk.logger.Debug("开始解散倒计时") 45 | 46 | //解散房间倒计时 47 | d.restTime = restTime 48 | d.timer = scheduler.NewTimer(time.Second, func() { 49 | if d.desk.status() == constant.DeskStatusDestory { 50 | d.desk.logger.Error("解散倒计时过程中已退出") 51 | d.stop() 52 | return 53 | } 54 | 55 | d.restTime-- 56 | rest := d.restTime 57 | // 每30秒记录日志 58 | if rest%30 == 0 { 59 | d.desk.logger.Debugf("解散倒计时: %d", rest) 60 | } 61 | if rest < 0 { 62 | d.stop() 63 | d.desk.doDissolve() 64 | return 65 | } 66 | }) 67 | } 68 | 69 | func (d *dissolveContext) isOnline(uid int64) bool { 70 | return !d.pause[uid] 71 | } 72 | 73 | func (d *dissolveContext) updateOnlineStatus(uid int64, online bool) { 74 | if online { 75 | delete(d.pause, uid) 76 | } else { 77 | d.pause[uid] = true 78 | } 79 | 80 | d.desk.logger.Debugf("玩家在线状态: %+v", d.pause) 81 | d.desk.group.Broadcast("onPlayerOfflineStatus", &protocol.PlayerOfflineStatus{Uid: uid, Offline: !online}) 82 | } 83 | 84 | func (d *dissolveContext) setUidStatus(uid int64, agree bool, desc string) { 85 | d.status[uid] = agree 86 | d.desc[uid] = desc 87 | 88 | d.desk.logger.Debugf("玩家解散状态: %+v, %+v", d.status, d.desc) 89 | } 90 | 91 | // 是否已经是申请解散状态 92 | func (d *dissolveContext) isDissolving() bool { 93 | return d.timer != nil 94 | } 95 | 96 | func (d *dissolveContext) agreeCount() int { 97 | return len(d.status) 98 | } 99 | 100 | func (d *dissolveContext) offlineCount() int { 101 | return len(d.pause) 102 | } 103 | -------------------------------------------------------------------------------- /internal/game/game.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/lonng/nano" 11 | "github.com/lonng/nano/component" 12 | "github.com/lonng/nano/pipeline" 13 | "github.com/lonng/nano/serialize/json" 14 | log "github.com/sirupsen/logrus" 15 | "github.com/spf13/viper" 16 | ) 17 | 18 | var ( 19 | version = "" // 游戏版本 20 | consume = map[int]int{} // 房卡消耗配置 21 | forceUpdate = false 22 | logger = log.WithField("component", "game") 23 | ) 24 | 25 | // SetCardConsume 设置房卡消耗数量 26 | func SetCardConsume(cfg string) { 27 | for _, c := range strings.Split(cfg, ",") { 28 | parts := strings.Split(c, "/") 29 | if len(parts) < 2 { 30 | logger.Warnf("无效的房卡配置: %s", c) 31 | continue 32 | } 33 | round, card := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) 34 | rd, err := strconv.Atoi(round) 35 | if err != nil { 36 | continue 37 | } 38 | cd, err := strconv.Atoi(card) 39 | if err != nil { 40 | continue 41 | } 42 | consume[rd] = cd 43 | } 44 | 45 | logger.Infof("当前游戏房卡消耗配置: %+v", consume) 46 | } 47 | 48 | // Startup 初始化游戏服务器 49 | func Startup() { 50 | rand.Seed(time.Now().Unix()) 51 | version = viper.GetString("update.version") 52 | 53 | heartbeat := viper.GetInt("core.heartbeat") 54 | if heartbeat < 5 { 55 | heartbeat = 5 56 | } 57 | 58 | // 房卡消耗配置 59 | csm := viper.GetString("core.consume") 60 | SetCardConsume(csm) 61 | forceUpdate = viper.GetBool("update.force") 62 | 63 | logger.Infof("当前游戏服务器版本: %s, 是否强制更新: %t, 当前心跳时间间隔: %d秒", version, forceUpdate, heartbeat) 64 | logger.Info("game service starup") 65 | 66 | // register game handler 67 | comps := &component.Components{} 68 | comps.Register(defaultManager) 69 | comps.Register(defaultDeskManager) 70 | comps.Register(new(ClubManager)) 71 | 72 | // 加密管道 73 | c := newCrypto() 74 | pip := pipeline.New() 75 | pip.Inbound().PushBack(c.inbound) 76 | pip.Outbound().PushBack(c.outbound) 77 | 78 | addr := fmt.Sprintf(":%d", viper.GetInt("game-server.port")) 79 | nano.Listen(addr, 80 | nano.WithPipeline(pip), 81 | nano.WithHeartbeatInterval(time.Duration(heartbeat)*time.Second), 82 | nano.WithLogger(log.WithField("component", "nano")), 83 | nano.WithSerializer(json.NewSerializer()), 84 | nano.WithComponents(comps), 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /internal/game/helper.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "runtime" 5 | "strings" 6 | 7 | "github.com/lonng/nanoserver/pkg/errutil" 8 | "github.com/lonng/nanoserver/protocol" 9 | 10 | "github.com/lonng/nano/session" 11 | ) 12 | 13 | const ( 14 | ModeTrios = 3 // 三人模式 15 | ModeFours = 4 // 四人模式 16 | ) 17 | 18 | func verifyOptions(opts *protocol.DeskOptions) bool { 19 | if opts == nil { 20 | return false 21 | } 22 | 23 | if opts.Mode != ModeTrios && opts.Mode != 4 { 24 | return false 25 | } 26 | 27 | if opts.MaxRound != 1 && opts.MaxRound != 4 && opts.MaxRound != 8 && opts.MaxRound != 16 { 28 | return false 29 | } 30 | 31 | return true 32 | } 33 | 34 | func requireCardCount(round int) int { 35 | if c, ok := consume[round]; ok { 36 | return c 37 | } 38 | 39 | c := 2 40 | switch round { 41 | case 8: 42 | c = 3 43 | case 16: 44 | c = 4 45 | } 46 | 47 | return c 48 | } 49 | 50 | func playerWithSession(s *session.Session) (*Player, error) { 51 | p, ok := s.Value(kCurPlayer).(*Player) 52 | if !ok { 53 | return nil, errutil.ErrPlayerNotFound 54 | } 55 | return p, nil 56 | } 57 | 58 | func stack() string { 59 | buf := make([]byte, 10000) 60 | n := runtime.Stack(buf, false) 61 | buf = buf[:n] 62 | 63 | s := string(buf) 64 | 65 | // skip nano frames lines 66 | const skip = 7 67 | count := 0 68 | index := strings.IndexFunc(s, func(c rune) bool { 69 | if c != '\n' { 70 | return false 71 | } 72 | count++ 73 | return count == skip 74 | }) 75 | return s[index+1:] 76 | } 77 | -------------------------------------------------------------------------------- /internal/game/history/history.go: -------------------------------------------------------------------------------- 1 | package history 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/lonng/nanoserver/db" 9 | "github.com/lonng/nanoserver/db/model" 10 | "github.com/lonng/nanoserver/protocol" 11 | ) 12 | 13 | type SnapShot struct { 14 | Enter *protocol.PlayerEnterDesk `json:"enter"` 15 | BasicInfo *protocol.DeskBasicInfo `json:"basicInfo"` 16 | DuanPai *protocol.DuanPai `json:"duanPai"` 17 | End *protocol.RoundOverStats `json:"end"` 18 | 19 | // 如果在此遇到了gang操作就去GangScoreChanges中按序拿数据, 20 | // 如果遇到了hu就去HuScoreChanges中拿数据, 21 | // 即杠与和数据分开放 22 | Do []*protocol.OpTypeDo `json:"do"` 23 | 24 | GangScoreChanges []*protocol.GangPaiScoreChange `json:"gangScoreChanges"` 25 | HuScoreChanges []*protocol.HuInfo `json:"huScoreChanges"` 26 | } 27 | 28 | type History struct { 29 | mode int 30 | beginAt int64 31 | endAt int64 32 | deskID int64 33 | playerName0 string 34 | playerName1 string 35 | playerName2 string 36 | playerName3 string 37 | scoreChange0 int 38 | scoreChange1 int 39 | scoreChange2 int 40 | scoreChange3 int 41 | 42 | SnapShot 43 | } 44 | 45 | func New(deskID int64, mode int, name0, name1, name2, name3 string, basic *protocol.DeskBasicInfo, enter *protocol.PlayerEnterDesk, duan *protocol.DuanPai) *History { 46 | return &History{ 47 | beginAt: time.Now().Unix(), 48 | deskID: deskID, 49 | mode: mode, 50 | playerName0: name0, 51 | playerName1: name1, 52 | playerName2: name2, 53 | playerName3: name3, 54 | SnapShot: SnapShot{ 55 | BasicInfo: basic, 56 | DuanPai: duan, 57 | Enter: enter, 58 | }, 59 | } 60 | } 61 | 62 | func (h *History) PushAction(op *protocol.OpTypeDo) { 63 | h.Do = append(h.Do, op) 64 | } 65 | 66 | func (h *History) PushGangScoreChange(g *protocol.GangPaiScoreChange) error { 67 | h.GangScoreChanges = append(h.GangScoreChanges, g) 68 | return nil 69 | } 70 | 71 | func (h *History) PushHuScoreChange(hsc *protocol.HuInfo) error { 72 | h.HuScoreChanges = append(h.HuScoreChanges, hsc) 73 | return nil 74 | } 75 | 76 | func (h *History) SetEndStats(ge *protocol.RoundOverStats) error { 77 | h.End = ge 78 | 79 | return nil 80 | } 81 | 82 | func (h *History) SetScoreChangeForTurn(turn uint8, sc int) error { 83 | switch turn { 84 | case 0: 85 | h.scoreChange0 = sc 86 | case 1: 87 | h.scoreChange1 = sc 88 | case 2: 89 | h.scoreChange2 = sc 90 | case 3: 91 | h.scoreChange3 = sc 92 | default: 93 | return nil 94 | 95 | } 96 | return nil 97 | } 98 | 99 | func (h *History) Save() error { 100 | data, err := json.Marshal(&h.SnapShot) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | t := &model.History{ 106 | DeskId: h.deskID, 107 | BeginAt: h.beginAt, 108 | Mode: h.mode, 109 | EndAt: time.Now().Unix(), 110 | PlayerName0: h.playerName0, 111 | PlayerName1: h.playerName1, 112 | PlayerName2: h.playerName2, 113 | PlayerName3: h.playerName3, 114 | ScoreChange0: h.scoreChange0, 115 | ScoreChange1: h.scoreChange1, 116 | ScoreChange2: h.scoreChange2, 117 | ScoreChange3: h.scoreChange3, 118 | Snapshot: string(data), 119 | } 120 | 121 | return db.InsertHistory(t) 122 | } 123 | 124 | type Record struct { 125 | ZiMoNum int `json:"ziMo"` 126 | HuNum int `json:"hu"` 127 | PaoNum int `json:"pao"` 128 | MingGangNum int `json:"mingGang"` 129 | AnGangNum int `json:"anGang"` 130 | TotalScore int `json:"totalScore"` 131 | } 132 | 133 | //场统计 134 | type MatchStats map[int64][]*Record 135 | 136 | //局统计 137 | type RoundStats map[int64]*Record 138 | 139 | func (ps MatchStats) Push(rs RoundStats) error { 140 | if len(rs) == 0 { 141 | return nil 142 | } 143 | for uid, r := range rs { 144 | ps[uid] = append(ps[uid], r) 145 | } 146 | 147 | return nil 148 | } 149 | 150 | func (ps MatchStats) Result() map[int64]*Record { 151 | ret := make(map[int64]*Record) 152 | 153 | for p, records := range ps { 154 | if _, ok := ret[p]; !ok { 155 | ret[p] = &Record{} 156 | } 157 | 158 | for _, m := range records { 159 | 160 | ret[p].AnGangNum += m.AnGangNum 161 | ret[p].MingGangNum += m.MingGangNum 162 | ret[p].TotalScore += m.TotalScore 163 | ret[p].ZiMoNum += m.ZiMoNum 164 | ret[p].HuNum += m.HuNum 165 | ret[p].PaoNum += m.PaoNum 166 | } 167 | 168 | fmt.Printf("统计 玩家: %d 明杠: %d 暗杠: %d\n", p, ret[p].MingGangNum, ret[p].AnGangNum) 169 | 170 | } 171 | return ret 172 | } 173 | 174 | func (ps MatchStats) Round() int { 175 | round := 0 176 | for _, r := range ps { 177 | if l := len(r); l > round { 178 | round = l 179 | } 180 | } 181 | 182 | return round 183 | } 184 | -------------------------------------------------------------------------------- /internal/game/mahjong/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | 条 筒 3 | ---------------------- 4 | 0 1条 | 36 1筒 | 5 | 1 1条 | 37 1筒 | 6 | 2 1条 | 38 1筒 | 7 | 3 1条 | 39 1筒 | 8 | 4 2条 | 40 2筒 | 9 | 5 2条 | 41 2筒 | 10 | 6 2条 | 42 2筒 | 11 | 7 2条 | 43 2筒 | 12 | 8 3条 | 44 3筒 | 13 | 9 3条 | 45 3筒 | 14 | 10 3条 | 46 3筒 | 15 | 11 3条 | 47 3筒 | 16 | 12 4条 | 48 4筒 | 17 | 13 4条 | 49 4筒 | 18 | 14 4条 | 50 4筒 | 19 | 15 4条 | 51 4筒 | 20 | 16 5条 | 52 5筒 | 21 | 17 5条 | 53 5筒 | 22 | 18 5条 | 54 5筒 | 23 | 19 5条 | 55 5筒 | 24 | 20 6条 | 56 6筒 | 25 | 21 6条 | 57 6筒 | 26 | 22 6条 | 58 6筒 | 27 | 23 6条 | 59 6筒 | 28 | 24 7条 | 60 7筒 | 29 | 25 7条 | 61 7筒 | 30 | 26 7条 | 62 7筒 | 31 | 27 7条 | 63 7筒 | 32 | 28 8条 | 64 8筒 | 33 | 29 8条 | 65 8筒 | 34 | 30 8条 | 66 8筒 | 35 | 31 8条 | 67 8筒 | 36 | 32 9条 | 68 9筒 | 37 | 33 9条 | 69 9筒 | 38 | 34 9条 | 70 9筒 | 39 | 35 9条 | 71 9筒 | 40 | ``` -------------------------------------------------------------------------------- /internal/game/mahjong/algorithm.go: -------------------------------------------------------------------------------- 1 | package mahjong 2 | 3 | func isLegal(indexes Indexes) bool { 4 | triplet, tripletCount := indexes.UnmarkedTriplet() // 刻子 5 | if tripletCount == 3 { 6 | indexes.Mark(int(triplet[0].I), int(triplet[1].I), int(triplet[2].I)) 7 | return isLegal(indexes) 8 | } 9 | 10 | sequence, sequenceCount := indexes.UnmarkedSequence() // 顺子 11 | if sequenceCount == 3 { 12 | if sequence[0].Index > 30 { 13 | return false // 字牌不能组合成顺子 14 | } 15 | indexes.Mark(int(sequence[0].I), int(sequence[1].I), int(sequence[2].I)) 16 | return isLegal(indexes) 17 | } 18 | return sequenceCount == 0 && tripletCount == 0 19 | } 20 | 21 | func CheckWin(indexes Indexes) bool { 22 | indexes.Sort() 23 | stats := Stats{} 24 | stats.FromIndex(indexes) 25 | if isQiDui(&stats) { 26 | return true 27 | } 28 | 29 | var prevIndex int 30 | for i := 0; i < len(indexes)-1; i++ { 31 | if indexes[i] != indexes[i+1] || indexes[i] == prevIndex { 32 | continue 33 | } 34 | 35 | prevIndex = indexes[i] 36 | indexes.Mark(i, i+1) 37 | if isLegal(indexes) { 38 | indexes.Reset() 39 | return true 40 | } 41 | indexes.Reset() 42 | } 43 | return false 44 | } 45 | -------------------------------------------------------------------------------- /internal/game/mahjong/algorithm_test.go: -------------------------------------------------------------------------------- 1 | package mahjong 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCheckWin(t *testing.T) { 8 | cases := []struct { 9 | indexes Indexes 10 | result bool 11 | }{ 12 | {indexes: Indexes{1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9, 9}, result: true}, 13 | {indexes: Indexes{1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9, 9, 21}, result: false}, 14 | {indexes: Indexes{31, 32, 33, 3, 4, 5, 6, 7, 8, 9, 9, 9, 21, 21}, result: false}, 15 | {indexes: Indexes{1, 1, 1, 1, 2, 3, 3, 3, 22, 23, 25, 33, 33, 33}, result: false}, 16 | {indexes: Indexes{1, 1, 1, 1, 2, 3, 3, 3, 22, 23, 24, 33, 33, 33}, result: true}, 17 | {indexes: Indexes{1, 1, 1, 1, 2, 3, 3, 3, 22, 23, 24, 33, 34, 35}, result: false}, 18 | {indexes: Indexes{1, 1, 2, 2, 3, 3, 3, 3, 4, 4, 5, 5, 7, 7}, result: true}, 19 | {indexes: Indexes{1, 1, 2, 2, 3, 3, 3, 3, 4, 5, 6, 7, 8, 9}, result: true}, 20 | {indexes: Indexes{11, 12, 12, 13, 13, 14, 3, 3, 4, 5, 6, 7, 8, 9}, result: true}, 21 | {indexes: Indexes{22, 22, 23, 23, 23, 24, 24, 24, 25, 5, 5, 7, 8, 9}, result: true}, 22 | {indexes: Indexes{2, 2, 3, 4, 5, 5, 5, 5, 6, 7, 7, 7, 7, 8}, result: true}, 23 | {indexes: Indexes{1, 2, 3, 3, 3, 3, 4, 5, 6, 7, 7, 7, 7, 8}, result: true}, 24 | {indexes: Indexes{1, 2, 2, 3, 3, 3, 3, 4, 6, 7, 7, 7, 7, 8}, result: true}, 25 | {indexes: Indexes{1, 2, 2, 2, 3, 3, 3, 4, 4, 6, 7, 7, 7, 8}, result: true}, 26 | {indexes: Indexes{1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 6, 7, 8}, result: true}, 27 | } 28 | 29 | for _, c := range cases { 30 | if r := CheckWin(c.indexes); r != c.result { 31 | t.Fatalf("expect: %v, got: %v, indexes: %s", c.result, r, String()) 32 | } 33 | } 34 | } 35 | 36 | func BenchmarkCheckWin(b *testing.B) { 37 | b.ReportAllocs() 38 | b.ResetTimer() 39 | //indexes := Indexes{1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9, 9} 40 | indexes := Indexes{2, 2, 3, 4, 5, 5, 5, 5, 6, 7, 7, 7, 7, 8} 41 | for i := 0; i < b.N; i++ { 42 | CheckWin(indexes) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /internal/game/mahjong/base.go: -------------------------------------------------------------------------------- 1 | package mahjong 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lonng/nanoserver/protocol" 7 | ) 8 | 9 | const ( 10 | PingHu = iota 11 | PongPong // 碰碰胡:由4副刻子(或杠)、将牌组成的胡牌 12 | QiDui // 七对: 7个对子组成的的胡牌 13 | LongQiDui 14 | ShuangLongQiDui 15 | HaoHuaLongQiDui 16 | QYS // 清一色:由一种花色的牌组成的胡牌 17 | HaiDiLao // 海底捞:抓棹面上最后一张牌的人正好能胡牌 18 | GangShangHua // 杠上花: 杠来自己要胡的牌胡牌 19 | ) 20 | 21 | // 番型对应的番数 22 | var points = [...]int{ 23 | PingHu: 0, // 平胡 24 | PongPong: 1, // 大对子:由4副刻子(或杠)、将牌组成的胡牌。×2。 25 | QiDui: 2, // 七对: 7个对子组成的的胡牌。×4 26 | LongQiDui: 3, 27 | ShuangLongQiDui: 4, 28 | HaoHuaLongQiDui: 5, 29 | QYS: 2, // 清一色:由一种花色的牌组成的胡牌。×4 30 | HaiDiLao: 1, 31 | GangShangHua: 1, 32 | } 33 | 34 | var descriptions = [...]string{ 35 | QiDui: "七对", // 七对: 7个对子组成的的胡牌。×4 36 | LongQiDui: "龙七对", 37 | ShuangLongQiDui: "双龙七对", 38 | HaoHuaLongQiDui: "豪华龙七对", 39 | } 40 | 41 | // 返回番数 42 | func Multiple(ctx *Context, onHand, pongKong Indexes) int { 43 | if len(onHand)%3 != 2 { 44 | panic("error tile count") 45 | } 46 | 47 | ctx.Desc = []string{} 48 | 49 | // 选项 50 | opts := ctx.Opts 51 | 52 | ms := NewStats(onHand, pongKong) 53 | // 番数 54 | multiple := 0 55 | 56 | // 检测清一色 57 | isQYS := isQingYiSe(ms) 58 | if isQYS { 59 | point := points[QYS] 60 | multiple += point 61 | ctx.Desc = append(ctx.Desc, "清一色") 62 | } 63 | 64 | println("===>", ctx.String(), "IsQYS", isQYS) 65 | if ctx.LastHint != nil { 66 | println("===>", "LastHint", ctx.LastHint.String()) 67 | } 68 | 69 | if opts.Menqing { 70 | // 中张 71 | if isZhongzhang(ms) { 72 | println("===>", "中张", "+1") 73 | ctx.Desc = append(ctx.Desc, "中张") 74 | multiple++ 75 | } 76 | // 门清 77 | if len(pongKong) == 0 { 78 | println("===>", "门清", "+1") 79 | ctx.Desc = append(ctx.Desc, "门清") 80 | multiple++ 81 | } 82 | } 83 | 84 | // 杠上花/杠上炮单人只可能出现一次 85 | println("===>", "IsLastTile || IsGangShangHua || IsGangShangPao || IsQiangGangHu", "+1") 86 | if ctx.IsLastTile { 87 | multiple++ 88 | ctx.Desc = append(ctx.Desc, "海底") 89 | } 90 | if ctx.IsGangShangHua { 91 | multiple++ 92 | ctx.Desc = append(ctx.Desc, "杠上花") 93 | } 94 | if ctx.IsGangShangPao { 95 | multiple++ 96 | ctx.Desc = append(ctx.Desc, "杠上炮") 97 | } 98 | if ctx.IsQiangGangHu { 99 | multiple++ 100 | ctx.Desc = append(ctx.Desc, "抢杠胡") 101 | } 102 | 103 | gangCount := gangCount(ms) 104 | // 除7对外,所有的和牌型都是可以分数组合 105 | if isQiDui(ms) { 106 | point := points[QiDui+gangCount] 107 | multiple += int(point) 108 | println("===>", "七对", "+", point) 109 | ctx.Desc = append(ctx.Desc, descriptions[QiDui+gangCount]) 110 | 111 | // 将七对 112 | if opts.Jiangdui && is258(ms) { 113 | println("===>", "将对", "+2") 114 | multiple += 2 115 | ctx.Desc = append(ctx.Desc, "将对") 116 | } 117 | return multiple 118 | } 119 | 120 | if isDaDui(ms) { 121 | point := points[PongPong] 122 | multiple += point 123 | println("===>", "碰碰胡", "+1") 124 | ctx.Desc = append(ctx.Desc, "碰碰胡") 125 | 126 | // 大对子两番选项 127 | if opts.Pengpeng { 128 | println("===>", "大对子两番", "+1") 129 | multiple++ 130 | } 131 | 132 | // 将大对 133 | if opts.Jiangdui && is258(ms) { 134 | println("===>", "将对", "+2") 135 | multiple += 2 136 | ctx.Desc = append(ctx.Desc, "将对") 137 | } 138 | 139 | // 金钩胡 140 | if len(onHand) == 2 { 141 | println("===>", "金钩胡", "+1") 142 | multiple += 1 143 | ctx.Desc = append(ctx.Desc, "金钩胡") 144 | } 145 | } else { 146 | // TODO:夹心五是否可以和其他牌叠加 147 | if opts.Jiaxin && isJiaxin(ctx, onHand) { 148 | println("===>", "夹心五", "+1") 149 | multiple += 1 150 | ctx.Desc = append(ctx.Desc, "夹心五") 151 | } 152 | 153 | if opts.Yaojiu && isYJ(onHand, pongKong) { 154 | println("===>", "幺九", "+3") 155 | multiple += 3 156 | ctx.Desc = append(ctx.Desc, "全幺九") 157 | } 158 | } 159 | 160 | if gangCount > 0 { 161 | println("===>", "杠", "+1") 162 | ctx.Desc = append(ctx.Desc, fmt.Sprintf("根x%d", gangCount)) 163 | multiple += gangCount 164 | } 165 | 166 | return multiple 167 | } 168 | 169 | // 返回听牌最大番数 170 | func MaxMultiple(opts *protocol.DeskOptions, onHand, pongKong Indexes) (multiple int, index int) { 171 | tings := TingTiles(onHand) 172 | index = IllegalIndex 173 | multiple = -1 174 | for _, idx := range tings { 175 | handTiles := make(Indexes, len(onHand)+1) 176 | copy(handTiles, onHand) 177 | handTiles[len(onHand)] = idx 178 | 179 | ctx := &Context{NewOtherDiscardID: idx, Opts: opts} 180 | 181 | if m := Multiple(ctx, handTiles, pongKong); m > multiple { 182 | index = idx 183 | multiple = m 184 | } 185 | } 186 | 187 | return 188 | } 189 | -------------------------------------------------------------------------------- /internal/game/mahjong/base_test.go: -------------------------------------------------------------------------------- 1 | package mahjong 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/lonng/nanoserver/protocol" 7 | ) 8 | 9 | func _TestBase_CanWinBySelfDrawing(t *testing.T) { 10 | t.SkipNow() 11 | tables := []int{4, 6, 15, 15, 7, 4, 7, 6, 17, 17, 18, 18, 16, 16} 12 | 13 | if CanZimo(tables) != true { 14 | t.FailNow() 15 | } 16 | } 17 | 18 | func _TestBase_CanWinByOtherDiscard(t *testing.T) { 19 | t.SkipNow() 20 | 21 | onHand := []int{5, 15, 6, 2, 12, 16, 14, 3, 7, 3, 1, 3, 12} 22 | discard := 3 23 | 24 | if !CanHu(onHand, discard) { 25 | t.FailNow() 26 | } 27 | } 28 | 29 | func _TestBase_IsReadyTiles(t *testing.T) { 30 | t.SkipNow() 31 | row := []int{16, 4, 17, 4, 17, 3, 3, 17, 16, 16} 32 | 33 | if !IsTing(row) { 34 | t.FailNow() 35 | } 36 | } 37 | 38 | func _TestBase_Tings(t *testing.T) { 39 | //origin := []int {5, 6, 14, 8, 16, 15, 3, 15, 13, 4, 18, 17, 15, 15} 40 | 41 | //origin := []int {5, 6, 14, 8, 16, 3, 13, 4, 18, 17, 2} 42 | //5条 2条 8筒 7筒 2条 9筒 发财 6条 1条 发财 7条 43 | origin := []int{5, 2, 18, 17, 2, 19, 23, 6, 1, 23, 7} 44 | 45 | size := len(origin) 46 | 47 | f := func(origin []int, i int) ([]int, int) { 48 | size := len(origin) 49 | temp := make([]int, size) 50 | copy(temp, origin) 51 | 52 | idx := temp[i] 53 | 54 | copy(temp[i:], temp[i+1:]) 55 | temp[len(temp)-1] = 0 56 | temp = temp[:len(temp)-1] 57 | return temp, idx 58 | 59 | } 60 | 61 | for i := 0; i < size; i++ { 62 | mjIndexs, idx := f(origin, i) 63 | 64 | huIndexs := TingTiles(mjIndexs) 65 | if len(huIndexs) > 0 { 66 | 67 | t.Logf("出牌: %d 和牌: %+v 手牌: %+v", idx, huIndexs, mjIndexs) 68 | } else { 69 | t.Log("fuck") 70 | } 71 | } 72 | } 73 | 74 | // #issue: 2017-08-19/916829#6 75 | func TestIsTing(t *testing.T) { 76 | ctx := &Context{ 77 | WinningID: 34, 78 | NewDrawingID: 16, 79 | NewOtherDiscardID: 34, ResultType: 0, 80 | Opts: &protocol.DeskOptions{ 81 | MaxRound: 8, 82 | MaxFan: 3, 83 | }, 84 | LastHint: &protocol.Hint{ 85 | Ops: []protocol.Op{{Type: 4, TileIDs: []int{34}}, {Type: 5, TileIDs: []int{}}, {Type: 2, TileIDs: []int{34}}}, 86 | }, 87 | } 88 | 89 | //2条 4条 2筒 9条 5筒 1条 1筒 5筒 9条 6条 3筒 5条 3条 90 | onHand := Indexes{2, 4, 12, 9, 15, 1, 11, 15, 9, 6, 13, 5, 3, 9} 91 | Multiple(ctx, onHand, Indexes{}) 92 | } 93 | 94 | func TestBase_IsYJ(t *testing.T) { 95 | tests := []struct { 96 | onhand, pongkong Indexes 97 | isYaoJiu bool 98 | }{ 99 | { 100 | Indexes{1, 2, 3, 1, 1, 9, 9, 9, 7, 8, 9}, 101 | Indexes{11, 11, 11}, 102 | true, 103 | }, 104 | { 105 | Indexes{1, 2, 3, 1, 1, 9, 9, 9, 7, 8, 9}, 106 | Indexes{12, 12, 12}, 107 | false, 108 | }, 109 | { 110 | Indexes{1, 2, 3, 1, 1, 9, 9, 9, 7, 8, 9}, 111 | Indexes{11, 11, 11, 11}, 112 | true, 113 | }, 114 | { 115 | Indexes{1, 2, 3, 2, 2, 9, 9, 9, 7, 8, 9}, 116 | Indexes{11, 11, 11}, 117 | false, 118 | }, 119 | { 120 | Indexes{1, 2, 3, 1, 1, 1, 9, 9, 7, 8, 9}, 121 | Indexes{11, 11, 11}, 122 | true, 123 | }, 124 | { 125 | Indexes{4, 2, 3, 1, 1, 9, 9, 9, 7, 8, 9}, 126 | Indexes{11, 11, 11}, 127 | false, 128 | }, 129 | { 130 | Indexes{7, 8, 9, 11, 11, 17, 17, 18, 18, 19, 19}, 131 | Indexes{1, 1, 1}, 132 | true, 133 | }, 134 | { 135 | Indexes{1, 2, 3, 7, 8, 9, 11, 11, 17, 17, 18, 18, 19, 19}, 136 | Indexes{}, 137 | true, 138 | }, 139 | } 140 | 141 | for _, c := range tests { 142 | if result := isYJ(c.onhand, c.pongkong); result != c.isYaoJiu { 143 | t.Fatalf("isYJ, expect=%t, got=%t", c.isYaoJiu, result) 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /internal/game/mahjong/heler.go: -------------------------------------------------------------------------------- 1 | package mahjong 2 | 3 | import ( 4 | "github.com/lonng/nanoserver/protocol" 5 | ) 6 | 7 | func NewStats(indexes ...Indexes) *Stats { 8 | ts := &Stats{} 9 | ts.FromIndex(indexes...) 10 | return ts 11 | } 12 | 13 | //是否是清一色 14 | func isQingYiSe(ms *Stats) bool { 15 | var flag byte 16 | 17 | for i, v := range ms { 18 | if v == 0 { 19 | continue 20 | } 21 | 22 | if i > 20 { 23 | flag |= 1 << 2 24 | } else if i > 10 { 25 | flag |= 1 << 1 26 | } else { 27 | flag |= 1 28 | } 29 | 30 | } 31 | 32 | return flag == 4 || flag == 2 || flag == 1 33 | } 34 | 35 | //7对, 返回是否是七对, 以及包含杠的个数 36 | func isQiDui(ms *Stats) bool { 37 | pairCount := 0 38 | 39 | for _, v := range ms { 40 | if v == 0 { 41 | continue 42 | } 43 | //七对 44 | if v != 2 && v != 4 { 45 | return false 46 | } 47 | 48 | if v == 2 { 49 | pairCount++ 50 | } else if v == 4 { 51 | pairCount += 2 52 | } 53 | } 54 | 55 | return pairCount == 7 56 | } 57 | 58 | //大对子 59 | func isDaDui(ms *Stats) bool { 60 | counter := 0 61 | 62 | for _, v := range ms { 63 | if v == 0 { 64 | continue 65 | } 66 | 67 | // 有单牌不可能是大对子 68 | if v < 2 { 69 | return false 70 | } 71 | 72 | if v >= 3 { 73 | counter++ 74 | } 75 | } 76 | 77 | return counter == 4 78 | } 79 | 80 | // 检查大对子和七对是不是只包含258 81 | func is258(ms *Stats) bool { 82 | for index, v := range ms { 83 | if v == 0 { 84 | continue 85 | } 86 | 87 | switch mod := index % 10; mod { 88 | case 2, 5, 8: 89 | continue 90 | default: 91 | return false 92 | } 93 | } 94 | return true 95 | } 96 | 97 | // 胡牌时, 所有牌没有1和9 98 | func isZhongzhang(ms *Stats) bool { 99 | for index, v := range ms { 100 | if v == 0 { 101 | continue 102 | } 103 | 104 | if mod := index % 10; mod == 1 || mod == 9 { 105 | return false 106 | } 107 | } 108 | return true 109 | } 110 | 111 | // 是否是夹心五 112 | func isJiaxin(ctx *Context, onHand Indexes) bool { 113 | index := IndexFromID(ctx.NewDrawingID) 114 | if id := ctx.NewOtherDiscardID; id != protocol.OptypeIllegal && id >= 0 { 115 | index = IndexFromID(ctx.NewOtherDiscardID) 116 | } 117 | 118 | // 5,15,25 119 | if index%10 != 5 { 120 | return false 121 | } 122 | 123 | //默认胡5条 124 | willRemoveTiles := Indexes{4, 5, 6} 125 | if index == 15 { 126 | willRemoveTiles = Indexes{14, 15, 16} 127 | } else if index == 25 { 128 | willRemoveTiles = Indexes{24, 25, 26} 129 | } 130 | 131 | //卡5星判断规则: 132 | //胡的牌必须是5条、5同 133 | //移除4,5,6 OR 14,15,16 OR 24,25,26后仍然可以和牌 134 | 135 | marker := func(tiles Indexes, r int) { 136 | for i := 0; i < len(tiles); i++ { 137 | //只移除第一个 138 | if tiles[i] == r { 139 | tiles[i] = IllegalIndex 140 | return 141 | } 142 | } 143 | 144 | } 145 | 146 | temp := make(Indexes, len(onHand)) 147 | 148 | for i := 0; i < len(onHand); i++ { 149 | temp[i] = onHand[i] 150 | } 151 | 152 | for _, t := range willRemoveTiles { 153 | marker(temp, t) 154 | } 155 | 156 | var tiles Indexes 157 | 158 | for _, t := range temp { 159 | if t == IllegalIndex { 160 | continue 161 | } 162 | 163 | tiles = append(tiles, t) 164 | } 165 | 166 | return CheckWin(tiles) 167 | } 168 | 169 | func min(n byte, ns ...byte) byte { 170 | m := n 171 | for _, x := range ns { 172 | if x < m { 173 | m = x 174 | } 175 | } 176 | return m 177 | } 178 | 179 | // 判断是不是幺九 180 | // 1. 排除1和9的刻字,如果有不是1和9的刻子就不可能是幺九 181 | func isYJ(onHand, pongkong Indexes) bool { 182 | pg := NewStats(pongkong) 183 | for index, count := range pg { 184 | if count < 3 { 185 | continue 186 | } 187 | if m := index % 10; m != 1 && m != 9 { 188 | return false 189 | } 190 | } 191 | 192 | ms := NewStats(onHand) 193 | //println(ms.String()) 194 | 195 | // 清理顺子,如果有1就删除2/3,如果有9就删除7/8,如果剩下的对子是1/9则成功 196 | yao := []byte{1, 11, 21} 197 | for _, y := range yao { 198 | if count1 := ms[y]; count1 > 0 { 199 | count2 := ms[y+1] 200 | count3 := ms[y+2] 201 | c := min(count1, count2, count3) 202 | 203 | ms[y] -= c 204 | ms[y+1] -= c 205 | ms[y+2] -= c 206 | } 207 | } 208 | 209 | jiu := []byte{9, 19, 29} 210 | for _, j := range jiu { 211 | if count1 := ms[j]; count1 > 0 { 212 | count2 := ms[j-1] 213 | count3 := ms[j-2] 214 | c := min(count1, count2, count3) 215 | 216 | ms[j] -= c 217 | ms[j-1] -= c 218 | ms[j-2] -= c 219 | } 220 | } 221 | 222 | //println(ms.String()) 223 | 224 | // 清理刻子 225 | for index, count := range ms { 226 | if count < 3 { 227 | continue 228 | } 229 | 230 | m := index % 10 231 | // 有不是1/9的刻子,不可能是幺九 232 | if m != 1 && m != 9 { 233 | return false 234 | } 235 | 236 | ms[index] -= 3 237 | } 238 | 239 | // 剩下的对子也只能是幺九 240 | for index, count := range ms { 241 | if count > 0 && count != 2 { 242 | return false 243 | } 244 | if count != 2 { 245 | continue 246 | } 247 | m := index % 10 248 | if m != 1 && m != 9 { 249 | return false 250 | } 251 | } 252 | 253 | //println(ms.String()) 254 | return true 255 | } 256 | 257 | func gangCount(ms *Stats) int { 258 | counter := 0 259 | for _, v := range ms { 260 | if v == 4 { 261 | counter++ 262 | } 263 | } 264 | return counter 265 | } 266 | 267 | func CanHu(onHand Indexes, discard int) bool { 268 | onHand = append(onHand, discard) 269 | return CheckWin(onHand) 270 | } 271 | 272 | func IsTing(onHand Indexes) bool { 273 | clone := make(Indexes, len(onHand)+1) 274 | for i := 0; i <= MaxTileIndex; i++ { 275 | if i%10 == 0 { 276 | continue 277 | } 278 | copy(clone, onHand) 279 | clone[len(onHand)] = i 280 | if CheckWin(clone) { 281 | return true 282 | } 283 | } 284 | return false 285 | } 286 | 287 | // 传入一副牌,返回所有的听牌 288 | func TingTiles(onHand Indexes) Indexes { 289 | clone := make(Indexes, len(onHand)+1) 290 | rts := make(Indexes, 0) 291 | for i := 0; i <= MaxTileIndex; i++ { 292 | if i%10 == 0 { 293 | continue 294 | } 295 | copy(clone, onHand) 296 | clone[len(onHand)] = i 297 | if CheckWin(clone) { 298 | rts = append(rts, i) 299 | } 300 | } 301 | 302 | return rts 303 | } 304 | -------------------------------------------------------------------------------- /internal/game/mahjong/indexes.go: -------------------------------------------------------------------------------- 1 | package mahjong 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type Indexes []int // 麻将的index 8 | 9 | func quickSort(values []int, left int, right int) { 10 | if left < right { 11 | temp := values[left] 12 | i, j := left, right 13 | for { 14 | for values[j] >= temp && i < j { 15 | j-- 16 | } 17 | for values[i] <= temp && i < j { 18 | i++ 19 | } 20 | 21 | if i >= j { 22 | break 23 | } 24 | 25 | values[i], values[j] = values[j], values[i] 26 | } 27 | 28 | values[left] = values[i] 29 | values[i] = temp 30 | 31 | quickSort(values, left, i-1) 32 | quickSort(values, i+1, right) 33 | } 34 | } 35 | 36 | func (indexes Indexes) Sort() { 37 | quickSort(indexes, 0, len(indexes)-1) 38 | } 39 | 40 | func (indexes Indexes) Mark(is ...int) { 41 | for _, i := range is { 42 | indexes[i] |= 0x80 43 | } 44 | } 45 | 46 | func (indexes Indexes) Unmark(is ...int) { 47 | for _, i := range is { 48 | indexes[i] &^= 0x80 49 | } 50 | } 51 | 52 | func (indexes Indexes) UnmarkedCount() int { 53 | var count int 54 | for i := 0; i < len(indexes); i++ { 55 | if indexes[i]&0x80 != 0 { 56 | continue 57 | } 58 | count++ 59 | } 60 | return count 61 | } 62 | 63 | type IndexInfo struct { 64 | Index int 65 | I int 66 | } 67 | 68 | // 返回一个刻子, 并返回数量 69 | func (indexes Indexes) UnmarkedSequence() ([3]IndexInfo, int) { 70 | var count int 71 | var ret = [3]IndexInfo{} 72 | var prev int 73 | for i := 0; i < len(indexes); i++ { 74 | index := indexes[i] 75 | if index&0x80 != 0 { 76 | continue 77 | } 78 | if count < 1 || prev+1 == index { 79 | prev = index 80 | ret[count] = IndexInfo{Index: index, I: i} 81 | count++ 82 | } 83 | if count == len(ret) { 84 | break 85 | } 86 | } 87 | return ret, count 88 | } 89 | 90 | func (indexes Indexes) UnmarkedTriplet() ([3]IndexInfo, int) { 91 | var count int 92 | var ret = [3]IndexInfo{} 93 | var prev int 94 | for i := 0; i < len(indexes); i++ { 95 | index := indexes[i] 96 | if index&0x80 != 0 { 97 | continue 98 | } 99 | if count < 1 || prev == index { 100 | prev = index 101 | ret[count] = IndexInfo{Index: index, I: i} 102 | count++ 103 | } 104 | if count == len(ret) { 105 | break 106 | } 107 | } 108 | return ret, count 109 | } 110 | 111 | // 返回所有未使用的Index, 并返回数量 112 | func (indexes Indexes) Unmarked() ([14]IndexInfo, int) { 113 | var count int 114 | var ret = [14]IndexInfo{} 115 | for i := 0; i < len(indexes); i++ { 116 | index := indexes[i] 117 | if index&0x80 != 0 { 118 | continue 119 | } 120 | ret[count] = IndexInfo{Index: index, I: i} 121 | count++ 122 | } 123 | return ret, count 124 | } 125 | 126 | func (indexes Indexes) UnmarkedString() string { 127 | var ret []string 128 | for i := 0; i < len(indexes); i++ { 129 | index := indexes[i] 130 | if index&0x80 != 0 { 131 | continue 132 | } 133 | ret = append(ret, TileFromIndex(index).String()) 134 | } 135 | return strings.Join(ret, ", ") 136 | } 137 | 138 | func (indexes Indexes) String() string { 139 | var ret []string 140 | for i := 0; i < len(indexes); i++ { 141 | index := indexes[i] 142 | if index&0x80 != 0 { 143 | index &^= 0x80 144 | } 145 | ret = append(ret, TileFromIndex(index).String()) 146 | } 147 | return strings.Join(ret, ", ") 148 | } 149 | 150 | func (indexes Indexes) TileString(i int) string { 151 | index := indexes[i] 152 | if index&0x80 != 0 { 153 | index &^= 0x80 154 | } 155 | return TileFromIndex(index).String() 156 | } 157 | 158 | func (indexes Indexes) Reset() { 159 | for i := 0; i < len(indexes); i++ { 160 | indexes[i] &^= 0x80 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /internal/game/mahjong/indexes_test.go: -------------------------------------------------------------------------------- 1 | package mahjong 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestIndexes_Sort(t *testing.T) { 10 | var indexes = Indexes{2, 3, 87, 5, 2, 2, 2, 1, 74, 29, 39, 56, 23, 91} 11 | Sort() 12 | fmt.Printf("%+v", indexes) 13 | } 14 | 15 | func BenchmarkIndexes_Sort(b *testing.B) { 16 | var indexes = Indexes{2, 3, 87, 5, 2, 2, 2, 1, 74, 29, 39, 56, 23, 91} 17 | b.ResetTimer() 18 | b.ReportAllocs() 19 | for i := 0; i < b.N; i++ { 20 | Sort() 21 | } 22 | } 23 | 24 | func TestIndexes_MakeUsed(t *testing.T) { 25 | var indexes = Indexes{2, 3, 87, 5, 2, 2, 2, 1, 74, 29, 39, 56, 23, 91} 26 | Mark(5, 6, 7) 27 | if u := UnmarkedCount(); u != len(indexes)-3 { 28 | t.Fatalf("unused: %v", u) 29 | } 30 | Reset() 31 | if u := UnmarkedCount(); u != len(indexes) { 32 | t.Fatalf("unused: %v", u) 33 | } 34 | if !reflect.DeepEqual(indexes, Indexes{2, 3, 87, 5, 2, 2, 2, 1, 74, 29, 39, 56, 23, 91}) { 35 | t.Fatalf("not equal") 36 | } 37 | } 38 | 39 | func BenchmarkIndexes_UnusedCount(b *testing.B) { 40 | var indexes = Indexes{2, 3, 87, 5, 2, 2, 2, 1, 74, 29, 39, 56, 23, 91} 41 | b.ReportAllocs() 42 | b.ResetTimer() 43 | for i := 0; i < b.N; i++ { 44 | UnmarkedCount() 45 | } 46 | } 47 | 48 | func TestIndexes_Unused(t *testing.T) { 49 | var indexes = Indexes{2, 3, 87, 5, 2, 2, 2, 1, 74, 29, 39, 56, 23, 91} 50 | var ret, count = UnmarkedSequence() 51 | if count != 3 { 52 | t.Fatalf("unexpect count: %d", count) 53 | } 54 | if !reflect.DeepEqual(ret, [3]byte{2, 3, 87}) { 55 | t.Fatalf("expece equal: %+v", ret) 56 | } 57 | 58 | Mark(1, 2) 59 | ret, count = UnmarkedSequence() 60 | if count != 3 { 61 | t.Fatalf("unexpect count: %d", count) 62 | } 63 | if !reflect.DeepEqual(ret, [3]byte{2, 5, 2}) { 64 | t.Fatalf("expece equal: %+v", ret) 65 | } 66 | 67 | Reset() 68 | ret, count = UnmarkedSequence() 69 | if count != 3 { 70 | t.Fatalf("unexpect count: %d", count) 71 | } 72 | if !reflect.DeepEqual(ret, [3]byte{2, 3, 87}) { 73 | t.Fatalf("expece equal: %+v", ret) 74 | } 75 | } 76 | 77 | func BenchmarkIndexes_Unused(b *testing.B) { 78 | var indexes = Indexes{2, 3, 87, 5, 2, 2, 2, 1, 74, 29, 39, 56, 23, 91} 79 | for i := 5; i < len(indexes); i++ { 80 | if i%2 == 0 { 81 | continue 82 | } 83 | Mark(i) 84 | } 85 | b.ResetTimer() 86 | b.ReportAllocs() 87 | for i := 0; i < b.N; i++ { 88 | UnmarkedSequence() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /internal/game/mahjong/mahjong.go: -------------------------------------------------------------------------------- 1 | package mahjong 2 | 3 | import ( 4 | "math/rand" 5 | "sort" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | //每种花色(条,筒)最多9种牌型,但牌是没有0点的共计 9+9 11 | // 1-9: 条 12 | // 11-19: 筒 13 | // 21-29: 万 14 | const MaxTileIndex = 29 15 | 16 | type Mahjong []*Tile 17 | 18 | func (m Mahjong) Len() int { 19 | return len(m) 20 | } 21 | 22 | func (m Mahjong) Swap(i, j int) { 23 | m[i], m[j] = m[j], m[i] 24 | } 25 | 26 | func (m Mahjong) Less(i, j int) bool { 27 | return m[i].Id < m[j].Id 28 | } 29 | 30 | func (m Mahjong) String() string { 31 | res := make([]string, len(m)) 32 | for i := range m { 33 | res[i] = m[i].String() 34 | } 35 | return strings.Join(res, " ") 36 | } 37 | 38 | func (m Mahjong) Shuffle() { 39 | s := rand.New(rand.NewSource(time.Now().Unix())) 40 | for i := range m { 41 | j := s.Intn(len(m)) 42 | m[i], m[j] = m[j], m[i] 43 | } 44 | } 45 | 46 | func (m Mahjong) Sort() { 47 | sort.Sort(m) 48 | } 49 | 50 | func (m Mahjong) Indexes() []int { 51 | idx := make([]int, len(m)) 52 | for i, t := range m { 53 | idx[i] = t.Index 54 | } 55 | return idx 56 | } 57 | 58 | func (m Mahjong) Ids() []int { 59 | ids := make([]int, len(m)) 60 | for i, t := range m { 61 | ids[i] = t.Id 62 | } 63 | return ids 64 | } 65 | 66 | func RemoveId(m *Mahjong, tid int) { 67 | size := len(*m) 68 | 69 | i := 0 70 | for ; i < size; i++ { 71 | if (*m)[i].Id == tid { 72 | break 73 | } 74 | } 75 | 76 | if i == size { 77 | return 78 | } 79 | 80 | *m = append((*m)[:i], (*m)[i+1:]...) 81 | } 82 | 83 | //根据id索引创建 84 | func FromID(ids []int) Mahjong { 85 | mj := make(Mahjong, len(ids)) 86 | for i, idx := range ids { 87 | mj[i] = TileFromID(idx) 88 | } 89 | return mj 90 | } 91 | 92 | func init() { 93 | rand.Seed(time.Now().Unix()) 94 | } 95 | -------------------------------------------------------------------------------- /internal/game/mahjong/mahjong_test.go: -------------------------------------------------------------------------------- 1 | package mahjong 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNew(t *testing.T) { 8 | for i := 0; i < 72; i++ { 9 | mj := TileFromID(i) 10 | if Suit > 1 { 11 | t.Fail() 12 | } 13 | if Rank > 9 { 14 | t.Fail() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /internal/game/mahjong/meta.go: -------------------------------------------------------------------------------- 1 | package mahjong 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math/rand" 7 | "time" 8 | 9 | "github.com/lonng/nanoserver/protocol" 10 | ) 11 | 12 | const ( 13 | MaxFan = -1 // 极品 14 | MeiHu = -2 // 没胡牌 15 | ) 16 | 17 | type Tiles []int //麻将内部表示(即72张牌的id号列表) 18 | 19 | func (m Tiles) Shuffle() { 20 | s := rand.New(rand.NewSource(time.Now().Unix())) 21 | for i := range m { 22 | j := s.Intn(len(m)) 23 | m[i], m[j] = m[j], m[i] 24 | } 25 | } 26 | 27 | func New(count int) Tiles { 28 | tiles := make(Tiles, count) 29 | 30 | for i := range tiles { 31 | tiles[i] = i 32 | } 33 | 34 | tiles.Shuffle() 35 | return tiles 36 | } 37 | 38 | type Stats [MaxTileIndex + 1]byte 39 | 40 | func (ms *Stats) String() string { 41 | buf := &bytes.Buffer{} 42 | 43 | for i, count := range ms { 44 | if count == 0 { 45 | continue 46 | } 47 | fmt.Fprintf(buf, "%s:%d ", TileFromIndex(i), count) 48 | } 49 | 50 | return buf.String() 51 | } 52 | 53 | func (ms *Stats) From(mjs ...Mahjong) { 54 | for _, mj := range mjs { 55 | for _, t := range mj { 56 | ms[t.Index] = ms[t.Index] + 1 57 | } 58 | 59 | } 60 | } 61 | 62 | func (ms *Stats) FromIndex(tiles ...Indexes) { 63 | for _, tile := range tiles { 64 | for _, idx := range tile { 65 | if idx >= 0 && idx <= MaxTileIndex { 66 | ms[idx] = ms[idx] + 1 67 | } 68 | } 69 | } 70 | } 71 | 72 | func (ms *Stats) CountWithIndex(idx int) int { 73 | if idx < 0 || idx%10 == 0 || idx > MaxTileIndex { 74 | return IllegalIndex 75 | } 76 | fmt.Println("CountWithIndex", idx, ms) 77 | return int(ms[idx]) 78 | } 79 | 80 | type ReadyTile struct { 81 | Index int //和牌的index 82 | Points int //番数 83 | } 84 | 85 | func (rt *ReadyTile) String() string { 86 | return fmt.Sprintf("%v: %d番", TileFromIndex(rt.Index), rt.Points) 87 | } 88 | 89 | func (rt *ReadyTile) Equals(t *ReadyTile) bool { 90 | return rt.Index == t.Index && rt.Points == t.Points 91 | } 92 | 93 | type ScoreChangeType byte 94 | 95 | type Context struct { 96 | WinningID int //自己要和的牌 97 | PrevOp int //上一个操作 98 | NewDrawingID int //最新上手的牌 99 | NewOtherDiscardID int //上家打出的最新一张牌 100 | LastDiscardId int // 最新打过的牌 101 | Desc []string // 描述 102 | 103 | IsLastTile bool //自己上手的最新一张牌,是否是桌面上的最后一张 104 | 105 | LastHint *protocol.Hint //最后一次提示 106 | 107 | ResultType int 108 | 109 | Opts *protocol.DeskOptions 110 | DeskNo string 111 | Uid int64 112 | 113 | Fan int // 番数, -1表示极品 114 | Que int // 定缺,0表示未定缺,1表示缺条/2缺筒/3缺万 115 | 116 | IsGangShangHua bool // 是不是杠上花 117 | IsGangShangPao bool // 是不是杠上炮 118 | IsQiangGangHu bool // 是不是抢杠胡 119 | } 120 | 121 | func (c *Context) Reset() { 122 | c.WinningID = -1 //真实id从0开始 123 | c.NewOtherDiscardID = -1 124 | c.PrevOp = protocol.OptypeIllegal 125 | c.LastDiscardId = IllegalIndex 126 | c.IsLastTile = false 127 | c.LastHint = nil 128 | c.ResultType = 0 129 | c.Fan = 0 130 | c.Que = 0 131 | 132 | c.IsGangShangHua = false 133 | c.IsGangShangPao = false 134 | c.IsQiangGangHu = false 135 | } 136 | 137 | func (c *Context) String() string { 138 | return fmt.Sprintf("Uid=%d, DeskNo=%s, WinningID=%d, PrevOp=%d, NewDrawingID=%d, NewOtherDiscardID=%d, IsLastTile=%t, ResultType=%d, Opts=%#v, IsGangShangHua=%t, IsGangShangPao=%t, IsQiangGangHu=%t", 139 | c.Uid, c.DeskNo, c.WinningID, c.PrevOp, c.NewDrawingID, c.NewOtherDiscardID, c.IsLastTile, 140 | c.ResultType, c.Opts, c.IsGangShangHua, c.IsGangShangPao, c.IsQiangGangHu) 141 | } 142 | 143 | func (c *Context) SetPrevOp(op int) { 144 | c.PrevOp = op 145 | } 146 | 147 | type Result []int 148 | 149 | func (res Result) String() string { 150 | 151 | buf := &bytes.Buffer{} 152 | 153 | fmt.Fprintf(buf, "%v%v\t", TileFromIndex(res[0]), TileFromIndex(res[1])) 154 | 155 | for i, res := range res[2:] { 156 | if i%3 == 0 { 157 | fmt.Fprintf(buf, "\t") 158 | } 159 | fmt.Fprintf(buf, "%v", TileFromIndex(res)) 160 | } 161 | 162 | return buf.String() 163 | } 164 | 165 | const ( 166 | IllegalIndex = -1 167 | ) 168 | -------------------------------------------------------------------------------- /internal/game/mahjong/tile.go: -------------------------------------------------------------------------------- 1 | package mahjong 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | var tileNames = []string{"条", "筒", "万"} 8 | 9 | type Tile struct { 10 | Id int 11 | Suit int //花色 12 | Rank int //点数 13 | Index int //索引(1~9, 11~19) 14 | } 15 | 16 | func (t *Tile) String() string { 17 | return fmt.Sprintf("%d%s", t.Rank, tileNames[t.Suit]) 18 | } 19 | 20 | func (t *Tile) Equals(other *Tile) bool { 21 | return t.Index == other.Index 22 | } 23 | 24 | func TileFromIndex(idx int) *Tile { 25 | if idx < 0 || idx > MaxTileIndex || idx%10 == 0 { 26 | return nil 27 | } 28 | 29 | return &Tile{ 30 | Suit: idx / 10, 31 | Rank: idx % 10, 32 | Index: idx, 33 | } 34 | } 35 | 36 | func IndexFromID(id int) int { 37 | if id < 0 { 38 | panic(fmt.Errorf("ilegal tile id: %d", id)) 39 | } 40 | var ( 41 | tmp = id / 4 42 | h = tmp / 9 43 | v = tmp%9 + 1 44 | i = h*10 + v 45 | ) 46 | 47 | return i 48 | } 49 | 50 | //id: 0~3 -> 1条 4~7 -> 2条 ... 51 | //0~35 =>条 36~71 =>筒 52 | func TileFromID(id int) *Tile { 53 | if id < 0 { 54 | panic("illegal tile id") 55 | } 56 | 57 | var ( 58 | tmp = id / 4 59 | h = tmp / 9 60 | v = tmp%9 + 1 61 | i = h*10 + v 62 | ) 63 | 64 | return &Tile{Suit: h, Rank: v, Index: i, Id: id} 65 | } 66 | -------------------------------------------------------------------------------- /internal/game/manager.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "github.com/lonng/nano/scheduler" 5 | "github.com/lonng/nanoserver/protocol" 6 | 7 | "time" 8 | 9 | "github.com/lonng/nano" 10 | "github.com/lonng/nano/component" 11 | "github.com/lonng/nano/session" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | const kickResetBacklog = 8 16 | 17 | var defaultManager = NewManager() 18 | 19 | type ( 20 | Manager struct { 21 | component.Base 22 | group *nano.Group // 广播channel 23 | players map[int64]*Player // 所有的玩家 24 | chKick chan int64 // 退出队列 25 | chReset chan int64 // 重置队列 26 | chRecharge chan RechargeInfo // 充值信息 27 | } 28 | 29 | RechargeInfo struct { 30 | Uid int64 // 用户ID 31 | Coin int64 // 房卡数量 32 | } 33 | ) 34 | 35 | func NewManager() *Manager { 36 | return &Manager{ 37 | group: nano.NewGroup("_SYSTEM_MESSAGE_BROADCAST"), 38 | players: map[int64]*Player{}, 39 | chKick: make(chan int64, kickResetBacklog), 40 | chReset: make(chan int64, kickResetBacklog), 41 | chRecharge: make(chan RechargeInfo, 32), 42 | } 43 | } 44 | 45 | func (m *Manager) AfterInit() { 46 | session.Lifetime.OnClosed(func(s *session.Session) { 47 | m.group.Leave(s) 48 | }) 49 | 50 | // 处理踢出玩家和重置玩家消息(来自http) 51 | scheduler.NewTimer(time.Second, func() { 52 | ctrl: 53 | for { 54 | select { 55 | case uid := <-m.chKick: 56 | p, ok := defaultManager.player(uid) 57 | if !ok || p.session == nil { 58 | logger.Errorf("玩家%d不在线", uid) 59 | } 60 | p.session.Close() 61 | logger.Infof("踢出玩家, UID=%d", uid) 62 | 63 | case uid := <-m.chReset: 64 | p, ok := defaultManager.player(uid) 65 | if !ok { 66 | return 67 | } 68 | if p.session != nil { 69 | logger.Errorf("玩家正在游戏中,不能重置: %d", uid) 70 | return 71 | } 72 | p.desk = nil 73 | logger.Infof("重置玩家, UID=%d", uid) 74 | 75 | case ri := <-m.chRecharge: 76 | player, ok := m.player(ri.Uid) 77 | // 如果玩家在线 78 | if s := player.session; ok && s != nil { 79 | s.Push("onCoinChange", &protocol.CoinChangeInformation{Coin: ri.Coin}) 80 | } 81 | 82 | default: 83 | break ctrl 84 | } 85 | } 86 | }) 87 | } 88 | 89 | func (m *Manager) Login(s *session.Session, req *protocol.LoginToGameServerRequest) error { 90 | uid := req.Uid 91 | s.Bind(uid) 92 | 93 | log.Infof("玩家: %d登录: %+v", uid, req) 94 | if p, ok := m.player(uid); !ok { 95 | log.Infof("玩家: %d不在线,创建新的玩家", uid) 96 | p = newPlayer(s, uid, req.Name, req.HeadUrl, req.IP, req.Sex) 97 | m.setPlayer(uid, p) 98 | } else { 99 | log.Infof("玩家: %d已经在线", uid) 100 | // 移除广播频道 101 | m.group.Leave(s) 102 | 103 | // 重置之前的session 104 | if prevSession := p.session; prevSession != nil && prevSession != s { 105 | // 如果之前房间存在,则退出来 106 | if p, err := playerWithSession(prevSession); err == nil && p != nil && p.desk != nil && p.desk.group != nil { 107 | p.desk.group.Leave(prevSession) 108 | } 109 | 110 | prevSession.Clear() 111 | prevSession.Close() 112 | } 113 | 114 | // 绑定新session 115 | p.bindSession(s) 116 | } 117 | 118 | // 添加到广播频道 119 | m.group.Add(s) 120 | 121 | res := &protocol.LoginToGameServerResponse{ 122 | Uid: s.UID(), 123 | Nickname: req.Name, 124 | Sex: req.Sex, 125 | HeadUrl: req.HeadUrl, 126 | FangKa: req.FangKa, 127 | } 128 | 129 | return s.Response(res) 130 | } 131 | 132 | func (m *Manager) player(uid int64) (*Player, bool) { 133 | p, ok := m.players[uid] 134 | 135 | return p, ok 136 | } 137 | 138 | func (m *Manager) setPlayer(uid int64, p *Player) { 139 | if _, ok := m.players[uid]; ok { 140 | log.Warnf("玩家已经存在,正在覆盖玩家, UID=%d", uid) 141 | } 142 | m.players[uid] = p 143 | } 144 | 145 | func (m *Manager) CheckOrder(s *session.Session, msg *protocol.CheckOrderReqeust) error { 146 | log.Infof("%+v", msg) 147 | 148 | return s.Response(&protocol.CheckOrderResponse{ 149 | FangKa: 20, 150 | }) 151 | } 152 | 153 | func (m *Manager) sessionCount() int { 154 | return len(m.players) 155 | } 156 | 157 | func (m *Manager) offline(uid int64) { 158 | delete(m.players, uid) 159 | log.Infof("玩家: %d从在线列表中删除, 剩余:%d", uid, len(m.players)) 160 | } 161 | -------------------------------------------------------------------------------- /internal/game/prepare_context.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | type prepareContext struct { 4 | sortedStatus map[int64]bool //是否已经齐牌完毕 5 | readyStatus map[int64]bool //是否已经ready完毕 6 | } 7 | 8 | func newPrepareContext() *prepareContext { 9 | return &prepareContext{ 10 | sortedStatus: map[int64]bool{}, 11 | readyStatus: map[int64]bool{}, 12 | } 13 | } 14 | 15 | func (p *prepareContext) isReady(uid int64) bool { 16 | return p.readyStatus[uid] 17 | } 18 | 19 | func (p *prepareContext) ready(uid int64) { 20 | p.readyStatus[uid] = true 21 | } 22 | 23 | func (p *prepareContext) sorted(uid int64) { 24 | p.sortedStatus[uid] = true 25 | } 26 | 27 | func (p *prepareContext) isSorted(uid int64) bool { 28 | return p.sortedStatus[uid] 29 | } 30 | 31 | func (p *prepareContext) reset() { 32 | p.sortedStatus = map[int64]bool{} 33 | p.readyStatus = map[int64]bool{} 34 | } 35 | -------------------------------------------------------------------------------- /internal/game/rest_api.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "github.com/lonng/nanoserver/protocol" 5 | ) 6 | 7 | // TODO: conc 8 | func Kick(uid int64) error { 9 | defaultManager.chKick <- uid 10 | return nil 11 | } 12 | 13 | func BroadcastSystemMessage(message string) { 14 | defaultManager.group.Broadcast("onBroadcast", &protocol.StringMessage{Message: message}) 15 | } 16 | 17 | func Reset(uid int64) { 18 | defaultManager.chReset <- uid 19 | } 20 | 21 | func Recharge(uid, coin int64) { 22 | defaultManager.chRecharge <- RechargeInfo{uid, coin} 23 | } 24 | -------------------------------------------------------------------------------- /internal/game/types.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lonng/nanoserver/internal/game/mahjong" 7 | ) 8 | 9 | type scoreChangeInfo struct { 10 | uid int64 //谁的score发生了变化(+/-) 11 | score int //变化了多少 12 | 13 | tileID int //引起此种变化的tile id 14 | typ ScoreChangeType //变化的类型 15 | } 16 | 17 | func (s *scoreChangeInfo) String() string { 18 | return fmt.Sprintf("Uid=%d, Score=%d, TileId=%s, Type=%s", 19 | s.uid, s.score, mahjong.TileFromID(s.tileID).String(), s.typ.String()) 20 | } 21 | -------------------------------------------------------------------------------- /internal/web/api/desk.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/lonng/nanoserver/db" 10 | "github.com/lonng/nanoserver/pkg/errutil" 11 | "github.com/lonng/nanoserver/pkg/whitelist" 12 | "github.com/lonng/nanoserver/protocol" 13 | "github.com/lonng/nex" 14 | ) 15 | 16 | func MakeDeskService() http.Handler { 17 | router := mux.NewRouter() 18 | router.Handle("/v1/desk/player/{id}", nex.Handler(deskList)).Methods("GET") //获取desk列表(lite) 19 | router.Handle("/v1/desk/{id}", nex.Handler(deskByID)).Methods("GET") //获取desk记录 20 | return router 21 | } 22 | 23 | func DeskByID(id int64) (*protocol.Desk, error) { 24 | p, err := db.QueryDesk(id) 25 | if err != nil { 26 | return nil, err 27 | } 28 | return &protocol.Desk{ 29 | Id: p.Id, 30 | Creator: p.Creator, 31 | Mode: p.Mode, 32 | Round: p.Round, 33 | DeskNo: p.DeskNo, 34 | Player0: p.Player0, 35 | Player1: p.Player1, 36 | Player2: p.Player2, 37 | Player3: p.Player3, 38 | PlayerName0: p.PlayerName0, 39 | PlayerName1: p.PlayerName1, 40 | PlayerName2: p.PlayerName2, 41 | PlayerName3: p.PlayerName3, 42 | ScoreChange0: p.ScoreChange0, 43 | ScoreChange1: p.ScoreChange1, 44 | ScoreChange2: p.ScoreChange2, 45 | ScoreChange3: p.ScoreChange3, 46 | CreatedAt: p.CreatedAt, 47 | DismissAt: p.Creator, 48 | }, nil 49 | 50 | } 51 | 52 | func DeskList(playerId int64) ([]protocol.Desk, int64, error) { 53 | //默认全部 54 | ps, total, err := db.DeskList(playerId) 55 | if err != nil { 56 | return nil, 0, err 57 | } 58 | list := make([]protocol.Desk, total) 59 | 60 | const ( 61 | format = "2006-01-02 15:04:05" 62 | ) 63 | 64 | for i, p := range ps { 65 | 66 | createdAtStr := time.Unix(p.CreatedAt, 0).Format(format) 67 | 68 | list[i] = protocol.Desk{ 69 | Id: p.Id, 70 | Creator: p.Creator, 71 | Round: p.Round, 72 | Mode: p.Mode, 73 | DeskNo: p.DeskNo, 74 | Player0: p.Player0, 75 | Player1: p.Player1, 76 | Player2: p.Player2, 77 | Player3: p.Player3, 78 | PlayerName0: p.PlayerName0, 79 | PlayerName1: p.PlayerName1, 80 | PlayerName2: p.PlayerName2, 81 | PlayerName3: p.PlayerName3, 82 | ScoreChange0: p.ScoreChange0, 83 | ScoreChange1: p.ScoreChange1, 84 | ScoreChange2: p.ScoreChange2, 85 | ScoreChange3: p.ScoreChange3, 86 | CreatedAt: p.CreatedAt, 87 | CreatedAtStr: createdAtStr, 88 | DismissAt: p.Creator, 89 | } 90 | } 91 | return list, int64(len(list)), nil 92 | } 93 | 94 | func deskList(r *http.Request) (*protocol.DeskListResponse, error) { 95 | if !whitelist.VerifyIP(r.RemoteAddr) { 96 | return nil, errutil.ErrPermissionDenied 97 | } 98 | vars := mux.Vars(r) 99 | idStr, ok := vars["id"] 100 | if !ok || idStr == "" { 101 | return nil, errutil.ErrInvalidParameter 102 | } 103 | 104 | id, err := strconv.ParseInt(idStr, 10, 0) 105 | if err != nil { 106 | return nil, errutil.ErrInvalidParameter 107 | } 108 | 109 | list, t, err := DeskList(id) 110 | if err != nil { 111 | return nil, err 112 | } 113 | return &protocol.DeskListResponse{Data: list, Total: t}, nil 114 | } 115 | 116 | func deskByID(r *http.Request) (*protocol.DeskByIDResponse, error) { 117 | if !whitelist.VerifyIP(r.RemoteAddr) { 118 | return nil, errutil.ErrPermissionDenied 119 | } 120 | vars := mux.Vars(r) 121 | idStr, ok := vars["id"] 122 | if !ok || idStr == "" { 123 | return nil, errutil.ErrInvalidParameter 124 | } 125 | 126 | id, err := strconv.ParseInt(idStr, 10, 0) 127 | if err != nil { 128 | return nil, errutil.ErrInvalidParameter 129 | } 130 | 131 | h, err := DeskByID(id) 132 | if err != nil { 133 | return nil, err 134 | } 135 | return &protocol.DeskByIDResponse{Data: h}, nil 136 | } 137 | -------------------------------------------------------------------------------- /internal/web/api/history.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/lonng/nanoserver/db" 9 | "github.com/lonng/nanoserver/pkg/whitelist" 10 | "github.com/lonng/nex" 11 | 12 | "github.com/gorilla/mux" 13 | "github.com/lonng/nanoserver/pkg/errutil" 14 | "github.com/lonng/nanoserver/protocol" 15 | "golang.org/x/net/context" 16 | ) 17 | 18 | const ( 19 | format = "01-02 15:04:05" 20 | ) 21 | 22 | func MakeHistoryService() http.Handler { 23 | router := mux.NewRouter() 24 | router.Handle("/v1/history/lite/{desk_id}", nex.Handler(historyList)).Methods("GET") //获取历史列表(lite),参数为deskid 25 | router.Handle("/v1/history/{id}", nex.Handler(historyByID)).Methods("GET") //获取历史记录 26 | return router 27 | } 28 | 29 | func HistoryByID(id int64) (*protocol.History, error) { 30 | p, err := db.QueryHistory(id) 31 | if err != nil { 32 | return nil, err 33 | } 34 | return &protocol.History{ 35 | HistoryLite: protocol.HistoryLite{ 36 | Id: p.Id, 37 | DeskId: p.DeskId, 38 | BeginAt: p.BeginAt, 39 | Mode: p.Mode, 40 | BeginAtStr: time.Unix(p.BeginAt, 0).Format(format), 41 | EndAt: p.EndAt, 42 | PlayerName0: p.PlayerName0, 43 | PlayerName1: p.PlayerName1, 44 | PlayerName2: p.PlayerName2, 45 | PlayerName3: p.PlayerName3, 46 | ScoreChange0: p.ScoreChange0, 47 | ScoreChange1: p.ScoreChange1, 48 | ScoreChange2: p.ScoreChange2, 49 | ScoreChange3: p.ScoreChange3, 50 | }, 51 | Snapshot: p.Snapshot, 52 | }, nil 53 | 54 | } 55 | 56 | func HistoryLiteList(deskId int64) ([]protocol.HistoryLite, int64, error) { 57 | //默认全部 58 | ps, total, err := db.QueryHistoriesByDeskID(deskId) 59 | if err != nil { 60 | return nil, 0, err 61 | } 62 | list := make([]protocol.HistoryLite, total) 63 | for i, p := range ps { 64 | beginAtStr := time.Unix(p.BeginAt, 0).Format(format) 65 | list[i] = protocol.HistoryLite{ 66 | Id: p.Id, 67 | DeskId: p.DeskId, 68 | Mode: p.Mode, 69 | BeginAt: p.BeginAt, 70 | BeginAtStr: beginAtStr, 71 | EndAt: p.EndAt, 72 | PlayerName0: p.PlayerName0, 73 | PlayerName1: p.PlayerName1, 74 | PlayerName2: p.PlayerName2, 75 | PlayerName3: p.PlayerName3, 76 | ScoreChange0: p.ScoreChange0, 77 | ScoreChange1: p.ScoreChange1, 78 | ScoreChange2: p.ScoreChange2, 79 | ScoreChange3: p.ScoreChange3, 80 | } 81 | } 82 | return list, int64(len(list)), nil 83 | } 84 | 85 | func HistoryList(req *protocol.HistoryListRequest) ([]protocol.History, int64, error) { 86 | //默认全部 87 | ps, total, err := db.QueryHistoriesByDeskID(req.DeskID) 88 | if err != nil { 89 | return nil, 0, err 90 | } 91 | 92 | list := make([]protocol.History, total) 93 | for i, p := range ps { 94 | beginAtStr := time.Unix(p.BeginAt, 0).Format(format) 95 | list[i] = protocol.History{ 96 | HistoryLite: protocol.HistoryLite{ 97 | Id: p.Id, 98 | Mode: p.Mode, 99 | DeskId: p.DeskId, 100 | BeginAt: p.BeginAt, 101 | BeginAtStr: beginAtStr, 102 | EndAt: p.EndAt, 103 | PlayerName0: p.PlayerName0, 104 | PlayerName1: p.PlayerName1, 105 | PlayerName2: p.PlayerName2, 106 | PlayerName3: p.PlayerName3, 107 | ScoreChange0: p.ScoreChange0, 108 | ScoreChange1: p.ScoreChange1, 109 | ScoreChange2: p.ScoreChange2, 110 | ScoreChange3: p.ScoreChange3, 111 | }, 112 | Snapshot: p.Snapshot, 113 | } 114 | } 115 | return list, int64(len(list)), nil 116 | } 117 | 118 | func historyList(_ context.Context, r *http.Request) (*protocol.HistoryLiteListResponse, error) { 119 | if !whitelist.VerifyIP(r.RemoteAddr) { 120 | return nil, errutil.ErrPermissionDenied 121 | } 122 | vars := mux.Vars(r) 123 | idStr, ok := vars["desk_id"] 124 | if !ok || idStr == "" { 125 | return nil, errutil.ErrInvalidParameter 126 | } 127 | 128 | id, err := strconv.ParseInt(idStr, 10, 0) 129 | if err != nil { 130 | return nil, errutil.ErrInvalidParameter 131 | } 132 | 133 | list, t, err := HistoryLiteList(id) 134 | if err != nil { 135 | return nil, err 136 | } 137 | return &protocol.HistoryLiteListResponse{Data: list, Total: t}, nil 138 | } 139 | 140 | func historyByID(r *http.Request) (interface{}, error) { 141 | if !whitelist.VerifyIP(r.RemoteAddr) { 142 | return nil, errutil.ErrPermissionDenied 143 | } 144 | vars := mux.Vars(r) 145 | idStr, ok := vars["id"] 146 | if !ok || idStr == "" { 147 | return nil, errutil.ErrInvalidParameter 148 | } 149 | 150 | id, err := strconv.ParseInt(idStr, 10, 0) 151 | if err != nil { 152 | return nil, errutil.ErrInvalidParameter 153 | } 154 | 155 | h, err := HistoryByID(id) 156 | if err != nil { 157 | return nil, err 158 | } 159 | return protocol.HistoryByIDResponse{Data: h}, nil 160 | } 161 | -------------------------------------------------------------------------------- /internal/web/api/order.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/xml" 5 | "io/ioutil" 6 | "net/http" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/gorilla/mux" 12 | "github.com/lonng/nanoserver/db" 13 | "github.com/lonng/nanoserver/db/model" 14 | provider2 "github.com/lonng/nanoserver/internal/web/api/provider" 15 | "github.com/lonng/nex" 16 | "github.com/pborman/uuid" 17 | "golang.org/x/net/context" 18 | 19 | "github.com/lonng/nanoserver/pkg/errutil" 20 | "github.com/lonng/nanoserver/pkg/whitelist" 21 | "github.com/lonng/nanoserver/protocol" 22 | ) 23 | 24 | func MakeOrderService() http.Handler { 25 | router := mux.NewRouter() 26 | router.Handle("/v1/order/console/", nex.Handler(orderList)).Methods("GET") //订单列表 27 | router.Handle("/v1/order/", nex.Handler(createOrder)).Methods("GET") //创建订单 28 | router.Handle("/v1/order/notify/wechat", nex.Handler(wechatCallback)).Methods("POST") //微信订单回调 29 | return router 30 | } 31 | 32 | func CreateOrder(r *protocol.CreateOrderRequest) (interface{}, error) { 33 | order := &model.Order{ 34 | OrderId: strings.Replace(uuid.New(), "-", "", -1), 35 | AppId: r.AppID, 36 | Uid: r.Uid, 37 | ChannelId: r.ChannelID, 38 | PayPlatform: r.Platform, 39 | Extra: r.Extra, 40 | ProductName: r.ProductionName, 41 | ProductCount: r.ProductCount, 42 | CreatedAt: time.Now().Unix(), 43 | Status: db.OrderStatusCreated, 44 | Remote: r.Device.Remote, 45 | Ip: r.Device.Remote, 46 | Imei: r.Device.IMEI, 47 | Model: r.Device.Model, 48 | Os: r.Device.OS, 49 | } 50 | 51 | resp, err := provider2.Wechat.CreateOrderResponse(order) 52 | if err != nil { 53 | logger.Error(err.Error()) 54 | return nil, err 55 | } 56 | 57 | if err := db.InsertOrder(order); err != nil { 58 | logger.Error(err.Error()) 59 | return nil, err 60 | } 61 | 62 | return resp, nil 63 | } 64 | 65 | func createOrder(r *http.Request) (interface{}, error) { 66 | if err := r.ParseForm(); err != nil { 67 | return nil, err 68 | } 69 | count, err := strconv.Atoi(strings.TrimSpace(r.Form.Get("count"))) 70 | if err != nil { 71 | return nil, err 72 | } 73 | uid, err := strconv.ParseInt(strings.TrimSpace(r.Form.Get("uid")), 10, 64) 74 | if err != nil { 75 | return nil, err 76 | } 77 | appId := strings.TrimSpace(r.Form.Get("appId")) 78 | channelId := strings.TrimSpace(r.Form.Get("channelId")) 79 | platform := strings.TrimSpace(r.Form.Get("platform")) 80 | extra := strings.TrimSpace(r.Form.Get("extra")) 81 | name := strings.TrimSpace(r.Form.Get("name")) 82 | if appId == "" || channelId == "" || platform == "" || count == 0 || name == "" { 83 | return nil, errutil.ErrIllegalParameter 84 | } 85 | 86 | request := &protocol.CreateOrderRequest{ 87 | AppID: appId, 88 | ChannelID: channelId, 89 | Platform: platform, 90 | ProductionName: name, 91 | ProductCount: count, 92 | Extra: extra, 93 | Uid: uid, 94 | Device: protocol.Device{Remote: r.RemoteAddr}, 95 | } 96 | 97 | return CreateOrder(request) 98 | } 99 | 100 | func TradeList(r *protocol.TradeListRequest) ([]protocol.TradeInfo, int, error) { 101 | if r == nil { 102 | return nil, 0, errutil.ErrIllegalParameter 103 | } 104 | 105 | list, total, err := db.TradeList( 106 | r.AppID, 107 | r.ChannelID, 108 | r.OrderID, 109 | r.Start, 110 | r.End, 111 | r.Offset, 112 | r.Count) 113 | 114 | if err != nil { 115 | return nil, 0, err 116 | } 117 | 118 | result := make([]protocol.TradeInfo, len(list)) 119 | for i, order := range list { 120 | result[i] = protocol.TradeInfo{ 121 | OrderId: order.OrderId, 122 | Uid: strconv.FormatInt(order.Uid, 10), 123 | Money: order.Money, 124 | RealMoney: order.RealMoney, 125 | ProductName: order.ProductName, 126 | ProductCount: order.ProductCount, 127 | ServerName: order.ServerName, 128 | RoleId: order.RoleId, 129 | PayBy: order.PayPlatform, 130 | AppId: order.AppId, 131 | ChannelId: order.ChannelId, 132 | PayPlatformUid: order.ComsumerId, 133 | PayAt: order.PayAt, 134 | Currency: order.Currency, 135 | } 136 | 137 | } 138 | return result, total, nil 139 | } 140 | 141 | func OrderList(r *protocol.OrderListRequest) ([]protocol.OrderInfo, int, error) { 142 | if r == nil { 143 | return nil, 0, errutil.ErrIllegalParameter 144 | } 145 | 146 | id, err := strconv.ParseInt(r.Uid, 10, 0) 147 | if err != nil { 148 | return nil, 0, err 149 | } 150 | 151 | list, total, err := db.OrderList( 152 | id, 153 | r.AppID, 154 | r.ChannelID, 155 | r.OrderID, 156 | r.PayBy, 157 | r.Start, 158 | r.End, 159 | int(r.Status), 160 | r.Offset, 161 | r.Count) 162 | 163 | if err != nil { 164 | return nil, 0, err 165 | } 166 | 167 | result := make([]protocol.OrderInfo, len(list)) 168 | for i, order := range list { 169 | result[i] = protocol.OrderInfo{ 170 | OrderId: order.OrderId, 171 | Uid: strconv.FormatInt(order.Uid, 10), 172 | Money: order.Money, 173 | RealMoney: order.RealMoney, 174 | ProductName: order.ProductName, 175 | ProductCount: order.ProductCount, 176 | ServerName: order.ServerName, 177 | RoleID: order.RoleId, 178 | PayBy: order.PayPlatform, 179 | AppId: order.AppId, 180 | Imei: order.Imei, 181 | Status: order.Status, 182 | Extra: order.Extra, 183 | CreatedAt: order.CreatedAt, 184 | } 185 | 186 | } 187 | return result, total, nil 188 | } 189 | 190 | //订单列表 191 | func orderList(r *http.Request, form *nex.Form) (*protocol.OrderListResponse, error) { 192 | if !whitelist.VerifyIP(r.RemoteAddr) { 193 | return nil, errutil.ErrPermissionDenied 194 | } 195 | 196 | request := &protocol.OrderListRequest{} 197 | err := r.ParseForm() 198 | if err != nil { 199 | return nil, err 200 | } 201 | 202 | request.Offset = form.IntOrDefault("offset", 0) 203 | request.Count = form.IntOrDefault("count", -1) 204 | request.PayBy = strings.ToLower(form.Get("pay_by")) 205 | request.Status = uint8(form.IntOrDefault(form.Get("status"), 0)) 206 | request.AppID = form.Get("appid") 207 | request.ChannelID = form.Get("channel_id") 208 | request.Start = form.Int64OrDefault(form.Get("start"), -1) 209 | request.End = form.Int64OrDefault(form.Get("end"), -1) 210 | request.Uid = form.Get("uid") 211 | request.OrderID = form.Get("order_id") 212 | 213 | list, total, err := OrderList(request) 214 | if err != nil { 215 | return nil, err 216 | } 217 | return &protocol.OrderListResponse{Data: list, Total: total}, nil 218 | } 219 | 220 | func WechatNotify(r *protocol.WechatOrderCallbackRequest) (resp interface{}, err error) { 221 | var trade *model.Trade 222 | var order *model.Order 223 | if trade, resp, err = provider2.Wechat.Notify(r); err != nil { 224 | logger.Error(err.Error()) 225 | return nil, err 226 | } 227 | 228 | if order, err = db.QueryOrder(trade.OrderId); err != nil { 229 | logger.Error(err.Error()) 230 | return nil, err 231 | } 232 | 233 | if err := db.InsertTrade(trade); err != nil { 234 | //如果是重复通知,直接忽略之 235 | if err == errutil.ErrTradeExisted { 236 | return resp, nil 237 | } 238 | 239 | logger.Error(err.Error()) 240 | return nil, err 241 | } 242 | 243 | if err := db.UserAddCoin(order.Uid, int64(10)); err != nil { 244 | logger.Error(err.Error()) 245 | return nil, err 246 | } 247 | return resp, nil 248 | } 249 | 250 | func wechatCallback(_ context.Context, r *http.Request) (interface{}, error) { 251 | request := &protocol.WechatOrderCallbackRequest{} 252 | 253 | data, err := ioutil.ReadAll(r.Body) 254 | if err != nil { 255 | return nil, err 256 | } 257 | 258 | if err := xml.Unmarshal(data, request); err != nil { 259 | return nil, err 260 | } 261 | 262 | return WechatNotify(request) 263 | } 264 | -------------------------------------------------------------------------------- /internal/web/gm.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | "strings" 8 | "time" 9 | 10 | "github.com/lonng/nanoserver/db" 11 | "github.com/lonng/nanoserver/internal/game" 12 | "github.com/lonng/nanoserver/internal/web/api" 13 | "github.com/lonng/nanoserver/pkg/errutil" 14 | "github.com/lonng/nanoserver/protocol" 15 | "github.com/lonng/nex" 16 | log "github.com/sirupsen/logrus" 17 | ) 18 | 19 | func authFilter(_ context.Context, r *http.Request) (context.Context, error) { 20 | parts := strings.Split(r.RemoteAddr, ":") 21 | if len(parts) < 2 { 22 | return context.Background(), errutil.ErrPermissionDenied 23 | } 24 | 25 | if parts[0] != "127.0.0.1" { 26 | return context.Background(), errutil.ErrPermissionDenied 27 | } 28 | 29 | return context.Background(), nil 30 | } 31 | 32 | func broadcast(query *nex.Form) (*protocol.StringMessage, error) { 33 | message := strings.TrimSpace(query.Get("message")) 34 | if message == "" || len(message) < 5 { 35 | return nil, errors.New("消息不可小于5个字") 36 | } 37 | api.AddMessage(message) 38 | game.BroadcastSystemMessage(message) 39 | return protocol.SuccessMessage, nil 40 | } 41 | 42 | func resetPlayerHandler(query *nex.Form) (*protocol.StringMessage, error) { 43 | uid := query.Int64OrDefault("uid", -1) 44 | if uid <= 0 { 45 | return nil, errutil.ErrIllegalParameter 46 | } 47 | log.Infof("手动重置玩家数据: Uid=%d", uid) 48 | game.Reset(uid) 49 | return protocol.SuccessMessage, nil 50 | } 51 | 52 | func kickHandler(query *nex.Form) (*protocol.StringMessage, error) { 53 | uid := query.Int64OrDefault("uid", -1) 54 | if uid <= 0 { 55 | return nil, errutil.ErrIllegalParameter 56 | } 57 | 58 | log.Infof("踢玩家下线: Uid=%d", uid) 59 | if err := game.Kick(uid); err != nil { 60 | return nil, err 61 | } 62 | 63 | return protocol.SuccessMessage, nil 64 | } 65 | 66 | func onlineHandler(query *nex.Form) (interface{}, error) { 67 | begin := query.Int64OrDefault("begin", 0) 68 | end := query.Int64OrDefault("end", -1) 69 | if end < 0 { 70 | end = time.Now().Unix() 71 | } 72 | 73 | log.Infof("获取在线数据信息: begin=%s, end=%s", time.Unix(begin, 0).String(), time.Unix(end, 0).String()) 74 | return db.OnlineStats(begin, end) 75 | } 76 | 77 | func rechargeHandler(data *protocol.RechargeRequest) (*protocol.StringMessage, error) { 78 | if data.Uid < 1 || data.Count < 1 { 79 | return nil, errutil.ErrIllegalParameter 80 | } 81 | u, err := db.QueryUser(data.Uid) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | u.Coin += data.Count 87 | 88 | if err := db.UpdateUser(u); err != nil { 89 | return nil, err 90 | } 91 | 92 | // 通知客户端 93 | game.Recharge(u.Id, u.Coin) 94 | 95 | log.Infof("给玩家充值: Uid=%d, end=%d", data.Uid, data.Count) 96 | return protocol.SuccessMessage, nil 97 | } 98 | 99 | // http://127.0.0.1:12306/v1/gm/consume?consume="4/1,8/1,16/2" 100 | func cardConsumeHandler(query *nex.Form) (*protocol.StringMessage, error) { 101 | consume := query.Get("consume") 102 | if consume == "" { 103 | return nil, errutil.ErrIllegalParameter 104 | } 105 | log.Infof("手动重置房卡消耗数据: %s", consume) 106 | game.SetCardConsume(consume) 107 | return protocol.SuccessMessage, nil 108 | } 109 | func userInfoHandler(query *nex.Form) (interface{}, error) { 110 | id := query.Int64OrDefault("id", -1) 111 | if id <= 0 { 112 | return nil, errutil.ErrIllegalParameter 113 | } 114 | 115 | return db.QueryUserInfo(id) 116 | } 117 | -------------------------------------------------------------------------------- /internal/web/static/update/v1.9.3.kl.2.patch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/internal/web/static/update/v1.9.3.kl.2.patch -------------------------------------------------------------------------------- /internal/web/static/update/v1.9.3.kl.6.patch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/internal/web/static/update/v1.9.3.kl.6.patch -------------------------------------------------------------------------------- /internal/web/static/update/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.9.3", 3 | "patch": 6, 4 | "android": "https://fir.im/tand", 5 | "ios": "https://fir.im/tios", 6 | "download": "http://39.108.135.229:12307/static/update/v1.9.3.kl.6.patch" 7 | } -------------------------------------------------------------------------------- /internal/web/stats.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "github.com/lonng/nex" 5 | log "github.com/sirupsen/logrus" 6 | 7 | "time" 8 | 9 | "github.com/lonng/nanoserver/db" 10 | "github.com/lonng/nanoserver/protocol" 11 | 12 | "github.com/lonng/nanoserver/pkg/errutil" 13 | ) 14 | 15 | var dayInternal = 24 * 60 * 60 16 | 17 | //注册用户数 18 | func registerUsersHandler(query *nex.Form) (interface{}, error) { 19 | begin := query.Int64OrDefault("from", 0) 20 | end := query.Int64OrDefault("to", -1) 21 | if end < 0 { 22 | end = time.Now().Unix() 23 | } 24 | 25 | log.Infof("获取注册用户信息: begin=%s, end=%s", time.Unix(begin, 0).String(), time.Unix(end, 0).String()) 26 | c, err := db.QueryRegisterUsers(begin, end) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | return protocol.CommonResponse{ 32 | Data: c, 33 | }, nil 34 | } 35 | 36 | //实时在线人数 37 | func onlineLiteHandler() (interface{}, error) { 38 | 39 | c, err := db.OnlineStatsLite() 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | return protocol.CommonResponse{ 45 | Data: c, 46 | }, nil 47 | } 48 | 49 | //某日的n日留存 50 | func retentionHandler(query *nex.Form) (interface{}, error) { 51 | from := query.IntOrDefault("from", -1) 52 | to := query.IntOrDefault("to", -1) 53 | 54 | if from < 0 || to < 0 || to < from { 55 | return nil, errutil.ErrIllegalParameter 56 | } 57 | 58 | list := []*protocol.Retention{} 59 | for i := from; i <= to; i += dayInternal { 60 | ret, err := db.RetentionList(i) 61 | if err != nil { 62 | return nil, err 63 | } 64 | list = append(list, ret) 65 | } 66 | 67 | return &protocol.RetentionResponse{Data: list}, nil 68 | 69 | } 70 | 71 | //从指定日开始到当前的每日房卡消耗 72 | func cardConsumeStatsHandler(query *nex.Form) (interface{}, error) { 73 | from := query.Int64OrDefault("from", 0) 74 | to := query.Int64OrDefault("to", 0) 75 | 76 | if to == 0 { 77 | from = time.Now().Unix() 78 | } 79 | 80 | ret, err := db.ConsumeStats(from, to) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | return &protocol.RetentionResponse{Data: ret}, nil 86 | 87 | } 88 | 89 | //从指定日开始到当前的每日活跃人数 90 | func activationUsersHandler(query *nex.Form) (interface{}, error) { 91 | from := query.Int64OrDefault("from", 0) 92 | to := query.Int64OrDefault("to", 0) 93 | 94 | if to == 0 { 95 | to = time.Now().Unix() 96 | } 97 | 98 | ret, err := db.QueryActivationUser(from, to) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | return &protocol.RetentionResponse{Data: ret}, nil 104 | 105 | } 106 | -------------------------------------------------------------------------------- /internal/web/web.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | 11 | "github.com/lonng/nanoserver/db" 12 | "github.com/lonng/nanoserver/internal/web/api" 13 | "github.com/lonng/nanoserver/pkg/algoutil" 14 | "github.com/lonng/nanoserver/pkg/whitelist" 15 | "github.com/lonng/nanoserver/protocol" 16 | "github.com/lonng/nex" 17 | log "github.com/sirupsen/logrus" 18 | "github.com/spf13/viper" 19 | ) 20 | 21 | type Closer func() 22 | 23 | var logger = log.WithField("component", "http") 24 | 25 | func dbStartup() func() { 26 | dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?%s", 27 | viper.GetString("database.username"), 28 | viper.GetString("database.password"), 29 | viper.GetString("database.host"), 30 | viper.GetString("database.port"), 31 | viper.GetString("database.dbname"), 32 | viper.GetString("database.args")) 33 | 34 | return db.MustStartup( 35 | dsn, 36 | db.MaxIdleConns(viper.GetInt("database.max_idle_conns")), 37 | db.MaxIdleConns(viper.GetInt("database.max_open_conns")), 38 | db.ShowSQL(viper.GetBool("database.show_sql"))) 39 | } 40 | 41 | func enableWhiteList() { 42 | whitelist.Setup(viper.GetStringSlice("whitelist.ip")) 43 | } 44 | 45 | func version() (*protocol.Version, error) { 46 | return &protocol.Version{ 47 | Version: viper.GetInt("update.version"), 48 | Android: viper.GetString("update.android"), 49 | IOS: viper.GetString("update.ios"), 50 | }, nil 51 | } 52 | 53 | func pongHandler() (string, error) { 54 | return "pong", nil 55 | } 56 | 57 | func logRequest(ctx context.Context, r *http.Request) (context.Context, error) { 58 | if uri := r.RequestURI; uri != "/ping" { 59 | logger.Debugf("Method=%s, RemoteAddr=%s URL=%s", r.Method, r.RemoteAddr, uri) 60 | } 61 | return ctx, nil 62 | } 63 | 64 | func startupService() http.Handler { 65 | var ( 66 | mux = http.NewServeMux() 67 | webDir = viper.GetString("webserver.static_dir") 68 | ) 69 | 70 | nex.Before(logRequest) 71 | mux.Handle("/v1/user/", api.MakeLoginService()) 72 | mux.Handle("/v1/order/", api.MakeOrderService()) 73 | mux.Handle("/v1/history/", api.MakeHistoryService()) 74 | mux.Handle("/v1/desk/", api.MakeDeskService()) 75 | mux.Handle("/v1/version", nex.Handler(version)) 76 | 77 | // GM系统命令 78 | mux.Handle("/v1/gm/reset", nex.Handler(resetPlayerHandler).Before(authFilter)) // 重置玩家未完成房间状态 79 | mux.Handle("/v1/gm/consume", nex.Handler(cardConsumeHandler).Before(authFilter)) // 设置房卡消耗 80 | mux.Handle("/v1/gm/broadcast", nex.Handler(broadcast).Before(authFilter)) // 消息广播 81 | mux.Handle("/v1/gm/kick", nex.Handler(kickHandler).Before(authFilter)) // 踢人 82 | mux.Handle("/v1/gm/online", nex.Handler(onlineHandler).Before(authFilter)) // 在线信息 83 | mux.Handle("/v1/gm/recharge", nex.Handler(rechargeHandler).Before(authFilter)) // 玩家充值 84 | mux.Handle("/v1/gm/query/user/", nex.Handler(userInfoHandler)) // 玩家信息查询 85 | 86 | //统计后台 87 | mux.Handle("/v1/stats/user/register", nex.Handler(registerUsersHandler).Before(authFilter)) // 注册人数 88 | mux.Handle("/v1/stats/user/activation", nex.Handler(activationUsersHandler).Before(authFilter)) // 活跃人数 89 | mux.Handle("/v1/stats/online", nex.Handler(onlineLiteHandler).Before(authFilter)) // 同时在线人、桌数 90 | mux.Handle("/v1/stats/retention", nex.Handler(retentionHandler).Before(authFilter)) // 留存 91 | mux.Handle("/v1/stats/consume", nex.Handler(cardConsumeStatsHandler).Before(authFilter)) // 房卡消耗 92 | 93 | mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(webDir)))) 94 | mux.Handle("/ping", nex.Handler(pongHandler)) 95 | 96 | return algoutil.AccessControl(algoutil.OptionControl(mux)) 97 | } 98 | 99 | func Startup() { 100 | // setup database 101 | closer := dbStartup() 102 | defer closer() 103 | 104 | // enable white list 105 | enableWhiteList() 106 | 107 | var ( 108 | addr = viper.GetString("webserver.addr") 109 | cert = viper.GetString("webserver.certificates.cert") 110 | key = viper.GetString("webserver.certificates.key") 111 | enableSSL = viper.GetBool("webserver.enable_ssl") 112 | ) 113 | 114 | logger.Infof("Web service addr: %s(enable ssl: %v)", addr, enableSSL) 115 | go func() { 116 | // http service 117 | mux := startupService() 118 | if enableSSL { 119 | log.Fatal(http.ListenAndServeTLS(addr, cert, key, mux)) 120 | } else { 121 | log.Fatal(http.ListenAndServe(addr, mux)) 122 | } 123 | }() 124 | 125 | sg := make(chan os.Signal) 126 | signal.Notify(sg, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL) 127 | // stop server 128 | select { 129 | case s := <-sg: 130 | log.Infof("got signal: %s", s.String()) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /k8s-devops/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/k8s-devops/README.md -------------------------------------------------------------------------------- /k8s-devops/nanoserver/ingressroute-tcp.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: traefik.containo.us/v1alpha1 2 | kind: IngressRouteTCP 3 | metadata: 4 | name: nanoserver-game-route 5 | spec: 6 | entryPoints: 7 | - nanoserver-gm 8 | routes: 9 | - match: HostSNI(`*`) 10 | kind: Rule 11 | services: 12 | - name: nanoserver 13 | port: 30251 -------------------------------------------------------------------------------- /k8s-devops/nanoserver/nanoserver-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: nanoserver-config 5 | labels: 6 | app: nanoserver 7 | data: 8 | config.toml: |- 9 | [core] 10 | # enable debug mode 11 | debug = true 12 | heartbeat = 30 13 | consume = "4/2,8/3,16/4" #房卡消耗, 使用逗号隔开, 局数/房卡数, 例如4局消耗1张, 8局消耗1张, 16局消耗2张, 则为: 4/1,8/1,16/2 14 | 15 | #WEB服务器设置 16 | [webserver] 17 | addr = "0.0.0.0:12307" #监听地址 18 | enable_ssl = false #是否使用https, 如果为true, 则必须配置cert和key的路径 19 | static_dir = "web/static" 20 | 21 | #证书设置 22 | [webserver.certificates] 23 | cert = "configs/****.crt" #证书路径 24 | key = "configs/****.key" #Key路径 25 | 26 | [game-server] 27 | host = "nanoserver.your-domain.com" 28 | port = 30251 29 | 30 | # Redis server config 31 | [redis] 32 | host = "127.0.0.1" 33 | port = 6357 34 | 35 | # Mysql server config 36 | [database] 37 | host = "nanoserver-mysql" 38 | port = 3306 39 | dbname = "scmj" 40 | password = "hacker12345" 41 | username = "root" 42 | args = "charset=utf8mb4" 43 | buf_size = 10 44 | max_idle_conns = 20 45 | max_open_conns = 15 46 | show_sql = true 47 | 48 | # 微信 49 | [wechat] 50 | appid = "YOUR_WX_APPID" 51 | appsecret = "YOUR_APP_SECRET" 52 | callback_url = "YOUR_CALLBACK" 53 | mer_id = "YOUR_MER_ID" 54 | unify_order_url = "https://api.mch.weixin.qq.com/pay/unifiedorder" 55 | 56 | #Token设置 57 | [token] 58 | expires = 21600 #token过期时间 59 | 60 | #白名单设置 61 | [whitelist] 62 | ip = ["10.10.*", "127.0.0.1", ".*"] #白名单地址, 支持golang正则表达式语法 63 | 64 | #分享信息 65 | [share] 66 | title = "血战到底" 67 | desc = "纯正四川玩法,快捷便利的掌上血战,轻松组局,随时随地尽情游戏" 68 | 69 | #更新设置 70 | [update] 71 | force = true #是否强制更新 72 | version = "1.9.3" 73 | android = "https://fir.im/tand" 74 | ios = "https://fir.im/tios" 75 | 76 | #联系设置 77 | [contact] 78 | daili1 = "kefuweixin01" 79 | daili2 = "kefuweixin01" 80 | kefu1 = "kefuweixin01" 81 | 82 | #语音账号http://gcloud.qq.com/product/6 83 | [voice] 84 | appid = "xxx" 85 | appkey = "xxx" 86 | 87 | #广播消息 88 | [broadcast] 89 | message = ["系统消息:健康游戏,禁止赌博", "欢迎进入游戏"] 90 | 91 | #登陆相关 92 | [login] 93 | guest = true 94 | lists = ["test", "konglai"] -------------------------------------------------------------------------------- /k8s-devops/nanoserver/nanoserver/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /k8s-devops/nanoserver/nanoserver/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: nanoserver 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | version: 0.1.0 18 | 19 | # This is the version number of the application being deployed. This version number should be 20 | # incremented each time you make changes to the application. 21 | appVersion: APP_VERSION 22 | -------------------------------------------------------------------------------- /k8s-devops/nanoserver/nanoserver/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "nanoserver.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "nanoserver.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "nanoserver.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "nanoserver.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | echo "Visit http://127.0.0.1:8080 to use your application" 20 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:80 21 | {{- end }} 22 | -------------------------------------------------------------------------------- /k8s-devops/nanoserver/nanoserver/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "nanoserver.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "nanoserver.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "nanoserver.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "nanoserver.labels" -}} 38 | helm.sh/chart: {{ include "nanoserver.chart" . }} 39 | {{ include "nanoserver.selectorLabels" . }} 40 | {{- if .Chart.AppVersion }} 41 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 42 | {{- end }} 43 | app.kubernetes.io/managed-by: {{ .Release.Service }} 44 | {{- end -}} 45 | 46 | {{/* 47 | Selector labels 48 | */}} 49 | {{- define "nanoserver.selectorLabels" -}} 50 | app.kubernetes.io/name: {{ include "nanoserver.name" . }} 51 | app.kubernetes.io/instance: {{ .Release.Name }} 52 | {{- end -}} 53 | 54 | {{/* 55 | Create the name of the service account to use 56 | */}} 57 | {{- define "nanoserver.serviceAccountName" -}} 58 | {{- if .Values.serviceAccount.create -}} 59 | {{ default (include "nanoserver.fullname" .) .Values.serviceAccount.name }} 60 | {{- else -}} 61 | {{ default "default" .Values.serviceAccount.name }} 62 | {{- end -}} 63 | {{- end -}} 64 | -------------------------------------------------------------------------------- /k8s-devops/nanoserver/nanoserver/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "nanoserver.fullname" . }} 5 | labels: 6 | {{- include "nanoserver.labels" . | nindent 4 }} 7 | spec: 8 | replicas: {{ .Values.replicaCount }} 9 | selector: 10 | matchLabels: 11 | {{- include "nanoserver.selectorLabels" . | nindent 6 }} 12 | template: 13 | metadata: 14 | labels: 15 | {{- include "nanoserver.selectorLabels" . | nindent 8 }} 16 | spec: 17 | {{- with .Values.imagePullSecrets }} 18 | imagePullSecrets: 19 | {{- toYaml . | nindent 8 }} 20 | {{- end }} 21 | serviceAccountName: {{ include "nanoserver.serviceAccountName" . }} 22 | securityContext: 23 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 24 | containers: 25 | - name: {{ .Chart.Name }} 26 | securityContext: 27 | {{- toYaml .Values.securityContext | nindent 12 }} 28 | image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}" 29 | imagePullPolicy: {{ .Values.image.pullPolicy }} 30 | ports: 31 | - name: http 32 | containerPort: {{ .Values.service.containerPort }} 33 | protocol: TCP 34 | - name: game 35 | containerPort: {{ .Values.service.containerGamePort }} 36 | protocol: TCP 37 | livenessProbe: 38 | httpGet: 39 | path: /ping 40 | port: http 41 | readinessProbe: 42 | httpGet: 43 | path: /ping 44 | port: http 45 | {{- if .Values.configMap }} 46 | volumeMounts: 47 | - name: config 48 | mountPath: /home/app/configs 49 | {{- end }} 50 | resources: 51 | {{- toYaml .Values.resources | nindent 12 }} 52 | {{- if .Values.configMap }} 53 | volumes: 54 | - name: config 55 | configMap: 56 | name: {{ .Values.configMap }} 57 | {{- end }} 58 | {{- with .Values.nodeSelector }} 59 | nodeSelector: 60 | {{- toYaml . | nindent 8 }} 61 | {{- end }} 62 | {{- with .Values.affinity }} 63 | affinity: 64 | {{- toYaml . | nindent 8 }} 65 | {{- end }} 66 | {{- with .Values.tolerations }} 67 | tolerations: 68 | {{- toYaml . | nindent 8 }} 69 | {{- end }} 70 | -------------------------------------------------------------------------------- /k8s-devops/nanoserver/nanoserver/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "nanoserver.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 5 | apiVersion: networking.k8s.io/v1beta1 6 | {{- else -}} 7 | apiVersion: extensions/v1beta1 8 | {{- end }} 9 | kind: Ingress 10 | metadata: 11 | name: {{ $fullName }} 12 | labels: 13 | {{- include "nanoserver.labels" . | nindent 4 }} 14 | {{- with .Values.ingress.annotations }} 15 | annotations: 16 | {{- toYaml . | nindent 4 }} 17 | {{- end }} 18 | spec: 19 | {{- if .Values.ingress.tls }} 20 | tls: 21 | {{- range .Values.ingress.tls }} 22 | - hosts: 23 | {{- range .hosts }} 24 | - {{ . | quote }} 25 | {{- end }} 26 | secretName: {{ .secretName }} 27 | {{- end }} 28 | {{- end }} 29 | rules: 30 | {{- range .Values.ingress.hosts }} 31 | - host: {{ .host | quote }} 32 | http: 33 | paths: 34 | {{- range .paths }} 35 | - path: {{ . }} 36 | backend: 37 | serviceName: {{ $fullName }} 38 | servicePort: {{ $svcPort }} 39 | {{- end }} 40 | {{- end }} 41 | {{- end }} 42 | -------------------------------------------------------------------------------- /k8s-devops/nanoserver/nanoserver/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "nanoserver.fullname" . }} 5 | labels: 6 | {{- include "nanoserver.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | - port: {{ .Values.service.gamePort }} 15 | targetPort: game 16 | protocol: TCP 17 | name: game 18 | selector: 19 | {{- include "nanoserver.selectorLabels" . | nindent 4 }} -------------------------------------------------------------------------------- /k8s-devops/nanoserver/nanoserver/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "nanoserver.serviceAccountName" . }} 6 | labels: 7 | {{ include "nanoserver.labels" . | nindent 4 }} 8 | {{- end -}} 9 | -------------------------------------------------------------------------------- /k8s-devops/nanoserver/nanoserver/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "nanoserver.fullname" . }}-test-connection" 5 | labels: 6 | {{ include "nanoserver.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test-success 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "nanoserver.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /k8s-devops/nanoserver/nanoserver/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for react-notes. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: nginx 9 | pullPolicy: IfNotPresent 10 | 11 | imagePullSecrets: [] 12 | nameOverride: "" 13 | fullnameOverride: "" 14 | 15 | serviceAccount: 16 | # Specifies whether a service account should be created 17 | create: true 18 | # The name of the service account to use. 19 | # If not set and create is true, a name is generated using the fullname template 20 | name: 21 | 22 | podSecurityContext: {} 23 | # fsGroup: 2000 24 | 25 | securityContext: {} 26 | # capabilities: 27 | # drop: 28 | # - ALL 29 | # readOnlyRootFilesystem: true 30 | # runAsNonRoot: true 31 | # runAsUser: 1000 32 | 33 | service: 34 | type: ClusterIP 35 | port: 80 36 | 37 | ingress: 38 | enabled: false 39 | annotations: {} 40 | # kubernetes.io/ingress.class: nginx 41 | # kubernetes.io/tls-acme: "true" 42 | hosts: 43 | - host: chart-example.local 44 | paths: [] 45 | tls: [] 46 | # - secretName: chart-example-tls 47 | # hosts: 48 | # - chart-example.local 49 | 50 | resources: {} 51 | # We usually recommend not to specify default resources and to leave this as a conscious 52 | # choice for the user. This also increases chances charts run on environments with little 53 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 54 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 55 | # limits: 56 | # cpu: 100m 57 | # memory: 128Mi 58 | # requests: 59 | # cpu: 100m 60 | # memory: 128Mi 61 | 62 | nodeSelector: {} 63 | 64 | tolerations: [] 65 | 66 | affinity: {} 67 | -------------------------------------------------------------------------------- /k8s-devops/nanoserver/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for nanoserver. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: hub.your-domain.com/library/nanoserver 9 | pullPolicy: Always 10 | 11 | imagePullSecrets: [] 12 | nameOverride: "" 13 | fullnameOverride: "" 14 | 15 | serviceAccount: 16 | # Specifies whether a service account should be created 17 | create: true 18 | # The name of the service account to use. 19 | # If not set and create is true, a name is generated using the fullname template 20 | name: 21 | 22 | podSecurityContext: {} 23 | # fsGroup: 2000 24 | 25 | securityContext: {} 26 | # capabilities: 27 | # drop: 28 | # - ALL 29 | # readOnlyRootFilesystem: true 30 | # runAsNonRoot: true 31 | # runAsUser: 1000 32 | 33 | configMap: 'nanoserver-config' 34 | 35 | service: 36 | type: ClusterIP 37 | port: 80 38 | containerPort: 12307 39 | gamePort: 30251 40 | containerGamePort: 30251 41 | 42 | ingress: 43 | enabled: true 44 | annotations: 45 | ingress.kubernetes.io/ssl-redirect: "true" 46 | ingress.kubernetes.io/proxy-body-size: "0" 47 | kubernetes.io/ingress.class: "traefik" 48 | traefik.ingress.kubernetes.io/router.tls: "true" 49 | traefik.ingress.kubernetes.io/router.entrypoints: websecure 50 | hosts: 51 | - host: nanoserver.your-domain.com 52 | paths: 53 | - / 54 | tls: 55 | - secretName: your-domain-cert-tls 56 | hosts: 57 | - nanoserver.your-domain.com 58 | 59 | resources: 60 | # We usually recommend not to specify default resources and to leave this as a conscious 61 | # choice for the user. This also increases chances charts run on environments with little 62 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 63 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 64 | limits: 65 | cpu: 500m 66 | memory: 512Mi 67 | requests: 68 | cpu: 100m 69 | memory: 128Mi 70 | 71 | nodeSelector: {} 72 | 73 | tolerations: [] 74 | 75 | affinity: {} 76 | -------------------------------------------------------------------------------- /mahjong.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/mahjong.apk -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime/pprof" 7 | "sync" 8 | "time" 9 | 10 | "github.com/lonng/nanoserver/internal/game" 11 | "github.com/lonng/nanoserver/internal/web" 12 | 13 | _ "github.com/go-sql-driver/mysql" 14 | log "github.com/sirupsen/logrus" 15 | "github.com/spf13/viper" 16 | "github.com/urfave/cli" 17 | ) 18 | 19 | func main() { 20 | app := cli.NewApp() 21 | 22 | // base application info 23 | app.Name = "mahjong server" 24 | app.Author = "MaJong" 25 | app.Version = "0.0.1" 26 | app.Copyright = "majong team reserved" 27 | app.Usage = "majiang server" 28 | 29 | // flags 30 | app.Flags = []cli.Flag{ 31 | cli.StringFlag{ 32 | Name: "config, c", 33 | Value: "./configs/config.toml", 34 | Usage: "load configuration from `FILE`", 35 | }, 36 | cli.BoolFlag{ 37 | Name: "cpuprofile", 38 | Usage: "enable cpu profile", 39 | }, 40 | } 41 | 42 | app.Action = serve 43 | app.Run(os.Args) 44 | } 45 | 46 | func serve(c *cli.Context) error { 47 | viper.SetConfigType("toml") 48 | viper.SetConfigFile(c.String("config")) 49 | viper.ReadInConfig() 50 | 51 | log.SetFormatter(&log.TextFormatter{DisableColors: true}) 52 | if viper.GetBool("core.debug") { 53 | log.SetLevel(log.DebugLevel) 54 | } 55 | 56 | if c.Bool("cpuprofile") { 57 | filename := fmt.Sprintf("cpuprofile-%d.pprof", time.Now().Unix()) 58 | f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, os.ModePerm) 59 | if err != nil { 60 | panic(err) 61 | } 62 | pprof.StartCPUProfile(f) 63 | defer pprof.StopCPUProfile() 64 | } 65 | 66 | wg := sync.WaitGroup{} 67 | wg.Add(2) 68 | 69 | go func() { defer wg.Done(); game.Startup() }() // 开启游戏服 70 | go func() { defer wg.Done(); web.Startup() }() // 开启web服务器 71 | 72 | wg.Wait() 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /media/wechat-group.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/media/wechat-group.jpg -------------------------------------------------------------------------------- /media/wechat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/media/wechat.jpg -------------------------------------------------------------------------------- /media/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/media/wechat.png -------------------------------------------------------------------------------- /pkg/algoutil/algoutil_test.go: -------------------------------------------------------------------------------- 1 | package algoutil 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGenRSAKey(t *testing.T) { 8 | ts := []string{"hello world", "miss right"} 9 | priv, pub, err := GenRSAKey() 10 | if err != nil { 11 | t.Fatal(err) 12 | } 13 | 14 | for _, tstr := range ts { 15 | c, err := RSAEncrypt([]byte(tstr), pub) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | d, err := RSADecrypt(c, priv) 20 | if string(d) != tstr { 21 | t.Fail() 22 | } 23 | } 24 | } 25 | 26 | func BenchmarkGenRSAKey(b *testing.B) { 27 | for i := 0; i < b.N; i++ { 28 | GenRSAKey() 29 | } 30 | } 31 | 32 | func TestMaskPhone(t *testing.T) { 33 | _, err := MaskPhone("128888888888") 34 | if err == nil { 35 | t.Fail() 36 | } 37 | 38 | _, err = MaskPhone("12888888888") 39 | if err != nil { 40 | t.Fail() 41 | } 42 | 43 | _, err = MaskPhone("15222544") 44 | if err == nil { 45 | t.Fail() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pkg/algoutil/crypt.go: -------------------------------------------------------------------------------- 1 | package algoutil 2 | 3 | import ( 4 | "crypto/rsa" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "errors" 8 | "fmt" 9 | "io/ioutil" 10 | ) 11 | 12 | func pemParse(data []byte, pemType string) ([]byte, error) { 13 | block, _ := pem.Decode(data) 14 | if block == nil { 15 | return nil, errors.New("No PEM block found") 16 | } 17 | if pemType != "" && block.Type != pemType { 18 | return nil, fmt.Errorf("Key's type is '%s', expected '%s'", block.Type, pemType) 19 | } 20 | return block.Bytes, nil 21 | } 22 | 23 | func ParsePrivateKey(data []byte) (*rsa.PrivateKey, error) { 24 | pemData, err := pemParse(data, "RSA PRIVATE KEY") 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | return x509.ParsePKCS1PrivateKey(pemData) 30 | } 31 | 32 | func LoadPrivateKey(privKeyPath string) (*rsa.PrivateKey, error) { 33 | certPEMBlock, err := ioutil.ReadFile(privKeyPath) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return ParsePrivateKey(certPEMBlock) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/algoutil/params.go: -------------------------------------------------------------------------------- 1 | package algoutil 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/url" 8 | "sort" 9 | "strings" 10 | 11 | "github.com/lonng/nanoserver/pkg/errutil" 12 | ) 13 | 14 | const ( 15 | argSep = "&" 16 | kvSep = "=" 17 | ) 18 | 19 | func ParseParams(params string) map[string]string { 20 | m := map[string]string{} 21 | parts := strings.Split(params, argSep) 22 | for _, arg := range parts { 23 | // strings.Split(s, "=") will cause error when signature has 24 | // padding(that is something like "==") 25 | i := strings.IndexAny(arg, kvSep) 26 | if i < 0 { 27 | continue 28 | } 29 | k := arg[:i] 30 | v := arg[i+1:] 31 | m[k] = v 32 | } 33 | return m 34 | } 35 | 36 | // TODO: WARNING!!!!!!! 37 | // All struct field must be string type 38 | func ParamsToStruct(params string, v interface{}) error { 39 | if params == "" || len(strings.TrimSpace(params)) < 1 { 40 | return errutil.ErrIllegalParameter 41 | } 42 | m := ParseParams(params) 43 | bytes, err := json.Marshal(m) 44 | if err != nil { 45 | return err 46 | } 47 | return json.Unmarshal(bytes, v) 48 | } 49 | 50 | func SortParams(m map[string]string) string { 51 | if len(m) == 0 { 52 | return "" 53 | } 54 | var ( 55 | i = 0 56 | keys = make([]string, len(m)) 57 | buf = &bytes.Buffer{} 58 | ) 59 | for k := range m { 60 | keys[i] = k 61 | i++ 62 | } 63 | sort.Strings(keys) 64 | 65 | for _, k := range keys { 66 | buf.WriteString(k) 67 | buf.WriteString("=") 68 | buf.WriteString(m[k]) 69 | buf.WriteString("&") 70 | } 71 | buf.Truncate(buf.Len() - 1) 72 | return buf.String() 73 | } 74 | 75 | func ConcatWithURLEncode(params map[string]string) *bytes.Buffer { 76 | if params == nil || len(params) == 0 { 77 | return nil 78 | } 79 | 80 | buf := &bytes.Buffer{} 81 | for k, v := range params { 82 | buf.WriteString(k) 83 | buf.WriteString("=") 84 | buf.WriteString(url.QueryEscape(v)) 85 | buf.WriteString("&") 86 | } 87 | 88 | buf.Truncate(buf.Len() - 1) 89 | 90 | return buf 91 | } 92 | 93 | // SortAndConcat sort the map by key in ASCII order, 94 | // and concat it in form of "k1=v1&k2=v2" 95 | func SortAndConcat(params map[string]string, extras ...bool) []byte { 96 | if params == nil || len(params) == 0 { 97 | return nil 98 | } 99 | 100 | var trimSpace bool = true 101 | if len(extras) > 0 { 102 | trimSpace = extras[0] 103 | } 104 | 105 | keys := make([]string, len(params)) 106 | i := 0 107 | for k := range params { 108 | keys[i] = k 109 | i++ 110 | } 111 | sort.Strings(keys) 112 | buf := &bytes.Buffer{} 113 | for _, k := range keys { 114 | if trimSpace && params[k] == "" { 115 | continue 116 | } 117 | buf.WriteString(fmt.Sprintf("%s=%s&", k, params[k])) 118 | 119 | } 120 | buf.Truncate(buf.Len() - 1) 121 | return buf.Bytes() 122 | } 123 | -------------------------------------------------------------------------------- /pkg/algoutil/params_test.go: -------------------------------------------------------------------------------- 1 | package algoutil 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type Test struct { 8 | Foo, Bar, FooBar string 9 | } 10 | 11 | func TestParamsToStruct(t *testing.T) { 12 | test := &Test{} 13 | ParamsToStruct("foo=hello&bar=world&foobar=helloworld", test) 14 | if test.Foo != "hello" || test.Bar != "world" || test.FooBar != "helloworld" { 15 | t.Fail() 16 | } 17 | } 18 | 19 | func TestSortParams(t *testing.T) { 20 | if SortParams(ParseParams("zdf=sdff&b=c&a=c&aaaaa=cdf")) != "a=c&aaaaa=cdf&b=c&zdf=sdff" { 21 | t.Fail() 22 | } 23 | unsorted := "discount=0.00&payment_type=1&subject=金币&trade_no=2016031421001004170242826341&buyer_email=15520707860&gmt_create=2016-03-14 16:10:24¬ify_type=trade_status_sync&quantity=1&out_trade_no=426006374921535488&seller_id=2088801054724902¬ify_time=2016-03-14 16:10:25&body=500金币&trade_status=TRADE_SUCCESS&is_total_fee_adjust=N&total_fee=0.01&gmt_payment=2016-03-14 16:10:25&seller_email=854761339@qq.com&price=0.01&buyer_id=2088402810244179¬ify_id=cfdcab45ae04498a568e03357edd6cehba&use_coupon=N&sign_type=RSA&sign=e+VYyBpGyLubIdOMpQt0B/StveBZgthVcTbsNEFFmUgjJ+Bahl+3pf5g0Dim1yBZ3Sxz4C57qrozqfzjdVhWf/SEs8QWyf6+V4LgcosTTWpXBLXnVWfMvInGuxOUrufh9fG874tIISMkPPrkud+vsTn6wcqetipBh+wM+P7J9NI=" 24 | sorted := "body=500金币&buyer_email=15520707860&buyer_id=2088402810244179&discount=0.00&gmt_create=2016-03-14 16:10:24&gmt_payment=2016-03-14 16:10:25&is_total_fee_adjust=N¬ify_id=cfdcab45ae04498a568e03357edd6cehba¬ify_time=2016-03-14 16:10:25¬ify_type=trade_status_sync&out_trade_no=426006374921535488&payment_type=1&price=0.01&quantity=1&seller_email=854761339@qq.com&seller_id=2088801054724902&sign=e+VYyBpGyLubIdOMpQt0B/StveBZgthVcTbsNEFFmUgjJ+Bahl+3pf5g0Dim1yBZ3Sxz4C57qrozqfzjdVhWf/SEs8QWyf6+V4LgcosTTWpXBLXnVWfMvInGuxOUrufh9fG874tIISMkPPrkud+vsTn6wcqetipBh+wM+P7J9NI=&sign_type=RSA&subject=金币&total_fee=0.01&trade_no=2016031421001004170242826341&trade_status=TRADE_SUCCESS&use_coupon=N" 25 | if SortParams(ParseParams(unsorted)) != sorted { 26 | t.Fail() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/algoutil/password.go: -------------------------------------------------------------------------------- 1 | package algoutil 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha1" 6 | "encoding/base64" 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/pborman/uuid" 11 | ) 12 | 13 | func passwordHash(pwd, salt string) string { 14 | buf := &bytes.Buffer{} 15 | fmt.Fprintf(buf, "%s%x%s", salt, pwd, salt) 16 | result1 := sha1.Sum(buf.Bytes()) 17 | 18 | buf.Reset() 19 | 20 | fmt.Fprintf(buf, "%s%s%x%s%s", pwd, salt, result1, salt, pwd) 21 | result2 := sha1.Sum(buf.Bytes()) 22 | 23 | buf.Reset() 24 | fmt.Fprintf(buf, "%x", result2) 25 | return base64.StdEncoding.EncodeToString(buf.Bytes()) 26 | } 27 | 28 | // PasswordHash accept password and generate with uuid as salt 29 | // FORMAT: sha1.Sum(pwd + salt + sha1.Sum(salt + pwd + salt) + salt + pwd) 30 | func PasswordHash(pwd string) (hash, salt string) { 31 | salt = strings.Replace(uuid.New(), "-", "", -1) 32 | hash = passwordHash(pwd, salt) 33 | return hash, salt 34 | } 35 | 36 | func VerifyPassword(pwd, salt, hash string) bool { 37 | return passwordHash(pwd, salt) == hash 38 | } 39 | -------------------------------------------------------------------------------- /pkg/algoutil/password_test.go: -------------------------------------------------------------------------------- 1 | package algoutil 2 | 3 | import "testing" 4 | 5 | func TestPasswordHash(t *testing.T) { 6 | pwds := []string{ 7 | "hsdlfjsdlfjsldkjfsldj", 8 | "你好!!!", 9 | "superadmin", 10 | } 11 | 12 | for _, pwd := range pwds { 13 | hash, salt := PasswordHash(pwd) 14 | t.Logf("hash: %s\nsalt: %s\n pwd: %s\n", hash, salt, pwd) 15 | if !VerifyPassword(pwd, salt, hash) { 16 | t.Fail() 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pkg/async/async.go: -------------------------------------------------------------------------------- 1 | package async 2 | 3 | import "github.com/sirupsen/logrus" 4 | 5 | func pcall(fn func()) { 6 | defer func() { 7 | if err := recover(); err != nil { 8 | logrus.Errorf("aync/pcall: Error=%v", err) 9 | } 10 | }() 11 | 12 | fn() 13 | } 14 | 15 | func Run(fn func()) { 16 | go pcall(fn) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/constant/const.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | type Behavior int 4 | 5 | const ( 6 | BehaviorNone Behavior = iota 7 | BehaviorPeng 8 | BehaviorGang 9 | BehaviorAnGang 10 | BehaviorBaGang 11 | BehaviorHu 12 | ) 13 | 14 | type DeskStatus int32 15 | 16 | const ( 17 | //创建桌子 18 | DeskStatusCreate DeskStatus = iota 19 | //发牌 20 | DeskStatusDuanPai 21 | //齐牌 22 | DeskStatusQiPai 23 | //游戏 24 | DeskStatusPlaying 25 | DeskStatusRoundOver 26 | //游戏终/中止 27 | DeskStatusInterruption 28 | //已销毁 29 | DeskStatusDestory 30 | //已经清洗,即为下一轮准备好 31 | DeskStatusCleaned 32 | ) 33 | 34 | var stringify = [...]string{ 35 | DeskStatusCreate: "创建", 36 | DeskStatusDuanPai: "发牌", 37 | DeskStatusQiPai: "齐牌", 38 | DeskStatusPlaying: "游戏中", 39 | DeskStatusRoundOver: "单局完成", 40 | DeskStatusInterruption: "游戏终/中止", 41 | DeskStatusDestory: "已销毁", 42 | DeskStatusCleaned: "已清洗", 43 | } 44 | 45 | func (s DeskStatus) String() string { 46 | return stringify[s] 47 | } 48 | -------------------------------------------------------------------------------- /pkg/crypto/crypto.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto" 5 | "crypto/md5" 6 | "crypto/rsa" 7 | "crypto/sha1" 8 | "crypto/x509" 9 | "encoding/base64" 10 | "encoding/pem" 11 | "fmt" 12 | "io/ioutil" 13 | 14 | "github.com/lonng/nanoserver/pkg/errutil" 15 | "golang.org/x/crypto/pkcs12" 16 | ) 17 | 18 | func ParsePrivateKey(data []byte) (*rsa.PrivateKey, error) { 19 | pemData, err := pemParse(data, "RSA PRIVATE KEY") 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return x509.ParsePKCS1PrivateKey(pemData) 25 | } 26 | 27 | func ParsePublicKey(data []byte) (*rsa.PublicKey, error) { 28 | pemData, err := pemParse(data, "PUBLIC KEY") 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | keyInterface, err := x509.ParsePKIXPublicKey(pemData) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | pubKey, ok := keyInterface.(*rsa.PublicKey) 39 | if !ok { 40 | return nil, fmt.Errorf("Could not cast parsed key to *rsa.PublickKey") 41 | } 42 | 43 | return pubKey, nil 44 | } 45 | 46 | func ParseCertSerialNo(data []byte) (string, error) { 47 | 48 | if data == nil { 49 | return "", errutil.ErrInvalidParameter 50 | } 51 | 52 | // Extract the PEM-encoded data block 53 | pemData, err := pemParse(data, "CERTIFICATE") 54 | if err != nil { 55 | return "", err 56 | } 57 | 58 | // Decode the certificate 59 | cert, err := x509.ParseCertificate(pemData) 60 | if err != nil { 61 | return "", fmt.Errorf("bad private key: %s", err) 62 | } 63 | 64 | return cert.SerialNumber.String(), nil 65 | } 66 | 67 | func LoadCertSerialNo(certPath string) (string, error) { 68 | 69 | pemData, err := ioutil.ReadFile(certPath) 70 | if err != nil { 71 | return "", err 72 | } 73 | return ParseCertSerialNo(pemData) 74 | } 75 | 76 | func pemParse(data []byte, pemType string) ([]byte, error) { 77 | block, _ := pem.Decode(data) 78 | if block == nil { 79 | return nil, fmt.Errorf("No PEM block found") 80 | } 81 | if pemType != "" && block.Type != pemType { 82 | return nil, fmt.Errorf("Key's type is '%s', expected '%s'", block.Type, pemType) 83 | } 84 | return block.Bytes, nil 85 | } 86 | 87 | func LoadPublicKey(pubKeyPath string) (*rsa.PublicKey, error) { 88 | certPEMBlock, err := ioutil.ReadFile(pubKeyPath) 89 | if err != nil { 90 | return nil, err 91 | } 92 | return ParsePublicKey(certPEMBlock) 93 | } 94 | 95 | func LoadPubKeyFromCert(certPath string) (*rsa.PublicKey, error) { 96 | data, err := ioutil.ReadFile(certPath) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | // Extract the PEM-encoded data block 102 | pemData, err := pemParse(data, "CERTIFICATE") 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | // Decode the certificate 108 | cert, err := x509.ParseCertificate(pemData) 109 | if err != nil { 110 | return nil, fmt.Errorf("bad certificate : %s", err) 111 | } 112 | 113 | return cert.PublicKey.(*rsa.PublicKey), nil 114 | } 115 | 116 | func LoadPrivateKey(privKeyPath string) (*rsa.PrivateKey, error) { 117 | certPEMBlock, err := ioutil.ReadFile(privKeyPath) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | return ParsePrivateKey(certPEMBlock) 123 | } 124 | 125 | func LoadPrivKeyAndCert(pfxPath string, password string) (*rsa.PrivateKey, *x509.Certificate, error) { 126 | 127 | pfxBlock, err := ioutil.ReadFile(pfxPath) 128 | if err != nil { 129 | return nil, nil, err 130 | } 131 | 132 | keyInterface, cert, err := pkcs12.Decode(pfxBlock, password) 133 | if err != nil { 134 | return nil, nil, err 135 | } 136 | return keyInterface.(*rsa.PrivateKey), cert, nil 137 | } 138 | 139 | func Sign(priKey *rsa.PrivateKey, data []byte) (string, error) { 140 | bs, err := rsa.SignPKCS1v15(nil, priKey, crypto.SHA1, SHA1Digest(data)) 141 | if err != nil { 142 | return "", errutil.ErrSignFailed 143 | } 144 | 145 | return base64.StdEncoding.EncodeToString(bs), nil 146 | } 147 | 148 | func Verify(pubKey *rsa.PublicKey, data []byte, sign string) error { 149 | bs, err := base64.StdEncoding.DecodeString(sign) 150 | if err != nil { 151 | return errutil.ErrServerInternal 152 | } 153 | 154 | err = rsa.VerifyPKCS1v15(pubKey, crypto.SHA1, SHA1Digest(data), bs) 155 | if err != nil { 156 | return errutil.ErrVerifyFailed 157 | } 158 | return nil 159 | } 160 | 161 | func VerifyRSAWithMD5(pubKey *rsa.PublicKey, data []byte, sign string) error { 162 | bs, err := base64.StdEncoding.DecodeString(sign) 163 | if err != nil { 164 | return errutil.ErrServerInternal 165 | } 166 | 167 | err = rsa.VerifyPKCS1v15(pubKey, crypto.MD5, MD5Digest(data), bs) 168 | if err != nil { 169 | return errutil.ErrVerifyFailed 170 | } 171 | return nil 172 | } 173 | 174 | //SHA1Digest generate a digest 175 | func SHA1Digest(data []byte) []byte { 176 | h := sha1.New() 177 | h.Write(data) 178 | 179 | return h.Sum(nil) 180 | } 181 | 182 | func MD5Digest(data []byte) []byte { 183 | h := md5.New() 184 | h.Write(data) 185 | 186 | return h.Sum(nil) 187 | } 188 | -------------------------------------------------------------------------------- /pkg/errutil/code.go: -------------------------------------------------------------------------------- 1 | package errutil 2 | 3 | const ( 4 | codeBase = 1000 5 | ) 6 | 7 | const ( 8 | Unknown = codeBase + iota 9 | yxBadRoute 10 | yxNotFound 11 | yxWrongType 12 | yxUserNameExists 13 | yxAuthFailed 14 | yxIllegalParameter 15 | yxNotRSAPublicKey 16 | yxNotRSAPrivateKey 17 | yxIllegalLoginType 18 | yxWrongPassword 19 | yxUserNameNotFound 20 | yxInitFailed 21 | yxServerInternal 22 | yxDbOperation 23 | yxCacheOperation 24 | yxPermissionDenied 25 | yxNotImplemented 26 | 27 | yxUserNotFound 28 | yxTokenNotFound 29 | yxInvalidToken 30 | yxIllegalName 31 | yxTokenMismatchUser 32 | yxInvalidPlatform 33 | yxUnsupportSignType 34 | yxOrderNotFound 35 | yxInvalidParameter 36 | yxRequestFailed 37 | yxIllegalOrderType 38 | yxCoinNotEnough 39 | yxFrequencyLimited 40 | yxVerifyFailed 41 | yxSignFailed 42 | yxTradeExisted 43 | yxProviderNotFound 44 | yxThirdAccountNotFound 45 | yxWrongThirdLoginType 46 | yxDirNotExists 47 | yxPayTestDisable 48 | yxPropertyNotFound 49 | yxUuidNotFound 50 | yxProductionNotFound 51 | yxRequestPrePayIDFailed 52 | YXDeskNotFound 53 | ) 54 | 55 | var errs = map[error]int{ 56 | ErrBadRoute: yxBadRoute, 57 | ErrNotFound: yxNotFound, 58 | ErrWrongType: yxWrongType, 59 | ErrUserNameExists: yxUserNameExists, 60 | ErrIllegalParameter: yxIllegalParameter, 61 | ErrNotRSAPublicKey: yxNotRSAPublicKey, 62 | ErrNotRSAPrivateKey: yxNotRSAPrivateKey, 63 | ErrAuthFailed: yxAuthFailed, 64 | ErrIllegalLoginType: yxIllegalLoginType, 65 | ErrWrongPassword: yxWrongPassword, 66 | ErrUserNameNotFound: yxUserNameNotFound, 67 | ErrInitFailed: yxInitFailed, 68 | ErrServerInternal: yxServerInternal, 69 | ErrDBOperation: yxDbOperation, 70 | ErrCacheOperation: yxCacheOperation, 71 | ErrPermissionDenied: yxPermissionDenied, 72 | ErrNotImplemented: yxNotImplemented, 73 | ErrUserNotFound: yxUserNotFound, 74 | ErrTokenNotFound: yxTokenNotFound, 75 | ErrInvalidToken: yxInvalidToken, 76 | ErrIllegalName: yxIllegalName, 77 | ErrTokenMismatchUser: yxTokenMismatchUser, 78 | ErrInvalidPayPlatform: yxInvalidPlatform, 79 | ErrUnsupportSignType: yxUnsupportSignType, 80 | ErrOrderNotFound: yxOrderNotFound, 81 | ErrInvalidParameter: yxInvalidParameter, 82 | ErrRequestFailed: yxRequestFailed, 83 | ErrIllegalOrderType: yxIllegalOrderType, 84 | ErrCoinNotEnough: yxCoinNotEnough, 85 | ErrFrequencyLimited: yxFrequencyLimited, 86 | ErrVerifyFailed: yxVerifyFailed, 87 | ErrSignFailed: yxSignFailed, 88 | ErrTradeExisted: yxTradeExisted, 89 | ErrProviderNotFound: yxProviderNotFound, 90 | ErrThirdAccountNotFound: yxThirdAccountNotFound, 91 | ErrWrongThirdLoginType: yxWrongThirdLoginType, 92 | ErrDirNotExists: yxDirNotExists, 93 | ErrPayTestDisable: yxPayTestDisable, 94 | ErrPropertyNotFound: yxPropertyNotFound, 95 | ErrUUIDNotFound: yxUuidNotFound, 96 | ErrProductionNotFound: yxProductionNotFound, 97 | ErrRequestPrePayIDFailed: yxRequestPrePayIDFailed, 98 | ErrDeskNotFound: YXDeskNotFound, 99 | } 100 | -------------------------------------------------------------------------------- /pkg/errutil/errutil.go: -------------------------------------------------------------------------------- 1 | package errutil 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | ErrBadRoute = errors.New("bad route") 9 | ErrWrongType = errors.New("wrong type") 10 | ErrNotFound = errors.New("not found") 11 | ErrUserNameExists = errors.New("user name exists") 12 | ErrIllegalParameter = errors.New("illegal parameter") 13 | ErrDBOperation = errors.New("database opertaion failed") 14 | ErrNotRSAPublicKey = errors.New("not a rsa public key") 15 | ErrNotRSAPrivateKey = errors.New("not a rsa private key") 16 | ErrAuthFailed = errors.New("auth failed") 17 | ErrIllegalLoginType = errors.New("illegal login type") 18 | ErrWrongPassword = errors.New("wrong password") 19 | ErrUserNameNotFound = errors.New("username not found") 20 | ErrInitFailed = errors.New("initialize failed") 21 | ErrServerInternal = errors.New("server internal error") 22 | ErrCacheOperation = errors.New("cache opertaion failed") 23 | ErrPermissionDenied = errors.New("permission denied") 24 | ErrNotImplemented = errors.New("not implemented") 25 | ErrUserNotFound = errors.New("user not found") 26 | ErrTokenNotFound = errors.New("token not found") 27 | ErrInvalidToken = errors.New("invalid token") 28 | ErrIllegalName = errors.New("illegal user name") 29 | ErrTokenMismatchUser = errors.New("token mismatch user") 30 | ErrInvalidPayPlatform = errors.New("invalid pay platform") 31 | ErrUnsupportSignType = errors.New("unsupport sign type") 32 | ErrOrderNotFound = errors.New("order not found") 33 | ErrInvalidParameter = errors.New("invalid parameter") 34 | ErrRequestFailed = errors.New("request failed") 35 | ErrIllegalOrderType = errors.New("illegal order type") 36 | ErrCoinNotEnough = errors.New("youxian coin not enough") 37 | ErrFrequencyLimited = errors.New("frequency limited") 38 | ErrVerifyFailed = errors.New("verify failed") 39 | ErrSignFailed = errors.New("sign failed") 40 | ErrTradeExisted = errors.New("trade has existed") 41 | ErrProviderNotFound = errors.New("provider not found") 42 | ErrThirdAccountNotFound = errors.New("third account not found") 43 | ErrWrongThirdLoginType = errors.New("wrong third login type") 44 | ErrDirNotExists = errors.New("directory not exists") 45 | ErrPayTestDisable = errors.New("pay test disable") 46 | ErrPropertyNotFound = errors.New("property not found") 47 | ErrIllegalDeskStatus = errors.New("illegal desk status") 48 | ErrPlayerNotFound = errors.New("player not found") 49 | ErrNoSuchWinPoints = errors.New("no such win points") 50 | ErrDismatchTileNum = errors.New("a shortage or surplus of tiles") 51 | ErrNotWon = errors.New("not won now") 52 | ErrDeskNotFound = errors.New("desk not found") 53 | ErrUUIDNotFound = errors.New("uuid not found") 54 | ErrProductionNotFound = errors.New("production not found") 55 | ErrRequestPrePayIDFailed = errors.New("request prepay id failed") 56 | ErrAccountExists = errors.New("account exists") 57 | ) 58 | 59 | //Code code for the error 60 | func Code(err error) int { 61 | if c, ok := errs[err]; ok { 62 | return c 63 | } 64 | return Unknown 65 | } 66 | -------------------------------------------------------------------------------- /pkg/room/room.go: -------------------------------------------------------------------------------- 1 | package room 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | "time" 7 | 8 | "github.com/lonng/nanoserver/db" 9 | ) 10 | 11 | const ( 12 | roomNoLen = 6 13 | ) 14 | 15 | type Number string 16 | type numberManager struct { 17 | lock sync.Mutex 18 | } 19 | 20 | var rn *numberManager 21 | var numbers = [...]byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'} 22 | 23 | func init() { 24 | rn = &numberManager{} 25 | 26 | rand.Seed(time.Now().Unix()) 27 | } 28 | 29 | func (rn *numberManager) next() Number { 30 | no := make([]byte, roomNoLen) 31 | rn.lock.Lock() 32 | defer rn.lock.Unlock() 33 | 34 | for { 35 | for i := 0; i < roomNoLen; i++ { 36 | no[i] = numbers[rand.Intn(10)] 37 | } 38 | temp := Number(no) 39 | dn := string(no) 40 | if !db.DeskNumberExists(dn) { 41 | return temp 42 | } 43 | 44 | } 45 | } 46 | 47 | func (rn *numberManager) remove(no Number) { 48 | //rn.noPool.Remove(string(no)) 49 | } 50 | 51 | func Next() Number { 52 | return rn.next() 53 | } 54 | 55 | func (n Number) String() string { 56 | return string(n) 57 | } 58 | -------------------------------------------------------------------------------- /pkg/room/room_test.go: -------------------------------------------------------------------------------- 1 | package room 2 | 3 | import "testing" 4 | 5 | func TestNext(t *testing.T) { 6 | for i := 0; i < 10000; i++ { 7 | Next() 8 | //t.Log(Next()) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /pkg/security/sql.go: -------------------------------------------------------------------------------- 1 | package security 2 | 3 | // TODO: 验证SQL语句是否合法 4 | func ValidateSQL(sql string) bool { 5 | return true 6 | } 7 | -------------------------------------------------------------------------------- /pkg/security/validity.go: -------------------------------------------------------------------------------- 1 | package security 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | var ( 8 | phoneRE *regexp.Regexp 9 | nameRE *regexp.Regexp 10 | ) 11 | 12 | func init() { 13 | r, err := regexp.Compile("^1[0-9]{10}$") 14 | if err != nil { 15 | panic(err.Error()) 16 | } 17 | phoneRE = r 18 | 19 | r, err = regexp.Compile("^[0-9a-zA-Z.@]{6,32}$") 20 | if err != nil { 21 | panic(err.Error()) 22 | } 23 | nameRE = r 24 | } 25 | 26 | func ValidateName(name string) bool { 27 | return nameRE.MatchString(name) 28 | } 29 | 30 | // 验证电话号码 31 | func ValidatePhone(phone string) bool { 32 | return phoneRE.MatchString(phone) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/security/validity_test.go: -------------------------------------------------------------------------------- 1 | package security 2 | 3 | import "testing" 4 | 5 | var testdata = []string{ 6 | "123123kj123", 7 | "sdflsjsdfsdf", 8 | "SDdko300df", 9 | } 10 | 11 | var testdata2 = []string{ 12 | "skdfd", 13 | " sdkfjf", 14 | "23409.sdf0#", 15 | "!sdlkfj,/ lksjf", 16 | } 17 | 18 | func TestValidateName(t *testing.T) { 19 | for _, name := range testdata { 20 | if !ValidateName(name) { 21 | t.Errorf("should pass name: %s", name) 22 | t.Fail() 23 | } 24 | } 25 | 26 | for _, name := range testdata2 { 27 | if ValidateName(name) { 28 | t.Errorf("should not pass name: %s", name) 29 | t.Fail() 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pkg/set/set.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import "sync" 4 | 5 | type Set struct { 6 | mu sync.Mutex 7 | m map[string]struct{} 8 | } 9 | 10 | func (s *Set) Contains(val string) bool { 11 | if val == "" { 12 | return true 13 | } 14 | s.mu.Lock() 15 | defer s.mu.Unlock() 16 | 17 | if _, ok := s.m[val]; ok { 18 | return true 19 | } 20 | return false 21 | } 22 | 23 | func (s *Set) Add(val string) { 24 | s.mu.Lock() 25 | defer s.mu.Unlock() 26 | s.m[val] = struct{}{} 27 | } 28 | 29 | func (s *Set) Remove(val string) { 30 | s.mu.Lock() 31 | defer s.mu.Unlock() 32 | delete(s.m, val) 33 | } 34 | 35 | func New() *Set { 36 | return &Set{ 37 | m: make(map[string]struct{}), 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pkg/whitelist/white_list.go: -------------------------------------------------------------------------------- 1 | package whitelist 2 | 3 | import ( 4 | "regexp" 5 | "sync" 6 | ) 7 | 8 | var ( 9 | lock sync.RWMutex 10 | ips = map[string]*regexp.Regexp{} 11 | ) 12 | 13 | func Setup(list []string) error { 14 | lock.Lock() 15 | defer lock.Unlock() 16 | 17 | for _, ip := range list { 18 | re, err := regexp.Compile(ip) 19 | if err != nil { 20 | return err 21 | } 22 | ips[ip] = re 23 | } 24 | 25 | return nil 26 | } 27 | 28 | //VerifyIP check the ip is a legal ip or not 29 | func VerifyIP(ip string) bool { 30 | lock.RLock() 31 | defer lock.RUnlock() 32 | 33 | for _, r := range ips { 34 | if r.MatchString(ip) { 35 | return true 36 | } 37 | } 38 | return false 39 | } 40 | 41 | func RegisterIP(ip string) error { 42 | lock.Lock() 43 | defer lock.Unlock() 44 | 45 | _, ok := ips[ip] 46 | if ok { 47 | return nil 48 | } 49 | 50 | re, err := regexp.Compile(ip) 51 | if err != nil { 52 | return err 53 | } 54 | ips[ip] = re 55 | return nil 56 | } 57 | 58 | func RemoveIP(ip string) { 59 | lock.Lock() 60 | defer lock.Unlock() 61 | 62 | delete(ips, ip) 63 | } 64 | 65 | func IPList() []string { 66 | lock.RLock() 67 | defer lock.RUnlock() 68 | 69 | list := []string{} 70 | for ip := range ips { 71 | list = append(list, ip) 72 | } 73 | 74 | return list 75 | } 76 | 77 | func ClearIPList() { 78 | lock.Lock() 79 | defer lock.Unlock() 80 | 81 | ips = map[string]*regexp.Regexp{} 82 | } 83 | -------------------------------------------------------------------------------- /pkg/whitelist/white_list_test.go: -------------------------------------------------------------------------------- 1 | package whitelist 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestVerifyIP(t *testing.T) { 10 | m := map[string]bool{ 11 | "127.0.0.1": true, 12 | "127.0.0.2": false, 13 | "192.168.1.1": true, 14 | "192.168.1.255": true, 15 | "192.168.0.1": false, 16 | } 17 | 18 | for k, v := range m { 19 | if VerifyIP(k) != v { 20 | t.Fatal(k) 21 | } 22 | } 23 | } 24 | 25 | func TestClearIPList(t *testing.T) { 26 | ClearIPList() 27 | m := map[string]bool{ 28 | "127.0.0.1": false, 29 | "127.0.0.2": false, 30 | "192.168.1.1": false, 31 | "192.168.1.255": false, 32 | "192.168.0.1": false, 33 | } 34 | 35 | for k, v := range m { 36 | if VerifyIP(k) != v { 37 | t.Fatal(k) 38 | } 39 | } 40 | } 41 | 42 | func TestRegisterIP(t *testing.T) { 43 | RegisterIP("159.56.25.14") 44 | 45 | if !VerifyIP("159.56.25.14") { 46 | t.Fail() 47 | } 48 | } 49 | 50 | func TestRemoveIP(t *testing.T) { 51 | RegisterIP("159.56.25.14") 52 | 53 | if !VerifyIP("159.56.25.14") { 54 | t.Fail() 55 | } 56 | 57 | RemoveIP("159.56.25.14") 58 | if VerifyIP("159.56.25.14") { 59 | t.Fail() 60 | } 61 | } 62 | 63 | func TestIPList(t *testing.T) { 64 | ClearIPList() 65 | m := []string{"124.4.59.24", "58.57.1.*"} 66 | 67 | for _, ip := range m { 68 | RegisterIP(ip) 69 | } 70 | 71 | if !reflect.DeepEqual(m, IPList()) { 72 | t.Fail() 73 | } 74 | } 75 | 76 | func TestMain(m *testing.M) { 77 | Setup([]string{"127.0.0.1", "192.168.1.*"}) 78 | 79 | retCode := m.Run() 80 | os.Exit(retCode) 81 | 82 | } 83 | -------------------------------------------------------------------------------- /protocol/agent.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | type RegisterAgentRequest struct { 4 | Name string `json:"name"` 5 | Account string `json:"account"` 6 | Password string `json:"password"` 7 | Extra string `json:"extra"` 8 | } 9 | 10 | type AgentLoginRequest struct { 11 | Username string `json:"username"` 12 | Password string `json:"password"` 13 | } 14 | 15 | type AgentDetail struct { 16 | Id int64 `json:"id"` 17 | Name string `json:"name"` 18 | Account string `json:"account"` 19 | CardCount int64 `json:"card_count"` 20 | CreateAt int64 `json:"create_at"` 21 | } 22 | 23 | type AgentLoginResponse struct { 24 | Code int `json:"code"` 25 | Token string `json:"token"` 26 | Detail AgentDetail `json:"detail"` 27 | } 28 | 29 | type AgentListResponse struct { 30 | Code int `json:"code"` 31 | Agents []AgentDetail `json:"agents"` 32 | Total int64 `json:"total"` 33 | } 34 | 35 | type RechargeDetail struct { 36 | PlayerId int64 `json:"player_id"` 37 | Extra string `json:"extra"` 38 | CreateAt int64 `json:"create_at"` 39 | CardCount int64 `json:"card_count"` 40 | } 41 | 42 | type RechargeListResponse struct { 43 | Code int `json:"code"` 44 | Recharges []RechargeDetail `json:"recharges"` 45 | Total int64 `json:"total"` 46 | } 47 | -------------------------------------------------------------------------------- /protocol/apps.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | type RegisterAppRequest struct { 4 | Name string `json:"name"` //应用名 5 | RedirectURI string `json:"redirect_uri"` //回调地址 6 | Extra string `json:"extra"` //应用描述 7 | CpID string `json:"cp_id"` //内容供应商ID 8 | ThirdProperties map[string]map[string]string `json:"third_properties"` //第三方属性 9 | } 10 | 11 | type RegisterAppResponse struct { 12 | Code int32 `json:"code"` //状态码 13 | Data AppInfo `json:"data"` //数据 14 | } 15 | 16 | type AppListRequest struct { 17 | CpID string `json:"cp_id"` 18 | Offset int `json:"offset"` 19 | Count int `json:"count"` 20 | } 21 | type AppListResponse struct { 22 | Code int `json:"code"` //状态码 23 | Data []AppInfo `json:"data"` //应用列表 24 | Total int64 `json:"total"` //总数量 25 | } 26 | 27 | type AppInfoRequest struct { 28 | AppID string `json:"appid"` //应用ID 29 | } 30 | 31 | type AppInfoResponse struct { 32 | Code int32 `json:"code"` //状态码 33 | Data AppInfo `json:"data"` //数据 34 | } 35 | 36 | type DeleteAppRequest struct { 37 | AppID string `json:"appid"` //应用ID 38 | } 39 | 40 | type UpdateAppRequest struct { 41 | Type int `json:"type"` //更新类型, 1为重新生成appkey/appsecret, 2为更新其他内容 42 | AppID string `json:"appid"` //应用ID 43 | Name string `json:"name"` //应用名 44 | RedirectURI string `json:"redirect_uri"` //回调地址 45 | Extra string `json:"extra"` //应用描述 46 | ThirdProperties map[string]map[string]string `json:"third_properties"` //第三方属性 47 | } 48 | -------------------------------------------------------------------------------- /protocol/club.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | type ( 4 | ClubItem struct { 5 | Id int64 `json:"id"` 6 | Name string `json:"name"` 7 | Desc string `json:"desc"` 8 | Member int `json:"member"` 9 | MaxMember int `json:"maxMember"` 10 | } 11 | 12 | ClubListResponse struct { 13 | Code int `json:"code"` 14 | Data []ClubItem `json:"data"` 15 | } 16 | 17 | ApplyClubRequest struct { 18 | ClubId int64 `json:"clubId"` 19 | } 20 | ) 21 | -------------------------------------------------------------------------------- /protocol/common.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import "fmt" 4 | 5 | type AppInfo struct { 6 | Name string `json:"name"` //应用名 7 | AppID string `json:"appid"` //应用id 8 | AppKey string `json:"appkey"` //应用key 9 | RedirectURI string `json:"redirect_uri"` //注册时填的redirect_uri 10 | Extra string `json:"extra"` //额外信息 11 | ThirdProperties map[string]map[string]string `json:"third_properties"` //此app在第三方平台(eg: wechat)上的相关配置 12 | } 13 | 14 | type SnakePayOrderInfo struct { 15 | OrderId string `json:"order_id"` //订单号 16 | Uid string `json:"uid"` //接收者id 17 | ServerName string `json:"server_name"` //区服名 18 | RoleID string `json:"role_id"` //角色id 19 | AppId string `json:"appid"` //应用id 20 | ChannelId string `json:"channel_id"` //渠道id 21 | Extra string `json:"extra"` //额外信息 22 | Imei string `json:"imei"` //imei 23 | ProductName string `json:"product_name"` //商品名 24 | Type int `json:"type"` //收支类型: 1-购买代币 2-消费代币 25 | Money int `json:"money"` //标价 26 | RealMoney int `json:"real_money"` //实际售价 27 | ProductCount int `json:"product_count"` //商品数量 28 | Status int `json:"status"` // 订单状态 1-创建 2-完成 3-游戏服务器已经确认 29 | 30 | CreatedAt int64 `json:"created_at"` //发放时间 31 | } 32 | 33 | type OrderInfo struct { 34 | OrderId string `json:"order_id"` //订单号 35 | Uid string `json:"uid"` //接收者id 36 | AppId string `json:"appid"` //应用id 37 | ServerName string `json:"server_name"` //区服名 38 | RoleID string `json:"role_id"` //角色id 39 | Extra string `json:"extra"` //额外信息 40 | Imei string `json:"imei"` //imei 41 | ProductName string `json:"product_name"` //商品名 42 | PayBy string `json:"pay_by"` //收支类型: alipay, wechat ... 43 | ProductCount int `json:"product_count"` //商品数量 44 | Money int `json:"money"` //标价 45 | RealMoney int `json:"real_money"` //实际售价 46 | Status int `json:"status"` // 订单状态 1-创建 2-完成 3-游戏服务器已经确认 47 | CreatedAt int64 `json:"created_at"` //发放时间 48 | } 49 | 50 | type TradeInfo struct { 51 | OrderId string `json:"order_id"` //订单号 52 | Uid string `json:"uid"` //snake uid 53 | PayPlatformUid string `json:"pay_platform_uid"` //支付平台uid 54 | AppId string `json:"appid"` //应用id 55 | ChannelId string `json:"channel_id"` //渠道id 56 | ProductName string `json:"product_name"` //商品名 57 | PayBy string `json:"pay_by"` //收支类型: alipay, wechat ... 58 | ServerName string `json:"server_name"` 59 | RoleName string `json:"role_name"` 60 | RoleId string `json:"role_id"` 61 | Currency string `json:"currency"` 62 | ProductCount int `json:"product_count"` //商品数量 63 | Money int `json:"money"` //标价 64 | RealMoney int `json:"real_money"` //实际售价 65 | PayAt int64 `json:"pay_at"` //支付时间 66 | 67 | } 68 | 69 | type UserInfo struct { 70 | UID int64 `json:"uid"` 71 | Name string `json:"name"` //用户名, 可空,当非游客注册时用户名与手机号必须至少出现一项 72 | Phone string `json:"phone"` //手机号,可空 73 | Role int `json:"role"` //玩家类型 74 | Status int `json:"status"` //状态 75 | IsOnline int `json:"is_online"` //是否在线 76 | LastLoginAt int64 `json:"last_login_time"` //最后登录时间 77 | } 78 | 79 | type DailyStats struct { 80 | Score int `json:"score"` //战绩 81 | AsCreator int64 `json:"as_creator"` //开房次数 82 | Win int `json:"win"` // 赢的次数 83 | DeskNos []string `json:"desks"` //所参加的桌号 84 | 85 | } 86 | 87 | type UserStatsInfo struct { 88 | ID int64 `json:"id"` 89 | Uid int64 `json:"uid"` 90 | Name string `json:"name"` 91 | RegisterAt int64 `json:"register_at"` 92 | RegisterIP string `json:"register_ip"` 93 | LastestLoginAt int64 `json:"lastest_login_at"` 94 | LastestLoginIP string `json:"lastest_login_ip"` 95 | 96 | TotalMatch int64 `json:"total_match"` //总对局数 97 | RemainCard int64 `json:"remain_card"` //剩余房卡 98 | 99 | StatsAt []int64 //统计时间 100 | Stats map[int64]*DailyStats //时间对应的数据 101 | } 102 | 103 | type Device struct { 104 | IMEI string `json:"imei"` //设备的imei号 105 | OS string `json:"os"` //os版本号 106 | Model string `json:"model"` //硬件型号 107 | IP string `json:"ip"` //内网IP 108 | Remote string `json:"remote"` //外网IP 109 | } 110 | 111 | type StringResponse struct { 112 | Code int `json:"code"` //状态码 113 | Data string `json:"data"` //字符串数据 114 | } 115 | 116 | type CommonResponse struct { 117 | Code int `json:"code"` //状态码 118 | Data interface{} `json:"data"` //整数状态 119 | } 120 | 121 | var SuccessResponse = StringResponse{0, "success"} 122 | 123 | const ( 124 | RegTypeThird = 5 //三方平台添加账号 125 | ) 126 | 127 | var EmptyMessage = &None{} 128 | 129 | type EmptyRequest struct{} 130 | 131 | var SuccessMessage = &StringMessage{Message: "success"} 132 | 133 | type None struct{} 134 | 135 | type StringMessage struct { 136 | Code int `json:"code"` 137 | Message string `json:"message"` 138 | } 139 | 140 | type ErrorResponse struct { 141 | Code int `json:"code"` 142 | Error string `json:"error"` 143 | } 144 | 145 | type ErrorMessage struct { 146 | ErrorType int `json:"errorType"` 147 | Message string `json:"msg"` 148 | } 149 | 150 | type DailyMatchProgressInfo struct { 151 | HasProgress bool `json:"hasProgress"` 152 | IsHaveFanPai bool `json:"isHaveFanPai"` 153 | Heart int `json:"heart"` //最大只能是3 154 | BaoPaiMax int `json:"baoPaiMax"` 155 | BaoPaiNum int `json:"baoPaiNum"` 156 | Coin int64 `json:"coin"` 157 | Score int `json:"score"` 158 | RoomType int `json:"roomType"` 159 | BaoPaiID int `json:"baoPaiId"` 160 | } 161 | 162 | type PlayerReady struct { 163 | Account int64 `json:"account"` 164 | } 165 | 166 | //听牌信息 167 | type Ting struct { 168 | Index int `json:"index"` 169 | Hu []int `json:"hu"` 170 | } 171 | 172 | //所有被听的牌 173 | type Tings []Ting 174 | 175 | //所有可执行的操作 176 | type Ops []Op 177 | 178 | //提示 179 | type Hint struct { 180 | Ops Ops `json:"ops"` 181 | Tings Tings `json:"tings"` 182 | Uid int64 `json:"uid"` 183 | } 184 | 185 | func (h *Hint) String() string { 186 | return fmt.Sprintf("UID=%d, Ops=%+v, Tings=%+v", h.Uid, h.Ops, h.Tings) 187 | } 188 | -------------------------------------------------------------------------------- /protocol/const.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | type HuPaiType int 4 | 5 | //登录状态 6 | const ( 7 | LoginStatusSucc = 1 8 | LoginStatusFail = 2 9 | ) 10 | 11 | const ( 12 | ActionNewAccountSignIn = "accountLogin" 13 | ActionGuestSignIn = "anonymousLogin" 14 | ActionOldAccountSignIn = "oldAccountLogin" 15 | ActionWebChatSignIn = "webChatSignIn" 16 | ActionPhoneNumberRegister = "phoneRegister" 17 | ActionNormalRegister = "normalRegister" 18 | ActionGetVerification = "getVerification" 19 | ActionAccountRegister = "accountRegister" 20 | ) 21 | 22 | const ( 23 | LoginTypeAuto = "auto" 24 | LoginTypeManual = "manual" 25 | ) 26 | 27 | const ( 28 | VerificationTypeRegister = "register" 29 | VerificationTypeFindPW = "findPW" 30 | ) 31 | 32 | // 匹配类型 33 | const ( 34 | MatchTypeClassic = 1 //经典 35 | MatchTypeDaily = 3 //每日匹配 36 | ) 37 | 38 | const ( 39 | CoinTypeSliver = 0 //银币 40 | CoinTypeGold = 1 //金币 41 | ) 42 | 43 | const ( 44 | RoomTypeClassic = 0 45 | RoomTypeDailyMatch = 1 46 | RoomTypeMonthlyMatch = 2 47 | RoomTypeFinalMatch = 3 48 | ) 49 | 50 | const ( 51 | DailyMatchLevelJunior = 0 52 | DailyMatchLevelSenior = 1 53 | DailyMatchLevelMaster = 2 54 | ) 55 | 56 | const ( 57 | ClassicLevelJunior = 0 58 | ClassicLevelMiddle = 1 59 | ClassicLevelSenior = 2 60 | ClassicLevelElite = 3 61 | ClassicLevelMaster = 4 62 | ) 63 | 64 | const ( 65 | ExitTypeExitDeskUI = -1 66 | ExitTypeDissolve = 6 67 | ExitTypeSelfRequest = 0 68 | ExitTypeClassicCoinNotEnough = 1 69 | ExitTypeDailyMatchEnd = 2 70 | ExitTypeNotReadyForStart = 3 71 | ExitTypeChangeDesk = 4 72 | ExitTypeRepeatLogin = 5 73 | ) 74 | 75 | const ( 76 | DeskStatusZb = 1 77 | DeskStatusDq = 2 78 | DeskStatusPlaying = 3 79 | DeskStatusEnded = 4 80 | ) 81 | 82 | const ( 83 | HuTypeDianPao HuPaiType = iota 84 | HuTypeZiMo 85 | HuTypePei 86 | ) 87 | 88 | const ( 89 | SexTypeUnknown = 0 90 | SexTypeMale = 1 91 | SexTypeFemale = 2 92 | ) 93 | 94 | const ( 95 | UserTypeGuest = 0 96 | UserTypeLaoBaShi = 1 97 | ) 98 | 99 | const ( 100 | FanPaiStepK91 = 0 101 | FanPaiStepK61 = 1 102 | FanPaiStepK41 = 2 103 | FanPaiStepK31 = 3 104 | FanPaiStepK21 = 4 105 | ) 106 | 107 | const ( 108 | FanPaiStatusKNotOpen1 = 0 109 | FanPaiStatusKOpenFailed1 = 1 110 | FanPaiStatusKOpenSuccessY1 = 2 111 | FanPaiStatusKOpenSuccessN1 = 3 112 | FanPaiStatusKNotOpen2 = 4 113 | FanPaiStatusKOpenFailed2 = 5 114 | FanPaiStatusKOpenSuccessY2 = 6 115 | FanPaiStatusKOpenSuccessN2 = 7 116 | ) 117 | 118 | // OpType 119 | const ( 120 | OptypeIllegal = 0 121 | OptypeChu = 1 122 | OptypePeng = 2 123 | OptypeGang = 3 124 | OptypeHu = 4 125 | OptypePass = 5 126 | 127 | OptyMoPai = 500 //摸牌 128 | //以下三种杠的分类主要用以解决上面的 OptypeGang分类不细致,导致抢杠等操作处理麻烦的问题 129 | //在判定时必须满足两条件 x % 10 == 4 && x >1000 130 | OptypeAnGang = 1004 131 | OptypeMingGang = 1014 132 | OptypeBaGang = 1024 133 | ) 134 | 135 | // 番型 136 | const ( 137 | FanXingQingYiSe = 1 // "清一色" 138 | FanXingQingQiDui = 2 // "清七对" 139 | FanXingQingDaDui = 3 // "清大对" 140 | FanXingQingDaiYao = 4 // "清带幺" 141 | FanXingQingJiangDui = 5 // "清将对" 142 | FanXingSuFan = 6 // "素番" 143 | FanXingQiDui = 7 // "七对" 144 | FanXingDaDui = 8 // "大对子" 145 | FanXingQuanDaiYao = 9 // "全带幺" 146 | FanXingJiangDui = 10 // "将对" 147 | FanXingYaoJiuQiDui = 11 // "幺九七对" 148 | FanXingQingLongQiDui = 12 // "清龙七对" 149 | FanXingLongQiDui = 13 // "龙七对" 150 | ) 151 | 152 | // 创建房间频道选项 153 | const ( 154 | ChannelOptionAll = "allChannel" 155 | ChannelOptionHalf = "halfChannel" 156 | ) 157 | -------------------------------------------------------------------------------- /protocol/history.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | type HistoryListRequest struct { 4 | DeskID int64 `json:"desk_id"` 5 | Offset int `json:"offset"` 6 | Count int `json:"count"` 7 | } 8 | 9 | type DeleteHistoryRequest struct { 10 | ID string `json:"id"` //历史ID 11 | } 12 | type HistoryByIDRequest struct { 13 | ID int64 `json:"id"` //历史ID 14 | } 15 | 16 | type HistoryLiteListRequest struct { 17 | DeskID int64 `json:"desk_id"` 18 | Offset int `json:"offset"` 19 | Count int `json:"count"` 20 | } 21 | 22 | type HistoryLite struct { 23 | Id int64 `json:"id"` 24 | DeskId int64 `json:"desk_id"` 25 | Mode int `json:"mode"` 26 | BeginAt int64 `json:"begin_at"` 27 | BeginAtStr string `json:"begin_at_str"` 28 | EndAt int64 `json:"end_at"` 29 | PlayerName0 string `json:"player_name0"` 30 | PlayerName1 string `json:"player_name1"` 31 | PlayerName2 string `json:"player_name2"` 32 | PlayerName3 string `json:"player_name3"` 33 | ScoreChange0 int `json:"score_change0"` 34 | ScoreChange1 int `json:"score_change1"` 35 | ScoreChange2 int `json:"score_change2"` 36 | ScoreChange3 int `json:"score_change3"` 37 | } 38 | 39 | type History struct { 40 | HistoryLite 41 | Snapshot string `json:"snapshot"` 42 | } 43 | 44 | type HistoryLiteListResponse struct { 45 | Code int `json:"code"` 46 | Total int64 `json:"total"` //总数量 47 | Data []HistoryLite `json:"data"` 48 | } 49 | 50 | type HistoryListResponse struct { 51 | Code int `json:"code"` 52 | Total int64 `json:"total"` //总数量 53 | Data []History `json:"data"` 54 | } 55 | 56 | type HistoryByIDResponse struct { 57 | Code int `json:"code"` 58 | Data *History `json:"data"` 59 | } 60 | -------------------------------------------------------------------------------- /protocol/login.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | type ThirdUserLoginRequest struct { 4 | Platform string `json:"platform"` //三方平台/渠道 5 | AppID string `json:"appId"` //用户来自于哪一个应用 6 | ChannelID string `json:"channelId"` //用户来自于哪一个渠道 7 | Device Device `json:"device"` //设备信息 8 | Name string `json:"name"` //微信平台名 9 | OpenID string `json:"openid"` //微信平台openid 10 | AccessToken string `json:"access_token"` //微信AccessToken 11 | } 12 | 13 | type LoginInfo struct { 14 | // 三方登录字段 15 | Platform string `json:"platform"` //三方平台 16 | ThirdAccount string `json:"third_account"` //三方平台唯一ID 17 | ThirdName string `json:"account"` //三方平台账号名 18 | 19 | Token string `json:"token"` //用户Token 20 | ExpireTime int64 `json:"expire_time"` //Token过期时间 21 | 22 | AccountID int64 `json:"acId"` //用户的uuid,即user表的pk 23 | 24 | GameServerIP string `json:"ip"` //游戏服的ip&port 25 | GameServerPort int `json:"port"` 26 | } 27 | 28 | type UserLoginResponse struct { 29 | Code int32 `json:"code"` //状态码 30 | Data LoginInfo `json:"data"` 31 | } 32 | 33 | type LoginRequest struct { 34 | AppID string `json:"appId"` //用户来自于哪一个应用 35 | ChannelID string `json:"channelId"` //用户来自于哪一个渠道 36 | IMEI string `json:"imei"` 37 | Device Device `json:"device"` 38 | } 39 | 40 | type ClientConfig struct { 41 | Version string `json:"version"` 42 | Android string `json:"android"` 43 | IOS string `json:"ios"` 44 | Heartbeat int `json:"heartbeat"` 45 | ForceUpdate bool `json:"forceUpdate"` 46 | 47 | Title string `json:"title"` // 分享标题 48 | Desc string `json:"desc"` // 分享描述 49 | 50 | Daili1 string `json:"daili1"` 51 | Daili2 string `json:"daili2"` 52 | Kefu1 string `json:"kefu1"` 53 | 54 | AppId string `json:"appId"` 55 | AppKey string `json:"appKey"` 56 | } 57 | 58 | type LoginResponse struct { 59 | Code int `json:"code"` 60 | Name string `json:"name"` 61 | Uid int64 `json:"uid"` 62 | HeadUrl string `json:"headUrl"` 63 | FangKa int64 `json:"fangka"` 64 | Sex int `json:"sex"` //[0]未知 [1]男 [2]女 65 | IP string `json:"ip"` 66 | Port int `json:"port"` 67 | PlayerIP string `json:"playerIp"` 68 | Config ClientConfig `json:"config"` 69 | Messages []string `json:"messages"` 70 | ClubList []ClubItem `json:"clubList"` 71 | Debug int `json:"debug"` 72 | } 73 | 74 | type LoginToGameServerResponse struct { 75 | Uid int64 `json:"acId"` 76 | Nickname string `json:"nickname"` 77 | HeadUrl string `json:"headURL"` 78 | Sex int `json:"sex"` 79 | FangKa int `json:"fangka"` 80 | } 81 | 82 | type LoginToGameServerRequest struct { 83 | Name string `json:"name"` 84 | Uid int64 `json:"uid"` 85 | HeadUrl string `json:"headUrl"` 86 | Sex int `json:"sex"` //[0]未知 [1]男 [2]女 87 | FangKa int `json:"fangka"` 88 | IP string `json:"ip"` 89 | } 90 | 91 | type EncryptTest struct { 92 | Payload string `json:"payload"` 93 | Key string `json:"key"` 94 | } 95 | 96 | type EncryptTestTest struct { 97 | Result string `json:"result"` 98 | } 99 | -------------------------------------------------------------------------------- /protocol/order.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | type CreateOrderRequest struct { 4 | AppID string //来自哪个应用的订单 5 | ChannelID string //来自哪个渠道的订单 6 | Platform string //支付平台 7 | ProductionName string 8 | ProductCount int //房卡数量 9 | Extra string //描述信息 10 | Device Device //设备信息 11 | Uid int64 //Token 12 | } 13 | 14 | type CreateOrderByAdminRequest struct { 15 | AppID string `json:"appid"` //来自哪个应用的订单 16 | ChannelID string `json:"channel_id"` //来自哪个渠道的订单 17 | Extra string `json:"extra"` //描述信息 18 | Operator string `json:"operator"` //管理员账号 19 | Money int `json:"money"` //金额 20 | Uid int64 `json:"uid"` //用户ID 21 | Device Device `json:"device"` //设备信息 22 | } 23 | 24 | type BalanceListRequest struct { 25 | Uids []string `json:"uids"` //uid列表 26 | } 27 | 28 | type BalanceListResponse struct { 29 | Code int `json:"code"` //状态码 30 | Data map[string]string `json:"data"` //渠道列表 31 | } 32 | 33 | //OrderByAdminListRequest 由管理员创建的订单列表 34 | type OrderByAdminListRequest struct { 35 | Offset int `json:"offset"` 36 | Count int `json:"count"` 37 | Start int64 `json:"start"` //时间起点 38 | End int64 `json:"end"` //时间终点 39 | Uid int64 `json:"uid"` //用户id 40 | OrderId string `json:"order_id"` 41 | AppID string `json:"appid"` //来自哪个应用的订单 42 | ChannelID string `json:"channel_id"` //来自哪个渠道的订单 43 | } 44 | 45 | //PayOrderListRequest 由管理员创建的订单列表 46 | type PayOrderListRequest struct { 47 | Offset int `json:"offset"` 48 | Count int `json:"count"` 49 | Type int `json:"type"` //查询类型: 1-购买代币 2-消费代币 50 | Start int64 `json:"start"` //时间起点 51 | End int64 `json:"end"` //时间终点 52 | Uid int64 `json:"uid"` //用户id 53 | OrderID string `json:"order_id"` //订单号 54 | AppID string `json:"appid"` //来自哪个应用的订单 55 | ChannelID string `json:"channel_id"` //来自哪个渠道的订单 56 | } 57 | 58 | //OrderListRequest 订单列表 59 | type OrderListRequest struct { 60 | Offset int `json:"offset"` 61 | Count int `json:"count"` 62 | Status uint8 `json:"status"` 63 | Start int64 `json:"start"` //时间起点 64 | End int64 `json:"end"` //时间终点 65 | PayBy string `json:"pay_by"` 66 | Uid string `json:"uid"` //用户id 67 | OrderID string `json:"order_id"` //订单号 68 | AppID string `json:"appid"` //来自哪个应用的订单 69 | ChannelID string `json:"channel_id"` //来自哪个渠道的订单 70 | } 71 | 72 | //TradeListRequest 交易列表 73 | type TradeListRequest struct { 74 | Offset int `json:"offset"` 75 | Count int `json:"count"` 76 | Start int64 `json:"start"` //时间起点 77 | End int64 `json:"end"` //时间终点 78 | OrderID string `json:"order_id"` //订单号 79 | AppID string `json:"appid"` //来自哪个应用的订单 80 | ChannelID string `json:"channel_id"` //来自哪个渠道的订单 81 | } 82 | 83 | type PayOrderListResponse struct { 84 | Code int `json:"code"` //状态码 85 | Data []SnakePayOrderInfo `json:"data"` //渠道列表 86 | Total int `json:"total"` //总数量 87 | } 88 | 89 | type OrderListResponse struct { 90 | Code int `json:"code"` //状态码 91 | Data []OrderInfo `json:"data"` //渠道列表 92 | Total int `json:"total"` //总数量 93 | 94 | } 95 | 96 | type TradeListResponse struct { 97 | Code int `json:"code"` //状态码 98 | Data []TradeInfo `json:"data"` //渠道列表 99 | Total int `json:"total"` //总数量 100 | 101 | } 102 | 103 | type ObtainBalanceReqeust struct { 104 | Token string `json:"token"` 105 | } 106 | 107 | type ObtainBalanceResponse struct { 108 | Code int `json:"code"` 109 | Data int64 `json:"data"` 110 | } 111 | 112 | type CreateOrderSnakeResponse struct { 113 | Result string `json:"result"` 114 | PayPlatform string `json:"pay_platform"` 115 | } 116 | 117 | type CreateOrderWechatReponse struct { 118 | AppID string `json:"appid"` 119 | PartnerId string `json:"partnerid"` 120 | OrderId string `json:"orderid"` 121 | PrePayID string `json:"prepayid"` 122 | NonceStr string `json:"noncestr"` 123 | Sign string `json:"sign"` 124 | Timestamp string `json:"timestamp"` 125 | Extra string `json:"extData"` 126 | } 127 | 128 | type UnifyOrderCallbackRequest struct { 129 | PayPlatform string 130 | RawRequest interface{} 131 | } 132 | 133 | type WechatOrderCallbackRequest struct { 134 | ReturnMsg string `xml:"return_msg,omitempty"` 135 | DeviceInfo string `xml:"device_info,omitempty"` 136 | ErrCode string `xml:"err_code,omitempty"` 137 | ErrCodeDes string `xml:"err_code_des,omitempty"` 138 | Attach string `xml:"attach,omitempty"` 139 | CashFeeType string `xml:"cash_fee_type,omitempty"` 140 | CouponFee int `xml:"coupon_fee,omitempty"` 141 | CouponCount int `xml:"coupon_count,omitempty"` 142 | CouponIDDollarN string `xml:"coupon_id_$n,omitempty"` 143 | CouponFeeDollarN string `xml:"coupon_fee_$n,omitempty"` 144 | 145 | ReturnCode string `xml:"return_code"` 146 | Appid string `xml:"appid"` 147 | MchID string `xml:"mch_id"` 148 | Nonce string `xml:"nonce_str"` 149 | Sign string `xml:"sign"` 150 | ResultCode string `xml:"result_code"` 151 | Openid string `xml:"openid"` 152 | IsSubscribe string `xml:"is_subscribe"` 153 | TradeType string `xml:"trade_type"` 154 | BankType string `xml:"bank_type"` 155 | TotalFee int `xml:"total_fee"` 156 | FeeType string `xml:"fee_type"` 157 | CashFee int `xml:"cash_fee"` 158 | TransactionID string `xml:"transaction_id"` 159 | OutTradeNo string `xml:"out_trade_no"` 160 | TimeEnd string `xml:"time_end"` 161 | 162 | Raw string 163 | } 164 | 165 | type WechatOrderCallbackResponse struct { 166 | ReturnCode string `xml:"return_code,cdata"` 167 | ReturnMsg string `xml:"return_msg,cdata"` 168 | } 169 | 170 | type RechargeRequest struct { 171 | Count int64 `json:"count"` 172 | Uid int64 `json:"uid"` 173 | } 174 | -------------------------------------------------------------------------------- /protocol/req.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import ( 4 | "github.com/lonng/nanoserver/pkg/constant" 5 | ) 6 | 7 | type GetRankInfoRequest struct { 8 | IsSelf bool `json:"isself"` 9 | Start int `json:"start"` 10 | Len int `json:"len"` 11 | } 12 | 13 | type MailOperateRequest struct { 14 | MailIDs []int64 `json:"mailids"` 15 | } 16 | 17 | type ApplyForDailyMatchRequest struct { 18 | Arg1 int `json:"arg1"` 19 | DailyMatchType int `json:"dailyMatchType"` 20 | Multiple int `json:"multiple"` 21 | } 22 | 23 | type JQToCoinRequest struct { 24 | Count int `json:"count"` 25 | } 26 | 27 | type BashiCoinOpRequest struct { 28 | Op int `json:"op"` 29 | Coin int `json:"coin"` 30 | } 31 | 32 | type ReJoinDeskRequest struct { 33 | DeskNo string `json:"deskId"` 34 | } 35 | 36 | type ReJoinDeskResponse struct { 37 | Code int `json:"code"` 38 | Error string `json:"error"` 39 | } 40 | 41 | type ReEnterDeskRequest struct { 42 | DeskNo string `json:"deskId"` 43 | } 44 | 45 | type ReEnterDeskResponse struct { 46 | Code int `json:"code"` 47 | Error string `json:"error"` 48 | } 49 | type JoinDeskRequest struct { 50 | Version string `json:"version"` 51 | //AccountId int64 `json:"acId"` 52 | DeskNo string `json:"deskId"` 53 | } 54 | 55 | type TableInfo struct { 56 | DeskNo string `json:"deskId"` 57 | CreatedAt int64 `json:"createdAt"` 58 | Creator int64 `json:"creator"` 59 | Title string `json:"title"` 60 | Desc string `json:"desc"` 61 | Status constant.DeskStatus `json:"status"` 62 | Round uint32 `json:"round"` 63 | Mode int `json:"mode"` 64 | } 65 | 66 | type JoinDeskResponse struct { 67 | Code int `json:"code"` 68 | Error string `json:"error"` 69 | TableInfo TableInfo `json:"tableInfo"` 70 | } 71 | 72 | type DestoryDeskRequest struct { 73 | DeskNo string `json:"deskId"` 74 | } 75 | 76 | //选择执行的动作 77 | type OpChoosed struct { 78 | Type int 79 | TileID int 80 | } 81 | 82 | type MingAction struct { 83 | KouIndexs []int `json:"kou"` //index 84 | ChuPaiID int `json:"chu"` 85 | HuIndexs []int `json:"hu"` 86 | } 87 | 88 | type OpChooseRequest struct { 89 | OpType int `json:"optype"` 90 | Index int `json:"idx"` 91 | } 92 | 93 | type ChooseOneScoreRequest struct { 94 | Pos int `json:"pos"` 95 | } 96 | 97 | type CheckOrderReqeust struct { 98 | OrderID string `json:"orderid"` 99 | } 100 | 101 | type CheckOrderResponse struct { 102 | Code int `json:"code"` 103 | Error string `json:"error"` 104 | FangKa int `json:"fangka"` 105 | } 106 | type FanPaiRequest struct { 107 | Pos int `json:"pos"` 108 | IsUseCoin bool `json:"isUseCoin"` 109 | IsMultiple bool `json:"isMultiple"` 110 | OpType int `json:"opType"` 111 | } 112 | 113 | type DissolveStatusItem struct { 114 | DeskPos int `json:"deskPos"` 115 | Status string `json:"status"` 116 | } 117 | 118 | type DissolveResponse struct { 119 | DissolveUid int64 `json:"dissolveUid"` 120 | DissolveStatus []DissolveStatusItem `json:"dissolveStatus"` 121 | RestTime int32 `json:"restTime"` 122 | } 123 | 124 | type DissolveStatusRequest struct { 125 | Result bool `json:"result"` 126 | } 127 | 128 | type DissolveStatusResponse struct { 129 | DissolveStatus []DissolveStatusItem `json:"dissolveStatus"` 130 | RestTime int32 `json:"restTime"` 131 | } 132 | 133 | type DissolveResult struct { 134 | DeskPos int `json:"deskPos"` 135 | } 136 | 137 | type CalcLastTingRequest struct { 138 | KouIndexs []int `json:"kou"` 139 | TingIndexs []int `json:"ting"` 140 | } 141 | 142 | type CalcLastTingResponse struct { 143 | ForbidIndexs []int `json:"forbid"` 144 | Tings Tings `json:"tings"` 145 | } 146 | 147 | type PlayerOfflineStatus struct { 148 | Uid int64 `json:"uid"` 149 | Offline bool `json:"offline"` 150 | } 151 | 152 | type CoinChangeInformation struct { 153 | Coin int64 `json:"coin"` 154 | } 155 | -------------------------------------------------------------------------------- /protocol/route.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | const ( 4 | RouteOpTypeHint = "onOpTypeHint" 5 | RouteTypeDo = "onOpTypeDo" 6 | ) 7 | -------------------------------------------------------------------------------- /protocol/stats.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | type AppStatsRequest struct { 4 | AppID string `json:"app_id"` 5 | ChannelID string `json:"channel_id"` 6 | Remote string `json:"remote"` 7 | Event string `json:"event"` 8 | Extra string `json:"extra"` 9 | Device *Device `json:"device"` 10 | } 11 | 12 | type ChannelAndAppStatsSummaryRequest struct { 13 | AppIds []string `json:"app_ids"` 14 | ChannelIds []string `json:"channel_ids"` 15 | Start int64 `json:"start"` 16 | End int64 `json:"end"` 17 | SortBy byte `json:"sort_by"` 18 | } 19 | 20 | type UserStatsSummaryRequest struct { 21 | AppIds []string `json:"app_ids"` 22 | ChannelIds []string `json:"channel_ids"` 23 | Role byte `json:"role"` //账号类型 24 | Uid int64 `json:"uid"` 25 | Start int64 `json:"start"` //注册起始时间 26 | End int64 `json:"end"` //注册结束时间 27 | SortBy byte `json:"sort_by"` 28 | } 29 | 30 | type ChannelAndAPPStatsSummary struct { 31 | Start int64 `json:"start"` 32 | AppId string `json:"app_id"` 33 | ChannelId string `json:"channel_id"` 34 | AppName string `json:"app_name"` 35 | ChannelName string `json:"channel_name"` 36 | AccountInc int64 `json:"account_inc"` //新增用户 37 | DeviceInc int64 `json:"device_inc"` //新增设备 38 | TotalRecharge int64 `json:"total_recharge"` //总充值 39 | TotalRechargeAccount int64 `json:"total_recharge_account"` //总充值人数 40 | 41 | PaidAccountInc int64 `json:"paid_account_inc"` //新增付费用户 42 | PaidTotalRechargeInc int64 `json:"paid_total_recharge_inc"` //新增付费总额 43 | 44 | RegPaidAccountInc int64 `json:"reg_paid_account_inc"` //新增注册并付费用户 45 | RegPaidTotalRechargeInc int64 `json:"reg_paid_total_recharge_inc"` //新增注册并付费总额 46 | 47 | //PaidAccountIncRate float32 `json:"paid_account_inc_rate"` //新增用户付费率 48 | RegPaidAccountIncRate string `json:"reg_paid_account_inc_rate"` //新增注册用户付费率 49 | } 50 | 51 | type UserStatsSummary struct { 52 | Name string `json:"name"` //名字 53 | Uid int64 `json:"uid"` //uid 54 | Role byte `json:"role"` //角色 55 | AppID string `json:"app_id"` //appid 56 | ChannelID string `json:"channel_id"` //channel id 57 | 58 | OS string `json:"os"` 59 | IP string `json:"ip"` //最后登录ip 60 | Device string `json:"deivce"` //最后登录设备 61 | LoginAt int64 `json:"login_at"` //最后登录时间 62 | 63 | RegisterAt int64 `json:"register_at"` //注册时间 64 | LoginNum int64 `json:"login_num"` //登录次数 65 | RechargeNum int64 `json:"recharge_num"` //充值次数 66 | RechargeTotal int64 `json:"total_recharge"` //充值总金额 67 | } 68 | 69 | type ChannelAndAPPStatsSummaryResponse struct { 70 | Code int `json:"code"` //状态码 71 | Data []*ChannelAndAPPStatsSummary `json:"data"` // 72 | Total int `json:"total"` //总数量 73 | } 74 | 75 | type UserStatsSummaryResponse struct { 76 | Code int `json:"code"` //状态码 77 | Data []*UserStatsSummary `json:"data"` 78 | Total int `json:"total"` //总数量 79 | } 80 | 81 | type RetentionLite struct { 82 | Login int64 `json:"login"` 83 | Rate string `json:"rate"` 84 | } 85 | type Retention struct { 86 | Date int `json:"date"` 87 | Register int64 `json:"register"` 88 | 89 | Retention_1 RetentionLite `json:"retention_1"` //次日 90 | Retention_2 RetentionLite `json:"retention_2"` //2日 91 | Retention_3 RetentionLite `json:"retention_3"` //3日 92 | Retention_7 RetentionLite `json:"retention_7"` //7日 93 | Retention_14 RetentionLite `json:"retention_14"` //14日 94 | Retention_30 RetentionLite `json:"retention_30"` //30日 95 | 96 | } 97 | 98 | type RetentionResponse struct { 99 | Code int `json:"code"` 100 | 101 | Data interface{} `json:"data"` 102 | } 103 | 104 | type RetentionListRequest struct { 105 | Start int `json:"start"` 106 | End int `json:"end"` 107 | } 108 | 109 | type Rank struct { 110 | Uid int64 `json:"uid"` 111 | Name string `json:"name"` 112 | Value int64 `json:"value"` 113 | } 114 | 115 | type CommonStatsItem struct { 116 | Date int64 `json:"date"` 117 | Value int64 `json:"value"` 118 | } 119 | 120 | //房卡消耗 121 | type CardConsume CommonStatsItem 122 | 123 | //活跃用户 124 | type ActivationUser CommonStatsItem 125 | -------------------------------------------------------------------------------- /protocol/test.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | type TestRequest struct { 4 | IntField int `json:"int_field"` 5 | StringField string `json:"string_field"` 6 | } 7 | 8 | type TestMessage struct { 9 | Code int `json:"code"` 10 | Message string `json:"message"` 11 | } 12 | -------------------------------------------------------------------------------- /protocol/users.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | type RegisterUserRequest struct { 4 | Type int `json:"type"` //注册方式: 1-手机 2-贪玩蛇 5 | Name string `json:"name"` //用户名, 可空,当非游客注册时用户名与手机号必须至少出现一项 6 | Password string `json:"password"` //MD5后的用户密码, 长度>=6 7 | VerifyID string `json:"verify_id"` //验证码ID 8 | VerifyCode string `json:"verify_code"` //验证码 9 | Phone string `json:"phone"` //手机号,可空 10 | AppID string `json:"appid"` //来自于哪一个应用的注册 11 | ChannelID string `json:"channel_id"` //来自于哪一个渠道的注册 12 | Device Device `json:"device"` //设备信息 13 | Token string `json:"token"` //Token, 游客注册并绑定时, 验证游客身份 14 | } 15 | 16 | type CheckUserInfoRequest struct { 17 | Type int `json:"type"` //注册方式: 1-手机 2-贪玩蛇 18 | Name string `json:"name"` //用户名, 可空,当非游客注册时用户名与手机号必须至少出现一项 19 | VerifyID string `json:"verify_id"` //验证码ID 20 | VerifyCode string `json:"verify_code"` //验证码 21 | Phone string `json:"phone"` //手机号,可空 22 | AppID string `json:"appid"` 23 | } 24 | 25 | type UserListRequest struct { 26 | Offset int `json:"offset"` 27 | Count int `json:"count"` 28 | } 29 | 30 | type UserListResponse struct { 31 | Code int `json:"code"` //状态码 32 | Data []UserInfo `json:"users"` //用户列表 33 | Total int `json:"total"` 34 | } 35 | 36 | type UserInfoRequest struct { 37 | UID int64 `json:"uid"` 38 | } 39 | 40 | type UserInfoResponse struct { 41 | Code int `json:"code"` //状态码 42 | Data UserInfo `json:"data"` //数据 43 | } 44 | 45 | type DeleteUserRequest struct { 46 | UID int64 `json:"uid"` 47 | } 48 | 49 | type QueryUserRequest struct { 50 | Name string `json:"name"` //用户名 51 | } 52 | 53 | //用属性查询用户 54 | type QueryUserByAttrRequest struct { 55 | Attr string `json:"attr"` //属性 56 | } 57 | 58 | //用户统计信息列表 59 | type UserStatsInfoListRequest struct { 60 | RoleTypes []uint8 `json:"role_types"` 61 | Account string `json:"account"` 62 | } 63 | 64 | type UserStatsInfoListResponse struct { 65 | Code int `json:"code"` //状态码 66 | Data []UserInfo `json:"users"` //用户列表 67 | Total int `json:"total"` 68 | } 69 | 70 | type QueryInfo struct { 71 | Name string `json:"name"` 72 | MaskedPhone string `json:"masked_phone"` 73 | } 74 | 75 | type QueryUserResponse struct { 76 | Code int `json:"code"` 77 | Data QueryInfo `json:"data"` 78 | } 79 | -------------------------------------------------------------------------------- /protocol/web.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | type Version struct { 4 | Version int `json:"version"` 5 | Android string `json:"android"` 6 | IOS string `json:"ios"` 7 | } 8 | -------------------------------------------------------------------------------- /screenshot/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/screenshot/1.png -------------------------------------------------------------------------------- /screenshot/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/screenshot/10.png -------------------------------------------------------------------------------- /screenshot/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/screenshot/11.png -------------------------------------------------------------------------------- /screenshot/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/screenshot/12.png -------------------------------------------------------------------------------- /screenshot/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/screenshot/13.png -------------------------------------------------------------------------------- /screenshot/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/screenshot/14.png -------------------------------------------------------------------------------- /screenshot/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/screenshot/2.png -------------------------------------------------------------------------------- /screenshot/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/screenshot/3.png -------------------------------------------------------------------------------- /screenshot/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/screenshot/4.png -------------------------------------------------------------------------------- /screenshot/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/screenshot/5.png -------------------------------------------------------------------------------- /screenshot/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/screenshot/6.png -------------------------------------------------------------------------------- /screenshot/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/screenshot/7.png -------------------------------------------------------------------------------- /screenshot/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/screenshot/8.png -------------------------------------------------------------------------------- /screenshot/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/screenshot/9.png -------------------------------------------------------------------------------- /tools/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/tools/README.md -------------------------------------------------------------------------------- /tools/sql2struct/dbr.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/tools/sql2struct/dbr.exe -------------------------------------------------------------------------------- /tools/sql2struct/sql2struct.bat: -------------------------------------------------------------------------------- 1 | %~dp0dbr --driver mysql --source root:88888888@tcp(10.10.30.52:3306)/broker --destination ../../internal/model --template struct.xorm.kwx.tpl --single --file struct_kwx.go 2 | -------------------------------------------------------------------------------- /tools/sql2struct/struct.xorm.kwx.tpl: -------------------------------------------------------------------------------- 1 | {{if not .IsAppend}}package {{.Model}}{{end}} 2 | 3 | {{$ilen := len .Imports}}{{if gt $ilen 0}} 4 | import ( 5 | {{range .Imports}}"{{.}}"{{end}} 6 | ){{end}} 7 | 8 | {{range .Tables}} 9 | type {{Mapper .Name}} struct { 10 | {{$table := .}} 11 | {{range .ColumnsSeq}}{{$col := $table.GetColumn .}} {{Mapper $col.Name}} {{Type $col}} {{Tag $table $col}} 12 | {{end}} 13 | } 14 | {{end}} 15 | 16 | func syncSchema() { 17 | DB.StoreEngine("InnoDB").Sync2({{range .Tables}} 18 | new({{Mapper .Name}}),{{end}} 19 | ) 20 | } -------------------------------------------------------------------------------- /wx-bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirk-Wang/nanoserver/2ced3a6bfd0491199832b3787c177abdf969b2fd/wx-bot.png --------------------------------------------------------------------------------