├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── container.config.json ├── db ├── dao │ ├── dao.go │ └── interface.go ├── init.go └── model │ └── counter.go ├── go.mod ├── go.sum ├── index.html ├── main.go └── service └── counter_service.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 二开推荐阅读[如何提高项目构建效率](https://developers.weixin.qq.com/miniprogram/dev/wxcloudrun/src/scene/build/speed.html) 2 | # 选择构建用基础镜像(选择原则:在包含所有用到的依赖前提下尽可能体积小)。如需更换,请到[dockerhub官方仓库](https://hub.docker.com/_/golang?tab=tags)自行选择后替换。 3 | FROM golang:1.17.1-alpine3.14 as builder 4 | 5 | # 指定构建过程中的工作目录 6 | WORKDIR /app 7 | 8 | # 将当前目录(dockerfile所在目录)下所有文件都拷贝到工作目录下(.dockerignore中文件除外) 9 | COPY . /app/ 10 | 11 | # 执行代码编译命令。操作系统参数为linux,编译后的二进制产物命名为main,并存放在当前目录下。 12 | RUN GOOS=linux go build -o main . 13 | 14 | # 选用运行时所用基础镜像(GO语言选择原则:尽量体积小、包含基础linux内容的基础镜像) 15 | FROM alpine:3.13 16 | 17 | # 容器默认时区为UTC,如需使用上海时间请启用以下时区设置命令 18 | # RUN apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo Asia/Shanghai > /etc/timezone 19 | 20 | # 使用 HTTPS 协议访问容器云调用证书安装 21 | RUN apk add ca-certificates 22 | 23 | # 指定运行时的工作目录 24 | WORKDIR /app 25 | 26 | # 将构建产物/app/main拷贝到运行时的工作目录中 27 | COPY --from=builder /app/main /app/index.html /app/ 28 | 29 | # 执行启动命令 30 | # 写多行独立的CMD命令是错误写法!只有最后一行CMD命令会被执行,之前的都会被忽略,导致业务报错。 31 | # 请参考[Docker官方文档之CMD命令](https://docs.docker.com/engine/reference/builder/#cmd) 32 | CMD ["/app/main"] 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Tencent 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wxcloudrun-golang 2 | [![GitHub license](https://img.shields.io/github/license/WeixinCloud/wxcloudrun-express)](https://github.com/WeixinCloud/wxcloudrun-express) 3 | ![GitHub package.json dependency version (prod)](https://img.shields.io/badge/golang-1.17.1-green) 4 | 5 | 微信云托管 golang 模版,实现简单的计数器读写接口,使用云托管 MySQL 读写、记录计数值。 6 | 7 | ![](https://qcloudimg.tencent-cloud.cn/raw/be22992d297d1b9a1a5365e606276781.png) 8 | 9 | 10 | ## 快速开始 11 | 前往 [微信云托管快速开始页面](https://developers.weixin.qq.com/miniprogram/dev/wxcloudrun/src/basic/guide.html),选择相应语言的模板,根据引导完成部署。 12 | 13 | ## 本地调试 14 | 下载代码在本地调试,请参考[微信云托管本地调试指南](https://developers.weixin.qq.com/miniprogram/dev/wxcloudrun/src/guide/debug/) 15 | 16 | ## 实时开发 17 | 代码变动时,不需要重新构建和启动容器,即可查看变动后的效果。请参考[微信云托管实时开发指南](https://developers.weixin.qq.com/miniprogram/dev/wxcloudrun/src/guide/debug/dev.html) 18 | 19 | ## Dockerfile最佳实践 20 | 请参考[如何提高项目构建效率](https://developers.weixin.qq.com/miniprogram/dev/wxcloudrun/src/scene/build/speed.html) 21 | 22 | ## 目录结构说明 23 | ~~~ 24 | . 25 | ├── Dockerfile Dockerfile 文件 26 | ├── LICENSE LICENSE 文件 27 | ├── README.md README 文件 28 | ├── container.config.json 模板部署「服务设置」初始化配置(二开请忽略) 29 | ├── db 数据库逻辑目录 30 | ├── go.mod go.mod 文件 31 | ├── go.sum go.sum 文件 32 | ├── index.html 主页 html 33 | ├── main.go 主函数入口 34 | └── service 接口服务逻辑目录 35 | ~~~ 36 | 37 | 38 | ## 服务 API 文档 39 | 40 | ### `GET /api/count` 41 | 42 | 获取当前计数 43 | 44 | #### 请求参数 45 | 46 | 无 47 | 48 | #### 响应结果 49 | 50 | - `code`:错误码 51 | - `data`:当前计数值 52 | 53 | ##### 响应结果示例 54 | 55 | ```json 56 | { 57 | "code": 0, 58 | "data": 42 59 | } 60 | ``` 61 | 62 | #### 调用示例 63 | 64 | ``` 65 | curl https://<云托管服务域名>/api/count 66 | ``` 67 | 68 | 69 | 70 | ### `POST /api/count` 71 | 72 | 更新计数,自增或者清零 73 | 74 | #### 请求参数 75 | 76 | - `action`:`string` 类型,枚举值 77 | - 等于 `"inc"` 时,表示计数加一 78 | - 等于 `"clear"` 时,表示计数重置(清零) 79 | 80 | ##### 请求参数示例 81 | 82 | ``` 83 | { 84 | "action": "inc" 85 | } 86 | ``` 87 | 88 | #### 响应结果 89 | 90 | - `code`:错误码 91 | - `data`:当前计数值 92 | 93 | ##### 响应结果示例 94 | 95 | ```json 96 | { 97 | "code": 0, 98 | "data": 42 99 | } 100 | ``` 101 | 102 | #### 调用示例 103 | 104 | ``` 105 | curl -X POST -H 'content-type: application/json' -d '{"action": "inc"}' https://<云托管服务域名>/api/count 106 | ``` 107 | 108 | ## 使用注意 109 | 如果不是通过微信云托管控制台部署模板代码,而是自行复制/下载模板代码后,手动新建一个服务并部署,需要在「服务设置」中补全以下环境变量,才可正常使用,否则会引发无法连接数据库,进而导致部署失败。 110 | - MYSQL_ADDRESS 111 | - MYSQL_PASSWORD 112 | - MYSQL_USERNAME 113 | 以上三个变量的值请按实际情况填写。如果使用云托管内MySQL,可以在控制台MySQL页面获取相关信息。 114 | 115 | 116 | 117 | ## License 118 | 119 | [MIT](./LICENSE) 120 | -------------------------------------------------------------------------------- /container.config.json: -------------------------------------------------------------------------------- 1 | { 2 | // 本配置文件仅配合模板部署使用,为模板部署的服务生成「服务设置」的初始值。 3 | // 模板部署结束后,后续服务发布与本配置文件完全无关,修改「服务设置」请到控制台操作。 4 | // 复制模板代码自行开发请忽略本配置文件。 5 | "containerPort": 80, 6 | "minNum": 0, 7 | "maxNum": 5, 8 | "cpu": 1, 9 | "mem": 2, 10 | "policyType": "cpu", 11 | "policyThreshold": 60, 12 | "policyDetails": [ 13 | { 14 | "PolicyType": "cpu", 15 | "PolicyThreshold": 60 16 | }, 17 | { 18 | "PolicyType": "mem", 19 | "PolicyThreshold": 60 20 | } 21 | ], 22 | "envParams": {}, 23 | "customLogs": "stdout", 24 | "dataBaseName":"golang_demo", 25 | "executeSQLs":[ 26 | "CREATE DATABASE IF NOT EXISTS golang_demo;", 27 | "USE golang_demo;", 28 | "CREATE TABLE IF NOT EXISTS `Counters` (`id` int(11) NOT NULL AUTO_INCREMENT, `count` int(11) NOT NULL DEFAULT 1, `createdAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `updatedAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`)) ENGINE = InnoDB DEFAULT CHARSET = utf8;" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /db/dao/dao.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "wxcloudrun-golang/db" 5 | "wxcloudrun-golang/db/model" 6 | ) 7 | 8 | const tableName = "Counters" 9 | 10 | // ClearCounter 清除Counter 11 | func (imp *CounterInterfaceImp) ClearCounter(id int32) error { 12 | cli := db.Get() 13 | return cli.Table(tableName).Delete(&model.CounterModel{Id: id}).Error 14 | } 15 | 16 | // UpsertCounter 更新/写入counter 17 | func (imp *CounterInterfaceImp) UpsertCounter(counter *model.CounterModel) error { 18 | cli := db.Get() 19 | return cli.Table(tableName).Save(counter).Error 20 | } 21 | 22 | // GetCounter 查询Counter 23 | func (imp *CounterInterfaceImp) GetCounter(id int32) (*model.CounterModel, error) { 24 | var err error 25 | var counter = new(model.CounterModel) 26 | 27 | cli := db.Get() 28 | err = cli.Table(tableName).Where("id = ?", id).First(counter).Error 29 | 30 | return counter, err 31 | } 32 | -------------------------------------------------------------------------------- /db/dao/interface.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "wxcloudrun-golang/db/model" 5 | ) 6 | 7 | // CounterInterface 计数器数据模型接口 8 | type CounterInterface interface { 9 | GetCounter(id int32) (*model.CounterModel, error) 10 | UpsertCounter(counter *model.CounterModel) error 11 | ClearCounter(id int32) error 12 | } 13 | 14 | // CounterInterfaceImp 计数器数据模型实现 15 | type CounterInterfaceImp struct{} 16 | 17 | // Imp 实现实例 18 | var Imp CounterInterface = &CounterInterfaceImp{} 19 | -------------------------------------------------------------------------------- /db/init.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "gorm.io/driver/mysql" 9 | "gorm.io/gorm" 10 | "gorm.io/gorm/schema" 11 | ) 12 | 13 | var dbInstance *gorm.DB 14 | 15 | // Init 初始化数据库 16 | func Init() error { 17 | 18 | source := "%s:%s@tcp(%s)/%s?readTimeout=1500ms&writeTimeout=1500ms&charset=utf8&loc=Local&&parseTime=true" 19 | user := os.Getenv("MYSQL_USERNAME") 20 | pwd := os.Getenv("MYSQL_PASSWORD") 21 | addr := os.Getenv("MYSQL_ADDRESS") 22 | dataBase := os.Getenv("MYSQL_DATABASE") 23 | if dataBase == "" { 24 | dataBase = "golang_demo" 25 | } 26 | source = fmt.Sprintf(source, user, pwd, addr, dataBase) 27 | fmt.Println("start init mysql with ", source) 28 | 29 | db, err := gorm.Open(mysql.Open(source), &gorm.Config{ 30 | NamingStrategy: schema.NamingStrategy{ 31 | SingularTable: true, // use singular table name, table for `User` would be `user` with this option enabled 32 | }}) 33 | if err != nil { 34 | fmt.Println("DB Open error,err=", err.Error()) 35 | return err 36 | } 37 | 38 | sqlDB, err := db.DB() 39 | if err != nil { 40 | fmt.Println("DB Init error,err=", err.Error()) 41 | return err 42 | } 43 | 44 | // 用于设置连接池中空闲连接的最大数量 45 | sqlDB.SetMaxIdleConns(100) 46 | // 设置打开数据库连接的最大数量 47 | sqlDB.SetMaxOpenConns(200) 48 | // 设置了连接可复用的最大时间 49 | sqlDB.SetConnMaxLifetime(time.Hour) 50 | 51 | dbInstance = db 52 | 53 | fmt.Println("finish init mysql with ", source) 54 | return nil 55 | } 56 | 57 | // Get ... 58 | func Get() *gorm.DB { 59 | return dbInstance 60 | } 61 | -------------------------------------------------------------------------------- /db/model/counter.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | // CounterModel 计数器模型 6 | type CounterModel struct { 7 | Id int32 `gorm:"column:id" json:"id"` 8 | Count int32 `gorm:"column:count" json:"count"` 9 | CreatedAt time.Time `gorm:"column:createdAt" json:"createdAt"` 10 | UpdatedAt time.Time `gorm:"column:updatedAt" json:"updatedAt"` 11 | } 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module wxcloudrun-golang 2 | 3 | go 1.16 4 | 5 | require ( 6 | gorm.io/driver/mysql v1.1.2 7 | gorm.io/gorm v1.21.16 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 2 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 3 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 4 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 5 | github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI= 6 | github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 7 | gorm.io/driver/mysql v1.1.2 h1:OofcyE2lga734MxwcCW9uB4mWNXMr50uaGRVwQL2B0M= 8 | gorm.io/driver/mysql v1.1.2/go.mod h1:4P/X9vSc3WTrhTLZ259cpFd6xKNYiSSdSZngkSBGIMM= 9 | gorm.io/gorm v1.21.12/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= 10 | gorm.io/gorm v1.21.16 h1:YBIQLtP5PLfZQz59qfrq7xbrK7KWQ+JsXXCH/THlMqs= 11 | gorm.io/gorm v1.21.16/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= 12 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 12 | 14 | 欢迎使用微信云托管 15 | 203 | 204 | 205 | 206 |
207 |
208 | 210 |
欢迎使用微信云托管
211 |
212 |
213 | 214 | 当前计数: 215 | 216 | 清零 217 | 218 | 计数+1 220 |
221 |
222 | 224 | 扫码加入微信云托管用户群 225 |
226 |
227 |
228 |
229 | 快速入门 230 |
231 |
232 | 235 | 237 | icons_outline_warrant copy 238 | 239 | 240 | 241 | 244 | 245 | 246 | 247 | 开发者文档 248 | 251 | 253 | icons_outlined_play 254 | 256 | 257 | 258 | 261 | 262 | 263 | 264 | 视频教程 265 |
266 |
267 |
268 |
269 | 270 | 271 | 273 | 299 | 300 | 301 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "wxcloudrun-golang/db" 8 | "wxcloudrun-golang/service" 9 | ) 10 | 11 | func main() { 12 | if err := db.Init(); err != nil { 13 | panic(fmt.Sprintf("mysql init failed with %+v", err)) 14 | } 15 | 16 | http.HandleFunc("/", service.IndexHandler) 17 | http.HandleFunc("/api/count", service.CounterHandler) 18 | 19 | log.Fatal(http.ListenAndServe(":80", nil)) 20 | } 21 | -------------------------------------------------------------------------------- /service/counter_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "time" 9 | 10 | "wxcloudrun-golang/db/dao" 11 | "wxcloudrun-golang/db/model" 12 | 13 | "gorm.io/gorm" 14 | ) 15 | 16 | // JsonResult 返回结构 17 | type JsonResult struct { 18 | Code int `json:"code"` 19 | ErrorMsg string `json:"errorMsg,omitempty"` 20 | Data interface{} `json:"data"` 21 | } 22 | 23 | // IndexHandler 计数器接口 24 | func IndexHandler(w http.ResponseWriter, r *http.Request) { 25 | data, err := getIndex() 26 | if err != nil { 27 | fmt.Fprint(w, "内部错误") 28 | return 29 | } 30 | fmt.Fprint(w, data) 31 | } 32 | 33 | // CounterHandler 计数器接口 34 | func CounterHandler(w http.ResponseWriter, r *http.Request) { 35 | res := &JsonResult{} 36 | 37 | if r.Method == http.MethodGet { 38 | counter, err := getCurrentCounter() 39 | if err != nil { 40 | res.Code = -1 41 | res.ErrorMsg = err.Error() 42 | } else { 43 | res.Data = counter.Count 44 | } 45 | } else if r.Method == http.MethodPost { 46 | count, err := modifyCounter(r) 47 | if err != nil { 48 | res.Code = -1 49 | res.ErrorMsg = err.Error() 50 | } else { 51 | res.Data = count 52 | } 53 | } else { 54 | res.Code = -1 55 | res.ErrorMsg = fmt.Sprintf("请求方法 %s 不支持", r.Method) 56 | } 57 | 58 | msg, err := json.Marshal(res) 59 | if err != nil { 60 | fmt.Fprint(w, "内部错误") 61 | return 62 | } 63 | w.Header().Set("content-type", "application/json") 64 | w.Write(msg) 65 | } 66 | 67 | // modifyCounter 更新计数,自增或者清零 68 | func modifyCounter(r *http.Request) (int32, error) { 69 | action, err := getAction(r) 70 | if err != nil { 71 | return 0, err 72 | } 73 | 74 | var count int32 75 | if action == "inc" { 76 | count, err = upsertCounter(r) 77 | if err != nil { 78 | return 0, err 79 | } 80 | } else if action == "clear" { 81 | err = clearCounter() 82 | if err != nil { 83 | return 0, err 84 | } 85 | count = 0 86 | } else { 87 | err = fmt.Errorf("参数 action : %s 错误", action) 88 | } 89 | 90 | return count, err 91 | } 92 | 93 | // upsertCounter 更新或修改计数器 94 | func upsertCounter(r *http.Request) (int32, error) { 95 | currentCounter, err := getCurrentCounter() 96 | var count int32 97 | createdAt := time.Now() 98 | if err != nil && err != gorm.ErrRecordNotFound { 99 | return 0, err 100 | } else if err == gorm.ErrRecordNotFound { 101 | count = 1 102 | createdAt = time.Now() 103 | } else { 104 | count = currentCounter.Count + 1 105 | createdAt = currentCounter.CreatedAt 106 | } 107 | 108 | counter := &model.CounterModel{ 109 | Id: 1, 110 | Count: count, 111 | CreatedAt: createdAt, 112 | UpdatedAt: time.Now(), 113 | } 114 | err = dao.Imp.UpsertCounter(counter) 115 | if err != nil { 116 | return 0, err 117 | } 118 | return counter.Count, nil 119 | } 120 | 121 | func clearCounter() error { 122 | return dao.Imp.ClearCounter(1) 123 | } 124 | 125 | // getCurrentCounter 查询当前计数器 126 | func getCurrentCounter() (*model.CounterModel, error) { 127 | counter, err := dao.Imp.GetCounter(1) 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | return counter, nil 133 | } 134 | 135 | // getAction 获取action 136 | func getAction(r *http.Request) (string, error) { 137 | decoder := json.NewDecoder(r.Body) 138 | body := make(map[string]interface{}) 139 | if err := decoder.Decode(&body); err != nil { 140 | return "", err 141 | } 142 | defer r.Body.Close() 143 | 144 | action, ok := body["action"] 145 | if !ok { 146 | return "", fmt.Errorf("缺少 action 参数") 147 | } 148 | 149 | return action.(string), nil 150 | } 151 | 152 | // getIndex 获取主页 153 | func getIndex() (string, error) { 154 | b, err := ioutil.ReadFile("./index.html") 155 | if err != nil { 156 | return "", err 157 | } 158 | return string(b), nil 159 | } 160 | --------------------------------------------------------------------------------