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的內(nèi)容是自定義的荷載,可以根據(jù)里面的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正確缔杉,可以進行后續(xù)的操作。設(shè)置用戶的ID和手機號搁料,供后續(xù)方法使用
c.Set("ID",claims.ID)
//c.Set("Mobile",claims.Mobile)
c.Next()
}
}
}
}
從請求的header中獲取token或详,如果沒有的話直接返回錯誤信息系羞,并終結(jié)此次請求。否則的話對token進行校驗霸琴。當token無誤后椒振,進行下一步的操作。
接下來需要在路由文件中使用中間件方法
routers/routers.go
...
func InitRouter() *gin.Engine {
...
apiv1 := r.Group("/api/v1/") //路由分組梧乘,apiv1代表v1版本的路由組
{
...
apiv1Token := apiv1.Group("token/") //創(chuàng)建使用token中間件的路由組
apiv1Token.Use(middleware.TokenVer()) //使用token鑒權(quán)中間件
{
}
...
創(chuàng)建了apiv1Token
路由組杠人,凡是這個路由組中的路由都需要使用token。
APP版本升級
APP版本更新升級功能的重要性不言而喻宋下。這里把APP版本升級計劃成兩種方式嗡善,強制更新和選擇更新。
整體思路如下:
用戶在后臺需要更新app版本號学歧、iOS最低可兼容的版本罩引、安卓最低可兼容的版本、app升級文案枝笨、app下載地址袁铐、上傳安卓更新的apk文件。
首先客戶端請求更新接口横浑,上傳以下請求參數(shù):
- 客戶端類型(iOS或安卓)
- 客戶端版本號
1剔桨,根據(jù)客戶端上傳的版本號參數(shù),先與后臺記錄的最新版本號相比較徙融。如果小于最新版本號則記錄選擇更新洒缀。
2,再與后臺記錄的最低可兼容版本相比較欺冀,如果小于最低可兼容版本树绩,則記錄為強制更新。
由于iOS只能在AppStore下載更新(非企業(yè)賬號)隐轩。所以對于iOS客戶端返回地址為AppStore的地址饺饭,由客戶端打開這個地址更新即可。
根據(jù)流程职车,設(shè)計app更新的數(shù)據(jù)表
版本更新表
字段名 | 類型 | 描述 | 備注 |
---|---|---|---|
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下載地址 | 安卓可以做應(yīng)用內(nèi)升級瘫俊,iOS跳轉(zhuǎn)應(yīng)用商店 |
app_size | int | apk文件大小 | apk文件大小 |
創(chuàng)建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{})
...
先來完成創(chuàng)建app版本功能。現(xiàn)在使用簡單的html頁面實現(xiàn)悴灵,后期做后臺管理功能再整合進去扛芽。
創(chuàng)建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文件的保存路徑庐船,就是配置文件設(shè)置的 如: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) //檢查文件權(quán)限
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
}
更新網(wǎng)頁做好了嘲更,后臺需要做兩件事
1筐钟,加載appversion.html
文件
2,實現(xiàn)/manage/appversion
這個接口赋朦。
routers/routers.go
...
func InitRouter() *gin.Engine {
...
r.LoadHTMLGlob("templates/*") //渲染模版
appManage := r.Group("/manage/") //后續(xù)做后臺管理頁面
{
appManage.GET("appversion", v1.GetAppVersionIndex) //app版本升級網(wǎng)頁文件
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頁面(暫時這樣寫篓冲,后續(xù)的話完成后臺管理頁面)
func GetAppVersionIndex(c *gin.Context) {
c.HTML(e.SUCCESS,"appversion.html",gin.H{
"title": "App版本升級",
})
}
//創(chuàng)建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
//檢查文件路徑,這里面做了包括創(chuàng)建文件夾宠哄,檢查權(quán)限等操作
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
}
//設(shè)置結(jié)構(gòu)體的值
appVersion.AppSize = int(apkFile.Size) //獲取并設(shè)置apk文件大小
appVersion.AppUrl = savePath + dataPath + apkFile.Filename //數(shù)據(jù)庫中保存apk文件的路徑
}
//對參數(shù)做校驗
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
}
//數(shù)據(jù)庫創(chuàng)建數(shù)據(jù)
if err := appVersion.CreateAppVersion(); err != nil {
util.ResponseWithJson(e.ERROR,"保存版本信息失敗",c)
return
}
//返回正確的數(shù)據(jù)
util.ResponseWithJson(e.SUCCESS,appVersion,c)
}
models/app_version.go
...
//數(shù)據(jù)庫操作創(chuàng)建app升級版本
func (appVersion *AppVersion)CreateAppVersion()error {
err := db.Create(appVersion).Error
return err
}
//獲取最新版本的信息
func GetVersion() *AppVersion {
var appVersion AppVersion
db.Last(&appVersion)
return &appVersion
}
//數(shù)據(jù)庫查詢鉤子壹将,在數(shù)據(jù)庫查詢之后執(zhí)行的方法
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鑒權(quán)中間件
{
apiv1Token.POST("version", v1.GetAppVersion) //app版本更新
}
...
這里是把GetAppVersion路由放到了需要token鑒權(quán)的中間件中了诽俯,所以請求這個接口需要token
routers/v1/app_version.go
...
//獲取最新版本的app版本號
func GetAppVersion(c *gin.Context){
//客戶端上傳的參數(shù)
appID := c.PostForm("app_id") //客戶端上傳的app_id參數(shù),=1是iOS客戶端承粤,=2是Android客戶端
version := c.PostForm("version") //客戶端上傳的安裝的app版本號
appVersion := models.GetVersion() //獲取數(shù)據(jù)庫中最新的版本信息
//要返回的數(shù)據(jù)
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 //返回應(yīng)用商店地址
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)
}