From e7da38bd9c86b0f79c0592346feb468197d99ac6 Mon Sep 17 00:00:00 2001
From: l1nk4i <l1nk@foxmail.com>
Date: Wed, 25 Sep 2024 23:32:34 +0800
Subject: [PATCH 01/14] feat: mysql handler

---
 l1nk4i/db/mysql.go | 59 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 59 insertions(+)
 create mode 100644 l1nk4i/db/mysql.go

diff --git a/l1nk4i/db/mysql.go b/l1nk4i/db/mysql.go
new file mode 100644
index 0000000..0667f9a
--- /dev/null
+++ b/l1nk4i/db/mysql.go
@@ -0,0 +1,59 @@
+package db
+
+import (
+	"fmt"
+	"gorm.io/driver/mysql"
+	"gorm.io/gorm"
+	"l1nk4i/config"
+	"log"
+)
+
+type User struct {
+	gorm.Model
+	Username string `gorm:"not null;column:username;unique"`
+	Password string `gorm:"not null;column:password"`
+	Role     string `gorm:"not null;column:role"`
+}
+
+var db *gorm.DB
+
+func init() {
+	username := config.Mysql.Username
+	password := config.Mysql.Password
+	host := config.Mysql.Host
+	port := config.Mysql.Port
+	dbname := config.Mysql.Dbname
+
+	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", username, password, host, port, dbname)
+	conn, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
+	if err != nil {
+		panic("Cannot connect to mysql: " + err.Error())
+	}
+
+	if err = conn.AutoMigrate(&User{}); err != nil {
+		panic("AutoMigrate failed: " + err.Error())
+	}
+
+	db = conn
+}
+
+func CreateUser(user *User) error {
+	err := db.Create(user).Error
+	if err != nil {
+		log.Printf("Create user failed: %s\n", err.Error())
+		return err
+	}
+
+	return nil
+}
+
+func GetUser(username string) (*User, error) {
+	var user User
+	err := db.Where("username = ?", username).First(&user).Error
+	if err != nil {
+		log.Printf("Get user failed: %s\n", err.Error())
+		return nil, err
+	}
+
+	return &user, nil
+}

From f8cf8fbd1cf84a6ecd818422f145e8fa3793a8b6 Mon Sep 17 00:00:00 2001
From: l1nk4i <l1nk@foxmail.com>
Date: Wed, 25 Sep 2024 23:33:09 +0800
Subject: [PATCH 02/14] feat: login and register handler

---
 l1nk4i/api/user/login.go    | 29 +++++++++++++++++++++++++++++
 l1nk4i/api/user/register.go | 30 ++++++++++++++++++++++++++++++
 l1nk4i/utils/hash.go        | 15 +++++++++++++++
 3 files changed, 74 insertions(+)
 create mode 100644 l1nk4i/api/user/login.go
 create mode 100644 l1nk4i/api/user/register.go
 create mode 100644 l1nk4i/utils/hash.go

diff --git a/l1nk4i/api/user/login.go b/l1nk4i/api/user/login.go
new file mode 100644
index 0000000..4cbb91b
--- /dev/null
+++ b/l1nk4i/api/user/login.go
@@ -0,0 +1,29 @@
+package user
+
+import (
+	"github.com/gin-gonic/gin"
+	"l1nk4i/db"
+	"l1nk4i/utils"
+	"net/http"
+)
+
+func Login(c *gin.Context) {
+	var loginInfo struct {
+		Username string `json:"username"`
+		Password string `json:"password"`
+	}
+
+	if err := c.ShouldBind(&loginInfo); err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+	} else {
+		if user, err := db.GetUser(loginInfo.Username); err != nil {
+			c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid username"})
+		} else {
+			if utils.CheckPasswordHash(loginInfo.Password, user.Password) {
+				c.JSON(http.StatusOK, gin.H{"message": "login successful!"})
+			} else {
+				c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid password"})
+			}
+		}
+	}
+}
diff --git a/l1nk4i/api/user/register.go b/l1nk4i/api/user/register.go
new file mode 100644
index 0000000..dbdb2f8
--- /dev/null
+++ b/l1nk4i/api/user/register.go
@@ -0,0 +1,30 @@
+package user
+
+import (
+	"github.com/gin-gonic/gin"
+	"l1nk4i/db"
+	"l1nk4i/utils"
+	"net/http"
+)
+
+func Register(c *gin.Context) {
+	var registerInfo struct {
+		Username string `json:"username"`
+		Password string `json:"password"`
+		Role     string `json:"role"`
+	}
+
+	if err := c.ShouldBindJSON(&registerInfo); err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+	} else {
+		user := db.User{
+			Username: registerInfo.Username,
+			Password: utils.HashPassword(registerInfo.Password),
+			Role:     registerInfo.Role,
+		}
+		if err := db.CreateUser(&user); err != nil {
+			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+		}
+		c.JSON(http.StatusOK, gin.H{"message": "register successful!"})
+	}
+}
diff --git a/l1nk4i/utils/hash.go b/l1nk4i/utils/hash.go
new file mode 100644
index 0000000..b069491
--- /dev/null
+++ b/l1nk4i/utils/hash.go
@@ -0,0 +1,15 @@
+package utils
+
+import (
+	"golang.org/x/crypto/bcrypt"
+)
+
+func HashPassword(password string) string {
+	hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
+	return string(hashedPassword)
+}
+
+func CheckPasswordHash(password, hashedPassword string) bool {
+	err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
+	return err == nil
+}

From df0bd3c95b31f12228af94cd54707b89fd06a06e Mon Sep 17 00:00:00 2001
From: l1nk4i <l1nk@foxmail.com>
Date: Thu, 26 Sep 2024 15:18:25 +0800
Subject: [PATCH 03/14] feat: sql api

---
 l1nk4i/db/answer.go   | 40 ++++++++++++++++++++++++++++++++++++
 l1nk4i/db/mysql.go    | 47 ++++++++++++++++++++++---------------------
 l1nk4i/db/question.go | 40 ++++++++++++++++++++++++++++++++++++
 l1nk4i/db/user.go     | 32 +++++++++++++++++++++++++++++
 4 files changed, 136 insertions(+), 23 deletions(-)
 create mode 100644 l1nk4i/db/answer.go
 create mode 100644 l1nk4i/db/question.go
 create mode 100644 l1nk4i/db/user.go

