Cách thu Lead về Google Sheet với Golang

Google mang tới rất nhiều sân chơi cho các nhà phát triển từ đủ loại trình độ, trong đó có Junior (Như mình) và với vai trò là Growth Hacker / Digital Marketer, Nam sẽ chỉ quan tâm tới một số tính năng quan trọng trong lập trình nhằm phục vụ cho hoạt động Marketing và đó là thu Lead về Google Sheet, điểm khác biệt là Nam sử dụng Nam Digital CMS (Postgres + Go) trong việc xây dựng các tài nguyên Marketing, nên việc thu Lead về Google Sheet cũng cần tuân theo chuẩn mực của Go

Thật may là đã có hướng dẫn vô cùng chi tiết từ phía nhà Google rồi, bạn có thể xem tại đây

Bật API và OAuth Consent Screen

Để chạy được hoạt động đẩy Lead về Google Sheet, ta cần bật Sheets API, bạn có thể click vào đây

Thiết lập OAuth consent Screen là bước tiếp theo cần làm, đây là một bước quan trọng giúp bạn thực hiện hoạt động testing kỹ với người dùng demo trước khi đưa API vào vận hành. 

Xin cấp phép ứng dụng (Authorize Credentials)

Một bước khá quan trọng là bạn cần xin cấp phép ứng dụng, tạo Client ID và Secret, đồng thời URI Redirect để dùng cho việc xác minh. 

Cơ bản thì luồng Nam thực hiện như sau: Khi thực hiện gửi Form đi, sẽ chạy 1 hàm xác thực trước, nếu quá trình xác thực thành công, sẽ tiến hành tạo Token, từ những lần sau thì khi đã được xác thực, hàm cứ thế chạy thôi. 

Điều quan trọng là khi xác thực xong, bạn cần tải file Json credentials.json về và để vào thư mục gốc của ứng dụng, ta sẽ cần nó để xác thực khi chạy hàm gọi lại. 

func getClient(config *oauth2.Config) *http.Client {
    // The file token.json stores the user's access and refresh tokens, and is
    // created automatically when the authorization flow completes for the first
    // time.
    tokFile := "token.json"
    tok, err := tokenFromFile(tokFile)
    if err != nil {
        tok = getTokenFromWeb(config)
        saveToken(tokFile, tok)
    }
    return config.Client(context.Background(), tok)
}


func getTokenFromWeb(config *oauth2.Config) *oauth2.Token {
    authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
    fmt.Printf("Go to the following link in your browser then type the "+
        "authorization code: \n%v\n", authURL)


    var authCode string
    if _, err := fmt.Scan(&authCode); err != nil {
        log.Fatalf("Unable to read authorization code: %v", err)
    }


    tok, err := config.Exchange(context.TODO(), authCode)
    if err != nil {
        log.Fatalf("Unable to retrieve token from web: %v", err)
    }
    return tok
}


func saveToken(path string, token *oauth2.Token) {
    fmt.Printf("Saving credential file to: %s\n", path)
    f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
    if err != nil {
        log.Fatalf("Unable to cache oauth token: %v", err)
    }
    defer f.Close()
    json.NewEncoder(f).Encode(token)
}


func tokenFromFile(file string) (*oauth2.Token, error) {
    f, err := os.Open(file)
    if err != nil {
        return nil, err
    }
    defer f.Close()
    tok := &oauth2.Token{}
    err = json.NewDecoder(f).Decode(tok)
    return tok, err
}
 

Trông thì có vẻ phức tạp, nhưng thực tế đây là 4 hàm để xác minh, yêu cầu ủy quyền và nếu thành công sẽ tiến hành saveToken vào ứng dụng. 

Xây dựng hàm Callback cho việc xác minh

Đối với ứng dụng Web thì Google khuyến cáo cần tạo 1 hàm gọi lại (Callback) từ ứng dụng, thực hiện các logic xác thực (như ở trên), nếu thành công sẽ hoàn tất thủ tục xác thực để ứng dụng dùng ở những lần sau: 

func oauth2CallbackHandler(w http.ResponseWriter, r *http.Request) {
    ctx := context.Background()
    b, err := os.ReadFile("credentials.json")
    if err != nil {
        log.Fatalf("Unable to read client secret file: %v", err)
    }


    config, err := google.ConfigFromJSON(b, "https://www.googleapis.com/auth/spreadsheets")
    if err != nil {
        log.Fatalf("Unable to parse client secret file to config: %v", err)
    }


    code := r.URL.Query().Get("code")
    tok, err := config.Exchange(ctx, code)
    if err != nil {
        log.Fatalf("Unable to retrieve token from web: %v", err)
    }


    saveToken("token.json", tok)
    // Redirect về trang chủ vì đây chỉ là thao tác 1 lần
    http.Redirect(w, r, "/", http.StatusSeeOther)
}

Ở hàm này, chúng ta sẽ sử dụng kết quả của quá trình xác thực thông tin từ file credentials.json (lúc tải về thì nó sẽ có dạng khác, nhưng ta đổi tên cho nhất quán), tiếp đó ta sử dụng thư viện 

    "google.golang.org/api/sheets/v4"
Để đưa những Config từ file JSON ra và áp dụng vào API hiện tại. Tiếp đó thì hàm callback sẽ gọi tới cả trường "code", ta sẽ convert chúng thành token, đồng thời dùng hàm saveToken để tạo file token.json, kết quả của hàm là sẽ đẩy về trang chủ Redirect. 

Khi submit Form, chạy hàm callback trước nếu chưa xác minh, đã xác minh rồi thì dựa theo Token để thực thi logic

// Hàm xử lý form submission và ghi dữ liệu vào Google Sheets
func submitOrderHandler(w http.ResponseWriter, r *http.Request) {
    ctx := context.Background()


    // Đọc file cấu hình
    b, err := os.ReadFile("credentials.json")
    if err != nil {
        log.Fatalf("Unable to read client secret file: %v", err)
    }


    // Tạo config từ file cấu hình
    config, err := google.ConfigFromJSON(b, "https://www.googleapis.com/auth/spreadsheets")
    if err != nil {
        log.Fatalf("Unable to parse client secret file to config: %v", err)
    }


    // Lấy client từ token
    client := getClient(config)


    // Khởi tạo dịch vụ Sheets
    srv, err := sheets.NewService(ctx, option.WithHTTPClient(client))
    if err != nil {
        log.Fatalf("Unable to retrieve Sheets client: %v", err)
    }


    // Đọc dữ liệu từ form
    customerName := r.FormValue("customerName")
    phoneNumber := r.FormValue("phoneNumber")
    productQuantity := r.FormValue("productQuantity")
    shippingAddress := r.FormValue("shippingAddress")
    unitPriceStr := r.FormValue("unitPrice")
    totalAmountStr := r.FormValue("totalAmountHidden")


    // Chuyển đổi giá trị số
    unitPrice, err := strconv.Atoi(unitPriceStr)
    if err != nil {
        log.Fatalf("Unable to convert unit price: %v", err)
        fmt.Println(unitPrice)
    }
    totalAmount, err := strconv.Atoi(totalAmountStr)
    if err != nil {
        log.Fatalf("Unable to convert total amount: %v", err)
    }


    // Xác định ID bảng tính và phạm vi ghi dữ liệu
    spreadsheetId := "ID trang tính"
    writeRange := "Bảng!A:E" <== bảng và dải ô nhé


    // Dữ liệu cần ghi vào bảng tính
    vr := &sheets.ValueRange{
        Values: [][]interface{}{
            {customerName, phoneNumber, productQuantity, shippingAddress, unitPrice, totalAmount},
        },
    }


    // Ghi dữ liệu vào Google Sheets
    _, err = srv.Spreadsheets.Values.Append(spreadsheetId, writeRange, vr).ValueInputOption("RAW").Do()
    if err != nil {
        log.Fatalf("Unable to write data to sheet: %v", err)
    }
    // Redirect về trang chủ với param thông báo
    http.Redirect(w, r, "/?orderSubmitted=true", http.StatusSeeOther)
}

Hết rồi, hy vọng cả nhà sẽ triển khai được phần thu Lead từ google sheet này ngon lành nhé

Cập nhật, khi sử dụng API Google Sheet của Google thì bạn chỉ sẽ có thời gian hết hạn, tức là chỉ chạy được khoảng 1 tiếng, để có thể sử dụng trong dài hạn, bạn sẽ cần tạo Refresh Token, thật may mắn vì thư viện Google Oauth của Golang có hàm TokenSource giúp tự gia hạn Token. 

Dưới đây là hàm getClient sau khi đã cập nhật lại phần tạo Refresh token nếu hết hạn. 

func getClient(config *oauth2.Config) *http.Client {
	// The file token.json stores the user's access and refresh tokens, and is
	// created automatically when the authorization flow completes for the first
	// time.
	tokFile := "token.json"
	tok, err := tokenFromFile(tokFile)
	if err != nil {
		tok = getTokenFromWeb(config)
		saveToken(tokFile, tok)
	}

	// use token source to retrieve the configured token or a new token
	token, err := config.TokenSource(context.Background(), tok).Token()
	if err != nil && strings.Contains(err.Error(), "oauth2: token expired and refresh token is not set") {
		// update the token source
		token, err = config.TokenSource(context.Background(), tok).Token()
		if err != nil {
			log.Fatalf("Unable to retrieve new token: %v", err)
		}
	} else if err != nil { // some other error than refresh token error
		log.Fatalf("Error retrieving token: %v", err)
	}

	return config.Client(context.Background(), token)
}