03 . Gin+Vue開發一個線上外賣應用(用戶數據創建,插入,跨域處理)


功能和背景介紹

在項目的登錄功能中,如果在登錄時發現用戶名和密碼在用戶表中不存在,會自動將用戶名和密碼保存在用戶表中,創建一個新的用戶。

因此,除了使用手機號和驗證碼登錄以外,還支持使用用戶名、密碼進行登錄。

如果首次使用手機號和驗證碼進行登錄,會默認將手機號作為用戶名創建新的用戶,將用戶結構體對象的數據保存在數據庫中。

因此,我們有必要創建用戶表。

用戶數據結構體定義

在項目中,使用結構體定義用戶數據結構。結構體定義如下所示:

type Member struct {
	Id           int64   `xorm:"pk autoincr" json:"id"`
	UserName     string  `xorm:"varchar(20)" json:"user_name"`
	Mobile       string  `xorm:"varchar(11)" json:"mobile"`
	Password     string  `xorm:"varchar(255)" json:"password"`
	RegisterTime int64   `xorm:"bigint" json:"register_time"`
	Avatar       string  `xorm:"varchar(255)" json:"avatar"`
	Balance      float64 `xorm:"double" json:"balance"`
	IsActive     int8    `xorm:"tinyint" json:"is_active"`
	City         string  `xorm:"varchar(10)" json:"city"`
}

通過定義Member結構體,表示應用的用戶信息。通過TAG中的xorm來指定結構體在數據庫表中的約束。

ORM映射

通過engine.Sync2方法將Member同步映射成為數據庫中的member表:

err = engine.Sync2(new(model.Member),
		new(model.SmsCode))
if err != nil {
    return nil,err
}

插入數據

當用戶獲取完驗證碼,並填寫驗證碼以后,用戶點擊登錄,會發起登錄請求。因此,我們需要來完成登錄相關的邏輯操作和處理。用戶手機號碼和驗證碼登錄的接口是api/login_sms,因此我們在已經創建的MemberController中解析短信驗證碼接口。如下所示:

func (mc *MemberController) Router(engine *gin.Engine) {
    ...
    //發送手機驗證碼
	engine.GET("/api/sendcode", mc.sendSmsCode)
	//手機號和短信登錄
	engine.OPTIONS("/api/login_sms", mc.smsLogin)
}

在MemberController中創建smsLogin方法完成用戶手機號和密碼登錄的邏輯,詳細實現如下:

//短信登錄
func (mc *MemberController) smsLogin(context *gin.Context) {

	var smsParam param.SmsLoginParam
	err := toolbox.Decode(context.Request.Body, &smsParam)

	fmt.Println(err.Error())

	fmt.Println(context.PostForm("phone"))
	fmt.Println(context.Query("code"))
	if err != nil {
		toolbox.Failed(context, "參數解析錯誤")
		return
	}

	us := service.NewMemberService()
	member := us.SmsLogin(smsParam)
	if member != nil {
		toolbox.Success(context, member)
		return
	}
	toolbox.Failed(context, "登錄失敗")
}

用戶服務層

在MemberService.go文件中,編寫SmsLogin方法完成手機號和密碼登錄。

func (msi *MemberService) SmsLogin(param param.SmsLoginParam) *model.Member {

	dao := dao.NewMemberDao()
	sms := dao.ValidateSmsCode(param.Phone, param.Code)

	if sms == nil || time.Now().Unix()-sms.CreateTime > 300 {
		return nil
	}

	member := dao.QueryByPhone(param.Phone)
	if member != nil {
		return member
	}

	user := model.Member{}
	user.UserName = param.Phone
	user.Mobile = param.Phone
	user.RegisterTime = time.Now().Unix()

	user.Id = dao.InsertMember(user)
	return &user
}

在MemberService中,首先驗證手機號和驗證碼是否正確。如果通過了手機號和驗證碼的驗證,通過手機號查詢用戶是否已經存在。如果用戶記錄不存在,則創建新的用戶記錄並保存到數據庫中,如果用戶記錄已經存在,則表示登錄成功,返回用戶信息。

數據庫操作的MemberDao實現如下

在MemberDao中,實現用戶模塊的數據庫操作。
首先是手機驗證碼驗證功能,如下所示:

func (md *MemberDao) ValidateSmsCode(phone string, code string) *model.SmsCode {
	var sms model.SmsCode

	if err := md.Where(" phone = ? and code = ? ", phone, code).Find(&sms); err != nil {
		toolbox.Error(err.Error())
	}
	return &sms
}

其次是根據手機號查詢用戶數據庫表中是否存在手機號對應的用戶,如下所示:

func (md *MemberDao) QueryByPhone(phone string) *model.Member {
	var member model.Member

	if err := md.Where(" phone = ? ", phone).Find(&member); err != nil {
		toolbox.Error(err.Error())
	}
	return &member
}

