以下记录了使用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 models
import (
"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 main
import (
"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 = true
beego.BConfig.WebConfig.StaticDir["/swagger"] = "swagger"
}
beego.Run()
}
bee run .
没有打印错误,则说明数据库连接成功
日志设置
新建logs/logs.go文件进行初始化日志:
package logs
import (
"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 controllers
import (
"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 []string
var sortby []string
var order []string
var query = make(map[string]string)
var limit int64 = 10
var offset int64
// fields: col1,col2,entity.col3
if 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,col2
if v := m.GetString("sortby"); v != "" {
sortby = strings.Split(v, ",")
}
// order: desc,asc
if v := m.GetString("order"); v != "" {
order = strings.Split(v, ",")
}
// query: k:v,k:v
if 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"] = menList
m.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 := 0
if 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.Menu
err := 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.Menu
err = 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 = mid
if 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 controllers
import (
"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 []string
var sortby []string
var order []string
var query = make(map[string]string)
var limit int64 = 10
var offset int64
// fields: col1,col2,entity.col3
if 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,col2
if v := o.GetString("sortby"); v != "" {
sortby = strings.Split(v, ",")
}
// order: desc,asc
if v := o.GetString("order"); v != "" {
order = strings.Split(v, ",")
}
// query: k:v,k:v
if 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"] = orderList
o.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 := 0
if 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.OrderInfo
err := 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.OrderInfo
err = 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 = mid
if 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 controllers
import (
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 controllers
import (
"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")
// 更细redis
err, 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"] = num
d.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.OrderInfo
err = 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 = oId
if 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 []string
var sortby []string
var order []string
var query = map[string]string{
"MenuId": menuId,
}
var limit int64 = 10
var offset int64
ml, 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,再Unmarshal
var data models.Data
if 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 = likeNum
data.OrderNumber = orderNum
data.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 + fileName
defer 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/#/
接口如下: