以下记录了使用go语言框架Beego,Mysql数据库,Redis数据库实现一个点菜/菜谱应用API的全过程。
技术方案
github地址
数据库设计
新建数据库:
CREATE DATABASE menu;
新建数据表:
CREATE TABLE `menu` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(200) NOT NULL DEFAULT '',
`content` text NOT NULL,
`pictures` varchar(1000) NOT NULL DEFAULT '',
`tags` varchar(1000) NOT NULL DEFAULT '',
`status` tinyint(4) NOT NULL DEFAULT '0',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `data` (`id` int(10) unsigned NOT NULL AUTO_INCREMENT,`menu_id` int(10) unsigned NOT NULL DEFAULT '0',`like_number` int(10) NOT NULL DEFAULT '0',`visit_number` int(10) NOT NULL DEFAULT '0',`order_number` int(10) NOT NULL DEFAULT '0',`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,PRIMARY KEY (`id`),UNIQUE KEY `unique_menu` (`menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
CREATE TABLE `order_info` (`id` int(10) unsigned NOT NULL AUTO_INCREMENT,`menu_id_list` varchar(200) NOT NULL DEFAULT '',`status` tinyint(4) NOT NULL DEFAULT '0',`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
生成models层
进入终端项目,执行命令生成数据库表对应的models:
bee generate appcode -tables="data" -driver=mysql -conn="root:123456@tcp(127.0.0.1:3306)/menu" -level=1
bee generate appcode -tables="order_info" -driver=mysql -conn="root:123456@tcp(127.0.0.1:3306)/menu" -level=1
bee generate appcode -tables="menu" -driver=mysql -conn="root:123456@tcp(127.0.0.1:3306)/menu" -level=1
生成data、menu、order_info对应的model文件
我们数据库设置了创建时间和更新时间的自动更新
配置环境变量连接数据库:
conf/app.conf中添加:
# Mysql setting
mysqluser = "root"
mysqlpass = "root"
mysqlhost = "127.0.0.1"
mysqldb = "liuyan"
mysqlport = 3306
新建core文件,用于数据库连接的核心代码:
package modelsimport ("fmt""github.com/beego/beego/v2/client/orm"beego "github.com/beego/beego/v2/server/web"_ "github.com/go-sql-driver/mysql"
)func Init() {username, err := beego.AppConfig.String("mysqlname")if err != nil {fmt.Println("err:", err)}password, err := beego.AppConfig.String("mysqlpass")host, err := beego.AppConfig.String("mysqlhost")database, err := beego.AppConfig.String("mysqldb")port, err := beego.AppConfig.String("mysqlport")dsn := username + ":" + password + "@tcp(" + host + ":" + port + ")/" + database + "?charset=utf8&loc=Local"err = orm.RegisterDataBase("default", "mysql", dsn)if err != nil {fmt.Println("err::", err)}
}
之后在main.go中初始化数据库连接:
package mainimport ("menu_api/models"_ "menu_api/routers"beego "github.com/beego/beego/v2/server/web"
)func init() {models.Init()
}func main() {if beego.BConfig.RunMode == "dev" {beego.BConfig.WebConfig.DirectoryIndex = truebeego.BConfig.WebConfig.StaticDir["/swagger"] = "swagger"}beego.Run()
}
bee run .
没有打印错误,则说明数据库连接成功
日志设置
新建logs/logs.go文件进行初始化日志:
package logsimport ("github.com/beego/beego/v2/core/logs"
)func Init() {// 设置日志模式为文件,输出的目录logs.SetLogger("file", `{"filename":"logs/menu.log"}`)
}
将日志模式设置为文件格式,输出的目录为logs/menu.log(无需手动创建)
在main.go中初始化日志:
bee run . 后会发现新建了文件logs/menu.log,并且可以在其中发现项目打印的日志,关于日志,可以参考官方文档:Welcome to Beego | Beego
核心逻辑
菜单逻辑
注册路由
新增逻辑:
beego.NSNamespace("/menu",beego.NSInclude(&controllers.MenuController{},),),
models层已经自动构建,只需要实现controller即可
新建controller/menu.go
查询列表,查询详情,增加,更新方法:
package controllersimport ("encoding/json""errors""github.com/beego/beego/v2/core/logs"beego "github.com/beego/beego/v2/server/web""menu_api/models""strings"
)type MenuController struct {beego.Controller
}// @Title GetList
// @Description get Menu List
// @Param query query string false "Filter. e.g. col1:v1,col2:v2 ..."
// @Param fields query string false "Fields returned. e.g. col1,col2 ..."
// @Param sortby query string false "Sorted-by fields. e.g. col1,col2 ..."
// @Param order query string false "Order corresponding to each sortby field, if single value, apply to all sortby fields. e.g. desc,asc ..."
// @Param limit query string false "Limit the size of result set. Must be an integer"
// @Param offset query string false "Start position of result set. Must be an integer"
// @Success 200 {object} models.Menu
// @router / [get]
func (m *MenuController) GetList() {var fields []stringvar sortby []stringvar order []stringvar query = make(map[string]string)var limit int64 = 10var offset int64// fields: col1,col2,entity.col3if v := m.GetString("fields"); v != "" {fields = strings.Split(v, ",")}// limit: 10 (default is 10)if v, err := m.GetInt64("limit"); err == nil {limit = v}// offset: 0 (default is 0)if v, err := m.GetInt64("offset"); err == nil {offset = v}// sortby: col1,col2if v := m.GetString("sortby"); v != "" {sortby = strings.Split(v, ",")}// order: desc,ascif v := m.GetString("order"); v != "" {order = strings.Split(v, ",")}// query: k:v,k:vif v := m.GetString("query"); v != "" {for _, cond := range strings.Split(v, ",") {kv := strings.SplitN(cond, ":", 2)if len(kv) != 2 {m.Data["json"] = errors.New("Error: invalid query key/value pair")m.ServeJSON()return}k, v := kv[0], kv[1]query[k] = v}}menList, err := models.GetAllMenu(query, fields, sortby, order, offset, limit)if err != nil {logs.Error("Get Database Menu List Error, ERR:", err)m.Data["json"] = errors.New("Error: Get Database Menu Error")}m.Data["json"] = menListm.ServeJSON()
}// @Title Get
// @Description get menu by id
// @Param id path int true "The key for staticblock"
// @Success 200 {object} models.Menu
// @Failure 403 :id is empty
// @router /:id [get]
func (m *MenuController) Get() {mid := 0if v, err := m.GetInt(":id"); err == nil {mid = v}if mid != 0 {menu, err := models.GetMenuById(mid)if err != nil {logs.Error("Get Database Menu Error, ERR:", err)m.Data["json"] = err.Error()} else {m.Data["json"] = menu}}m.ServeJSON()
}// @Title CreateMenu
// @Description create menus
// @Param body body models.Meau true "body for user content"
// @Success 200 {int} models.Menu.Id
// @Failure 403 body is empty
// @router / [post]
func (m *MenuController) Post() {var menu models.Menuerr := json.Unmarshal(m.Ctx.Input.RequestBody, &menu)if err != nil || menu.Title == "" || menu.Content == "" || menu.Pictures == "" {if err != nil {logs.Error("Add Database Menu Unmarshal Error, ERR:", err)} else {err = errors.New("title or content or picture is not empty")}m.Data["json"] = err.Error()m.ServeJSON()return}mid, err := models.AddMenu(&menu)if err != nil {logs.Error("Add Database Menu Error, ERR:", err)m.Data["json"] = err.Error()m.ServeJSON()return}m.Data["json"] = map[string]int64{"mid:": mid}m.ServeJSON()
}// @Title Update
// @Description update the menu
// @Param id path string true "The menu_id you want to update"
// @Param body body models.Menu true "body for user content"
// @Success 200 {object} models.Menu
// @Failure 403 :id is not int
// @router /:id [put]
func (m *MenuController) Put() {mid, err := m.GetInt(":id")var menu models.Menuerr = json.Unmarshal(m.Ctx.Input.RequestBody, &menu)if err != nil {logs.Error("Add Database Menu Error, ERR:", err)m.Data["json"] = err.Error()m.ServeJSON()return}menu.Id = midif mid != 0 {err := models.UpdateMenuById(&menu)if err != nil {m.Data["json"] = err.Error()} else {m.Data["json"] = "update success"}}m.ServeJSON()
}
订单逻辑
注册路由
新增逻辑:
beego.NSNamespace("/order_info",beego.NSInclude(&controllers.OrderInfoController{},),),
新建controller/order_info.go
查询列表,查询详情,增加,更新方法:
package controllersimport ("encoding/json""errors""github.com/beego/beego/v2/core/logs"beego "github.com/beego/beego/v2/server/web""menu_api/models""strings"
)type OrderInfoController struct {beego.Controller
}// @Title GetList
// @Description get OrderInfo List
// @Param query query string false "Filter. e.g. col1:v1,col2:v2 ..."
// @Param fields query string false "Fields returned. e.g. col1,col2 ..."
// @Param sortby query string false "Sorted-by fields. e.g. col1,col2 ..."
// @Param order query string false "Order corresponding to each sortby field, if single value, apply to all sortby fields. e.g. desc,asc ..."
// @Param limit query string false "Limit the size of result set. Must be an integer"
// @Param offset query string false "Start position of result set. Must be an integer"
// @Success 200 {object} models.OrderInfo
// @router / [get]
func (o *OrderInfoController) GetList() {var fields []stringvar sortby []stringvar order []stringvar query = make(map[string]string)var limit int64 = 10var offset int64// fields: col1,col2,entity.col3if v := o.GetString("fields"); v != "" {fields = strings.Split(v, ",")}// limit: 10 (default is 10)if v, err := o.GetInt64("limit"); err == nil {limit = v}// offset: 0 (default is 0)if v, err := o.GetInt64("offset"); err == nil {offset = v}// sortby: col1,col2if v := o.GetString("sortby"); v != "" {sortby = strings.Split(v, ",")}// order: desc,ascif v := o.GetString("order"); v != "" {order = strings.Split(v, ",")}// query: k:v,k:vif v := o.GetString("query"); v != "" {for _, cond := range strings.Split(v, ",") {kv := strings.SplitN(cond, ":", 2)if len(kv) != 2 {o.Data["json"] = errors.New("Error: invalid query key/value pair")o.ServeJSON()return}k, v := kv[0], kv[1]query[k] = v}}orderList, err := models.GetAllOrderInfo(query, fields, sortby, order, offset, limit)if err != nil {logs.Error("Get Database OrderInfo List Error, ERR:", err)o.Data["json"] = errors.New("Error: Get Database OrderInfo Error")}o.Data["json"] = orderListo.ServeJSON()
}// @Title Get
// @Description get orderInfo by id
// @Param id path int true "The key for staticblock"
// @Success 200 {object} models.OrderInfo
// @Failure 403 :id is empty
// @router /:id [get]
func (o *OrderInfoController) Get() {oid := 0if v, err := o.GetInt(":id"); err == nil {oid = v}if oid != 0 {orderInfo, err := models.GetOrderInfoById(oid)if err != nil {logs.Error("Get Database OrderInfo Error, ERR:", err)o.Data["json"] = err.Error()} else {o.Data["json"] = orderInfo}}o.ServeJSON()
}// @Title CreateOrderInfo
// @Description create OrderInfo
// @Param body body models.OrderInfo true "body for user content"
// @Success 200 {int} models.OrderInfo.Id
// @Failure 403 body is empty
// @router / [post]
func (o *OrderInfoController) Post() {var orderInfo models.OrderInfoerr := json.Unmarshal(o.Ctx.Input.RequestBody, &orderInfo)if err != nil || orderInfo.MenuIdList == "" {if err != nil {logs.Error("Add Database OrderInfo Unmarshal Error, ERR:", err)} else {err = errors.New("title or content or picture is not empty")}o.Data["json"] = err.Error()o.ServeJSON()return}oid, err := models.AddOrderInfo(&orderInfo)if err != nil {logs.Error("Add Database OrderInfo Error, ERR:", err)o.Data["json"] = err.Error()o.ServeJSON()return}o.Data["json"] = map[string]int64{"oid:": oid}o.ServeJSON()
}// @Title Update
// @Description update the OrderInfo
// @Param id path string true "The menu_id you want to update"
// @Param body body models.OrderInfo true "body for user content"
// @Success 200 {object} models.OrderInfo
// @Failure 403 :id is not int
// @router /:id [put]
func (o *OrderInfoController) Put() {mid, err := o.GetInt(":id")var orderInfo models.OrderInfoerr = json.Unmarshal(o.Ctx.Input.RequestBody, &orderInfo)if err != nil {logs.Error("Add Database OrderInfo Error, ERR:", err)o.Data["json"] = err.Error()o.ServeJSON()return}orderInfo.Id = midif mid != 0 {err := models.UpdateOrderInfoById(&orderInfo)if err != nil {o.Data["json"] = err.Error()} else {o.Data["json"] = "update success"}}o.ServeJSON()
}
数据更新
使用redis缓存+定时任务的方式维护data数据库,先存储在redis中,再由定时脚本进行更新(同步redis数据到mysql中)。
1.安装redis包
go get github.com/gomodule/redigo/redis
2.配置环境变量
redis_host = localhost
redis_port = 6379
redis_password =
redis_db = 0
3.redis初始化连接
func RedisContent() redis.Conn {redis_host, err := beego.AppConfig.String("redis_host")redis_port, err := beego.AppConfig.String("redis_port")redis_password, err := beego.AppConfig.String("redis_password")redis_db, err := beego.AppConfig.String("redis_db")if err != nil {logs.Error("database get appConfig err", err)}Redis_pool := &redis.Pool{MaxIdle: 1, //最大空闲连接数MaxActive: 10, // 最大连接数IdleTimeout: 180 * time.Second, //空闲连接超时时间Wait: true, // 超过最大连接数的操作:等待Dial: func() (redis.Conn, error) {c, err := redis.Dial("tcp", fmt.Sprintf("%s:%s", redis_host, redis_port))if err != nil {return nil, err}if redis_password != "" {if _, err := c.Do("AUTH", redis_password); err != nil {c.Close()return nil, err}}if redis_db != "" {if _, err := c.Do("SELECT", redis_db); err != nil {c.Close()return nil, err}}return c, nil},}return Redis_pool.Get()
}
新建controller/common.go文件
逻辑:redis的菜单ID的key加一,并且添加到redis的菜单ID列表。
这个文件存放公共的function:
package controllersimport (beego "github.com/beego/beego/v2/server/web""menu_api/models""time"
)const (OptLikeNum = "like_num"OptOrderNum = "order_num"OptVisitNum = "visit_num"
)func getRedisKey() (string, string, error) {// 获取key配置numKey, err := beego.AppConfig.String("redis_menu_num_key")if err != nil {return "", "", err}updateKey, err := beego.AppConfig.String("redis_menu_update_key")if err != nil {return "", "", err}return numKey, updateKey, nil
}func redisNumUpdate(mid string, operateType string) (error, interface{}) {numKey, updateKey, err := getRedisKey()if err != nil {return err, nil}// like_number++conn, err := models.RedisContent()if err != nil {return err, nil}defer conn.Close()newValue, err := conn.Do("HINCRBY", numKey+mid, operateType, 1)if err != nil {return err, nil}// TODO 确定是否为第一次+1_, err = conn.Do("SADD",updateKey+time.Now().Format("20060102"), mid)if err != nil {return err, nil}return nil, newValue
}
点赞数量更新redis
注册路由:
新增逻辑:
beego.NSNamespace("/data",beego.NSInclude(&controllers.DataController{},),),
新建controller/data.go文件
实现点赞数量存储:
package controllersimport ("github.com/beego/beego/v2/core/logs"beego "github.com/beego/beego/v2/server/web"_ "menu_api/models"
)type DataController struct {beego.Controller
}// @Title Update
// @Description update the menu
// @Param menu_id path string true "The menu_id you want to update"
// @Success 200 {int} 1
// @Failure 403 :menu_id is not int
// @router /:menu_id [put]
func (d *DataController) Put() {mid := d.GetString(":menu_id")// 更细rediserr, num := redisNumUpdate(mid, OptLikeNum)if err != nil {logs.Error("Get AppConfig Error, ERR:", err)d.Data["json"] = err.Error()d.ServeJSON()return}d.Data["json"] = numd.ServeJSON()
}
更新订单数量和浏览量
浏览量主要在菜单详情接口中,点击一次+1;
err, _ := redisNumUpdate(strconv.Itoa(mid), OptBrowseNum)if err != nil {logs.Error("Get AppConfig Error, ERR:", err)// 不影响主流程,不return}
订单数量主要在订单添加接口中,下单一次+1;
下单在更新接口中,改订单状态为已下单:
// @Title Update
// @Description update the OrderInfo
// @Param id path string true "The menu_id you want to update"
// @Param body body models.OrderInfo true "body for user content"
// @Success 200 {object} models.OrderInfo
// @Failure 403 :id is not int
// @router /:id [put]
func (o *OrderInfoController) Put() {oId, err := o.GetInt(":id")var orderInfo models.OrderInfoerr = json.Unmarshal(o.Ctx.Input.RequestBody, &orderInfo)if err != nil {logs.Error("Add Database OrderInfo Error, ERR:", err)o.Data["json"] = err.Error()o.ServeJSON()return}orderInfo.Id = oIdif oId != 0 {err := models.UpdateOrderInfoById(&orderInfo)if err != nil {o.Data["json"] = err.Error()} else {o.Data["json"] = "update success"}}if orderInfo.Status == 1 {// 遍历菜单列表,对每个菜进行加一操作menuIdList := strings.Split(orderInfo.MenuIdList, ",")for _, menuId := range menuIdList {err, _ = redisNumUpdate(menuId, OptOrderNum)if err != nil {logs.Error("Get AppConfig Error, ERR:", err)// 不影响主流程,不return}}}o.ServeJSON()
}
同步redis数据到数据库
定时任务
func SyncDataFromRedisToMysql() error {numKey, updateKey, err := getRedisKey()if err != nil {return err}// like_number++conn, err := models.RedisContent()if err != nil {return err}defer conn.Close()// 获取集合数据menuIds, err := redis.Strings(conn.Do("SMEMBERS", updateKey+time.Now().Format("20060102")))if err != nil {return err}for _, menuId := range menuIds {// 根据Id获取数据var fields []stringvar sortby []stringvar order []stringvar query = map[string]string{"MenuId": menuId,}var limit int64 = 10var offset int64ml, err := models.GetAllData(query, fields, sortby, order, offset, limit)if err != nil {return err}// 根据menuId获取更新mysql数据values, err := redis.StringMap(conn.Do("HGETALL", numKey+menuId))if err != nil {return err}// 先marshal,再Unmarshalvar data models.Dataif len(ml) > 0 && ml != nil {marshal, err := json.Marshal(ml[0])if err != nil {return err}err = json.Unmarshal((marshal), &data)}likeNum, err := strconv.Atoi(values[OptLikeNum])orderNum, err := strconv.Atoi(values[OptOrderNum])visitNum, err := strconv.Atoi(values[OptVisitNum])data.LikeNumber = likeNumdata.OrderNumber = orderNumdata.VisitNumber = visitNum// 如果找到则更新,找不到则添加if len(ml) < 1 || ml == nil {mId, _ := strconv.Atoi(menuId)data.MenuId = mId_, err = models.AddData(&data)} else {err = models.UpdateDataById(&data)}if err != nil {return err}}return nil
}
main.go中设置协程,五分钟刷新一次:
func init() {logs.Init()models.Init()controllers.SyncDataFromRedisToMysql()go func() {for {// 每隔五分钟执行一次同步方法time.Sleep(5 * time.Minute)err := controllers.SyncDataFromRedisToMysql()if err != nil {beeLogs.Error("Data SyncDataFromRedisToMysql Error:", err)} else {beeLogs.Info(time.Now(), "SyncDataFromRedisToMysql Success")}}}()
}
文件上传
需要上传图片到服务器
注册路由
新增逻辑:
beego.NSNamespace("/common",beego.NSInclude(&controllers.CommonController{},),),
controller/commpn.go中新增逻辑
// @router /upload [post]
// @Summary 上传图片
// @Description 上传图片到服务器
// @Accept multipart/form-data
// @Param image formData file true "图片文件"
// @Success 200 {string} success "上传成功"
// @Failure 400 {string} error "上传失败"
// @router /upload [post]
func (c *CommonController) Post() {f, h, err := c.GetFile("image")if err != nil {c.Ctx.WriteString("File upload failed: " + err.Error())return}ext := path.Ext(h.Filename)//验证后缀名是否符合要求var AllowExtMap map[string]bool = map[string]bool{".jpg": true,".jpeg": true,".png": true,}if _, ok := AllowExtMap[ext]; !ok {c.Ctx.WriteString("后缀名不符合上传要求")return}//创建目录uploadDir := "static/upload/"//构造文件名称rand.Seed(time.Now().UnixNano())randNum := fmt.Sprintf("%d", rand.Intn(9999)+1000)hashName := md5.Sum([]byte(time.Now().Format("2006_01_02_15_04_05_") + randNum))fileName := fmt.Sprintf("%x", hashName) + ext//c.Ctx.WriteString( fileName )fpath := uploadDir + fileNamedefer f.Close() //关闭上传的文件,不然的话会出现临时文件不能清除的情况err = c.SaveToFile("image", fpath)if err != nil {c.Ctx.WriteString(fmt.Sprintf("%v", err))}}
以上就是整体服务端的实现。
运行
生成路由
bee generate routers
生成swagger配置文件,并运行
bee run -gendoc=true
访问swagger
http://127.0.0.1:8080/swagger/#/
接口如下: