这篇开始搭建项目,首先规划一下整体的目录。
目录就不过多解释了,这里并不复杂,主要想谈谈其他的点。
在做项目的时候,我不太喜欢上来就是干,我也不提倡这种方式。
最理想的方式应该是从设计做起。比如需求下来,大体先过一遍,从表设计开始做起,会涉及到哪些表,表与表之间的关系,当前的设计是否能满足未来扩展点需求......,画 ER 图也好,手写也罢,这是第一步。
然后具体落地到项目中会对应哪些模块,哪些类,需要定义类的哪些行为,类与类之间的交互关系,这又是一大块。
这样一圈下来你也大概知道这个需求是否存在坑,有坑的话可以及时进行沟通调整。最怕一上来就开干,快做完了发现有个大坑。
对应到我们这个项目,目前我们只需要一张保存任务的表即可。
CREATE TABLE `jobs` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `content` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '待办事项', `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `notice_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `status` tinyint(3) unsigned NOT NULL DEFAULT '2' COMMENT '1已通知2待通知3失败', `phone` varchar(11) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '手机号码', `email` varchar(25) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '邮箱', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=46 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
配置文件
既然提到数据库,那么我们还需要初始化数据库行为。初始化数据库前,我们先得搞定配置。
在 conf 目录下创建 config.go 文件,
package config import ( "encoding/json" "os" ) type Wechat struct { AppID string AppSecret string Token string EncodingAESKey string } type Db struct { Address string DbName string User string Password string Port int } type Email struct { User string Pass string Host string Port int } type Configuration struct { Wechat *Wechat Db *Db Email *Email } var ConfAll *Configuration func LoadConfig() error { file, err := os.Open("config.json") if err != nil { return err } decoder := json.NewDecoder(file) ConfAll = &Configuration{} err = decoder.Decode(ConfAll) if err != nil { return err } return nil }
涉及到数据库配置、微信平台配置以及发送邮件配置信息。LoadConfig 就是配置项的初始化操作,赋值给变量 ConfAll,后续关于配置的信息就从这个变量取。
在 conf.json 文件中,设置对应的配置项值。只要别把这个文件上传到版本库就行。
{ "Wechat": { "AppID": "xxx", "AppSecret": "xxx", "Token": "xxx", "EncodingAESKey": "xxxxx" }, "Db": { "Address": "127.0.0.1", "DbName": "remind", "User": "root", "Password": "Passw0rd", "Port": 3306 }, "Email": { "User": "1185079673@qq.com", "Pass": "xxx", "Host": "smtp.qq.com", "Port": 25 }, }
连接池
接着开始初始化数据库操作。在 db 目录下创建文件 mysql.go。然后,
package db import ( "database/sql" "fmt" "go-remind/config" "time" "gorm.io/driver/mysql" "gorm.io/gorm" ) const ( Url = "%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=5s" MaxOpen = 5 // 最大打开数 MaxIdle = 2 // 最大保留连接数 LifeMinuteTime = 5 // 连接可重用最大时间 ) var Gorm *gorm.DB func InitDb(conf *config.Db) { var err error var sqlDb *sql.DB Gorm, err = gorm.Open(mysql.Open( fmt.Sprintf( Url, conf.User, conf.Password, conf.Address, conf.Port, conf.DbName)), &gorm.Config{}) if err != nil { fmt.Printf("open db:%v", err) } sqlDb, err = Gorm.DB() if err != nil { fmt.Printf("sql Db:%v", err) } // 允许最大并发打开连接数 sqlDb.SetMaxOpenConns(MaxOpen) // 允许连接池中最多保留连接数 sqlDb.SetConnMaxIdleTime(MaxIdle) // 允许连接可重用的最长时间 sqlDb.SetConnMaxLifetime(LifeMinuteTime * time.Minute) }
这一段代码主要是初始化数据库,创建一个数据库连接池。
SetMaxOpenConns 允许最大并发打开连接数。SetConnMaxIdleTime 允许连接池中最多保留连接数。SetConnMaxLifetime 允许连接可重用的最长时间。
为什么需要使用数据库连接池?
从性能的角度上考虑,如果没有连接池,那么一个请求就创建一条与数据库的连接,然后操作完成事务提交,断开连接,下次请求重新创建连接。而连接必然需要经过 TCP 的三次握手,很大一部分取决于网络情况,这是一个耗时的过程。还有一点,如果系统层面不加以控制,在高并发的场景下,经常会出现数据库连接数超过最大值。
加了连接池,那么我们只需要在初始化的时候创建若干个预备连接放入池中,等到有需要的时候直接从池中拿出已有的连接和数据库进行交互,不必经过三次握手。等操作完成,再还回到连接池中,有助于提升系统的性能。
而且你也不必再去担心 To Many Connections。因为当应用程序发现连接池中没有可用的空闲连接时,应用程序将被迫进行等待,直到有新的空闲连接为止。
但是连接池也有不好的地方,比如当空闲连接过多,会导致资源大量的浪费。某种情况下空闲连接已关闭,但是没从连接池中移除,导致在使用的时候出现异常。
所以设置这些参数值成了一门学问。并没有标准的设置具体值的说法,只能根据具体的业务流量去加以测试判断。
接下来考虑有哪些操作。这个项目中会存在创建任务、获取即将执行通知的任务列表、发送成功或者失败修改对应任务状态,我们去完成这些基本操作。
数据操作
首先定义模型。在 models 下面创建一个 job.go 文件。
package models import ( "time" ) var ( // 通知成功 JobSuccess = 1 // 待通知 JobWait = 2 // 通知失败 JobFail = 3 ) type Job struct { Id int64 Content string CreatedAt time.Time NoticeTime time.Time Status int8 Phone string Email string } func (Job) TableName() string { return "jobs" }
在 logic 目录下也创建文件 job.go,这是真正和数据库交互的地方。
package logic import ( "fmt" "go-remind/db" "go-remind/models" "time" ) type JobLogic struct{} func NewJob(content string, sendTime time.Time, phone, email string) *models.Job { return &models.Job{ Content: content, NoticeTime: sendTime, Phone: phone, Email: email, } } // 插入任务 func (j *JobLogic) Insert(job models.Job) error { fmt.Printf("值是:%v",db.Gorm) result := db.Gorm.Create(&job) return result.Error } // 根据时间获取近期要执行的任务列表 func (j *JobLogic) GetJobsByTime(startTime string, endTime string) (jobs []models.Job, err error) { err = db.Gorm.Where("status=? and notice_time>=? and notice_time<=?", models.JobWait, startTime, endTime). Find(&jobs).Error return } // 修改任务状态 func (j *JobLogic) UpdateStatusById(id, status int) error { return db.Gorm.Where("id=?", id).Update("status", status).Error }
我们定义了一个 JobLogic 的结构体类型,JobLogic 提供了三个指针方法,分别用于用于创建任务、获取批量任务以及修改任务状态。
微信相关
我们是和微信公众号交互的,必然要接微信公众号消息回调。这一块有成熟的库,直接用就行,我用的是 silenceper/wechat。
在 handles 目录下创建 wechat.go,
package handlers import ( "fmt" "github.com/gin-gonic/gin" "github.com/silenceper/wechat/cache" "github.com/silenceper/wechat/v2" offConfig "github.com/silenceper/wechat/v2/officialaccount/config" "github.com/silenceper/wechat/v2/officialaccount/message" . "go-remind/config" ) func Message(c *gin.Context) { defer func() { if err := recover(); err != nil { fmt.Printf("运行错误:%v", err) } }() //使用 memcache 保存access_token,也可选择redis或自定义cache wc := wechat.NewWechat() memory := cache.NewMemory() cfg := &offConfig.Config{ AppID: ConfAll.Wechat.AppID, AppSecret: ConfAll.Wechat.AppSecret, Token: ConfAll.Wechat.Token, EncodingAESKey: ConfAll.Wechat.EncodingAESKey, Cache: memory, } officialAccount := wc.GetOfficialAccount(cfg) // 传入request和responseWriter server := officialAccount.GetServer(c.Request, c.Writer) //设置接收消息的处理方法 server.SetMessageHandler(func(msg message.MixMessage) *message.Reply { switch msg.MsgType { case message.MsgTypeText: //回复消息:演示回复用户发送的消息 res := message.NewText(msg.Content) //res := message.NewText(HandleMessage(msg.Content)) return &message.Reply{MsgType: message.MsgTypeText, MsgData: res} case message.MsgTypeVoice: text := message.NewVoice(msg.Content) return &message.Reply{MsgType: message.MsgTypeText, MsgData: text} default: return &message.Reply{MsgType: message.MsgTypeText, MsgData: message.NewText("我睡着了,听不懂你在说啥")} } }) //处理消息接收以及回复 err := server.Serve() if err != nil { fmt.Println(err) return } //发送回复的消息 server.Send() }
上面的逻辑主要是当用户在公众号发送对应信息,微信根据开发者配置回调地址,把信息订阅给你。由你来进行进一步的处理。当然了,目前业务上的信息提取工作我们还暂时没写,不急。
注意看最上面,这句话很眼熟吧。
defer func() { if err := recover(); err != nil { fmt.Printf("运行错误:%v", err) } }()
到这里,整体的基础工作做的差不多了,还需要给微信提供一个接口,并且把这些服务连接并运行,让程序跑起来。
在 main.go 下,
package main import ( "github.com/gin-gonic/gin" . "go-remind/config" "go-remind/db" "go-remind/handlers" "log" ) func init() { // 初始化配置文件 err := LoadConfig() if err != nil { log.Fatal("初始化错误:", err) } // 初始化数据库连接池 if err = db.InitDb(ConfAll.Db); err != nil { log.Fatal("初始化错误:", err) } } func main() { r := gin.Default() // 开放一个路由接口 r.GET("/msg", handlers.Message) _ = r.Run() }
很简单吧。虽然只需要提供一个接口,但是还是使用了 gin。不要在意这些。
go 相关的路由包很多,如果有特殊场景或者对性能敏感的话,就需要去好好调研各个包了。我用 gin 的原因是下一个项目会用到 gin,当然这是后话了。
最后我们来总结一下这一篇文章。主要完成了初始化配置文件、初始化数据库连接池,完成表的设计并且实现具体的业务操作。完成公众号的基础回调事件等操作,让我们继续。
另外这个项目我放在:https://github.com/wuqinqiang/go-remind 感兴趣可以 clone。