diff --git a/l1nk4i/db/answer.go b/l1nk4i/db/answer.go
new file mode 100644
index 0000000..731542d
--- /dev/null
+++ b/l1nk4i/db/answer.go
@@ -0,0 +1,40 @@
+package db
+
+import "log"
+
+func CreateAnswer(answer *Answer) error {
+	err := db.Create(answer).Error
+	if err != nil {
+		log.Printf("[ERROR] Create answer error:%s\n", err.Error())
+		return err
+	}
+	return nil
+}
+
+func GetAnswer(uuid string) (*Answer, error) {
+	var answer Answer
+	err := db.Where("uuid = ?", uuid).First(&answer).Error
+	if err != nil {
+		log.Printf("[ERROR] Get answer error:%s\n", err.Error())
+		return nil, err
+	}
+	return &answer, nil
+}
+
+func DeleteAnswer(uuid string) error {
+	err := db.Unscoped().Where("uuid = ?", uuid).Delete(&Answer{}).Error
+	if err != nil {
+		log.Printf("[ERROR] Delete answer error:%s\n", err.Error())
+		return err
+	}
+	return nil
+}
+
+func UpdateAnswer(uuid, content string) error {
+	err := db.Model(&Answer{}).Where("uuid = ?", uuid).Update("content", content).Error
+	if err != nil {
+		log.Printf("[ERROR] Update answer error:%s\n", err.Error())
+		return err
+	}
+	return nil
+}
diff --git a/l1nk4i/db/mysql.go b/l1nk4i/db/mysql.go
index 0667f9a..dc05169 100644
--- a/l1nk4i/db/mysql.go
+++ b/l1nk4i/db/mysql.go
@@ -10,11 +10,31 @@ import (
 
 type User struct {
 	gorm.Model
+	UUID string `gorm:"not null;column:uuid;unique;type:varchar(36)"`
+
 	Username string `gorm:"not null;column:username;unique"`
 	Password string `gorm:"not null;column:password"`
 	Role     string `gorm:"not null;column:role"`
 }
 
+type Question struct {
+	gorm.Model
+	UUID   string `gorm:"not null;column:uuid;unique;type:varchar(36)"`
+	UserId string `gorm:"not null;column:userid;unique"`
+
+	Title   string `gorm:"not null;column:title"`
+	Content string `gorm:"not null;column:content"`
+}
+
+type Answer struct {
+	gorm.Model
+	UUID       string `gorm:"not null;column:uuid;unique;type:varchar(36)"`
+	UserId     string `gorm:"not null;column:userid;unique"`
+	QuestionId string `gorm:"not null;column:questionid;unique"`
+
+	Content string `gorm:"not null;column:content"`
+}
+
 var db *gorm.DB
 
 func init() {
@@ -23,37 +43,18 @@ func init() {
 	host := config.Mysql.Host
 	port := config.Mysql.Port
 	dbname := config.Mysql.Dbname
+	params := "charset=utf8mb4&parseTime=True&loc=Local"
 
-	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", username, password, host, port, dbname)
+	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?%s", username, password, host, port, dbname, params)
 	conn, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
 	if err != nil {
 		panic("Cannot connect to mysql: " + err.Error())
 	}
+	log.Printf("[INFO] Connect to mysql successfully\n")
 
-	if err = conn.AutoMigrate(&User{}); err != nil {
+	if err = conn.AutoMigrate(&User{}, &Question{}, &Answer{}); err != nil {
 		panic("AutoMigrate failed: " + err.Error())
 	}
 
 	db = conn
 }
-
-func CreateUser(user *User) error {
-	err := db.Create(user).Error
-	if err != nil {
-		log.Printf("Create user failed: %s\n", err.Error())
-		return err
-	}
-
-	return nil
-}
-
-func GetUser(username string) (*User, error) {
-	var user User
-	err := db.Where("username = ?", username).First(&user).Error
-	if err != nil {
-		log.Printf("Get user failed: %s\n", err.Error())
-		return nil, err
-	}
-
-	return &user, nil
-}
diff --git a/l1nk4i/db/question.go b/l1nk4i/db/question.go
new file mode 100644
index 0000000..7029086
--- /dev/null
+++ b/l1nk4i/db/question.go
@@ -0,0 +1,40 @@
+package db
+
+import "log"
+
+func CreateQuestion(question *Question) error {
+	err := db.Create(question).Error
+	if err != nil {
+		log.Printf("[ERROR] Create Question failed %s\n", err.Error())
+		return err
+	}
+	return nil
+}
+
+func GetQuestion(uuid string) (*Question, error) {
+	var question Question
+	err := db.Where("uuid = ?", uuid).First(&question).Error
+	if err != nil {
+		log.Printf("[ERROR] Get Question failed %s\n", err.Error())
+		return nil, err
+	}
+	return &question, nil
+}
+
+func DeleteQuestion(uuid string) error {
+	err := db.Unscoped().Where("uuid = ?", uuid).Delete(&Question{}).Error
+	if err != nil {
+		log.Printf("[ERROR] Delete Question failed %s\n", err.Error())
+		return err
+	}
+	return nil
+}
+
+func UpdateQuestion(uuid, title, content string) error {
+	err := db.Model(&Question{}).Where("uuid = ?", uuid).Update("title", title).Update("content", content).Error
+	if err != nil {
+		log.Printf("[ERROR] Update Question failed %s\n", err.Error())
+		return err
+	}
+	return nil
+}
diff --git a/l1nk4i/db/user.go b/l1nk4i/db/user.go
new file mode 100644
index 0000000..e4bd38b
--- /dev/null
+++ b/l1nk4i/db/user.go
@@ -0,0 +1,32 @@
+package db
+
+import "log"
+
+func CreateUser(user *User) error {
+	err := db.Create(user).Error
+	if err != nil {
+		log.Printf("[ERROR] Create user failed: %s\n", err.Error())
+		return err
+	}
+	return nil
+}
+
+func GetUserByUsername(username string) (*User, error) {
+	var user User
+	err := db.Where("username = ?", username).First(&user).Error
+	if err != nil {
+		log.Printf("[ERROR] Get user failed: %s\n", err.Error())
+		return nil, err
+	}
+	return &user, nil
+}
+
+func GetUserByUUID(uuid string) (*User, error) {
+	var user User
+	err := db.Where("uuid = ?", uuid).First(&user).Error
+	if err != nil {
+		log.Printf("[ERROR] Get user failed: %s\n", err.Error())
+		return nil, err
+	}
+	return &user, nil
+}

From 8f1f7e9588e20c3a40fc0de79fb368b2f8049515 Mon Sep 17 00:00:00 2001
From: l1nk4i <l1nk@foxmail.com>
Date: Thu, 26 Sep 2024 15:21:16 +0800
Subject: [PATCH 04/14] add: config

---
 l1nk4i/config/config.go | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)
 create mode 100644 l1nk4i/config/config.go

diff --git a/l1nk4i/config/config.go b/l1nk4i/config/config.go
new file mode 100644
index 0000000..295c611
--- /dev/null
+++ b/l1nk4i/config/config.go
@@ -0,0 +1,24 @@
+package config
+
+import "github.com/pelletier/go-toml"
+
+var Mysql struct {
+	Username string `toml:"username"`
+	Password string `toml:"password"`
+	Host     string `toml:"host"`
+	Port     string `toml:"port"`
+	Dbname   string `toml:"dbname"`
+}
+
+var configFile = "config.toml"
+
+func init() {
+	conf, err := toml.LoadFile(configFile)
+	if err != nil {
+		panic("load config file failed: " + err.Error())
+	}
+
+	if err := conf.Get("mysql").(*toml.Tree).Unmarshal(&Mysql); err != nil {
+		panic("unmarshal config file failed: " + err.Error())
+	}
+}

From 40cba041b945b9b4927ddec9d80b6abdb7bf64f6 Mon Sep 17 00:00:00 2001
From: l1nk4i <l1nk@foxmail.com>
Date: Fri, 27 Sep 2024 23:36:06 +0800
Subject: [PATCH 05/14] feat: db handler

---
 l1nk4i/db/answer.go   | 24 +++++++++++++++++-------
 l1nk4i/db/mysql.go    | 15 +++++++++------
 l1nk4i/db/question.go | 35 ++++++++++++++++++++++++++++-------
 l1nk4i/db/user.go     |  2 +-
 4 files changed, 55 insertions(+), 21 deletions(-)

diff --git a/l1nk4i/db/answer.go b/l1nk4i/db/answer.go
index 731542d..137f428 100644
--- a/l1nk4i/db/answer.go
+++ b/l1nk4i/db/answer.go
@@ -11,18 +11,28 @@ func CreateAnswer(answer *Answer) error {
 	return nil
 }
 
-func GetAnswer(uuid string) (*Answer, error) {
+func GetAnswerByAnswerID(answerID string) (*Answer, error) {
 	var answer Answer
-	err := db.Where("uuid = ?", uuid).First(&answer).Error
+	err := db.Where("answer_id = ?", answerID).First(&answer).Error
 	if err != nil {
-		log.Printf("[ERROR] Get answer error:%s\n", err.Error())
+		log.Printf("[ERROR] Get answer by answerID error:%s\n", err.Error())
 		return nil, err
 	}
 	return &answer, nil
 }
 
-func DeleteAnswer(uuid string) error {
-	err := db.Unscoped().Where("uuid = ?", uuid).Delete(&Answer{}).Error
+func GetAnswersByQuestionID(questionID string) (*[]Answer, error) {
+	var answers []Answer
+	err := db.Where("question_id = ?", questionID).Limit(100).Find(&answers).Error
+	if err != nil {
+		log.Printf("[ERROR] Get answer by questionID error:%s\n", err.Error())
+		return nil, err
+	}
+	return &answers, nil
+}
+
+func DeleteAnswer(answerID string) error {
+	err := db.Unscoped().Where("answer_id = ?", answerID).Delete(&Answer{}).Error
 	if err != nil {
 		log.Printf("[ERROR] Delete answer error:%s\n", err.Error())
 		return err
@@ -30,8 +40,8 @@ func DeleteAnswer(uuid string) error {
 	return nil
 }
 
-func UpdateAnswer(uuid, content string) error {
-	err := db.Model(&Answer{}).Where("uuid = ?", uuid).Update("content", content).Error
+func UpdateAnswer(answerID, content string) error {
+	err := db.Model(&Answer{}).Where("answer_id = ?", answerID).Update("content", content).Error
 	if err != nil {
 		log.Printf("[ERROR] Update answer error:%s\n", err.Error())
 		return err
diff --git a/l1nk4i/db/mysql.go b/l1nk4i/db/mysql.go
index dc05169..e449827 100644
--- a/l1nk4i/db/mysql.go
+++ b/l1nk4i/db/mysql.go
@@ -10,7 +10,7 @@ import (
 
 type User struct {
 	gorm.Model
-	UUID string `gorm:"not null;column:uuid;unique;type:varchar(36)"`
+	UserID string `gorm:"not null;column:user_id;unique;type:varchar(36)"`
 
 	Username string `gorm:"not null;column:username;unique"`
 	Password string `gorm:"not null;column:password"`
@@ -19,8 +19,11 @@ type User struct {
 
 type Question struct {
 	gorm.Model
-	UUID   string `gorm:"not null;column:uuid;unique;type:varchar(36)"`
-	UserId string `gorm:"not null;column:userid;unique"`
+	QuestionID string `gorm:"not null;column:question_id;unique;type:varchar(36)"`
+	UserID     string `gorm:"not null;column:user_id;type:varchar(36)"`
+
+	BestAnswerID string `gorm:"column:best_answer_id;type:varchar(36)"`
+	//IsAccessible bool   `gorm:"not null;column:is_accessible;type:bool;default:false"`
 
 	Title   string `gorm:"not null;column:title"`
 	Content string `gorm:"not null;column:content"`
@@ -28,9 +31,9 @@ type Question struct {
 
 type Answer struct {
 	gorm.Model
-	UUID       string `gorm:"not null;column:uuid;unique;type:varchar(36)"`
-	UserId     string `gorm:"not null;column:userid;unique"`
-	QuestionId string `gorm:"not null;column:questionid;unique"`
+	AnswerID   string `gorm:"not null;column:answer_id;unique;type:varchar(36)"`
+	UserID     string `gorm:"not null;column:user_id;type:varchar(36)"`
+	QuestionID string `gorm:"not null;column:question_id;"`
 
 	Content string `gorm:"not null;column:content"`
 }
diff --git a/l1nk4i/db/question.go b/l1nk4i/db/question.go
index 7029086..e261e93 100644
--- a/l1nk4i/db/question.go
+++ b/l1nk4i/db/question.go
@@ -11,18 +11,28 @@ func CreateQuestion(question *Question) error {
 	return nil
 }
 
-func GetQuestion(uuid string) (*Question, error) {
+func GetQuestionByQuestionID(questionID string) (*Question, error) {
 	var question Question
-	err := db.Where("uuid = ?", uuid).First(&question).Error
+	err := db.Where("question_id = ?", questionID).First(&question).Error
 	if err != nil {
-		log.Printf("[ERROR] Get Question failed %s\n", err.Error())
+		log.Printf("[ERROR] Get Question by question_id failed %s\n", err.Error())
 		return nil, err
 	}
 	return &question, nil
 }
 
-func DeleteQuestion(uuid string) error {
-	err := db.Unscoped().Where("uuid = ?", uuid).Delete(&Question{}).Error
+func GetQuestionByUserID(userID string) (*[]Question, error) {
+	var questions []Question
+	err := db.Where("user_id = ?", userID).Limit(100).Find(&questions).Error
+	if err != nil {
+		log.Printf("[ERROR] Get Questions by user_id failed %s\n", err.Error())
+		return nil, err
+	}
+	return &questions, nil
+}
+
+func DeleteQuestion(questionID string) error {
+	err := db.Unscoped().Where("question_id = ?", questionID).Delete(&Question{}).Error
 	if err != nil {
 		log.Printf("[ERROR] Delete Question failed %s\n", err.Error())
 		return err
@@ -30,11 +40,22 @@ func DeleteQuestion(uuid string) error {
 	return nil
 }
 
-func UpdateQuestion(uuid, title, content string) error {
-	err := db.Model(&Question{}).Where("uuid = ?", uuid).Update("title", title).Update("content", content).Error
+func UpdateQuestion(questionID, title, content string) error {
+	err := db.Model(&Question{}).Where("question_id = ?", questionID).Updates(Question{Title: title, Content: content}).Error
 	if err != nil {
 		log.Printf("[ERROR] Update Question failed %s\n", err.Error())
 		return err
 	}
 	return nil
 }
+
+func SearchQuestions(content string) (*[]Question, error) {
+	var questions []Question
+	searchPattern := "%" + content + "%"
+	err := db.Where("title LIKE ? OR content LIKE ?", searchPattern, searchPattern).Limit(20).Find(&questions).Error
+	if err != nil {
+		log.Printf("[ERROR] Search Questions failed %s\n", err.Error())
+		return nil, err
+	}
+	return &questions, nil
+}
diff --git a/l1nk4i/db/user.go b/l1nk4i/db/user.go
index e4bd38b..ddc4ed5 100644
--- a/l1nk4i/db/user.go
+++ b/l1nk4i/db/user.go
@@ -23,7 +23,7 @@ func GetUserByUsername(username string) (*User, error) {
 
 func GetUserByUUID(uuid string) (*User, error) {
 	var user User
-	err := db.Where("uuid = ?", uuid).First(&user).Error
+	err := db.Where("user_id = ?", uuid).First(&user).Error
 	if err != nil {
 		log.Printf("[ERROR] Get user failed: %s\n", err.Error())
 		return nil, err

From 298c7adc00b6aa917c885d1102ddaa1f02a2cc43 Mon Sep 17 00:00:00 2001
From: l1nk4i <l1nk@foxmail.com>
Date: Fri, 27 Sep 2024 23:37:08 +0800
Subject: [PATCH 06/14] add: user api

---
 l1nk4i/api/user/login.go    | 38 +++++++++++++++++++++++++-------
 l1nk4i/api/user/logout.go   | 16 ++++++++++++++
 l1nk4i/api/user/register.go | 43 ++++++++++++++++++++++++++-----------
 l1nk4i/api/user/userinfo.go | 36 +++++++++++++++++++++++++++++++
 l1nk4i/api/user/utils.go    | 25 +++++++++++++++++++++
 5 files changed, 137 insertions(+), 21 deletions(-)
 create mode 100644 l1nk4i/api/user/logout.go
 create mode 100644 l1nk4i/api/user/userinfo.go
 create mode 100644 l1nk4i/api/user/utils.go

diff --git a/l1nk4i/api/user/login.go b/l1nk4i/api/user/login.go
index 4cbb91b..cbe3bff 100644
--- a/l1nk4i/api/user/login.go
+++ b/l1nk4i/api/user/login.go
@@ -1,6 +1,7 @@
 package user
 
 import (
+	"github.com/gin-contrib/sessions"
 	"github.com/gin-gonic/gin"
 	"l1nk4i/db"
 	"l1nk4i/utils"
@@ -14,16 +15,37 @@ func Login(c *gin.Context) {
 	}
 
 	if err := c.ShouldBind(&loginInfo); err != nil {
-		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
+		return
+	}
+
+	if !validateUsername(loginInfo.Username) {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid Username"})
+		return
+	}
+
+	if !validatePassword(loginInfo.Password) {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid Password"})
+		return
+	}
+
+	if user, err := db.GetUserByUsername(loginInfo.Username); err != nil {
+		c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid Username"})
+		return
 	} else {
-		if user, err := db.GetUser(loginInfo.Username); err != nil {
-			c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid username"})
+		if utils.CheckPasswordHash(loginInfo.Password, user.Password) {
+			session := sessions.Default(c)
+			session.Clear()
+			session.Set("user_id", user.UserID)
+			session.Set("role", user.Role)
+			session.Save()
+
+			c.JSON(http.StatusOK, gin.H{"msg": "login successful!"})
+			return
 		} else {
-			if utils.CheckPasswordHash(loginInfo.Password, user.Password) {
-				c.JSON(http.StatusOK, gin.H{"message": "login successful!"})
-			} else {
-				c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid password"})
-			}
+			c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid Password"})
+			return
 		}
 	}
+
 }
diff --git a/l1nk4i/api/user/logout.go b/l1nk4i/api/user/logout.go
new file mode 100644
index 0000000..058eabf
--- /dev/null
+++ b/l1nk4i/api/user/logout.go
@@ -0,0 +1,16 @@
+package user
+
+import (
+	"github.com/gin-contrib/sessions"
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func Logout(c *gin.Context) {
+	session := sessions.Default(c)
+	session.Clear()
+	session.Set("role", "guest")
+	session.Save()
+	c.JSON(http.StatusOK, gin.H{"msg": "Logout successful!"})
+	return
+}
diff --git a/l1nk4i/api/user/register.go b/l1nk4i/api/user/register.go
index dbdb2f8..261dc59 100644
--- a/l1nk4i/api/user/register.go
+++ b/l1nk4i/api/user/register.go
@@ -1,7 +1,9 @@
 package user
 
 import (
+	"fmt"
 	"github.com/gin-gonic/gin"
+	"github.com/google/uuid"
 	"l1nk4i/db"
 	"l1nk4i/utils"
 	"net/http"
@@ -11,20 +13,35 @@ func Register(c *gin.Context) {
 	var registerInfo struct {
 		Username string `json:"username"`
 		Password string `json:"password"`
-		Role     string `json:"role"`
 	}
-
+	fmt.Println(registerInfo)
 	if err := c.ShouldBindJSON(&registerInfo); err != nil {
-		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
-	} else {
-		user := db.User{
-			Username: registerInfo.Username,
-			Password: utils.HashPassword(registerInfo.Password),
-			Role:     registerInfo.Role,
-		}
-		if err := db.CreateUser(&user); err != nil {
-			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
-		}
-		c.JSON(http.StatusOK, gin.H{"message": "register successful!"})
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
+		return
+	}
+
+	fmt.Println(registerInfo)
+
+	if !validateUsername(registerInfo.Username) {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid Username"})
+		return
+	}
+
+	if !validatePassword(registerInfo.Password) {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid Password"})
+		return
+	}
+
+	user := db.User{
+		UserID:   uuid.New().String(),
+		Username: registerInfo.Username,
+		Password: utils.HashPassword(registerInfo.Password),
+		Role:     "user",
 	}
+	if err := db.CreateUser(&user); err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": "Create user error"})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{"msg": "register successful!"})
 }
diff --git a/l1nk4i/api/user/userinfo.go b/l1nk4i/api/user/userinfo.go
new file mode 100644
index 0000000..a277afa
--- /dev/null
+++ b/l1nk4i/api/user/userinfo.go
@@ -0,0 +1,36 @@
+package user
+
+import (
+	"github.com/gin-contrib/sessions"
+	"github.com/gin-gonic/gin"
+	"github.com/google/uuid"
+	"l1nk4i/db"
+	"net/http"
+)
+
+// UserInfo get Username by session
+func UserInfo(c *gin.Context) {
+	session := sessions.Default(c)
+	userid, exists := session.Get("user_id").(string)
+	if !exists {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid session"})
+		return
+	}
+
+	if _, err := uuid.Parse(userid); err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid session"})
+		return
+	}
+
+	user, err := db.GetUserByUUID(userid)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "user not found"})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"user_id":  user.UserID,
+		"username": user.Username,
+		"role":     user.Role,
+	})
+}
diff --git a/l1nk4i/api/user/utils.go b/l1nk4i/api/user/utils.go
new file mode 100644
index 0000000..5c7fb0a
--- /dev/null
+++ b/l1nk4i/api/user/utils.go
@@ -0,0 +1,25 @@
+package user
+
+import "regexp"
+
+// 2 <= length <= 20
+// ^[a-zA-Z0-9]+$
+func validateUsername(username string) bool {
+	if len(username) < 2 || len(username) > 20 {
+		return false
+	}
+
+	re := regexp.MustCompile(`^[a-zA-Z0-9]+$`)
+	return re.MatchString(username)
+}
+
+// 8 <= length <= 30
+// ^[a-zA-Z0-9\W_]+$
+func validatePassword(password string) bool {
+	if len(password) < 8 || len(password) > 30 {
+		return false
+	}
+
+	re := regexp.MustCompile(`^[a-zA-Z0-9\W_]+$`)
+	return re.MatchString(password)
+}

From 7df0c6265ddd497c95a22275ff846d2570da2381 Mon Sep 17 00:00:00 2001
From: l1nk4i <l1nk@foxmail.com>
Date: Sat, 28 Sep 2024 00:09:44 +0800
Subject: [PATCH 07/14] add: question api

---
 l1nk4i/api/question/create.go | 42 +++++++++++++++++++++
 l1nk4i/api/question/delete.go | 70 +++++++++++++++++++++++++++++++++++
 l1nk4i/api/question/get.go    | 27 ++++++++++++++
 l1nk4i/api/question/list.go   | 31 ++++++++++++++++
 l1nk4i/api/question/search.go | 24 ++++++++++++
 l1nk4i/api/question/update.go | 49 ++++++++++++++++++++++++
 6 files changed, 243 insertions(+)
 create mode 100644 l1nk4i/api/question/create.go
 create mode 100644 l1nk4i/api/question/delete.go
 create mode 100644 l1nk4i/api/question/get.go
 create mode 100644 l1nk4i/api/question/list.go
 create mode 100644 l1nk4i/api/question/search.go
 create mode 100644 l1nk4i/api/question/update.go

diff --git a/l1nk4i/api/question/create.go b/l1nk4i/api/question/create.go
new file mode 100644
index 0000000..6cbbbbe
--- /dev/null
+++ b/l1nk4i/api/question/create.go
@@ -0,0 +1,42 @@
+package question
+
+import (
+	"github.com/gin-contrib/sessions"
+	"github.com/gin-gonic/gin"
+	"github.com/google/uuid"
+	"l1nk4i/db"
+	"net/http"
+)
+
+func Create(c *gin.Context) {
+	var questionInfo struct {
+		Title   string `json:"title"`
+		Content string `json:"content"`
+	}
+
+	if err := c.ShouldBind(&questionInfo); err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
+		return
+	}
+
+	session := sessions.Default(c)
+	userID, exists := session.Get("user_id").(string)
+	if !exists {
+		c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid session"})
+		return
+	}
+
+	question := db.Question{
+		QuestionID: uuid.New().String(),
+		UserID:     userID,
+		Title:      questionInfo.Title,
+		Content:    questionInfo.Content,
+	}
+	if err := db.CreateQuestion(&question); err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": "create question error"})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{"question_id": question.QuestionID})
+	return
+}
diff --git a/l1nk4i/api/question/delete.go b/l1nk4i/api/question/delete.go
new file mode 100644
index 0000000..d68382a
--- /dev/null
+++ b/l1nk4i/api/question/delete.go
@@ -0,0 +1,70 @@
+package question
+
+import (
+	"github.com/gin-contrib/sessions"
+	"github.com/gin-gonic/gin"
+	"l1nk4i/db"
+	"net/http"
+)
+
+func Delete(c *gin.Context) {
+	var questionInfo struct {
+		QuestionID string `json:"question_id"`
+	}
+
+	if err := c.ShouldBind(&questionInfo); err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
+		return
+	}
+
+	// Verify user identity
+	session := sessions.Default(c)
+	userid, exists := session.Get("user_id").(string)
+	if !exists {
+		c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid session"})
+		return
+	}
+
+	question, err := db.GetQuestionByQuestionID(questionInfo.QuestionID)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid question_id"})
+		return
+	}
+
+	if question.UserID != userid {
+		c.JSON(http.StatusForbidden, gin.H{"error": "permission denied"})
+		return
+	}
+
+	// Delete question
+	err = db.DeleteQuestion(questionInfo.QuestionID)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": "delete question error"})
+		return
+	}
+
+	// Delete answers to the question
+	err = deleteAnswers(questionInfo.QuestionID)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": "delete answers error"})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{"msg": "delete question successful!"})
+}
+
+func deleteAnswers(questionID string) error {
+	answers, err := db.GetAnswersByQuestionID(questionID)
+	if err != nil {
+		return err
+	}
+
+	for _, answer := range *answers {
+		err = db.DeleteAnswer(answer.AnswerID)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/l1nk4i/api/question/get.go b/l1nk4i/api/question/get.go
new file mode 100644
index 0000000..1b6c453
--- /dev/null
+++ b/l1nk4i/api/question/get.go
@@ -0,0 +1,27 @@
+package question
+
+import (
+	"github.com/gin-gonic/gin"
+	"l1nk4i/db"
+	"net/http"
+)
+
+// Get gets question by question_id
+func Get(c *gin.Context) {
+	var questionInfo struct {
+		QuestionID string `json:"question_id"`
+	}
+
+	if err := c.ShouldBind(&questionInfo); err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
+		return
+	}
+
+	question, err := db.GetQuestionByQuestionID(questionInfo.QuestionID)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid question_id"})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{"data": question})
+}
diff --git a/l1nk4i/api/question/list.go b/l1nk4i/api/question/list.go
new file mode 100644
index 0000000..43d8468
--- /dev/null
+++ b/l1nk4i/api/question/list.go
@@ -0,0 +1,31 @@
+package question
+
+import (
+	"github.com/gin-contrib/sessions"
+	"github.com/gin-gonic/gin"
+	"l1nk4i/db"
+	"net/http"
+)
+
+// List lists user's all question_id
+func List(c *gin.Context) {
+	session := sessions.Default(c)
+	userID, exists := session.Get("user_id").(string)
+	if !exists {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid session"})
+		return
+	}
+
+	questions, err := db.GetQuestionByUserID(userID)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid session"})
+		return
+	}
+
+	var questionIDs []string
+	for _, question := range *questions {
+		questionIDs = append(questionIDs, question.QuestionID)
+	}
+
+	c.JSON(http.StatusOK, gin.H{"question_id": questionIDs})
+}
diff --git a/l1nk4i/api/question/search.go b/l1nk4i/api/question/search.go
new file mode 100644
index 0000000..d93b4ed
--- /dev/null
+++ b/l1nk4i/api/question/search.go
@@ -0,0 +1,24 @@
+package question
+
+import (
+	"github.com/gin-gonic/gin"
+	"l1nk4i/db"
+	"net/http"
+)
+
+func Search(c *gin.Context) {
+	var searchInfo struct {
+		Content string `json:"content"`
+	}
+
+	if err := c.ShouldBind(&searchInfo); err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
+	}
+
+	questions, err := db.SearchQuestions(searchInfo.Content)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid content"})
+	}
+
+	c.JSON(http.StatusOK, gin.H{"data": questions})
+}
diff --git a/l1nk4i/api/question/update.go b/l1nk4i/api/question/update.go
new file mode 100644
index 0000000..ba21433
--- /dev/null
+++ b/l1nk4i/api/question/update.go
@@ -0,0 +1,49 @@
+package question
+
+import (
+	"github.com/gin-contrib/sessions"
+	"github.com/gin-gonic/gin"
+	"l1nk4i/db"
+	"net/http"
+)
+
+func Update(c *gin.Context) {
+	var questionInfo struct {
+		QuestionID string `json:"question_id"`
+		Title      string `json:"title"`
+		Content    string `json:"content"`
+	}
+
+	if err := c.ShouldBind(&questionInfo); err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
+		return
+	}
+
+	// Verify user identity
+	session := sessions.Default(c)
+	userid, exists := session.Get("user_id").(string)
+	if !exists {
+		c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid session"})
+		return
+	}
+
+	question, err := db.GetQuestionByQuestionID(questionInfo.QuestionID)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid question_id"})
+		return
+	}
+
+	if question.UserID != userid {
+		c.JSON(http.StatusForbidden, gin.H{"error": "permission denied"})
+		return
+	}
+
+	// Update question
+	err = db.UpdateQuestion(questionInfo.QuestionID, questionInfo.Title, questionInfo.Content)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": "update question error"})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{"msg": "update question successful!"})
+}

From 6f478b92b8a955c2b6375f5cb824bf972c5de414 Mon Sep 17 00:00:00 2001
From: l1nk4i <l1nk@foxmail.com>
Date: Sat, 28 Sep 2024 00:28:33 +0800
Subject: [PATCH 08/14] add: answer api

---
 l1nk4i/api/answer/create.go | 49 +++++++++++++++++++++++++++++++++++++
 l1nk4i/api/answer/delete.go | 47 +++++++++++++++++++++++++++++++++++
 l1nk4i/api/answer/get.go    | 27 ++++++++++++++++++++
 l1nk4i/api/answer/update.go | 48 ++++++++++++++++++++++++++++++++++++
 4 files changed, 171 insertions(+)
 create mode 100644 l1nk4i/api/answer/create.go
 create mode 100644 l1nk4i/api/answer/delete.go
 create mode 100644 l1nk4i/api/answer/get.go
 create mode 100644 l1nk4i/api/answer/update.go

diff --git a/l1nk4i/api/answer/create.go b/l1nk4i/api/answer/create.go
new file mode 100644
index 0000000..59cd827
--- /dev/null
+++ b/l1nk4i/api/answer/create.go
@@ -0,0 +1,49 @@
+package answer
+
+import (
+	"github.com/gin-contrib/sessions"
+	"github.com/gin-gonic/gin"
+	"github.com/google/uuid"
+	"l1nk4i/db"
+	"net/http"
+)
+
+func Create(c *gin.Context) {
+	var answerInfo struct {
+		QuestionID string `json:"question_id"`
+		Content    string `json:"content"`
+	}
+
+	if err := c.ShouldBind(&answerInfo); err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
+		return
+	}
+
+	session := sessions.Default(c)
+	userID, exists := session.Get("user_id").(string)
+	if !exists {
+		c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid session"})
+		return
+	}
+
+	// Verify that the question exists
+	_, err := db.GetQuestionByQuestionID(answerInfo.QuestionID)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid question_id"})
+		return
+	}
+
+	answer := db.Answer{
+		AnswerID:   uuid.New().String(),
+		UserID:     userID,
+		QuestionID: answerInfo.QuestionID,
+		Content:    answerInfo.Content,
+	}
+	if err := db.CreateAnswer(&answer); err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": "create answer error"})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{"answer_id": answer.AnswerID})
+	return
+}
diff --git a/l1nk4i/api/answer/delete.go b/l1nk4i/api/answer/delete.go
new file mode 100644
index 0000000..0b1c803
--- /dev/null
+++ b/l1nk4i/api/answer/delete.go
@@ -0,0 +1,47 @@
+package answer
+
+import (
+	"github.com/gin-contrib/sessions"
+	"github.com/gin-gonic/gin"
+	"l1nk4i/db"
+	"net/http"
+)
+
+func Delete(c *gin.Context) {
+	var answerInfo struct {
+		AnswerID string `json:"answer_id"`
+	}
+
+	if err := c.ShouldBindJSON(&answerInfo); err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
+		return
+	}
+
+	// Verify user identity
+	session := sessions.Default(c)
+	userid, exists := session.Get("user_id").(string)
+	if !exists {
+		c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid session"})
+		return
+	}
+
+	answer, err := db.GetAnswerByAnswerID(answerInfo.AnswerID)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid answer_id"})
+		return
+	}
+
+	if answer.UserID != userid {
+		c.JSON(http.StatusForbidden, gin.H{"error": "permission denied"})
+		return
+	}
+
+	// Delete answer
+	err = db.DeleteAnswer(answerInfo.AnswerID)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": "delete answer error"})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{"msg": "delete answer successful!"})
+}
diff --git a/l1nk4i/api/answer/get.go b/l1nk4i/api/answer/get.go
new file mode 100644
index 0000000..fa49e77
--- /dev/null
+++ b/l1nk4i/api/answer/get.go
@@ -0,0 +1,27 @@
+package answer
+
+import (
+	"github.com/gin-gonic/gin"
+	"l1nk4i/db"
+	"net/http"
+)
+
+// Get gets answers to the question by question_id
+func Get(c *gin.Context) {
+	var QuestionInfo struct {
+		QuestionId string `json:"question_id"`
+	}
+
+	if err := c.ShouldBind(&QuestionInfo); err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
+		return
+	}
+
+	answers, err := db.GetAnswersByQuestionID(QuestionInfo.QuestionId)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid question_id"})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{"data": answers})
+}
diff --git a/l1nk4i/api/answer/update.go b/l1nk4i/api/answer/update.go
new file mode 100644
index 0000000..11b59f5
--- /dev/null
+++ b/l1nk4i/api/answer/update.go
@@ -0,0 +1,48 @@
+package answer
+
+import (
+	"github.com/gin-contrib/sessions"
+	"github.com/gin-gonic/gin"
+	"l1nk4i/db"
+	"net/http"
+)
+
+func Update(c *gin.Context) {
+	var answerInfo struct {
+		AnswerID string `json:"answer_id"`
+		Content  string `json:"content"`
+	}
+
+	if err := c.ShouldBindJSON(&answerInfo); err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
+		return
+	}
+
+	// Verify user identity
+	session := sessions.Default(c)
+	userid, exists := session.Get("user_id").(string)
+	if !exists {
+		c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid session"})
+		return
+	}
+
+	answer, err := db.GetAnswerByAnswerID(answerInfo.AnswerID)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid answer_id"})
+		return
+	}
+
+	if answer.UserID != userid {
+		c.JSON(http.StatusForbidden, gin.H{"error": "permission denied"})
+		return
+	}
+
+	// Update answer
+	err = db.UpdateAnswer(answerInfo.AnswerID, answerInfo.Content)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid answer"})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{"msg": "update answer successful!"})
+}

From 9a75317ad848689030aa1fb269f567c5031545d6 Mon Sep 17 00:00:00 2001
From: l1nk4i <l1nk@foxmail.com>
Date: Sat, 28 Sep 2024 00:30:42 +0800
Subject: [PATCH 09/14] add: backend router

---
 l1nk4i/main.go          |  7 ++++++
 l1nk4i/router/router.go | 50 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 57 insertions(+)
 create mode 100644 l1nk4i/main.go
 create mode 100644 l1nk4i/router/router.go

diff --git a/l1nk4i/main.go b/l1nk4i/main.go
new file mode 100644
index 0000000..e03e837
--- /dev/null
+++ b/l1nk4i/main.go
@@ -0,0 +1,7 @@
+package main
+
+import "l1nk4i/router"
+
+func main() {
+	router.Run()
+}
diff --git a/l1nk4i/router/router.go b/l1nk4i/router/router.go
new file mode 100644
index 0000000..849135c
--- /dev/null
+++ b/l1nk4i/router/router.go
@@ -0,0 +1,50 @@
+package router
+
+import (
+	"crypto/rand"
+	"github.com/gin-contrib/sessions"
+	"github.com/gin-contrib/sessions/cookie"
+	"github.com/gin-gonic/gin"
+	"l1nk4i/api/answer"
+	"l1nk4i/api/question"
+	"l1nk4i/api/user"
+)
+
+func Run() {
+	r := gin.Default()
+
+	secret := make([]byte, 32)
+	_, _ = rand.Read(secret)
+	store := cookie.NewStore(secret)
+	r.Use(sessions.Sessions("session", store))
+
+	apiGroup := r.Group("/api")
+	{
+		userApiGroup := apiGroup.Group("/user")
+		{
+			userApiGroup.POST("/login", user.Login)
+			userApiGroup.POST("/register", user.Register)
+			userApiGroup.GET("/logout", user.Logout)
+			userApiGroup.GET("/userinfo", user.UserInfo)
+		}
+		questionApiGroup := apiGroup.Group("/question")
+		{
+			questionApiGroup.POST("/create", question.Create)
+			questionApiGroup.POST("/delete", question.Delete)
+			questionApiGroup.POST("/update", question.Update)
+			questionApiGroup.POST("/get", question.Get)
+
+			questionApiGroup.POST("/list", question.List)
+			questionApiGroup.POST("/search", question.Search)
+		}
+		answerApiGroup := apiGroup.Group("/answer")
+		{
+			answerApiGroup.POST("/create", answer.Create)
+			answerApiGroup.POST("/delete", answer.Delete)
+			answerApiGroup.POST("/update", answer.Update)
+			answerApiGroup.POST("/get", answer.Get)
+		}
+	}
+
+	r.Run(":8080")
+}

From 2fc97d85b201ca198740ba93287b6791f7e0a3ee Mon Sep 17 00:00:00 2001
From: l1nk4i <l1nk@foxmail.com>
Date: Wed, 2 Oct 2024 14:43:47 +0800
Subject: [PATCH 10/14] refactor: fit RESTful

---
 .gitignore                    |   4 +-
 l1nk4i/api/answer/create.go   |   9 +--
 l1nk4i/api/answer/delete.go   |  13 +---
 l1nk4i/api/answer/get.go      |  11 +---
 l1nk4i/api/answer/update.go   |   9 +--
 l1nk4i/api/question/delete.go |  15 ++---
 l1nk4i/api/question/get.go    |  11 +---
 l1nk4i/api/question/search.go |  10 +--
 l1nk4i/api/question/update.go |  11 ++--
 l1nk4i/go.mod                 |  48 ++++++++++++++
 l1nk4i/go.sum                 | 114 ++++++++++++++++++++++++++++++++++
 l1nk4i/router/router.go       |  29 +++++----
 12 files changed, 208 insertions(+), 76 deletions(-)
 create mode 100644 l1nk4i/go.mod
 create mode 100644 l1nk4i/go.sum

diff --git a/.gitignore b/.gitignore
index 723ef36..ae234af 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
-.idea
\ No newline at end of file
+.idea
+l1nk4i/config.toml
+.gitignore
\ No newline at end of file
diff --git a/l1nk4i/api/answer/create.go b/l1nk4i/api/answer/create.go
index 59cd827..a8590fe 100644
--- a/l1nk4i/api/answer/create.go
+++ b/l1nk4i/api/answer/create.go
@@ -9,9 +9,10 @@ import (
 )
 
 func Create(c *gin.Context) {
+	questionID := c.Param("question-id")
+
 	var answerInfo struct {
-		QuestionID string `json:"question_id"`
-		Content    string `json:"content"`
+		Content string `json:"content"`
 	}
 
 	if err := c.ShouldBind(&answerInfo); err != nil {
@@ -27,7 +28,7 @@ func Create(c *gin.Context) {
 	}
 
 	// Verify that the question exists
-	_, err := db.GetQuestionByQuestionID(answerInfo.QuestionID)
+	_, err := db.GetQuestionByQuestionID(questionID)
 	if err != nil {
 		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid question_id"})
 		return
@@ -36,7 +37,7 @@ func Create(c *gin.Context) {
 	answer := db.Answer{
 		AnswerID:   uuid.New().String(),
 		UserID:     userID,
-		QuestionID: answerInfo.QuestionID,
+		QuestionID: questionID,
 		Content:    answerInfo.Content,
 	}
 	if err := db.CreateAnswer(&answer); err != nil {
diff --git a/l1nk4i/api/answer/delete.go b/l1nk4i/api/answer/delete.go
index 0b1c803..77bb250 100644
--- a/l1nk4i/api/answer/delete.go
+++ b/l1nk4i/api/answer/delete.go
@@ -8,14 +8,7 @@ import (
 )
 
 func Delete(c *gin.Context) {
-	var answerInfo struct {
-		AnswerID string `json:"answer_id"`
-	}
-
-	if err := c.ShouldBindJSON(&answerInfo); err != nil {
-		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
-		return
-	}
+	answerID := c.Param("answer-id")
 
 	// Verify user identity
 	session := sessions.Default(c)
@@ -25,7 +18,7 @@ func Delete(c *gin.Context) {
 		return
 	}
 
-	answer, err := db.GetAnswerByAnswerID(answerInfo.AnswerID)
+	answer, err := db.GetAnswerByAnswerID(answerID)
 	if err != nil {
 		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid answer_id"})
 		return
@@ -37,7 +30,7 @@ func Delete(c *gin.Context) {
 	}
 
 	// Delete answer
-	err = db.DeleteAnswer(answerInfo.AnswerID)
+	err = db.DeleteAnswer(answerID)
 	if err != nil {
 		c.JSON(http.StatusInternalServerError, gin.H{"error": "delete answer error"})
 		return
diff --git a/l1nk4i/api/answer/get.go b/l1nk4i/api/answer/get.go
index fa49e77..ede658c 100644
--- a/l1nk4i/api/answer/get.go
+++ b/l1nk4i/api/answer/get.go
@@ -8,16 +8,9 @@ import (
 
 // Get gets answers to the question by question_id
 func Get(c *gin.Context) {
-	var QuestionInfo struct {
-		QuestionId string `json:"question_id"`
-	}
-
-	if err := c.ShouldBind(&QuestionInfo); err != nil {
-		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
-		return
-	}
+	questionID := c.Param("question-id")
 
-	answers, err := db.GetAnswersByQuestionID(QuestionInfo.QuestionId)
+	answers, err := db.GetAnswersByQuestionID(questionID)
 	if err != nil {
 		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid question_id"})
 		return
diff --git a/l1nk4i/api/answer/update.go b/l1nk4i/api/answer/update.go
index 11b59f5..48b4fd6 100644
--- a/l1nk4i/api/answer/update.go
+++ b/l1nk4i/api/answer/update.go
@@ -8,9 +8,10 @@ import (
 )
 
 func Update(c *gin.Context) {
+	answerID := c.Param("answer-id")
+
 	var answerInfo struct {
-		AnswerID string `json:"answer_id"`
-		Content  string `json:"content"`
+		Content string `json:"content"`
 	}
 
 	if err := c.ShouldBindJSON(&answerInfo); err != nil {
@@ -26,7 +27,7 @@ func Update(c *gin.Context) {
 		return
 	}
 
-	answer, err := db.GetAnswerByAnswerID(answerInfo.AnswerID)
+	answer, err := db.GetAnswerByAnswerID(answerID)
 	if err != nil {
 		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid answer_id"})
 		return
@@ -38,7 +39,7 @@ func Update(c *gin.Context) {
 	}
 
 	// Update answer
-	err = db.UpdateAnswer(answerInfo.AnswerID, answerInfo.Content)
+	err = db.UpdateAnswer(answerID, answerInfo.Content)
 	if err != nil {
 		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid answer"})
 		return
diff --git a/l1nk4i/api/question/delete.go b/l1nk4i/api/question/delete.go
index d68382a..8f84903 100644
--- a/l1nk4i/api/question/delete.go
+++ b/l1nk4i/api/question/delete.go
@@ -8,14 +8,7 @@ import (
 )
 
 func Delete(c *gin.Context) {
-	var questionInfo struct {
-		QuestionID string `json:"question_id"`
-	}
-
-	if err := c.ShouldBind(&questionInfo); err != nil {
-		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
-		return
-	}
+	questionID := c.Param("question-id")
 
 	// Verify user identity
 	session := sessions.Default(c)
@@ -25,7 +18,7 @@ func Delete(c *gin.Context) {
 		return
 	}
 
-	question, err := db.GetQuestionByQuestionID(questionInfo.QuestionID)
+	question, err := db.GetQuestionByQuestionID(questionID)
 	if err != nil {
 		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid question_id"})
 		return
@@ -37,14 +30,14 @@ func Delete(c *gin.Context) {
 	}
 
 	// Delete question
-	err = db.DeleteQuestion(questionInfo.QuestionID)
+	err = db.DeleteQuestion(questionID)
 	if err != nil {
 		c.JSON(http.StatusInternalServerError, gin.H{"error": "delete question error"})
 		return
 	}
 
 	// Delete answers to the question
-	err = deleteAnswers(questionInfo.QuestionID)
+	err = deleteAnswers(questionID)
 	if err != nil {
 		c.JSON(http.StatusInternalServerError, gin.H{"error": "delete answers error"})
 		return
diff --git a/l1nk4i/api/question/get.go b/l1nk4i/api/question/get.go
index 1b6c453..f372bd4 100644
--- a/l1nk4i/api/question/get.go
+++ b/l1nk4i/api/question/get.go
@@ -8,16 +8,9 @@ import (
 
 // Get gets question by question_id
 func Get(c *gin.Context) {
-	var questionInfo struct {
-		QuestionID string `json:"question_id"`
-	}
-
-	if err := c.ShouldBind(&questionInfo); err != nil {
-		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
-		return
-	}
+	questionID := c.Param("question-id")
 
-	question, err := db.GetQuestionByQuestionID(questionInfo.QuestionID)
+	question, err := db.GetQuestionByQuestionID(questionID)
 	if err != nil {
 		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid question_id"})
 		return
diff --git a/l1nk4i/api/question/search.go b/l1nk4i/api/question/search.go
index d93b4ed..0af64bf 100644
--- a/l1nk4i/api/question/search.go
+++ b/l1nk4i/api/question/search.go
@@ -7,15 +7,9 @@ import (
 )
 
 func Search(c *gin.Context) {
-	var searchInfo struct {
-		Content string `json:"content"`
-	}
-
-	if err := c.ShouldBind(&searchInfo); err != nil {
-		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
-	}
+	searchContent := c.Query("content")
 
-	questions, err := db.SearchQuestions(searchInfo.Content)
+	questions, err := db.SearchQuestions(searchContent)
 	if err != nil {
 		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid content"})
 	}
diff --git a/l1nk4i/api/question/update.go b/l1nk4i/api/question/update.go
index ba21433..5bc6d71 100644
--- a/l1nk4i/api/question/update.go
+++ b/l1nk4i/api/question/update.go
@@ -8,10 +8,11 @@ import (
 )
 
 func Update(c *gin.Context) {
+	questionID := c.Param("question-id")
+
 	var questionInfo struct {
-		QuestionID string `json:"question_id"`
-		Title      string `json:"title"`
-		Content    string `json:"content"`
+		Title   string `json:"title"`
+		Content string `json:"content"`
 	}
 
 	if err := c.ShouldBind(&questionInfo); err != nil {
@@ -27,7 +28,7 @@ func Update(c *gin.Context) {
 		return
 	}
 
-	question, err := db.GetQuestionByQuestionID(questionInfo.QuestionID)
+	question, err := db.GetQuestionByQuestionID(questionID)
 	if err != nil {
 		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid question_id"})
 		return
@@ -39,7 +40,7 @@ func Update(c *gin.Context) {
 	}
 
 	// Update question
-	err = db.UpdateQuestion(questionInfo.QuestionID, questionInfo.Title, questionInfo.Content)
+	err = db.UpdateQuestion(questionID, questionInfo.Title, questionInfo.Content)
 	if err != nil {
 		c.JSON(http.StatusInternalServerError, gin.H{"error": "update question error"})
 		return
diff --git a/l1nk4i/go.mod b/l1nk4i/go.mod
new file mode 100644
index 0000000..ceedce0
--- /dev/null
+++ b/l1nk4i/go.mod
@@ -0,0 +1,48 @@
+module l1nk4i
+
+go 1.23.1
+
+require (
+	github.com/gin-contrib/sessions v1.0.1
+	github.com/gin-gonic/gin v1.10.0
+	golang.org/x/crypto v0.27.0
+	gorm.io/driver/mysql v1.5.7
+	gorm.io/gorm v1.25.12
+)
+
+require (
+	filippo.io/edwards25519 v1.1.0 // indirect
+	github.com/bytedance/sonic v1.12.3 // indirect
+	github.com/bytedance/sonic/loader v0.2.0 // indirect
+	github.com/cloudwego/base64x v0.1.4 // indirect
+	github.com/cloudwego/iasm v0.2.0 // indirect
+	github.com/gabriel-vasile/mimetype v1.4.5 // indirect
+	github.com/gin-contrib/sse v0.1.0 // indirect
+	github.com/go-playground/locales v0.14.1 // indirect
+	github.com/go-playground/universal-translator v0.18.1 // indirect
+	github.com/go-playground/validator/v10 v10.22.1 // indirect
+	github.com/go-sql-driver/mysql v1.8.1 // indirect
+	github.com/goccy/go-json v0.10.3 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/gorilla/context v1.1.2 // indirect
+	github.com/gorilla/securecookie v1.1.2 // indirect
+	github.com/gorilla/sessions v1.2.2 // indirect
+	github.com/jinzhu/inflection v1.0.0 // indirect
+	github.com/jinzhu/now v1.1.5 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.8 // indirect
+	github.com/leodido/go-urn v1.4.0 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/pelletier/go-toml v1.9.5 // indirect
+	github.com/pelletier/go-toml/v2 v2.2.3 // indirect
+	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+	github.com/ugorji/go/codec v1.2.12 // indirect
+	golang.org/x/arch v0.10.0 // indirect
+	golang.org/x/net v0.29.0 // indirect
+	golang.org/x/sys v0.25.0 // indirect
+	golang.org/x/text v0.18.0 // indirect
+	google.golang.org/protobuf v1.34.2 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/l1nk4i/go.sum b/l1nk4i/go.sum
new file mode 100644
index 0000000..66a1deb
--- /dev/null
+++ b/l1nk4i/go.sum
@@ -0,0 +1,114 @@
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU=
+github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
+github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
+github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
+github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
+github.com/gin-contrib/sessions v1.0.1 h1:3hsJyNs7v7N8OtelFmYXFrulAf6zSR7nW/putcPEHxI=
+github.com/gin-contrib/sessions v1.0.1/go.mod h1:ouxSFM24/OgIud5MJYQJLpy6AwxQ5EYO9yLhbtObGkM=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
+github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
+github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
+github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
+github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
+github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
+github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
+github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
+github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
+github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
+github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
+github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
+github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
+github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
+github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
+github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8=
+golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
+golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
+golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
+golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
+golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
+golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
+golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
+google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
+gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
+gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
+gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
diff --git a/l1nk4i/router/router.go b/l1nk4i/router/router.go
index 849135c..100ac7f 100644
--- a/l1nk4i/router/router.go
+++ b/l1nk4i/router/router.go
@@ -20,29 +20,28 @@ func Run() {
 
 	apiGroup := r.Group("/api")
 	{
-		userApiGroup := apiGroup.Group("/user")
+		userApiGroup := apiGroup.Group("/users")
 		{
 			userApiGroup.POST("/login", user.Login)
 			userApiGroup.POST("/register", user.Register)
-			userApiGroup.GET("/logout", user.Logout)
+			userApiGroup.POST("/logout", user.Logout)
 			userApiGroup.GET("/userinfo", user.UserInfo)
 		}
-		questionApiGroup := apiGroup.Group("/question")
+		questionApiGroup := apiGroup.Group("/questions")
 		{
-			questionApiGroup.POST("/create", question.Create)
-			questionApiGroup.POST("/delete", question.Delete)
-			questionApiGroup.POST("/update", question.Update)
-			questionApiGroup.POST("/get", question.Get)
-
-			questionApiGroup.POST("/list", question.List)
-			questionApiGroup.POST("/search", question.Search)
+			questionApiGroup.POST("/", question.Create)
+			questionApiGroup.DELETE("/:question-id", question.Delete)
+			questionApiGroup.PUT("/:question-id", question.Update)
+			questionApiGroup.GET("/:question-id", question.Get)
+			questionApiGroup.GET("/", question.List)
+			questionApiGroup.GET("/search", question.Search)
+			questionApiGroup.POST("/:question-id/answers", answer.Create)
+			questionApiGroup.GET("/:question-id/answers", answer.Get)
 		}
-		answerApiGroup := apiGroup.Group("/answer")
+		answerApiGroup := apiGroup.Group("/answers")
 		{
-			answerApiGroup.POST("/create", answer.Create)
-			answerApiGroup.POST("/delete", answer.Delete)
-			answerApiGroup.POST("/update", answer.Update)
-			answerApiGroup.POST("/get", answer.Get)
+			answerApiGroup.DELETE("/:answer-id", answer.Delete)
+			answerApiGroup.PUT("/:answer-id", answer.Update)
 		}
 	}
 

From 5c258767564ddf830634c3a78fa0a05908f8cef3 Mon Sep 17 00:00:00 2001
From: l1nk4i <l1nk@foxmail.com>
Date: Thu, 3 Oct 2024 00:18:08 +0800
Subject: [PATCH 11/14] fit: admin api and select best answer function

---
 .gitignore                         |  3 ++-
 l1nk4i/api/admin/deleteAnswer.go   | 32 +++++++++++++++++++++++
 l1nk4i/api/admin/deleteQuestion.go | 32 +++++++++++++++++++++++
 l1nk4i/api/question/best.go        | 41 ++++++++++++++++++++++++++++++
 l1nk4i/db/question.go              |  9 +++++++
 l1nk4i/router/router.go            |  7 +++++
 6 files changed, 123 insertions(+), 1 deletion(-)
 create mode 100644 l1nk4i/api/admin/deleteAnswer.go
 create mode 100644 l1nk4i/api/admin/deleteQuestion.go
 create mode 100644 l1nk4i/api/question/best.go

diff --git a/.gitignore b/.gitignore
index ae234af..c48e205 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
 .idea
 l1nk4i/config.toml
-.gitignore
\ No newline at end of file
+.gitignore
+l1nk4i/test
\ No newline at end of file
diff --git a/l1nk4i/api/admin/deleteAnswer.go b/l1nk4i/api/admin/deleteAnswer.go
new file mode 100644
index 0000000..470b4e9
--- /dev/null
+++ b/l1nk4i/api/admin/deleteAnswer.go
@@ -0,0 +1,32 @@
+package admin
+
+import (
+	"github.com/gin-contrib/sessions"
+	"github.com/gin-gonic/gin"
+	"l1nk4i/db"
+	"net/http"
+)
+
+func DeleteAnswer(c *gin.Context) {
+	answerID := c.Param("answer-id")
+
+	session := sessions.Default(c)
+	role, exists := session.Get("role").(string)
+	if !exists {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid session"})
+		return
+	}
+
+	if role != "admin" {
+		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
+		return
+	}
+
+	err := db.DeleteAnswer(answerID)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid answer-id"})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{"msg": "delete answer successful!"})
+}
diff --git a/l1nk4i/api/admin/deleteQuestion.go b/l1nk4i/api/admin/deleteQuestion.go
new file mode 100644
index 0000000..511a339
--- /dev/null
+++ b/l1nk4i/api/admin/deleteQuestion.go
@@ -0,0 +1,32 @@
+package admin
+
+import (
+	"github.com/gin-contrib/sessions"
+	"github.com/gin-gonic/gin"
+	"l1nk4i/db"
+	"net/http"
+)
+
+func DeleteQuestion(c *gin.Context) {
+	questionID := c.Param("question-id")
+
+	session := sessions.Default(c)
+	role, exists := session.Get("role").(string)
+	if !exists {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid session"})
+		return
+	}
+
+	if role != "admin" {
+		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
+		return
+	}
+
+	err := db.DeleteQuestion(questionID)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid question-id"})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{"msg": "delete question successful!"})
+}
diff --git a/l1nk4i/api/question/best.go b/l1nk4i/api/question/best.go
new file mode 100644
index 0000000..7c2c516
--- /dev/null
+++ b/l1nk4i/api/question/best.go
@@ -0,0 +1,41 @@
+package question
+
+import (
+	"github.com/gin-contrib/sessions"
+	"github.com/gin-gonic/gin"
+	"l1nk4i/db"
+	"net/http"
+)
+
+func Best(c *gin.Context) {
+	answerID := c.Param("answer-id")
+	questionID := c.Param("question-id")
+
+	// Verify user identity
+	session := sessions.Default(c)
+	userid, exists := session.Get("user_id").(string)
+	if !exists {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid session"})
+		return
+	}
+
+	question, err := db.GetQuestionByQuestionID(questionID)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid question_id"})
+		return
+	}
+
+	if question.UserID != userid {
+		c.JSON(http.StatusForbidden, gin.H{"error": "permission denied"})
+		return
+	}
+
+	// Set the best answer
+	err = db.UpdateBestAnswer(questionID, answerID)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": "update best answer failed"})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{"msg": "update best answer successful!"})
+}
diff --git a/l1nk4i/db/question.go b/l1nk4i/db/question.go
index e261e93..1451ff8 100644
--- a/l1nk4i/db/question.go
+++ b/l1nk4i/db/question.go
@@ -59,3 +59,12 @@ func SearchQuestions(content string) (*[]Question, error) {
 	}
 	return &questions, nil
 }
+
+func UpdateBestAnswer(questionID, answerID string) error {
+	err := db.Model(&Question{}).Where("question_id = ?", questionID).Updates(Question{BestAnswerID: answerID}).Error
+	if err != nil {
+		log.Printf("[ERROR] Update Question benst answer failed %s\n", err.Error())
+		return err
+	}
+	return nil
+}
diff --git a/l1nk4i/router/router.go b/l1nk4i/router/router.go
index 100ac7f..7fcb1ff 100644
--- a/l1nk4i/router/router.go
+++ b/l1nk4i/router/router.go
@@ -5,6 +5,7 @@ import (
 	"github.com/gin-contrib/sessions"
 	"github.com/gin-contrib/sessions/cookie"
 	"github.com/gin-gonic/gin"
+	"l1nk4i/api/admin"
 	"l1nk4i/api/answer"
 	"l1nk4i/api/question"
 	"l1nk4i/api/user"
@@ -37,12 +38,18 @@ func Run() {
 			questionApiGroup.GET("/search", question.Search)
 			questionApiGroup.POST("/:question-id/answers", answer.Create)
 			questionApiGroup.GET("/:question-id/answers", answer.Get)
+			questionApiGroup.PUT("/:question-id/best/:answer-id", question.Best)
 		}
 		answerApiGroup := apiGroup.Group("/answers")
 		{
 			answerApiGroup.DELETE("/:answer-id", answer.Delete)
 			answerApiGroup.PUT("/:answer-id", answer.Update)
 		}
+		adminApiGroup := apiGroup.Group("/admin")
+		{
+			adminApiGroup.DELETE("/questions/:question-id", admin.DeleteQuestion)
+			adminApiGroup.DELETE("/answers/:answer-id", admin.DeleteAnswer)
+		}
 	}
 
 	r.Run(":8080")

From fead4d76dc5ac144c8ae60b3fa8238e7429d3f7d Mon Sep 17 00:00:00 2001
From: l1nk4i <l1nk@foxmail.com>
Date: Thu, 3 Oct 2024 20:13:44 +0800
Subject: [PATCH 12/14] fix: validate input

---
 l1nk4i/api/question/best.go   | 14 +++++++++++++-
 l1nk4i/api/question/delete.go | 23 -----------------------
 l1nk4i/api/user/register.go   |  8 ++++++--
 l1nk4i/db/question.go         | 19 +++++++++++++------
 4 files changed, 32 insertions(+), 32 deletions(-)

diff --git a/l1nk4i/api/question/best.go b/l1nk4i/api/question/best.go
index 7c2c516..231a1c5 100644
--- a/l1nk4i/api/question/best.go
+++ b/l1nk4i/api/question/best.go
@@ -21,7 +21,7 @@ func Best(c *gin.Context) {
 
 	question, err := db.GetQuestionByQuestionID(questionID)
 	if err != nil {
-		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid question_id"})
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid question-id"})
 		return
 	}
 
@@ -30,6 +30,18 @@ func Best(c *gin.Context) {
 		return
 	}
 
+	// Validate answer-id
+	answer, err := db.GetAnswerByAnswerID(answerID)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid answer-id"})
+		return
+	}
+
+	if answer.QuestionID != questionID {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid answer-id"})
+		return
+	}
+
 	// Set the best answer
 	err = db.UpdateBestAnswer(questionID, answerID)
 	if err != nil {
diff --git a/l1nk4i/api/question/delete.go b/l1nk4i/api/question/delete.go
index 8f84903..6394260 100644
--- a/l1nk4i/api/question/delete.go
+++ b/l1nk4i/api/question/delete.go
@@ -36,28 +36,5 @@ func Delete(c *gin.Context) {
 		return
 	}
 
-	// Delete answers to the question
-	err = deleteAnswers(questionID)
-	if err != nil {
-		c.JSON(http.StatusInternalServerError, gin.H{"error": "delete answers error"})
-		return
-	}
-
 	c.JSON(http.StatusOK, gin.H{"msg": "delete question successful!"})
 }
-
-func deleteAnswers(questionID string) error {
-	answers, err := db.GetAnswersByQuestionID(questionID)
-	if err != nil {
-		return err
-	}
-
-	for _, answer := range *answers {
-		err = db.DeleteAnswer(answer.AnswerID)
-		if err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
diff --git a/l1nk4i/api/user/register.go b/l1nk4i/api/user/register.go
index 261dc59..305ef41 100644
--- a/l1nk4i/api/user/register.go
+++ b/l1nk4i/api/user/register.go
@@ -20,8 +20,6 @@ func Register(c *gin.Context) {
 		return
 	}
 
-	fmt.Println(registerInfo)
-
 	if !validateUsername(registerInfo.Username) {
 		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid Username"})
 		return
@@ -32,6 +30,12 @@ func Register(c *gin.Context) {
 		return
 	}
 
+	exists, _ := db.GetUserByUsername(registerInfo.Username)
+	if exists != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "username exists"})
+		return
+	}
+	
 	user := db.User{
 		UserID:   uuid.New().String(),
 		Username: registerInfo.Username,
diff --git a/l1nk4i/db/question.go b/l1nk4i/db/question.go
index 1451ff8..5ab2dfc 100644
--- a/l1nk4i/db/question.go
+++ b/l1nk4i/db/question.go
@@ -5,7 +5,7 @@ import "log"
 func CreateQuestion(question *Question) error {
 	err := db.Create(question).Error
 	if err != nil {
-		log.Printf("[ERROR] Create Question failed %s\n", err.Error())
+		log.Printf("[ERROR] Create question failed %s\n", err.Error())
 		return err
 	}
 	return nil
@@ -15,7 +15,7 @@ func GetQuestionByQuestionID(questionID string) (*Question, error) {
 	var question Question
 	err := db.Where("question_id = ?", questionID).First(&question).Error
 	if err != nil {
-		log.Printf("[ERROR] Get Question by question_id failed %s\n", err.Error())
+		log.Printf("[ERROR] Get question by question_id failed %s\n", err.Error())
 		return nil, err
 	}
 	return &question, nil
@@ -34,7 +34,14 @@ func GetQuestionByUserID(userID string) (*[]Question, error) {
 func DeleteQuestion(questionID string) error {
 	err := db.Unscoped().Where("question_id = ?", questionID).Delete(&Question{}).Error
 	if err != nil {
-		log.Printf("[ERROR] Delete Question failed %s\n", err.Error())
+		log.Printf("[ERROR] Delete question failed %s\n", err.Error())
+		return err
+	}
+
+	// Delete answers
+	err = db.Unscoped().Where("question_id = ?", questionID).Delete(&Answer{}).Error
+	if err != nil {
+		log.Printf("[ERROR] Delete answers error:%s\n", err.Error())
 		return err
 	}
 	return nil
@@ -43,7 +50,7 @@ func DeleteQuestion(questionID string) error {
 func UpdateQuestion(questionID, title, content string) error {
 	err := db.Model(&Question{}).Where("question_id = ?", questionID).Updates(Question{Title: title, Content: content}).Error
 	if err != nil {
-		log.Printf("[ERROR] Update Question failed %s\n", err.Error())
+		log.Printf("[ERROR] Update question failed %s\n", err.Error())
 		return err
 	}
 	return nil
@@ -54,7 +61,7 @@ func SearchQuestions(content string) (*[]Question, error) {
 	searchPattern := "%" + content + "%"
 	err := db.Where("title LIKE ? OR content LIKE ?", searchPattern, searchPattern).Limit(20).Find(&questions).Error
 	if err != nil {
-		log.Printf("[ERROR] Search Questions failed %s\n", err.Error())
+		log.Printf("[ERROR] Search questions failed %s\n", err.Error())
 		return nil, err
 	}
 	return &questions, nil
@@ -63,7 +70,7 @@ func SearchQuestions(content string) (*[]Question, error) {
 func UpdateBestAnswer(questionID, answerID string) error {
 	err := db.Model(&Question{}).Where("question_id = ?", questionID).Updates(Question{BestAnswerID: answerID}).Error
 	if err != nil {
-		log.Printf("[ERROR] Update Question benst answer failed %s\n", err.Error())
+		log.Printf("[ERROR] Update question benst answer failed %s\n", err.Error())
 		return err
 	}
 	return nil

From dfa39c8e79b497a6773a99636bffb07f77a78d63 Mon Sep 17 00:00:00 2001
From: l1nk4i <l1nk@foxmail.com>
Date: Fri, 4 Oct 2024 21:11:21 +0800
Subject: [PATCH 13/14] docs: api doc

---
 l1nk4i/docs/apiDoc.md | 890 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 890 insertions(+)
 create mode 100644 l1nk4i/docs/apiDoc.md

diff --git a/l1nk4i/docs/apiDoc.md b/l1nk4i/docs/apiDoc.md
new file mode 100644
index 0000000..6a849ca
--- /dev/null
+++ b/l1nk4i/docs/apiDoc.md
@@ -0,0 +1,890 @@
+---
+title: hduhelp_backend_task
+language_tabs:
+  - shell: Shell
+  - http: HTTP
+  - javascript: JavaScript
+  - ruby: Ruby
+  - python: Python
+  - php: PHP
+  - java: Java
+  - go: Go
+toc_footers: []
+includes: []
+search: true
+code_clipboard: true
+highlight_theme: darkula
+headingLevel: 2
+generator: "@tarslib/widdershins v4.0.23"
+
+---
+
+# hduhelp_backend_task
+
+Base URLs:
+
+# Authentication
+
+# users
+
+## POST 用户注册
+
+POST /api/users/register
+
+注册
+
+> Body 请求参数
+
+```json
+{
+  "username": "l1nkQAQ",
+  "password": "vidarteam"
+}
+```
+
+### 请求参数
+
+|名称|位置|类型|必选|说明|
+|---|---|---|---|---|
+|body|body|object| 否 |none|
+|» username|body|string| 是 |none|
+|» password|body|string| 是 |none|
+
+> 返回示例
+
+> 200 Response
+
+```json
+{
+  "msg": "string"
+}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|none|Inline|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|none|Inline|
+
+### 返回数据结构
+
+状态码 **200**
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|» msg|string|true|none||none|
+
+状态码 **400**
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|» error|string|true|none||none|
+
+状态码 **500**
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|» error|string|true|none||none|
+
+## GET 获取当前用户信息
+
+GET /api/users/userinfo
+
+查询当前用户信息
+
+> 返回示例
+
+> 200 Response
+
+```json
+{
+  "role": "string",
+  "user_id": "string",
+  "username": "string"
+}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|none|Inline|
+
+### 返回数据结构
+
+状态码 **200**
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|» role|string|true|none||none|
+|» user_id|string|true|none||none|
+|» username|string|true|none||none|
+
+状态码 **400**
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|» error|string|true|none||none|
+
+## POST 用户登录
+
+POST /api/users/login
+
+登录
+
+> Body 请求参数
+
+```json
+{
+  "username": "l1nkQAQ",
+  "password": "vidarteam"
+}
+```
+
+### 请求参数
+
+|名称|位置|类型|必选|说明|
+|---|---|---|---|---|
+|body|body|object| 否 |none|
+|» username|body|string| 是 |none|
+|» password|body|string| 是 |none|
+
+> 返回示例
+
+> 200 Response
+
+```json
+{
+  "msg": "string"
+}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline|
+
+### 返回数据结构
+
+状态码 **200**
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|» msg|string|true|none||none|
+
+## POST 用户登出
+
+POST /api/users/logout
+
+登出
+
+> 返回示例
+
+> 200 Response
+
+```json
+{
+  "msg": "string"
+}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline|
+
+### 返回数据结构
+
+状态码 **200**
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|» msg|string|true|none||none|
+
+# questions
+
+## POST 创建问题
+
+POST /api/questions
+
+创建问题
+
+> Body 请求参数
+
+```json
+{
+  "title": "man",
+  "content": "what can i say"
+}
+```
+
+### 请求参数
+
+|名称|位置|类型|必选|说明|
+|---|---|---|---|---|
+|body|body|object| 否 |none|
+|» title|body|string| 是 |none|
+|» content|body|string| 是 |none|
+
+> 返回示例
+
+> 200 Response
+
+```json
+{
+  "msg": "string"
+}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline|
+
+### 返回数据结构
+
+状态码 **200**
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|» msg|string|true|none||none|
+
+## GET 列出用户发表的所有问题
+
+GET /api/questions
+
+列出用户发表的所有问题
+
+> 返回示例
+
+> 200 Response
+
+```json
+{
+  "question_id": [
+    "string"
+  ]
+}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline|
+
+### 返回数据结构
+
+状态码 **200**
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|» question_id|[string]|true|none||none|
+
+## POST 创建指定问题的回答
+
+POST /api/questions/{question-id}/answers
+
+发送回答
+
+> Body 请求参数
+
+```json
+{
+  "content": "manman"
+}
+```
+
+### 请求参数
+
+|名称|位置|类型|必选|说明|
+|---|---|---|---|---|
+|question-id|path|string| 是 |none|
+|body|body|object| 否 |none|
+|» question_id|body|string| 是 |none|
+|» content|body|string| 是 |none|
+
+> 返回示例
+
+> 200 Response
+
+```json
+{
+  "answer_id": "string"
+}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline|
+
+### 返回数据结构
+
+状态码 **200**
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|» answer_id|string|true|none||none|
+
+## GET 获取指定问题的所有回答
+
+GET /api/questions/{question-id}/answers
+
+获得问题对应的所有回答
+
+### 请求参数
+
+|名称|位置|类型|必选|说明|
+|---|---|---|---|---|
+|question-id|path|string| 是 |none|
+
+> 返回示例
+
+> 200 Response
+
+```json
+{
+  "data": [
+    {
+      "ID": 0,
+      "CreatedAt": "string",
+      "UpdatedAt": "string",
+      "DeletedAt": null,
+      "AnswerID": "string",
+      "UserID": "string",
+      "QuestionID": "string",
+      "Content": "string"
+    }
+  ]
+}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline|
+
+### 返回数据结构
+
+状态码 **200**
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|» data|[object]|true|none||none|
+|»» ID|integer|true|none||none|
+|»» CreatedAt|string|true|none||none|
+|»» UpdatedAt|string|true|none||none|
+|»» DeletedAt|null|true|none||none|
+|»» AnswerID|string|true|none||none|
+|»» UserID|string|true|none||none|
+|»» QuestionID|string|true|none||none|
+|»» Content|string|true|none||none|
+
+## DELETE 删除指定问题
+
+DELETE /api/questions/{question-id}
+
+删除问题
+
+### 请求参数
+
+|名称|位置|类型|必选|说明|
+|---|---|---|---|---|
+|question-id|path|string| 是 |none|
+
+> 返回示例
+
+> 200 Response
+
+```json
+{
+  "msg": "string"
+}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline|
+
+### 返回数据结构
+
+状态码 **200**
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|» msg|string|true|none||none|
+
+## PUT 更新指定问题
+
+PUT /api/questions/{question-id}
+
+更改问题的标题或者内容
+
+> Body 请求参数
+
+```json
+{
+  "title": "manba",
+  "content": "out"
+}
+```
+
+### 请求参数
+
+|名称|位置|类型|必选|说明|
+|---|---|---|---|---|
+|question-id|path|string| 是 |none|
+|body|body|object| 否 |none|
+|» question_id|body|string| 是 |none|
+|» title|body|string| 是 |none|
+|» content|body|string| 是 |none|
+
+> 返回示例
+
+> 200 Response
+
+```json
+{
+  "msg": "string"
+}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline|
+
+### 返回数据结构
+
+状态码 **200**
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|» msg|string|true|none||none|
+
+## GET 获取指定问题
+
+GET /api/questions/{question-id}
+
+通过question_id查找对应问题对象
+
+### 请求参数
+
+|名称|位置|类型|必选|说明|
+|---|---|---|---|---|
+|question-id|path|string| 是 |none|
+
+> 返回示例
+
+> 200 Response
+
+```json
+{
+  "data": {
+    "ID": 0,
+    "CreatedAt": "string",
+    "UpdatedAt": "string",
+    "DeletedAt": null,
+    "QuestionID": "string",
+    "UserID": "string",
+    "BestAnswerID": "string",
+    "Title": "string",
+    "Content": "string"
+  }
+}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline|
+
+### 返回数据结构
+
+状态码 **200**
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|» data|object|true|none||none|
+|»» ID|integer|true|none||none|
+|»» CreatedAt|string|true|none||none|
+|»» UpdatedAt|string|true|none||none|
+|»» DeletedAt|null|true|none||none|
+|»» QuestionID|string|true|none||none|
+|»» UserID|string|true|none||none|
+|»» BestAnswerID|string|true|none||none|
+|»» Title|string|true|none||none|
+|»» Content|string|true|none||none|
+
+## GET 搜索问题
+
+GET /api/questions/search
+
+通过输入匹配问题标题和内容,搜索问题(无鉴权)
+
+### 请求参数
+
+|名称|位置|类型|必选|说明|
+|---|---|---|---|---|
+|content|query|string| 否 |none|
+
+> 返回示例
+
+> 200 Response
+
+```json
+{
+  "data": [
+    {
+      "ID": 0,
+      "CreatedAt": "string",
+      "UpdatedAt": "string",
+      "DeletedAt": null,
+      "QuestionID": "string",
+      "UserID": "string",
+      "BestAnswerID": "string",
+      "Title": "string",
+      "Content": "string"
+    }
+  ]
+}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline|
+
+### 返回数据结构
+
+状态码 **200**
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|» data|[object]|true|none||none|
+|»» ID|integer|true|none||none|
+|»» CreatedAt|string|true|none||none|
+|»» UpdatedAt|string|true|none||none|
+|»» DeletedAt|null|true|none||none|
+|»» QuestionID|string|true|none||none|
+|»» UserID|string|true|none||none|
+|»» BestAnswerID|string|true|none||none|
+|»» Title|string|true|none||none|
+|»» Content|string|true|none||none|
+
+## PUT 更新最佳答案
+
+PUT /api/questions/{question-id}/best/{answer-id}
+
+更新最佳答案
+
+### 请求参数
+
+|名称|位置|类型|必选|说明|
+|---|---|---|---|---|
+|question-id|path|string| 是 |none|
+|answer-id|path|string| 是 |none|
+
+> 返回示例
+
+> 200 Response
+
+```json
+{
+  "msg": "string"
+}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline|
+
+### 返回数据结构
+
+状态码 **200**
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|» msg|string|true|none||none|
+
+# answers
+
+## DELETE 删除指定回答
+
+DELETE /api/answers/{answer-id}
+
+删除回答
+
+### 请求参数
+
+|名称|位置|类型|必选|说明|
+|---|---|---|---|---|
+|answer-id|path|string| 是 |none|
+
+> 返回示例
+
+> 200 Response
+
+```json
+{
+  "msg": "string"
+}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline|
+
+### 返回数据结构
+
+状态码 **200**
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|» msg|string|true|none||none|
+
+## PUT 更新指定回答
+
+PUT /api/answers/{answer-id}
+
+更改回答内容
+
+> Body 请求参数
+
+```json
+{
+  "content": "manba out"
+}
+```
+
+### 请求参数
+
+|名称|位置|类型|必选|说明|
+|---|---|---|---|---|
+|answer-id|path|string| 是 |none|
+|body|body|object| 否 |none|
+|» answer_id|body|string| 是 |none|
+|» content|body|string| 是 |none|
+
+> 返回示例
+
+> 200 Response
+
+```json
+{
+  "msg": "string"
+}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline|
+
+### 返回数据结构
+
+状态码 **200**
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|» msg|string|true|none||none|
+
+# admin
+
+## DELETE 删除问题
+
+DELETE /api/admin/questions/{question-id}
+
+删除问题
+
+### 请求参数
+
+|名称|位置|类型|必选|说明|
+|---|---|---|---|---|
+|question-id|path|string| 是 |none|
+
+> 返回示例
+
+> 200 Response
+
+```json
+{
+  "msg": "string"
+}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline|
+
+### 返回数据结构
+
+状态码 **200**
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|» msg|string|true|none||none|
+
+## DELETE 删除答案
+
+DELETE /api/admin/answers/{answer-id}
+
+删除答案
+
+### 请求参数
+
+|名称|位置|类型|必选|说明|
+|---|---|---|---|---|
+|answer-id|path|string| 是 |none|
+
+> 返回示例
+
+> 200 Response
+
+```json
+{
+  "msg": "string"
+}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline|
+
+### 返回数据结构
+
+状态码 **200**
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|» msg|string|true|none||none|
+
+# 数据模型
+
+<h2 id="tocS_User">User</h2>
+
+<a id="schemauser"></a>
+<a id="schema_User"></a>
+<a id="tocSuser"></a>
+<a id="tocsuser"></a>
+
+```json
+{
+  "username": "string",
+  "password": "string",
+  "role": "string",
+  "created_at": "2019-08-24T14:15:22Z",
+  "deleted_at": "2019-08-24T14:15:22Z",
+  "id": 1,
+  "updated_at": "2019-08-24T14:15:22Z",
+  "user_id": "string"
+}
+
+```
+
+### 属性
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|username|string|true|none||none|
+|password|string|true|none||none|
+|role|string|true|none||none|
+|created_at|string(date-time)|false|none||none|
+|deleted_at|string(date-time)|false|none||none|
+|id|integer|true|none||none|
+|updated_at|string(date-time)|false|none||none|
+|user_id|string|true|none||none|
+
+<h2 id="tocS_Question">Question</h2>
+
+<a id="schemaquestion"></a>
+<a id="schema_Question"></a>
+<a id="tocSquestion"></a>
+<a id="tocsquestion"></a>
+
+```json
+{
+  "title": "string",
+  "content": "string",
+  "best_answer_id": "string",
+  "created_at": "2019-08-24T14:15:22Z",
+  "deleted_at": "2019-08-24T14:15:22Z",
+  "id": 1,
+  "question_id": "string",
+  "updated_at": "2019-08-24T14:15:22Z",
+  "user_id": "string"
+}
+
+```
+
+### 属性
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|title|string|true|none||none|
+|content|string|true|none||none|
+|best_answer_id|string|false|none||none|
+|created_at|string(date-time)|false|none||none|
+|deleted_at|string(date-time)|false|none||none|
+|id|integer|true|none||none|
+|question_id|string|true|none||none|
+|updated_at|string(date-time)|false|none||none|
+|user_id|string|true|none||none|
+
+<h2 id="tocS_Answer">Answer</h2>
+
+<a id="schemaanswer"></a>
+<a id="schema_Answer"></a>
+<a id="tocSanswer"></a>
+<a id="tocsanswer"></a>
+
+```json
+{
+  "content": "string",
+  "answer_id": "string",
+  "created_at": "2019-08-24T14:15:22Z",
+  "deleted_at": "2019-08-24T14:15:22Z",
+  "id": 1,
+  "question_id": "string",
+  "updated_at": "2019-08-24T14:15:22Z",
+  "user_id": "string"
+}
+
+```
+
+### 属性
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|content|string|true|none||none|
+|answer_id|string|true|none||none|
+|created_at|string(date-time)|false|none||none|
+|deleted_at|string(date-time)|false|none||none|
+|id|integer|true|none||none|
+|question_id|string|true|none||none|
+|updated_at|string(date-time)|false|none||none|
+|user_id|string|true|none||none|
+

From c8baf54eb24b06579a6543e31c4b607e9f3f74d0 Mon Sep 17 00:00:00 2001
From: l1nk4i <l1nk@foxmail.com>
Date: Sat, 5 Oct 2024 21:56:47 +0800
Subject: [PATCH 14/14] feat: support docker now

---
 l1nk4i/Dockerfile           | 20 ++++++++++++++++++++
 l1nk4i/api/user/register.go |  5 ++---
 l1nk4i/docker-compose.yml   | 23 +++++++++++++++++++++++
 l1nk4i/entrypoint.sh        | 15 +++++++++++++++
 l1nk4i/init.sql             |  4 ++++
 5 files changed, 64 insertions(+), 3 deletions(-)
 create mode 100644 l1nk4i/Dockerfile
 create mode 100644 l1nk4i/docker-compose.yml
 create mode 100644 l1nk4i/entrypoint.sh
 create mode 100644 l1nk4i/init.sql

diff --git a/l1nk4i/Dockerfile b/l1nk4i/Dockerfile
new file mode 100644
index 0000000..0ce0926
--- /dev/null
+++ b/l1nk4i/Dockerfile
@@ -0,0 +1,20 @@
+FROM golang:1.23.1 AS builder
+
+WORKDIR /app
+
+COPY . .
+
+RUN go build -o myapp
+
+FROM ubuntu:latest
+
+WORKDIR /app
+
+COPY --from=builder /app/myapp /app/
+COPY config.toml .
+COPY entrypoint.sh .
+RUN chmod +x entrypoint.sh
+
+ENTRYPOINT ["./entrypoint.sh"]
+
+EXPOSE 8080
\ No newline at end of file
diff --git a/l1nk4i/api/user/register.go b/l1nk4i/api/user/register.go
index 305ef41..8b184ef 100644
--- a/l1nk4i/api/user/register.go
+++ b/l1nk4i/api/user/register.go
@@ -1,7 +1,6 @@
 package user
 
 import (
-	"fmt"
 	"github.com/gin-gonic/gin"
 	"github.com/google/uuid"
 	"l1nk4i/db"
@@ -14,7 +13,7 @@ func Register(c *gin.Context) {
 		Username string `json:"username"`
 		Password string `json:"password"`
 	}
-	fmt.Println(registerInfo)
+
 	if err := c.ShouldBindJSON(&registerInfo); err != nil {
 		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
 		return
@@ -35,7 +34,7 @@ func Register(c *gin.Context) {
 		c.JSON(http.StatusBadRequest, gin.H{"error": "username exists"})
 		return
 	}
-	
+
 	user := db.User{
 		UserID:   uuid.New().String(),
 		Username: registerInfo.Username,
diff --git a/l1nk4i/docker-compose.yml b/l1nk4i/docker-compose.yml
new file mode 100644
index 0000000..f6acf33
--- /dev/null
+++ b/l1nk4i/docker-compose.yml
@@ -0,0 +1,23 @@
+version: '3.8'
+
+services:
+  db:
+    image: mysql:latest
+    environment:
+      MYSQL_RANDOM_ROOT_PASSWORD: true
+    volumes:
+      - /home/l1nk/mysql_data:/var/lib/mysql
+      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
+    expose:
+      - "3306"
+      - "33060"
+    ports:
+      - "3306:3306" # debug
+
+
+  app:
+    build: .
+    ports:
+      - "8080:8080"
+    depends_on:
+      - db
diff --git a/l1nk4i/entrypoint.sh b/l1nk4i/entrypoint.sh
new file mode 100644
index 0000000..17a3f78
--- /dev/null
+++ b/l1nk4i/entrypoint.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+# 循环执行 myapp
+while true; do
+    ./myapp  # 运行应用
+
+    # 检查上一个命令的退出状态
+    if [ $? -eq 0 ]; then
+        echo "myapp executed successfully."
+        break  # 如果成功,退出循环
+    else
+        echo "myapp failed. Retrying in 2 seconds..."
+        sleep 2  # 等待 2 秒后重试
+    fi
+done
diff --git a/l1nk4i/init.sql b/l1nk4i/init.sql
new file mode 100644
index 0000000..5b6ce06
--- /dev/null
+++ b/l1nk4i/init.sql
@@ -0,0 +1,4 @@
+CREATE DATABASE appdb;
+CREATE USER 'app'@'%' IDENTIFIED BY 'safe_password';
+GRANT ALL PRIVILEGES ON appdb.* TO 'app'@'%';
+FLUSH PRIVILEGES;
\ No newline at end of file