• 《Go Web 编程》之第2章 ChitChat论坛


    第2章 ChitChat论坛

    用户登录、发帖、回帖。

    2.1 ChitChat简介

    网上论坛相当于通过帖子(thread)进行对话的公告板。
    由拥有特殊权限的版主(moderator)管理。
    注册账号用户才能发帖和回帖,未注册账号用户只能查看帖子。

    2.2 应用设计

    Web应用的工作流程:
    (1)客户端向服务器发送HTTP请求;
    (2)服务器处理HTTP请求;
    (3)返回HTTP响应。

    ChitChat的请求格式:

    http://<servername>/<handlername>?<parameters>
    http://<服务器名><处理器名>?<参数>
    
    • 1
    • 2

    处理器名按层级划分,/thread/read。
    应用参数以URL查询形式传递。
    多路复用器(multiplexer),检查请求,并重定向至各处理器处理。
    处理器解析信息,处理,将数据传递给模板引擎。
    模板引擎将模板和数据生成返回给客户端的HTML。

    2.3 数据模型

    ChitChat数据模型,包含4种数据结构,映射到关系型数据库。

    • User,论坛用户信息;
    • Session,当前登录会话;
    • Thread,论坛帖子,记录多个用户对话;
    • Post,用户在论坛里的回复。

    2.4 请求的接收与处理

    2.4.1 多路复用器

    package main
    
    import (
    	"net/http"
    	"time"
    )
    
    func main() {
    	mux := http.NewServeMux()
    	
    	files := http.FileServer(http.Dir(config.Static))
    	mux.Handle("/static/", http.StripPrefix("/static/", files))
    	mux.HandleFunc("/", index)
    	
    	server := &http.Server{
    		Addr:           "0.0.0.0:8080",
    		Handler:        mux,
    	}
    	server.ListenAndServe()
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    2.4.2 服务静态文件

    http://localhost/static/css/bootstrap.min.css
    映射为
    <application root>/public/css/bootstrap.min.css
    
    • 1
    • 2
    • 3
    files := http.FileServer(http.Dir("/public"))
    mux.Handle("/static/", http.StripPrefix("/static/", files))
    
    • 1
    • 2

    2.4.3 创建处理器函数

    package main
    
    import (
    	"net/http"
    	"time"
    )
    
    func main() {
    	mux := http.NewServeMux()
    	files := http.FileServer(http.Dir(config.Static))
    	mux.Handle("/static/", http.StripPrefix("/static/", files))
    
    	mux.HandleFunc("/", index)
    	mux.HandleFunc("/err", err)
    
    	mux.HandleFunc("/login", login)
    	mux.HandleFunc("/logout", logout)
    	mux.HandleFunc("/signup", signup)
    	mux.HandleFunc("/signup_account", signupAccount)
    	mux.HandleFunc("/authenticate", authenticate)
    
    	mux.HandleFunc("/thread/new", newThread)
    	mux.HandleFunc("/thread/create", createThread)
    	mux.HandleFunc("/thread/post", postThread)
    	mux.HandleFunc("/thread/read", readThread)
    
    	server := &http.Server{
    		Addr:           "0.0.0.0:8080",
    		Handler:        mux,
    	}
    	server.ListenAndServe()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    2.4.4 使用cookie进行访问控制

    route_auth.go

    // POST /authenticate
    // Authenticate the user given the email and password
    func authenticate(writer http.ResponseWriter, request *http.Request) {
    	err := request.ParseForm()
    	user, err := data.UserByEmail(request.PostFormValue("email"))
    	if err != nil {
    		danger(err, "Cannot find user")
    	}
    	if user.Password == data.Encrypt(request.PostFormValue("password")) {
    		session, err := user.CreateSession()
    		if err != nil {
    			danger(err, "Cannot create session")
    		}
    		cookie := http.Cookie{
    			Name:     "_cookie",
    			Value:    session.Uuid,
    			HttpOnly: true,
    		}
    		http.SetCookie(writer, &cookie)
    		http.Redirect(writer, request, "/", 302)
    	} else {
    		http.Redirect(writer, request, "/login", 302)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    util.go

    // Checks if the user is logged in and has a session, if not err is not nil
    func session(writer http.ResponseWriter, request *http.Request) (sess data.Session, err error) {
    	cookie, err := request.Cookie("_cookie")
    	if err == nil {
    		sess = data.Session{Uuid: cookie.Value}
    		if ok, _ := sess.Check(); !ok {
    			err = errors.New("Invalid session")
    		}
    	}
    	return
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    2.5 使用模板生成HTML响应

    util.go

    func generateHTML(writer http.ResponseWriter, data interface{}, filenames ...string) {
    	var files []string
    	for _, file := range filenames {
    		files = append(files, fmt.Sprintf("templates/%s.html", file))
    	}
    
    	templates := template.Must(template.ParseFiles(files...))
    	templates.ExecuteTemplate(writer, "layout", data)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2.6 安装PostgreSQL

    2.6.1 Ubuntu上安装

    sudo apt-get install postgresql postgresql-contrib
    
    • 1
    //登入Postgres账号
    sudo su postgres
    //创建用户
    createuser -interactive
    //创建用户名命名的数据库
    createdb USER_NAME
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2.6.2 Mac上安装

    Postgres应用压缩包解压,Postgres.app文件放入Applications文件夹。

    ~/.profile或~/.bashrc文件添加路径
    export PATH=$PATH:/Applications/Postgres.app/Contents/Versions/9.4/bin
    
    • 1
    • 2

    2.6.3 Windows上安装

    https://www.enterprisedb.com/downloads/postgres-postgresql-downloads

    2.7 连接数据库

    setup.sql

    drop table if exists posts;
    drop table if exists threads;
    drop table if exists sessions;
    drop table if exists users;
    
    create table users (
      id         serial primary key,
      uuid       varchar(64) not null unique,
      name       varchar(255),
      email      varchar(255) not null unique,
      password   varchar(255) not null,
      created_at timestamp not null   
    );
    
    create table sessions (
      id         serial primary key,
      uuid       varchar(64) not null unique,
      email      varchar(255),
      user_id    integer references users(id),
      created_at timestamp not null   
    );
    
    create table threads (
      id         serial primary key,
      uuid       varchar(64) not null unique,
      topic      text,
      user_id    integer references users(id),
      created_at timestamp not null       
    );
    
    create table posts (
      id         serial primary key,
      uuid       varchar(64) not null unique,
      body       text,
      user_id    integer references users(id),
      thread_id  integer references threads(id),
      created_at timestamp not null  
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    createdb -p 5433 -U postgres chitchat
    
    psql -p 5433 -U postgres -d chitchat -f F:\study\go\gwp-master\Chapter_2_Go_ChitChat\chitchat\data\setup.sql
    
    • 1
    • 2
    • 3

    thread.go

    type Thread struct {
    	Id        int
    	Uuid      string
    	Topic     string
    	UserId    int
    	CreatedAt time.Time
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    data.go

    var Db *sql.DB
    
    func init() {
    	var err error
    	Db, err = sql.Open("postgres", "host=localhost port=5433 user=postgres dbname=chitchat password=postgres sslmode=disable")
    	if err != nil {
    		log.Fatal(err)
    	}
    	err = Db.Ping()
    	if err != nil {
    		panic(err)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    2.8 启动服务器

    // starting up the server
    	server := &http.Server{
    		Addr:           config.Address,
    		Handler:        mux,
    	}
    	server.ListenAndServe()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2.9 Web应用运作流程回顾

    (1)客户端向服务器发送请求;
    (2)多路复用器收到请求,重定向至正确的处理器;
    (3)处理器对请求进行处理;
    (4)需要访问数据库的情况下,处理器使用一或多个数据结构(根据数据库中数据建模);
    (5)处理器调用与数据结构有关函数或方法时,数据结构背后的模型与数据库连接并进行相应操作;
    (6)请求处理完毕时,处理器调用模板引擎,有时会向模板引擎传递一些通过模型获取到的数据;
    (7)模板引擎语法分析模板文件列表创建模板结构,与处理器传递数据合并生成最终HTML;
    (8)生成HTML作为响应回传至客户端。

    2.10 各组成部分

    2.10.1 模板

    template/error.html

    {{ define "content" }}
    <p>{{ . }}</p>
    {{ end }}
    
    • 1
    • 2
    • 3

    template/index.html

    {{ define "content" }}
    <h2><a href="/thread/new">Start a thread</a> or join one below!</h2>
    
    {{ range . }}
    <p><i>{{ .Topic }}</i> Started by {{ .User.Name }} - {{ .CreatedAtDate }} - {{ .NumReplies }} posts.</p>
    <p><a href="/thread/read?id={{ .Uuid }}">Read more</a></p>
    <br/>
    {{ end }}
    {{ end }}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    template/layout.html

    {{ define "layout" }}
    <!DOCTYPE html>
    <html>
    	<head>
    		<title>ChitChat</title>
    	</head>
    	<body>
    		{{ template "navbar" . }}
    		{{ template "content" . }}
    	</body>
    </html>
    {{ end }}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    template/login.html

    {{ define "content" }}
    <form action="/authenticate" method="post">
    	<h2><i>ChitChat</i></h2>
    	<input type="email" name="email" placeholder="Email address" required autofocus>
    	<br/>
    	<input type="password" name="password" placeholder="Password" required>
    	<br/>
    	<button type="submit">Sign in</button>
    	<br/>
    	<a href="/signup">Sign up</a>
    </form>
    {{ end }}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    template/login.layout.html

    {{ define "layout" }}
    <!DOCTYPE html>
    <html>
      <head>
        <title>ChitChat</title>
      </head>
      <body>
    	{{ template "content" . }} 
      </body>
    </html>
    {{ end }}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    template/new.thread.html

    {{ define "content" }}
    <form action="/thread/create" method="post">
    	<p>Start a new thread with the following topic</p>
        <textarea name="topic" id="topic" placeholder="Thread topic here" rows="4"></textarea>
        <br/>
        <br/>
        <button type="submit">Start this thread</button>
    </form>
    {{ end }}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    template/private.navbar.html

    {{ define "navbar" }}
    <a href="/"><i>ChitChat</i></a>
    <ul>
    	<li><a href="/">Home</a></li>
    	<li><a href="/logout">Logout</a></li>
    </ul>
    {{ end }}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    template/private.thread.html

    {{ define "content" }}
    <h2><i>{{ .Topic }}</i> Started by {{ .User.Name }} - {{ .CreatedAtDate }}</h2>
    
    {{ range .Posts }}
    <p>{{ .Body }}</p>
    <p>{{ .User.Name }} - {{ .CreatedAtDate }}</p>
    <br/>
    {{ end }}
    
    <form action="/thread/post" method="post">
    	<textarea name="body" id="body" placeholder="Write your reply here" rows="3"></textarea>
    	<input type="hidden" name="uuid" value="{{ .Uuid }}">
    	<br/>
    	<button type="submit">Reply</button>
    </form>
    {{ end }}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    template/public.navbar.html

    {{ define "navbar" }}
    <a href="/"><i>ChitChat</i></a>
    <ul>
    	<li><a href="/">Home</a></li>
    	<li><a href="/login">Login</a></li>
    </ul>
    {{ end }}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    template/public.thread.html

    {{ define "content" }}
    <h2><i>{{ .Topic }}</i> Started by {{ .User.Name }} - {{ .CreatedAtDate }}</h2>
    
    {{ range .Posts }}
    <p>{{ .Body }}</p>
    <p>{{ .User.Name }} - {{ .CreatedAtDate }}</p>
    <br/>
    {{ end }}
    {{ end }}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    template/signup.html

    {{ define "content" }}
    <form action="/signup_account" method="post">
      <h2><i>ChitChat</i></h2>
      <p>Sign up for an account below</p>
      <input id="name" type="text" name="name" placeholder="Name" required autofocus>
      <br/>
      <input type="email" name="email" placeholder="Email address" required>
      <br/>
      <input type="password" name="password" placeholder="Password" required>
      <br/>
      <button type="submit">Sign up</button>
    </form>
    {{ end }}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    2.10.2 数据库

    data/setup.sql

    drop table if exists posts;
    drop table if exists threads;
    drop table if exists sessions;
    drop table if exists users;
    
    create table users (
      id         serial primary key,
      uuid       varchar(64) not null unique,
      name       varchar(255),
      email      varchar(255) not null unique,
      password   varchar(255) not null,
      created_at timestamp not null   
    );
    
    create table sessions (
      id         serial primary key,
      uuid       varchar(64) not null unique,
      email      varchar(255),
      user_id    integer references users(id),
      created_at timestamp not null   
    );
    
    create table threads (
      id         serial primary key,
      uuid       varchar(64) not null unique,
      topic      text,
      user_id    integer references users(id),
      created_at timestamp not null       
    );
    
    create table posts (
      id         serial primary key,
      uuid       varchar(64) not null unique,
      body       text,
      user_id    integer references users(id),
      thread_id  integer references threads(id),
      created_at timestamp not null  
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    创建数据库和表

    createdb -p 5433 -U postgres chitchat
    
    psql -p 5433 -U postgres -d chitchat -f F:\study\go\chitchat\data\setup.sql
    
    • 1
    • 2
    • 3

    data/data.go

    package data
    
    import (
    	"crypto/rand"
    	"crypto/sha1"
    	"database/sql"
    	"fmt"
    	"log"
    
    	_ "github.com/lib/pq"
    )
    
    //数据库连接池句柄
    var Db *sql.DB
    
    //数据库连接
    func init() {
    	var err error
    	Db, err = sql.Open("postgres", "host=localhost port=5433 user=postgres dbname=chitchat password=postgres sslmode=disable")
    	if err != nil {
    		log.Fatal(err)
    	}
    	err = Db.Ping()
    	if err != nil {
    		panic(err)
    	}
    }
    
    //RFC 4122随机UUID(Universally Unique IDentifier
    func createUUID() (uuid string) {
    	u := new([16]byte)
    	if _, err := rand.Read(u[:]); err != nil {
    		log.Fatalln("Cannot generate UUID", err)
    	}
    
    	u[8] = (u[8] | 0x40) & 0x7F
    	u[6] = (u[6] & 0xF) | (0x4 << 4)
    	uuid = fmt.Sprintf("%x-%x-%x-%x-%x", u[0:4], u[4:6], u[6:8], u[8:10], u[10:])
    	return
    }
    
    //SHA-1散列明文
    func Encrypt(plaintext string) (cryptext string) {
    	cryptext = fmt.Sprintf("%x", sha1.Sum([]byte(plaintext)))
    	return
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    data/user.go

    package data
    
    import (
    	"time"
    )
    
    //用户
    type User struct {
    	Id        int
    	Uuid      string
    	Name      string
    	Email     string
    	Password  string
    	CreatedAt time.Time
    }
    
    // Create a new session for an existing user
    func (user *User) CreateSession() (session Session, err error) {
    	statement := "insert into sessions (uuid, email, user_id, created_at) values ($1, $2, $3, $4) returning id, uuid, email, user_id, created_at"
    	stmt, err := Db.Prepare(statement)
    	if err != nil {
    		return
    	}
    	defer stmt.Close()
    
    	// use QueryRow to return a row and scan the returned id into the Session struct
    	err = stmt.QueryRow(createUUID(), user.Email, user.Id, time.Now()).
    		Scan(&session.Id, &session.Uuid, &session.Email, &session.UserId, &session.CreatedAt)
    	return
    }
    
    // Get the session for an existing user
    func (user *User) Session() (session Session, err error) {
    	err = Db.QueryRow("SELECT id, uuid, email, user_id, created_at FROM sessions WHERE user_id = $1", user.Id).
    		Scan(&session.Id, &session.Uuid, &session.Email, &session.UserId, &session.CreatedAt)
    	return
    }
    
    // Create a new user, save user info into the database
    func (user *User) Create() (err error) {
    	// Postgres does not automatically return the last insert id, because it would be wrong to assume
    	// you're always using a sequence.You need to use the RETURNING keyword in your insert to get this
    	// information from postgres.
    	statement := "insert into users (uuid, name, email, password, created_at) values ($1, $2, $3, $4, $5) returning id, uuid, created_at"
    	stmt, err := Db.Prepare(statement)
    	if err != nil {
    		return
    	}
    	defer stmt.Close()
    
    	// use QueryRow to return a row and scan the returned id into the User struct
    	err = stmt.QueryRow(createUUID(), user.Name, user.Email, Encrypt(user.Password), time.Now()).
    		Scan(&user.Id, &user.Uuid, &user.CreatedAt)
    	return
    }
    
    // Delete user from database
    func (user *User) Delete() (err error) {
    	stmt, err := Db.Prepare("delete from users where id = $1")
    	if err != nil {
    		return
    	}
    	defer stmt.Close()
    
    	_, err = stmt.Exec(user.Id)
    	return
    }
    
    // Update user information in the database
    func (user *User) Update() (err error) {
    	stmt, err := Db.Prepare("update users set name = $2, email = $3 where id = $1")
    	if err != nil {
    		return
    	}
    	defer stmt.Close()
    
    	_, err = stmt.Exec(user.Id, user.Name, user.Email)
    	return
    }
    
    // Delete all users from database
    func UserDeleteAll() (err error) {
    	_, err = Db.Exec("delete from users")
    	return
    }
    
    // Get all users in the database and returns it
    func Users() (users []User, err error) {
    	rows, err := Db.Query("SELECT id, uuid, name, email, password, created_at FROM users")
    	if err != nil {
    		return
    	}
    	defer rows.Close()
    
    	for rows.Next() {
    		user := User{}
    		if err = rows.Scan(&user.Id, &user.Uuid, &user.Name, &user.Email, &user.Password, &user.CreatedAt); err != nil {
    			return
    		}
    		users = append(users, user)
    	}
    	return
    }
    
    // Get a single user given the UUID
    func UserNoPasswordByID(id int) (user User, err error) {
    	err = Db.QueryRow("SELECT id, uuid, name, email, created_at FROM users WHERE id = $1", id).
    		Scan(&user.Id, &user.Uuid, &user.Name, &user.Email, &user.CreatedAt)
    	return
    }
    
    // Get a single user given the UUID
    func UserByID(id int) (user User, err error) {
    	err = Db.QueryRow("SELECT id, uuid, name, email, password, created_at FROM users WHERE id = $1", id).
    		Scan(&user.Id, &user.Uuid, &user.Name, &user.Email, &user.Password, &user.CreatedAt)
    	return
    }
    
    // Get a single user given the UUID
    func UserByUUID(uuid string) (user User, err error) {
    	err = Db.QueryRow("SELECT id, uuid, name, email, password, created_at FROM users WHERE uuid = $1", uuid).
    		Scan(&user.Id, &user.Uuid, &user.Name, &user.Email, &user.Password, &user.CreatedAt)
    	return
    }
    
    // Get a single user given the email
    func UserByEmail(email string) (user User, err error) {
    	err = Db.QueryRow("SELECT id, uuid, name, email, password, created_at FROM users WHERE email = $1", email).
    		Scan(&user.Id, &user.Uuid, &user.Name, &user.Email, &user.Password, &user.CreatedAt)
    	return
    }
    
    // Create a new thread
    func (user *User) CreateThread(topic string) (thread Thread, err error) {
    	statement := "insert into threads (uuid, topic, user_id, created_at) values ($1, $2, $3, $4) returning id, uuid, topic, user_id, created_at"
    	stmt, err := Db.Prepare(statement)
    	if err != nil {
    		return
    	}
    	defer stmt.Close()
    
    	// use QueryRow to return a row and scan the returned id into the Session struct
    	err = stmt.QueryRow(createUUID(), topic, user.Id, time.Now()).
    		Scan(&thread.Id, &thread.Uuid, &thread.Topic, &thread.UserId, &thread.CreatedAt)
    	return
    }
    
    // Create a new post to a thread
    func (user *User) CreatePost(thread Thread, body string) (post Post, err error) {
    	statement := "insert into posts (uuid, body, user_id, thread_id, created_at) values ($1, $2, $3, $4, $5) returning id, uuid, body, user_id, thread_id, created_at"
    	stmt, err := Db.Prepare(statement)
    	if err != nil {
    		return
    	}
    	defer stmt.Close()
    
    	// use QueryRow to return a row and scan the returned id into the Session struct
    	err = stmt.QueryRow(createUUID(), body, user.Id, thread.Id, time.Now()).Scan(&post.Id, &post.Uuid, &post.Body, &post.UserId, &post.ThreadId, &post.CreatedAt)
    	return
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160

    data/thread.go

    package data
    
    import (
    	"time"
    )
    
    //主线
    type Thread struct {
    	Id        int
    	Uuid      string
    	Topic     string //话题
    	UserId    int
    	CreatedAt time.Time
    }
    
    // format the CreatedAt date to display nicely on the screen
    func (thread *Thread) CreatedAtDate() string {
    	return thread.CreatedAt.Format("Jan 2, 2006 at 3:04pm")
    }
    
    // get the number of posts in a thread
    func (thread *Thread) NumReplies() (count int) {
    	rows, err := Db.Query("SELECT count(*) FROM posts where thread_id = $1", thread.Id)
    	if err != nil {
    		return
    	}
    	defer rows.Close()
    
    	for rows.Next() {
    		if err = rows.Scan(&count); err != nil {
    			return
    		}
    	}
    	return
    }
    
    // get posts to a thread
    func (thread *Thread) Posts() (posts []Post, err error) {
    	rows, err := Db.Query("SELECT id, uuid, body, user_id, thread_id, created_at FROM posts where thread_id = $1", thread.Id)
    	if err != nil {
    		return
    	}
    	defer rows.Close()
    
    	for rows.Next() {
    		post := Post{}
    		if err = rows.Scan(&post.Id, &post.Uuid, &post.Body, &post.UserId, &post.ThreadId, &post.CreatedAt); err != nil {
    			return
    		}
    		posts = append(posts, post)
    	}
    	return
    }
    
    // Get all threads in the database and returns it
    func Threads() (threads []Thread, err error) {
    	rows, err := Db.Query("SELECT id, uuid, topic, user_id, created_at FROM threads ORDER BY created_at DESC")
    	if err != nil {
    		return
    	}
    	defer rows.Close()
    
    	for rows.Next() {
    		thread := Thread{}
    		if err = rows.Scan(&thread.Id, &thread.Uuid, &thread.Topic, &thread.UserId, &thread.CreatedAt); err != nil {
    			return
    		}
    		threads = append(threads, thread)
    	}
    	return
    }
    
    // Get a thread by the UUID
    func ThreadByUUID(uuid string) (thread Thread, err error) {
    	err = Db.QueryRow("SELECT id, uuid, topic, user_id, created_at FROM threads WHERE uuid = $1", uuid).
    		Scan(&thread.Id, &thread.Uuid, &thread.Topic, &thread.UserId, &thread.CreatedAt)
    	return
    }
    
    // Get the user who started this thread
    func (thread *Thread) User() (user User) {
    	user, _ = UserNoPasswordByID(thread.UserId)
    	return
    }
    
    // Delete all threads from database
    func ThreadDeleteAll() (err error) {
    	_, err = Db.Exec("delete from threads")
    	return
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90

    data/post.go

    package data
    
    import (
    	"time"
    )
    
    //帖子
    type Post struct {
    	Id        int
    	Uuid      string
    	Body      string //内容
    	UserId    int
    	ThreadId  int
    	CreatedAt time.Time
    }
    
    func (post *Post) CreatedAtDate() string {
    	return post.CreatedAt.Format("Jan 2, 2006 at 3:04pm")
    }
    
    // Get the user who wrote the post
    func (post *Post) User() (user User) {
    	user, _ = UserNoPasswordByID(post.UserId)
    	return
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    data/session.go

    package data
    
    import (
    	"time"
    )
    
    //会话
    type Session struct {
    	Id        int
    	Uuid      string
    	Email     string
    	UserId    int
    	CreatedAt time.Time
    }
    
    // Check if session is valid in the database
    func (session *Session) Check() (valid bool, err error) {
    	err = Db.QueryRow("SELECT id, uuid, email, user_id, created_at FROM sessions WHERE uuid = $1", session.Uuid).
    		Scan(&session.Id, &session.Uuid, &session.Email, &session.UserId, &session.CreatedAt)
    	if err != nil {
    		valid = false
    		return
    	}
    	if session.Id != 0 {
    		valid = true
    	}
    	return
    }
    
    // Check if session is valid in the database
    func CheckSeeeionByUUID(uuid string) bool {
    	count := 0
    	Db.QueryRow("SELECT count(*) FROM sessions WHERE uuid = $1 LIMIT 1", uuid).Scan(&count)
    	return count == 1
    }
    
    // Check if session is valid in the database
    func SessionByUUID(uuid string) (session Session, err error) {
    	err = Db.QueryRow("SELECT id, uuid, email, user_id, created_at FROM sessions WHERE uuid = $1", uuid).
    		Scan(&session.Id, &session.Uuid, &session.Email, &session.UserId, &session.CreatedAt)
    	return
    }
    
    func SessionDeleteByUUID(uuid string) (err error) {
    	stmt, err := Db.Prepare("delete from sessions where uuid = $1")
    	if err != nil {
    		return
    	}
    	defer stmt.Close()
    
    	_, err = stmt.Exec(uuid)
    	return
    }
    
    // Delete session from database
    func (session *Session) DeleteByUUID() (err error) {
    	stmt, err := Db.Prepare("delete from sessions where uuid = $1")
    	if err != nil {
    		return
    	}
    	defer stmt.Close()
    
    	_, err = stmt.Exec(session.Uuid)
    	return
    }
    
    // Get the user from the session
    func (session *Session) User() (user User, err error) {
    	user, _ = UserNoPasswordByID(session.UserId)
    	return
    }
    
    // Delete all sessions from database
    func SessionDeleteAll() (err error) {
    	_, err = Db.Exec("delete from sessions")
    	return
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77

    2.10.3 主模块

    config.json

    {
      "Address"        : "127.0.0.1:8080",
      "ReadTimeout"    : 10,
      "WriteTimeout"   : 600
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    util.go

    package main
    
    import (
    	"chitchat/data"
    	"encoding/json"
    	"fmt"
    	"html/template"
    	"log"
    	"net/http"
    	"os"
    )
    
    type Configuration struct {
    	Address      string
    	ReadTimeout  int64
    	WriteTimeout int64
    }
    
    var config Configuration
    var logger *log.Logger
    
    func loadConfig() {
    	fp, err := os.Open("config.json")
    	if err != nil {
    		log.Fatalln("Cannot open config file", err)
    	}
    	defer fp.Close()
    
    	decoder := json.NewDecoder(fp)
    	err = decoder.Decode(&config)
    	if err != nil {
    		log.Fatalln("Cannot get configuration from file", err)
    	}
    }
    
    func init() {
    	loadConfig()
    	fp, err := os.OpenFile("chitchat.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    	if err != nil {
    		log.Fatalln("Failed to open log file", err)
    	}
    	logger = log.New(fp, "INFO ", log.Ldate|log.Ltime|log.Lshortfile)
    }
    
    // Convenience function to redirect to the error message page
    func error_message(writer http.ResponseWriter, request *http.Request, msg string) {
    	http.Redirect(writer, request, "/err?msg="+msg, http.StatusFound)
    }
    
    // Checks if the user is logged in and has a session, if not err is not nil
    func session(request *http.Request) bool {
    	if cookie, err := request.Cookie("_cookie"); err == nil {
    		return data.CheckSeeeionByUUID(cookie.Value)
    	}
    	return false
    }
    
    func generateHTML(writer http.ResponseWriter, data interface{}, filenames ...string) {
    	var files []string
    	for _, file := range filenames {
    		files = append(files, fmt.Sprintf("templates/%s.html", file))
    	}
    	templates := template.Must(template.ParseFiles(files...))
    	templates.ExecuteTemplate(writer, "layout", data)
    }
    
    func logError(args ...interface{}) {
    	logger.SetPrefix("ERROR ")
    	logger.Println(args...)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    route.go

    package main
    
    import (
    	"chitchat/data"
    	"fmt"
    	"net/http"
    )
    
    func index(writer http.ResponseWriter, request *http.Request) {
    	if threads, err := data.Threads(); err != nil {
    		error_message(writer, request, "Cannot get threads")
    	} else {
    		if !session(request) {
    			generateHTML(writer, threads, "layout", "public.navbar", "index")
    		} else {
    			generateHTML(writer, threads, "layout", "private.navbar", "index")
    		}
    	}
    }
    
    // GET /err?msg=
    // shows the error message page
    func err(writer http.ResponseWriter, request *http.Request) {
    	if vals := request.URL.Query(); !session(request) {
    		generateHTML(writer, vals.Get("msg"), "layout", "public.navbar", "error")
    	} else {
    		generateHTML(writer, vals.Get("msg"), "layout", "private.navbar", "error")
    	}
    }
    
    // GET /login
    // Show the login page
    func login(writer http.ResponseWriter, request *http.Request) {
    	generateHTML(writer, nil, "login.layout", "login")
    }
    
    // GET /logout
    // Logs the user out
    func logout(writer http.ResponseWriter, request *http.Request) {
    	if cookie, err := request.Cookie("_cookie"); err == nil {
    		data.SessionDeleteByUUID(cookie.Value)
    	}
    	http.Redirect(writer, request, "/", http.StatusFound)
    }
    
    // GET /signup
    // Show the signup page
    func signup(writer http.ResponseWriter, request *http.Request) {
    	generateHTML(writer, nil, "login.layout", "signup")
    }
    
    // POST /signup
    // Create the user account
    func signupAccount(writer http.ResponseWriter, request *http.Request) {
    	user := data.User{
    		Name:     request.PostFormValue("name"),
    		Email:    request.PostFormValue("email"),
    		Password: request.PostFormValue("password"),
    	}
    	if err := user.Create(); err != nil {
    		logError(err, "Cannot create user")
    	}
    	http.Redirect(writer, request, "/login", http.StatusFound)
    }
    
    // POST /authenticate
    // Authenticate the user given the email and password
    func authenticate(writer http.ResponseWriter, request *http.Request) {
    	user, err := data.UserByEmail(request.PostFormValue("email"))
    	if err != nil {
    		logError(err, "Cannot find user")
    	}
    	if user.Password == data.Encrypt(request.PostFormValue("password")) {
    		session, err := user.CreateSession()
    		if err != nil {
    			logError(err, "Cannot create session")
    		}
    
    		cookie := http.Cookie{
    			Name:     "_cookie",
    			Value:    session.Uuid,
    			HttpOnly: true,
    		}
    		http.SetCookie(writer, &cookie)
    		http.Redirect(writer, request, "/", http.StatusFound)
    	} else {
    		http.Redirect(writer, request, "/login", http.StatusFound)
    	}
    }
    
    // GET /thread/new
    // Show the new thread form page
    func newThread(writer http.ResponseWriter, request *http.Request) {
    	if !session(request) {
    		http.Redirect(writer, request, "/login", http.StatusFound)
    	} else {
    		generateHTML(writer, nil, "layout", "private.navbar", "new.thread")
    	}
    }
    
    // POST /thread/create
    // Create the user account
    func createThread(writer http.ResponseWriter, request *http.Request) {
    	if !session(request) {
    		http.Redirect(writer, request, "/login", http.StatusFound)
    	} else {
    		cookie, _ := request.Cookie("_cookie")
    		session, _ := data.SessionByUUID(cookie.Value)
    		user, err := data.UserNoPasswordByID(session.UserId)
    		if err != nil {
    			logError(err, "Cannot get user from session")
    		}
    		topic := request.PostFormValue("topic")
    		if _, err := user.CreateThread(topic); err != nil {
    			logError(err, "Cannot create thread")
    		}
    		http.Redirect(writer, request, "/", http.StatusFound)
    	}
    }
    
    // POST /thread/post
    // Create the post
    func postThread(writer http.ResponseWriter, request *http.Request) {
    	if !session(request) {
    		http.Redirect(writer, request, "/login", http.StatusFound)
    	} else {
    		cookie, _ := request.Cookie("_cookie")
    		session, _ := data.SessionByUUID(cookie.Value)
    		user, err := data.UserByID(session.UserId)
    		if err != nil {
    			logError(err, "Cannot get user from session")
    		}
    		body := request.PostFormValue("body")
    		uuid := request.PostFormValue("uuid")
    		thread, err := data.ThreadByUUID(uuid)
    		if err != nil {
    			error_message(writer, request, "Cannot read thread")
    		}
    		if _, err := user.CreatePost(thread, body); err != nil {
    			logError(err, "Cannot create post")
    		}
    		url := fmt.Sprint("/thread/read?id=", uuid)
    		http.Redirect(writer, request, url, http.StatusFound)
    	}
    }
    
    // GET /thread/read
    // Show the details of the thread, including the posts and the form to write a post
    func readThread(writer http.ResponseWriter, request *http.Request) {
    	vals := request.URL.Query()
    	uuid := vals.Get("id")
    	thread, err := data.ThreadByUUID(uuid)
    	if err != nil {
    		error_message(writer, request, "Cannot read thread")
    	} else {
    		if !session(request) {
    			generateHTML(writer, &thread, "layout", "public.navbar", "public.thread")
    		} else {
    			generateHTML(writer, &thread, "layout", "private.navbar", "private.thread")
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162

    main.go

    package main
    
    import (
    	"net/http"
    	"time"
    )
    
    func main() {
    	mux := http.NewServeMux()
    
    	// index
    	mux.HandleFunc("/", index)
    
    	// error
    	mux.HandleFunc("/err", err)
    
    	// defined in route_auth.go
    	mux.HandleFunc("/login", login)
    	mux.HandleFunc("/logout", logout)
    	mux.HandleFunc("/signup", signup)
    	mux.HandleFunc("/signup_account", signupAccount)
    	mux.HandleFunc("/authenticate", authenticate)
    
    	// defined in route_thread.go
    	mux.HandleFunc("/thread/new", newThread)
    	mux.HandleFunc("/thread/create", createThread)
    	mux.HandleFunc("/thread/post", postThread)
    	mux.HandleFunc("/thread/read", readThread)
    
    	// starting up the server
    	server := &http.Server{
    		Addr:           config.Address,
    		Handler:        mux,
    		ReadTimeout:    time.Duration(config.ReadTimeout * int64(time.Second)),
    		WriteTimeout:   time.Duration(config.WriteTimeout * int64(time.Second)),
    		MaxHeaderBytes: 1 << 20,
    	}
    	server.ListenAndServe()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    运行及测试

    go mod init chitchat
    go get github.com/lib/pq
    
    go run main.go route.go utils.go
    
    curl -i http://127.0.0.1:8080/
    curl -i http://127.0.0.1:8080/err?msg=test
    
    curl -i http://127.0.0.1:8080/login
    curl -i http://127.0.0.1:8080/logout
    curl -i http://127.0.0.1:8080/signup
    
    //创建用户
    curl -v -d"name=test111&email=test111@136.com&password=test111" http://127.0.0.1:8080/signup_account
    
    //用户验证,获取cookie并保存
    curl -v -d"name=test111&email=test111@136.com&password=test111" -c cookie.txt http://127.0.0.1:8080/authenticate
    
    curl -i -b cookie.txt http://127.0.0.1:8080/thread/new
    
    //创建主题
    curl -i -b cookie.txt -d"topic=mytest" http://127.0.0.1:8080/thread/create
    
    //发布帖子
    //uuid指thread的uuid,curl -i http://127.0.0.1:8080/可以查看
    curl -i -b cookie.txt -d"body=contents&uuid=31115747-5b29-4a9f-55c4-2cf92c6e6150" http://127.0.0.1:8080/thread/post
    
    //查看主题下的所有帖子
    curl -i -b cookie.txt http://127.0.0.1:8080/thread/read?id=31115747-5b29-4a9f-55c4-2cf92c6e6150
    
    //查看数据表
    psql -p 5433 -U postgres -d chitchat -c "select * from users;"
    psql -p 5433 -U postgres -d chitchat -c "select * from threads;"
    psql -p 5433 -U postgres -d chitchat -c "select * from posts;"
    psql -p 5433 -U postgres -d chitchat -c "select * from sessions;"
    
    //sessions查看会话是否存在
    psql -p 5433 -U postgres -d chitchat -c "SELECT count(*) FROM sessions WHERE uuid = '84cb28ba-2af4-466a-5ba1-cc0f6471d6dc' LIMIT 1;"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
  • 相关阅读:
    JavaScript——关于JavaScript、在HTML中嵌入JS代码的三种方式、变量
    我的Vim学习笔记(不定期更新)
    leetcode二叉树系列通用输入定义
    深度学习系列34:统一图像视频文字的女娲模型
    UE4 C++ 笔记(二):基础知识
    spring-data-mongodb的Aggregation详解
    链式二叉树
    MySQL之数据查询(分类汇总与排序)
    SpringBoot 学习(九)Redis
    量化投资00
  • 原文地址:https://blog.csdn.net/oqqyx1234567/article/details/126730807