最后,對於新手機號,新建用戶,插入到數據庫中:

func (md *MemberDao) InsertMember(member model.Member) int64 {
	result, err := md.InsertOne(&member)
	if err != nil {
		toolbox.Error(err.Error())
	}
	return result
}

跨域

我們項目是使用gin開發一個接口項目,前端是使用vue+webpack進行開發和編譯運行的。
可以通過如下命令運行為大家提供的前端工程代碼,在前端項目的根目錄下執行:

npm run dev

在瀏覽器中訪問http://localhost:8080即可進入應用的首頁,切換到用戶登錄界面。

同時后端程序的運行端口是8090。

當使用我們上面兩節課已經開發完成的手機號+驗證碼的方式進行用戶登錄時。會發現遇到一個問題,如下圖所示:

之前我們已經開發完成了手機號+驗證碼登錄的功能,並且使用Postman已經測試成功了,為什么現在在瀏覽器中會出現這個問題呢?

跨域訪問的問題

先了解一下什么是跨域訪問。
在瀏覽器中的任意一個頁面地址,或者訪問后台的api接口url,其實都包含幾個相同的部分:

/*
	* 1、通信協議:又稱protocol,有很多通信協議,比如http,					tcp/ip協議等等。
	* 2、主機:也就是常說的host。
	* 3、端口:即服務所監聽的端口號。
	* 4、資源路徑:端口號后面的內容即是路徑。
*/

當在一個頁面中發起一個新的請求時,如果通信協議、主機和端口,這三部分內容中的任意一個與原頁面的不相同,就被稱之為跨域訪問。

如,在gin接口項目中,前端使用nodejs開發,運行在8080端口,我們訪問的應用首頁是:http://localhost:8080。 在使用gin框架開發的api項目中,服務端的監聽端口為8090。

一個端口數8080,一個是8090,兩者端口不同,因此按照規定,發生了跨域訪問。

OPTIONS請求

如上文所述,前端vue開發的功能,使用axios發送POST登錄請求。在請求時發生了跨域訪問,因此瀏覽器為了安全起見,會首先發起一個請求測試一下此次訪問是否安全,這種測試的請求類型為OPTIONS,又稱之為options嗅探,同時在header中會帶上origin,用來判斷是否有跨域請求權限。

然后服務器相應Access-Control-Allow-Origin的值,該值會與瀏覽器的origin值進行匹配,如果能夠匹配通過,則表示有跨域訪問的權限。

跨域訪問權限檢查通過,會正式發送POST請求。

服務端設置跨域訪問

可以在gin服務端,編寫程序進行全局設置。通過中間件的方式設置全局跨域訪問,用以返回Access-Control-Allow-Origin和瀏覽器進行匹配。

在服務端編寫跨域訪問中間件,詳細內容如下:

func Cors() gin.HandlerFunc {
	return func(context *gin.Context) {
		method := context.Request.Method
		origin := context.Request.Header.Get("Origin")
		var headerKeys []string
		for k, _ := range context.Request.Header {
			headerKeys = append(headerKeys, k)
		}
		headerStr := strings.Join(headerKeys, ",")
		if headerStr != "" {
			headerStr = fmt.Sprintf("access-control-allow-origin, access-control-allow-headers, %s", headerStr)
		} else {
			headerStr = "access-control-allow-origin, access-control-allow-headers"
		}

		if origin != "" {
			context.Writer.Header().Set("Access-Control-Allow-Origin", "*")
			context.Header("Access-Control-Allow-Origin", "*") // 設置允許訪問所有域
			context.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE")
			context.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session,X_Requested_With,Accept, Origin, Host, Connection, Accept-Encoding, Accept-Language,DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Pragma")
			context.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers,Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma,FooBar")
			context.Header("Access-Control-Max-Age", "172800")
			context.Header("Access-Control-Allow-Credentials", "false")
			context.Set("content-type", "application/json") //// 設置返回格式是json
		}

		if method == "OPTIONS" {
			context.JSON(http.StatusOK, "Options Request!")
		}
		//處理請求
		context.Next()
	}
}

其中的Access-Control-Allow-Origin的設置,表示允許進行跨域訪問,*表示可以訪問所有域。同時,通過Header方法進行了其他的設置。

最后context.Next()是中間件使用的標准用法,表示繼續處理請求。

服務器設置跨域調用

在main函數中,調用編寫好的跨域訪問。調用如下:

func main(){
    ...
    app := gin.Default()
    app.Use(Cors())
    ...
}

/*
		調用app.Use方法,設置跨域訪問
*/
功能演示

服務器設置好跨域訪問以后,重新啟動服務器api程序,並在瀏覽器端重新訪問。可以看到正常發送了OPTIONS嗅探后,正常發送了POST請求。如下圖所示:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM