确定需求和目标
需求
通过用户名和密码进行登录,登录成功后返回用户 id 和权限 token
请求参数
| 参数 | 类型 | 备注 |
|---|---|---|
| username | string | 登录用户名 |
| password | string | 登录密码 |
返回响应
| 参数 | 类型 | 备注 |
|---|---|---|
| status_code | integer | 状态码,0-成功,其他值-失败 |
| status_msg | string | 返回状态描述 |
| user_id | integer | 用户id |
| token | string | 用户鉴权token |
通过观察需求和简单分析我们不难发现,实际上登录和注册的大多数代码是可以复用的。因此,在后面的编写过程中省了不少力气。具体参考用户注册实践 | 青训营 - 掘金 (juejin.cn)
数据库
由于在编写注册接口时已经有了数据库我们可以直接拿来使用。
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
`token` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `name_password_index` (username,`password`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表\n';
编写接口代码
-
连接数据库
db, err := data.DbConnect(writer)DbConnector.go:
package data import ( "database/sql" "net/http" ) func DbConnect(writer http.ResponseWriter) (db *sql.DB, err error) { dsn := "root:123456@tcp(localhost:3306)/tiktok" // 要改成自己的数据源 // 连接数据库 db, err = sql.Open("mysql", dsn) if err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return db, err } // 检查是否连接成功 err = db.Ping() if err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return db, err } return db, err }连接数据库只需要写:
db := service.DbConnect(writer)其中writer是网络响应,在这里用来返回错误状态码 -
获取请求体数据
requestData := handler.HandleRequest(writer, request)userRequest.go:
package handler import ( "TikTok/model" "encoding/json" "io/ioutil" "net/http" ) func HandleRequest(writer http.ResponseWriter, request *http.Request) model.UserRequest { var requestData model.UserRequest // 检查请求方法是否为POST if request.Method != http.MethodPost { http.Error(writer, "Method Not Allowed", http.StatusMethodNotAllowed) return requestData } // 读取请求体数据 requestBody, err := ioutil.ReadAll(request.Body) defer request.Body.Close() if err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return requestData } // 解析JSON数据 err = json.Unmarshal(requestBody, &requestData) if err != nil { http.Error(writer, err.Error(), http.StatusBadRequest) return requestData } return requestData }首先,创建一个空的
model.UserRequest对象requestData用于存储请求数据。接下来,代码检查请求方法是否为 POST。如果请求方法不是 POST,则使用
http.Error函数向客户端返回一个 "Method Not Allowed" 的错误,并返回空的requestData。然后,代码通过使用
ioutil.ReadAll函数从请求体中读取数据,并将其保存到requestBody变量中。之后通过调用defer request.Body.Close()来确保在函数结束时关闭请求体。如果读取请求体时出现错误,代码使用
http.Error函数向客户端返回一个 HTTP 500 错误,并返回空的requestData。接下来,代码尝试解析
requestBody中的 JSON 数据到requestData变量中,通过调用json.Unmarshal函数实现此功能。如果解析过程出现错误,代码使用http.Error函数向客户端返回一个 HTTP 400 错误,并返回空的requestData。最后,函数返回解析后得到的
requestData对象。 -
执行查询操作,验证用户名和密码
query := ` SELECT id, token FROM user WHERE username = ? AND password = ? ` var userID int64 var token string err = db.QueryRow(query, requestData.Username, requestData.Password).Scan(&userID, &token) if err != nil { http.Error(writer, "用户名或密码错误", http.StatusUnauthorized) return }首先,代码定义了一个 SQL 查询语句并将其赋值给
query变量。该查询语句用于从名为 "user" 的表中选取 "id" 和 "token" 字段,条件是用户名(requestData.Username)和密码(requestData.Password)匹配。接下来,代码定义了两个变量
userID(类型为int64)和token(类型为string)用于存储查询结果。然后,代码使用
db.QueryRow函数执行查询,并通过调用Scan方法将查询结果扫描并赋值给userID和token变量。Scan方法以查询结果的列顺序作为参数,并将查询结果的值依次赋值给对应的变量。如果查询时发生错误,则将错误赋值给err变量。最后,代码检查
err变量是否为空。如果不为空,则说明查询过程中出现了错误,代码使用http.Error函数向客户端返回一个 HTTP 401 错误(未授权),并返回。此处的错误信息是 "用户名或密码错误"。 -
发送响应数据
handler.HandleResponse(userID, token, writer, "登录成功!")userResponse.go:
package handler import ( "TikTok/model" "encoding/json" "net/http" ) func HandleResponse(userID int64, token string, writer http.ResponseWriter, StatusMsg string) { // 构造响应数据 responseData := model.UserResponse{ StatusCode: http.StatusOK, StatusMsg: StatusMsg, UserID: userID, Token: token, } // 将响应转换为JSON并写入响应体中 responseJSON, err := json.Marshal(responseData) if err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return } // 设置响应头部 writer.Header().Set("Content-Type", "application/json") // 发送响应数据 writer.WriteHeader(http.StatusOK) writer.Write(responseJSON) }首先,代码创建了一个
model.UserResponse类型的变量responseData,并使用传入的参数填充其中的字段。StatusCode字段被设置为http.StatusOK,StatusMsg字段被设置为传入的StatusMsg参数,UserID字段被设置为传入的userID参数,Token字段被设置为传入的token参数。接下来,代码使用
json.Marshal函数将responseData对象转换为 JSON 格式的字节数组responseJSON。如果转换过程中发生错误,则使用http.Error函数向客户端返回一个 HTTP 500 错误,并返回。然后,代码使用
writer.Header().Set方法设置响应头部的 "Content-Type" 字段为 "application/json",以指示响应数据的格式为 JSON。接着,代码使用
writer.WriteHeader方法将响应的状态码设置为http.StatusOK(200),表示请求成功。最后,代码使用
writer.Write方法将 JSON 格式的响应数据responseJSON写入响应体中,将其发送给客户端。
测试和调试
使用单元测试发送模拟请求
func TestLoginHandler(t *testing.T) {
// 构造测试请求的JSON数据
jsonData := `{"username": "test_username", "password": "test_password"}`
// 创建请求体
requestBody := strings.NewReader(jsonData)
// 创建模拟请求
request := httptest.NewRequest(http.MethodPost, "/douyin/user/login/", requestBody)
// 创建模拟响应写入器
responseWriter := httptest.NewRecorder()
// 调用被测试的处理函数
service.LoginHandler(responseWriter, request)
// 获取响应
response := responseWriter.Result()
// 检查状态码
if response.StatusCode != http.StatusOK {
t.Errorf("Expected status code %d, but got %d", http.StatusOK, response.StatusCode)
}
}
经过测试,很顺利地通过,登录成功。