Middleware中间件
使用中间件可以对api接口访问前后做处理。例如token校验。接下来使用gin框架的中间件功能。
middleware/token.go
package middleware
import (
"api/pkg/e"
"api/pkg/util"
"github.com/gin-gonic/gin"
"time"
)
func TokenVer() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("authorization") //从请求的header中获取toekn字符串
if token == "" {
util.ResponseWithJson(e.ERROR_AUTH_TOKEN,"",c)
c.Abort()
return
}else {
claims, err := util.ParseToken(token) //token校验,claims的内容是自定义的荷载,可以根据里面的id取出用户信息
if err != nil { //token校验失败,返回错误信息
util.ResponseWithJson(e.ERROR_AUTH_CHECK_TOKEN_FAIL,"",c)
c.Abort()
return
}else if time.Now().Unix() > claims.ExpiresAt { //token过期,返回错误信息
util.ResponseWithJson(e.ERROR_AUTH_CHECK_TOKEN_TIMEOUT,"",c)
c.Abort()
return
}else { //token正确,可以进行后续的操作。设置用户的ID和手机号,供后续方法使用
c.Set("ID",claims.ID)
//c.Set("Mobile",claims.Mobile)
c.Next()
}
}
}
}
从请求的header中获取token,如果没有的话直接返回错误信息,并终结此次请求。否则的话对token进行校验。当token无误后,进行下一步的操作。
接下来需要在路由文件中使用中间件方法
routers/routers.go
...
func InitRouter() *gin.Engine {
...
apiv1 := r.Group("/api/v1/") //路由分组,apiv1代表v1版本的路由组
{
...
apiv1Token := apiv1.Group("token/") //创建使用token中间件的路由组
apiv1Token.Use(middleware.TokenVer()) //使用token鉴权中间件
{
}
...
创建了apiv1Token
路由组,凡是这个路由组中的路由都需要使用token。
APP版本升级
APP版本更新升级功能的重要性不言而喻。这里把APP版本升级计划成两种方式,强制更新和选择更新。
整体思路如下:
用户在后台需要更新app版本号、iOS最低可兼容的版本、安卓最低可兼容的版本、app升级文案、app下载地址、上传安卓更新的apk文件。
首先客户端请求更新接口,上传以下请求参数:
- 客户端类型(iOS或安卓)
- 客户端版本号
1,根据客户端上传的版本号参数,先与后台记录的最新版本号相比较。如果小于最新版本号则记录选择更新。
2,再与后台记录的最低可兼容版本相比较,如果小于最低可兼容版本,则记录为强制更新。
由于iOS只能在AppStore下载更新(非企业账号)。所以对于iOS客户端返回地址为AppStore的地址,由客户端打开这个地址更新即可。
根据流程,设计app更新的数据表
版本更新表
字段名 | 类型 | 描述 | 备注 |
---|---|---|---|
id | int | 自增长 ID | 主键 |
version | varchar(10) | app最新版本号 | 格式:1.0.0 |
ios_min_version | varchar(10) | iOS端最低支持的版本号 | 格式:1.0.0 |
android_min_version | varchar(10) | 安卓端最低支持的版本号 | 格式:1.0.0 |
desc | varchar(255) | 升级文案 | app端展示端升级文案 |
app_url | varchar(255) | app下载地址 | 安卓可以做应用内升级,iOS跳转应用商店 |
app_size | int | apk文件大小 | apk文件大小 |
创建model
models/app_version.go
package models
import "github.com/jinzhu/gorm"
type AppVersion struct {
gorm.Model
Version string `gorm:"type:varchar(10);not null"` //不为空
IosMinVersion string `gorm:"type:varchar(10)"`
AndriodMinVersion string `gorm:"type:varchar(10)"`
Desc string `gorm:"type:varchar(255)"`
AppUrl string `gorm:"type:varchar(255)"`
AppSize int
}
别忘了添加到自动迁移
models/models.go
...
db.AutoMigrate(&User{},&AppVersion{})
...
先来完成创建app版本功能。现在使用简单的html页面实现,后期做后台管理功能再整合进去。
创建html文件,放在templates
目录下
templates/appversion.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{.title}}</title>
</head>
<body>
<form id="form" method="post" action="/manage/appversion" enctype="multipart/form-data">
App版本号:<br>
<input type="text" name="version" placeholder="格式:1.0.0">
<br>
iOS最低支持版本号:<br>
<input type="text" name="ios_min_version" placeholder="格式:1.0.0">
<br>
安卓最低支持版本号:<br>
<input type="text" name="android_min_version" placeholder="格式:1.0.0">
<br>
版本更新说明:<br>
<textarea name="desc" rows="10" cols="30" placeholder="">
</textarea>
<br>
app下载地址(仅限安卓)注意:如果上传了apk文件,则此栏无效,下载地址与apk文件必须完成一个:<br>
<input type="text" name="app_url">
<br>
<input type="file" name="apk_file">
<br><br>
<input type="submit" value="Submit">
</form>
</body>
</html>
一个很简单的html文件,只有post上传功能。
因为需要上传apk功能,所以对apk上传封装一下
pkg/upload/apk.go
package upload
import (
"api/pkg/file"
"api/pkg/logging"
"api/pkg/setting"
"fmt"
"os"
"strings"
"time"
)
//获取apk文件的保存路径,就是配置文件设置的 如:upload/apks/
func GetApkFilePath() string {
return setting.AppSetting.ApkSavePath
}
//获取apk文件完整访问URL 如:http://127.0.0.1:8000/upload/apks/20190730/******.apk
func GetApkFullUrl(name string) string {
return setting.AppSetting.ImagePrefixUrl + "/" +GetApkFilePath() + GetApkDateName() + name
}
//日期文件夹 如:20190730/
func GetApkDateName() string {
t := time.Now()
return fmt.Sprintf("%d%02d%02d/",t.Year(),t.Month(),t.Day())
}
//获取apk文件在项目中的目录 如:runtime/upload/apks/
func GetApkFullPath() string {
return setting.AppSetting.RuntimeRootPath + GetApkFilePath()
}
//检查文件后缀,是否属于配置中允许的后缀名
func CheckApkExt(fileName string) bool {
ext := file.GetExt(fileName)
if strings.ToLower(ext) == strings.ToLower(setting.AppSetting.ApkAllowExt) {
return true
}
return false
}
//检查apk文件
func CheckApk(src string)error {
dir,err := os.Getwd()
if err != nil {
logging.Warn("pkg/upload/apk.go文件CheckApk方法os.Getwd出错",err)
return fmt.Errorf("os.Getwd err: %v", err)
}
err = file.IsNotExistMkDir(dir + "/" + src) //如果不存在则新建文件夹
if err != nil {
logging.Warn("pkg/upload/apk.go文件CheckApk方法file.IsNotExistMkDir出错",err)
return fmt.Errorf("file.IsNotExistMkDir err: %v", err)
}
perm := file.CheckPermission(src) //检查文件权限
if perm == true {
logging.Warn("pkg/upload/apk.go文件CheckApk方法file.CheckPermission出错",err)
return fmt.Errorf("file.CheckPermission Permission denied src: %s", src)
}
return nil
}
更新网页做好了,后台需要做两件事
1,加载appversion.html
文件
2,实现/manage/appversion
这个接口。
routers/routers.go
...
func InitRouter() *gin.Engine {
...
r.LoadHTMLGlob("templates/*") //渲染模版
appManage := r.Group("/manage/") //后续做后台管理页面
{
appManage.GET("appversion", v1.GetAppVersionIndex) //app版本升级网页文件
appManage.POST("appversion", v1.CreateAppVersion) //app版本升级api接口
}
return r
}
routers/v1/app_version.go
package v1
import (
"api/pkg/e"
"github.com/gin-gonic/gin"
)
//打开版本升级的html页面(暂时这样写,后续的话完成后台管理页面)
func GetAppVersionIndex(c *gin.Context) {
c.HTML(e.SUCCESS,"appversion.html",gin.H{
"title": "App版本升级",
})
}
//创建app版本升级
func CreateAppVersion(c *gin.Context) {
var appVersion models.AppVersion
appVersion.Version = c.PostForm("version") //新版本app版本号
appVersion.IosMinVersion = c.PostForm("ios_min_version") //iOS最低可兼容的版本
appVersion.AndriodMinVersion = c.PostForm("android_min_version") //安卓最低可兼容的版本
appVersion.Desc = c.PostForm("desc") //app升级文案
appVersion.AppUrl = c.PostForm("app_url") //app下载地址
//获取上传的apk文件
apkFile,_ := c.FormFile("apk_file")
//如果有上传的apk文件
if apkFile != nil {
//判断文件格式是否正确
if ! upload.CheckApkExt(apkFile.Filename) {
util.ResponseWithJson(e.ERROR,"apk文件格式不正确",c)
return
}
//把上传的文件移动到指定目录
savePath := upload.GetApkFilePath() //保存的目录 upload/apks/
dataPath := upload.GetApkDateName() //日期的目录 20190730/
fullPath := upload.GetApkFullPath() + dataPath //图片在项目中的目录 runtime/upload/apks/20190730/
src := fullPath + apkFile.Filename //图片在项目中的位置 runtime/upload/apks/****.apk
//检查文件路径,这里面做了包括创建文件夹,检查权限等操作
if err := upload.CheckApk(fullPath); err != nil{
util.ResponseWithJson(e.ERROR,"apk文件有问题",c)
return
}
//使用c.SaveUploadedFile()把上传的文件移动到指定到位置
if err := c.SaveUploadedFile(apkFile, src); err != nil {
util.ResponseWithJson(e.ERROR,"上传apk失败",c)
return
}
//设置结构体的值
appVersion.AppSize = int(apkFile.Size) //获取并设置apk文件大小
appVersion.AppUrl = savePath + dataPath + apkFile.Filename //数据库中保存apk文件的路径
}
//对参数做校验
valid := validation.Validation{}
valid.Required(appVersion.Version,"version").Message("版本号必须填写")
valid.MinSize(appVersion.Desc,1,"minVersion").Message("升级文案最少1个字符")
valid.Required(appVersion.AppUrl,"appUrl").Message("app升级地址必须填写")
if isOk := checkValidation(&valid, c); isOk == false { //校验不通过
return
}
//数据库创建数据
if err := appVersion.CreateAppVersion(); err != nil {
util.ResponseWithJson(e.ERROR,"保存版本信息失败",c)
return
}
//返回正确的数据
util.ResponseWithJson(e.SUCCESS,appVersion,c)
}
models/app_version.go
...
//数据库操作创建app升级版本
func (appVersion *AppVersion)CreateAppVersion()error {
err := db.Create(appVersion).Error
return err
}
//获取最新版本的信息
func GetVersion() *AppVersion {
var appVersion AppVersion
db.Last(&appVersion)
return &appVersion
}
//数据库查询钩子,在数据库查询之后执行的方法
func (appVersion *AppVersion)AfterFind() {
appVersion.AppUrl = setting.AppSetting.ImagePrefixUrl + "/" + appVersion.AppUrl //拼接完整的apk的url地址
}
这样就完成了升级app版本功能
如果是上传的apk文件,还需要让用户能访问到这个apk文件
routers/routers.go
...
func InitRouter() *gin.Engine {
...
/*
当访问 $HOST/upload/apks 时,将会读取到 项目/runtime/upload/apks 下的文件
这样就能让外部访问到图片资源了
*/
r.StaticFS(setting.AppSetting.ApkSavePath, http.Dir(setting.AppSetting.RuntimeRootPath+setting.AppSetting.ApkSavePath))
return r
}
下面进行app获取升级信息的接口,新增
routers/routers.go
...
apiv1Token.Use(middleware.TokenVer()) //使用token鉴权中间件
{
apiv1Token.POST("version", v1.GetAppVersion) //app版本更新
}
...
这里是把GetAppVersion路由放到了需要token鉴权的中间件中了,所以请求这个接口需要token
routers/v1/app_version.go
...
//获取最新版本的app版本号
func GetAppVersion(c *gin.Context){
//客户端上传的参数
appID := c.PostForm("app_id") //客户端上传的app_id参数,=1是iOS客户端,=2是Android客户端
version := c.PostForm("version") //客户端上传的安装的app版本号
appVersion := models.GetVersion() //获取数据库中最新的版本信息
//要返回的数据
var responseData = gin.H{
"needUpdate":0,
"apkUrl":appVersion.AppUrl,
"desc":appVersion.Desc,
"version":appVersion.Version,
"appSize":appVersion.AppSize,
}
//如果是iOS
if appID == "1" {
responseData["apkUrl"] = setting.AppSetting.AppStoreUrl //返回应用商店地址
a,b,c := VersionOrdinal(version),VersionOrdinal(appVersion.Version),VersionOrdinal(appVersion.IosMinVersion)
//先比较是否是最新版本
if a < b {
responseData["needUpdate"] = 1 //不是最新版本提示可选升级
}
//再比较是否是最低支持的版本号
if a < c {
responseData["needUpdate"] = 2 //需要强制升级
}
}
//如果是安卓
if appID == "2" {
a,b,c := VersionOrdinal(version),VersionOrdinal(appVersion.Version),VersionOrdinal(appVersion.AndriodMinVersion)
//先比较是否是最新版本
if a < b {
responseData["needUpdate"] = 1 //不是最新版本提示可选升级
}
//再比较是否是最低支持的版本号
if a < c {
responseData["needUpdate"] = 2 //需要强制升级
}
}
util.ResponseWithJson(e.SUCCESS,responseData,c)
}
//用于比较两个字符串版本号的大小
func VersionOrdinal(version string) string {
// ISO/IEC 14651:2011
const maxByte = 1<<8 - 1
vo := make([]byte, 0, len(version)+8)
j := -1
for i := 0; i < len(version); i++ {
b := version[i]
if '0' > b || b > '9' {
vo = append(vo, b)
j = -1
continue
}
if j == -1 {
vo = append(vo, 0x00)
j = len(vo) - 1
}
if vo[j] == 1 && vo[j+1] == '0' {
vo[j+1] = b
continue
}
if vo[j]+1 > maxByte {
panic("VersionOrdinal: invalid version")
}
vo = append(vo, b)
vo[j]++
}
return string(vo)
}