在线书签管理
了解了您决定使用原始 HTML 进行前端开发后,以下是基于 Go 语言和 MySQL 数据库构建在线书签整理平台的详细实施指南。这将涵盖系统架构设计、数据库设计、后端开发、前端集成(使用原生 HTML、CSS 和 JavaScript)、用户认证、安全性考虑以及部署与维护等方面。
一、系统架构设计
1. 总体架构
采用前后端集成的架构,通过 Go 语言的模板引擎(如 html/template
)实现服务器端渲染。前端使用原生 HTML、CSS 和 JavaScript,与后端通过表单提交和 AJAX 请求进行交互。
1 2 3 4 5 6
| +----------------+ +-----------------+ +----------------+ | | | | | | | 原生 HTML | <---->| 后端服务器 | <---->| MySQL数据库 | | (HTML/CSS/JS) | | (Go语言) | | | | | | | | | +----------------+ +-----------------+ +----------------+
|
2. 技术栈
- 后端:Go语言,使用 Gin 框架
- 数据库:MySQL
- 前端:原生 HTML、CSS、JavaScript
- 模板引擎:Go 的
html/template
- 身份验证:基于 Cookie 的会话管理或 JWT(JSON Web Tokens)
- 版本控制:Git(GitHub/GitLab)
二、数据库设计
1. 用户表(users)
字段名 |
数据类型 |
约束 |
描述 |
id |
INT AUTO_INCREMENT |
PRIMARY KEY |
用户唯一标识 |
username |
VARCHAR(50) |
UNIQUE, NOT NULL |
用户名 |
email |
VARCHAR(100) |
UNIQUE, NOT NULL |
用户邮箱 |
password |
VARCHAR(255) |
NOT NULL |
加密后的密码 |
created_at |
DATETIME |
DEFAULT CURRENT_TIMESTAMP |
注册时间 |
updated_at |
DATETIME |
DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP |
更新时间 |
2. 书签表(bookmarks)
字段名 |
数据类型 |
约束 |
描述 |
id |
INT AUTO_INCREMENT |
PRIMARY KEY |
书签唯一标识 |
user_id |
INT |
FOREIGN KEY REFERENCES users(id) |
所属用户 |
title |
VARCHAR(255) |
NOT NULL |
书签标题 |
url |
VARCHAR(2083) |
NOT NULL |
书签URL |
description |
TEXT |
NULL |
书签描述 |
folder |
VARCHAR(100) |
NULL |
文件夹名称 |
tags |
JSON |
NULL |
标签列表(JSON格式) |
created_at |
DATETIME |
DEFAULT CURRENT_TIMESTAMP |
创建时间 |
updated_at |
DATETIME |
DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP |
更新时间 |
如果需要更复杂的标签管理,可以创建单独的标签表并建立多对多关系。
字段名 |
数据类型 |
约束 |
描述 |
id |
INT AUTO_INCREMENT |
PRIMARY KEY |
标签唯一标识 |
user_id |
INT |
FOREIGN KEY REFERENCES users(id) |
所属用户 |
name |
VARCHAR(50) |
NOT NULL |
标签名称 |
created_at |
DATETIME |
DEFAULT CURRENT_TIMESTAMP |
创建时间 |
关联表 |
字段名 |
数据类型 |
约束 |
描述 |
bookmark_tags |
bookmark_id |
INT |
FOREIGN KEY REFERENCES bookmarks(id) |
所属书签 |
|
tag_id |
INT |
FOREIGN KEY REFERENCES tags(id) |
所属标签 |
三、后端开发
1. 选择框架
Gin 是一个高性能的 Go 语言 Web 框架,适合构建服务器端渲染的应用。
2. 项目结构
一个典型的 Go 项目结构如下:
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
| bookmark-platform/ ├── cmd/ │ └── main.go ├── internal/ │ ├── handlers/ │ ├── models/ │ ├── repositories/ │ ├── services/ │ ├── middleware/ │ ├── templates/ │ └── utils/ ├── static/ │ ├── css/ │ ├── js/ │ └── images/ ├── templates/ │ ├── index.html │ ├── login.html │ ├── register.html │ └── bookmarks.html ├── configs/ │ └── config.yaml ├── migrations/ │ └── ... SQL or migration files ... ├── scripts/ │ └── ... deployment scripts ... ├── go.mod └── go.sum
|
3. 主要模块
a. 模型(Models)
定义用户和书签的结构体,使用 GORM 等 ORM 工具可以简化数据库操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package models
import ( "time" )
type User struct { ID uint `gorm:"primaryKey" json:"id"` Username string `gorm:"unique;not null" json:"username"` Email string `gorm:"unique;not null" json:"email"` Password string `gorm:"not null" json:"-"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` Bookmarks []Bookmark `gorm:"foreignKey:UserID" json:"bookmarks,omitempty"` }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package models
import ( "time" )
type Bookmark struct { ID uint `gorm:"primaryKey" json:"id"` UserID uint `gorm:"not null" json:"user_id"` Title string `gorm:"not null" json:"title"` URL string `gorm:"not null" json:"url"` Description string `json:"description,omitempty"` Folder string `json:"folder,omitempty"` Tags string `json:"tags,omitempty"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` }
|
b. 数据库连接
使用 GORM 连接 MySQL 数据库。
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
| package utils
import ( "fmt" "log" "os"
"your_project/internal/models"
"gorm.io/driver/mysql" "gorm.io/gorm" )
var DB *gorm.DB
func InitDB() { dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_HOST"), os.Getenv("DB_NAME"), ) var err error DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { log.Fatal("Failed to connect to database: ", err) }
err = DB.AutoMigrate(&models.User{}, &models.Bookmark{}) if err != nil { log.Fatal("Failed to migrate database: ", err) } }
|
c. 处理器(Handlers)
实现用户注册、登录及书签管理的处理函数,渲染相应的 HTML 模板。
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
| package handlers
import ( "net/http" "time"
"github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" "github.com/dgrijalva/jwt-go"
"your_project/internal/models" "your_project/internal/utils" )
type RegisterInput struct { Username string `form:"username" binding:"required"` Email string `form:"email" binding:"required,email"` Password string `form:"password" binding:"required,min=6"` }
func Register(c *gin.Context) { var input RegisterInput if err := c.ShouldBind(&input); err != nil { c.HTML(http.StatusBadRequest, "register.html", gin.H{ "error": err.Error(), }) return }
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost) if err != nil { c.HTML(http.StatusInternalServerError, "register.html", gin.H{ "error": "服务器错误", }) return }
user := models.User{ Username: input.Username, Email: input.Email, Password: string(hashedPassword), }
if err := utils.DB.Create(&user).Error; err != nil { c.HTML(http.StatusBadRequest, "register.html", gin.H{ "error": "邮箱或用户名已存在", }) return }
c.Redirect(http.StatusSeeOther, "/login") }
type LoginInput struct { Email string `form:"email" binding:"required,email"` Password string `form:"password" binding:"required"` }
func Login(c *gin.Context) { var input LoginInput if err := c.ShouldBind(&input); err != nil { c.HTML(http.StatusBadRequest, "login.html", gin.H{ "error": err.Error(), }) return }
var user models.User if err := utils.DB.Where("email = ?", input.Email).First(&user).Error; err != nil { c.HTML(http.StatusUnauthorized, "login.html", gin.H{ "error": "无效的凭证", }) return }
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.Password)); err != nil { c.HTML(http.StatusUnauthorized, "login.html", gin.H{ "error": "无效的凭证", }) return }
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "user_id": user.ID, "exp": time.Now().Add(time.Hour * 72).Unix(), })
tokenString, err := token.SignedString([]byte("your_secret_key")) if err != nil { c.HTML(http.StatusInternalServerError, "login.html", gin.H{ "error": "无法生成令牌", }) return }
c.SetCookie("token", tokenString, 3600*72, "/", "", false, true)
c.Redirect(http.StatusSeeOther, "/bookmarks") }
|
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
| package middleware
import ( "fmt" "net/http" "strings"
"github.com/dgrijalva/jwt-go" "github.com/gin-gonic/gin" )
func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { tokenString, err := c.Cookie("token") if err != nil { c.Redirect(http.StatusSeeOther, "/login") c.Abort() return }
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } return []byte("your_secret_key"), nil })
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { userID := uint(claims["user_id"].(float64)) c.Set("user_id", userID) c.Next() } else { c.Redirect(http.StatusSeeOther, "/login") c.Abort() return } } }
|
d. 书签处理器
实现添加、编辑、删除、获取书签的处理函数,渲染相应的 HTML 模板。
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
| package handlers
import ( "net/http"
"github.com/gin-gonic/gin" "your_project/internal/models" "your_project/internal/utils" )
type CreateBookmarkInput struct { Title string `form:"title" binding:"required"` URL string `form:"url" binding:"required,url"` Description string `form:"description"` Folder string `form:"folder"` Tags string `form:"tags"` }
func CreateBookmark(c *gin.Context) { var input CreateBookmarkInput if err := c.ShouldBind(&input); err != nil { c.HTML(http.StatusBadRequest, "bookmarks.html", gin.H{ "error": err.Error(), }) return }
userID := c.MustGet("user_id").(uint)
bookmark := models.Bookmark{ UserID: userID, Title: input.Title, URL: input.URL, Description: input.Description, Folder: input.Folder, Tags: input.Tags, }
if err := utils.DB.Create(&bookmark).Error; err != nil { c.HTML(http.StatusInternalServerError, "bookmarks.html", gin.H{ "error": "创建书签失败", }) return }
c.Redirect(http.StatusSeeOther, "/bookmarks") }
func GetBookmarks(c *gin.Context) { userID := c.MustGet("user_id").(uint) var bookmarks []models.Bookmark if err := utils.DB.Where("user_id = ?", userID).Find(&bookmarks).Error; err != nil { c.HTML(http.StatusInternalServerError, "bookmarks.html", gin.H{ "error": "获取书签失败", }) return }
c.HTML(http.StatusOK, "bookmarks.html", gin.H{ "bookmarks": bookmarks, }) }
func UpdateBookmark(c *gin.Context) { var input CreateBookmarkInput if err := c.ShouldBind(&input); err != nil { c.HTML(http.StatusBadRequest, "bookmarks.html", gin.H{ "error": err.Error(), }) return }
userID := c.MustGet("user_id").(uint) bookmarkID := c.Param("id")
var bookmark models.Bookmark if err := utils.DB.Where("id = ? AND user_id = ?", bookmarkID, userID).First(&bookmark).Error; err != nil { c.HTML(http.StatusNotFound, "bookmarks.html", gin.H{ "error": "未找到书签", }) return }
bookmark.Title = input.Title bookmark.URL = input.URL bookmark.Description = input.Description bookmark.Folder = input.Folder bookmark.Tags = input.Tags
if err := utils.DB.Save(&bookmark).Error; err != nil { c.HTML(http.StatusInternalServerError, "bookmarks.html", gin.H{ "error": "更新书签失败", }) return }
c.Redirect(http.StatusSeeOther, "/bookmarks") }
func DeleteBookmark(c *gin.Context) { userID := c.MustGet("user_id").(uint) bookmarkID := c.Param("id")
if err := utils.DB.Where("id = ? AND user_id = ?", bookmarkID, userID).Delete(&models.Bookmark{}).Error; err != nil { c.HTML(http.StatusInternalServerError, "bookmarks.html", gin.H{ "error": "删除书签失败", }) return }
c.Redirect(http.StatusSeeOther, "/bookmarks") }
|
e. 路由设置
配置路由,并应用认证中间件。使用 Go 的模板引擎渲染页面。
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
| package main
import ( "your_project/internal/handlers" "your_project/internal/middleware" "your_project/internal/utils"
"github.com/gin-gonic/gin" )
func main() { utils.LoadConfig() utils.InitDB()
r := gin.Default()
r.LoadHTMLGlob("templates/*")
r.Static("/static", "./static")
r.GET("/", handlers.ShowHomePage) r.GET("/register", handlers.ShowRegisterPage) r.POST("/register", handlers.Register) r.GET("/login", handlers.ShowLoginPage) r.POST("/login", handlers.Login)
authorized := r.Group("/") authorized.Use(middleware.AuthMiddleware()) { authorized.GET("/bookmarks", handlers.GetBookmarks) authorized.POST("/bookmarks", handlers.CreateBookmark) authorized.POST("/bookmarks/update/:id", handlers.UpdateBookmark) authorized.POST("/bookmarks/delete/:id", handlers.DeleteBookmark) authorized.GET("/logout", handlers.Logout) }
r.Run(":8080") }
|
f. 处理首页、注册和登录页面
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
| package handlers
import ( "net/http"
"github.com/gin-gonic/gin" )
func ShowHomePage(c *gin.Context) { c.HTML(http.StatusOK, "index.html", nil) }
func ShowRegisterPage(c *gin.Context) { c.HTML(http.StatusOK, "register.html", nil) }
func ShowLoginPage(c *gin.Context) { c.HTML(http.StatusOK, "login.html", nil) }
func Logout(c *gin.Context) { c.SetCookie("token", "", -1, "/", "", false, true) c.Redirect(http.StatusSeeOther, "/login") }
|
4. 配置管理
使用环境变量管理敏感信息,如数据库连接字符串和 JWT 密钥。可以使用 .env
文件和 godotenv
库加载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package utils
import ( "log" "os"
"github.com/joho/godotenv" )
func LoadConfig() { if err := godotenv.Load(); err != nil { log.Println("No .env file found") } }
|
在 main.go
中调用 LoadConfig
:
1 2 3 4 5
| func main() { utils.LoadConfig() utils.InitDB() }
|
5. 错误处理与日志记录
使用 Gin 自带的日志功能,或者集成更高级的日志库如 logrus
或 zap
以增强日志记录能力。
四、前端集成(使用原生 HTML、CSS 和 JavaScript)
1. 模板设计
使用 Go 的 html/template
包来渲染动态内容。创建 HTML 模板文件,如 index.html
, login.html
, register.html
, bookmarks.html
等。
示例:templates/login.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>登录</title> <link rel="stylesheet" href="/static/css/styles.css"> </head> <body> <h1>登录</h1> {{if .error}} <p style="color:red;">{{.error}}</p> {{end}} <form action="/login" method="POST"> <label for="email">邮箱:</label> <input type="email" id="email" name="email" required> <br> <label for="password">密码:</label> <input type="password" id="password" name="password" required> <br> <button type="submit">登录</button> </form> <p>没有账号? <a href="/register">注册</a></p> </body> </html>
|
示例:templates/bookmarks.html
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
| <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>我的书签</title> <link rel="stylesheet" href="/static/css/styles.css"> <script src="/static/js/scripts.js" defer></script> </head> <body> <h1>我的书签</h1> <a href="/logout">退出</a>
{{if .error}} <p style="color:red;">{{.error}}</p> {{end}}
<h2>添加书签</h2> <form action="/bookmarks" method="POST"> <label for="title">标题:</label> <input type="text" id="title" name="title" required> <br> <label for="url">URL:</label> <input type="url" id="url" name="url" required> <br> <label for="description">描述:</label> <textarea id="description" name="description"></textarea> <br> <label for="folder">文件夹:</label> <input type="text" id="folder" name="folder"> <br> <label for="tags">标签 (逗号分隔):</label> <input type="text" id="tags" name="tags"> <br> <button type="submit">添加书签</button> </form>
<h2>书签列表</h2> <table> <thead> <tr> <th>标题</th> <th>URL</th> <th>描述</th> <th>文件夹</th> <th>标签</th> <th>操作</th> </tr> </thead> <tbody> {{range .bookmarks}} <tr> <td>{{.Title}}</td> <td><a href="{{.URL}}" target="_blank">{{.URL}}</a></td> <td>{{.Description}}</td> <td>{{.Folder}}</td> <td>{{.Tags}}</td> <td> <form action="/bookmarks/delete/{{.ID}}" method="POST" style="display:inline;"> <button type="submit">删除</button> </form> </td> </tr> {{end}} </tbody> </table> </body> </html>
|
2. 静态资源管理
将 CSS 和 JavaScript 文件放置在 static/css
和 static/js
目录下,并在模板中引用。
示例:static/css/styles.css
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
| body { font-family: Arial, sans-serif; margin: 20px; }
h1, h2 { color: #333; }
form { margin-bottom: 20px; }
label { display: inline-block; width: 100px; }
input, textarea { width: 300px; padding: 5px; margin-bottom: 10px; }
button { padding: 5px 10px; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ccc; padding: 10px; text-align: left; }
th { background-color: #f4f4f4; }
|
示例:static/js/scripts.js
1 2 3 4 5 6 7 8 9 10 11 12
| document.addEventListener('DOMContentLoaded', function() { const deleteForms = document.querySelectorAll('form[action^="/bookmarks/delete"]'); deleteForms.forEach(form => { form.addEventListener('submit', function(e) { if (!confirm('确定要删除这个书签吗?')) { e.preventDefault(); } }); }); });
|
3. 表单处理与交互
使用表单提交进行数据的创建和删除。对于更复杂的交互(如编辑书签、动态搜索),可以使用 JavaScript 和 AJAX 请求实现。
示例:添加书签
用户在 bookmarks.html
页面填写表单后,表单数据将通过 POST 请求提交到 /bookmarks
路由,后端处理后重定向回书签列表页面。
示例:删除书签
每个书签行中的删除按钮实际上是一个表单,提交到 /bookmarks/delete/:id
路由,后端处理后重定向回书签列表页面。
4. 动态内容渲染
使用 Go 的模板语法在服务器端渲染动态内容,如用户的书签列表。
五、身份验证与安全性
1. 身份验证
使用基于 Cookie 的会话管理或 JWT 进行身份验证。在前面的示例中,我们使用了 JWT 并将其存储在 Cookie 中。
2. 安全性措施
- 密码加密:使用
bcrypt
哈希用户密码。
- 输入验证:后端严格验证所有输入,防止 SQL 注入和 XSS 攻击。
- HTTPS:确保所有通信通过 HTTPS 加密。
- CORS:如果需要跨域访问,配置跨域资源共享(CORS)策略,限制允许的来源。
- CSRF 保护:防止跨站请求伪造攻击,可以使用 CSRF 令牌。
- 速率限制:防止暴力破解攻击,限制 API 请求频率。
六、部署与运维
1. 部署环境
- 云服务提供商:AWS, Google Cloud, Azure, DigitalOcean 等。
- 容器化:使用 Docker 封装应用,简化部署和扩展。
2. 部署步骤
构建 Docker 镜像
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| FROM golang:1.20-alpine
WORKDIR /app
COPY go.mod . COPY go.sum . RUN go mod download
COPY . .
RUN go build -o main ./cmd/main.go
EXPOSE 8080
CMD ["./main"]
|
构建并推送镜像
1 2
| docker build -t your_dockerhub_username/bookmark-platform:latest . docker push your_dockerhub_username/bookmark-platform:latest
|
部署到服务器
- 使用 Docker Compose 或 Kubernetes 进行编排。
- 配置环境变量和秘密管理。
3. 持续集成与部署(CI/CD)
4. 监控与日志
- 监控:使用 Prometheus 和 Grafana 监控应用性能和资源使用。
- 日志管理:集中日志管理,如 ELK Stack(Elasticsearch, Logstash, Kibana)或使用云服务提供商的日志解决方案。
七、测试
1. 单元测试
使用 Go 内置的 testing
包编写单元测试。测试各个功能模块,如用户注册、登录、书签管理等。
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
| package handlers
import ( "testing" "net/http/httptest" "github.com/gin-gonic/gin" "net/http" "bytes" "encoding/json" )
func TestRegister(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.Default() router.POST("/register", Register)
payload := RegisterInput{ Username: "testuser", Email: "[email protected]", Password: "password123", }
body, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", "/register", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") resp := httptest.NewRecorder() router.ServeHTTP(resp, req)
if resp.Code != http.StatusOK && resp.Code != http.StatusSeeOther { t.Errorf("Expected status 200 or 303, got %d", resp.Code) } }
|
2. 集成测试
测试 API 端点与数据库的集成。使用工具如 Postman 或 Insomnia 进行手动测试,或编写自动化测试脚本。
3. 前端测试
虽然使用原生 HTML,仍可使用工具如 Selenium 进行端到端测试,确保用户界面的功能正确性。
八、优化与扩展
1. 性能优化
- 数据库优化:添加索引,优化查询语句。
- 缓存:使用 Redis 等缓存常用数据,减少数据库负载。
- CDN:为静态资源使用 CDN 加速。
2. 功能扩展
- 批量操作:支持批量添加、编辑、删除书签。
- 标签管理:提供更丰富的标签功能,如标签建议、自动补全。
- 导入与导出:允许用户导入浏览器书签,导出为 HTML 或 JSON 格式。
- 书签分享:支持生成公开或私密的书签链接,方便分享。
- 多语言支持:国际化(i18n),支持多语言界面。
3. 用户体验提升
- 书签预览:在书签列表中显示网站预览或截图。
- 拖拽排序:用户可以通过拖拽调整书签的顺序。
- 快捷键:提供快捷键,提高用户操作效率。
九、示例代码
以下是一个简单的用户注册和登录的完整示例,基于 Gin 和 GORM,并使用 Go 的模板引擎进行前端渲染。
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
| package main
import ( "your_project/internal/handlers" "your_project/internal/middleware" "your_project/internal/utils"
"github.com/gin-gonic/gin" )
func main() { utils.LoadConfig() utils.InitDB()
r := gin.Default()
r.LoadHTMLGlob("templates/*")
r.Static("/static", "./static")
r.GET("/", handlers.ShowHomePage) r.GET("/register", handlers.ShowRegisterPage) r.POST("/register", handlers.Register) r.GET("/login", handlers.ShowLoginPage) r.POST("/login", handlers.Login)
authorized := r.Group("/") authorized.Use(middleware.AuthMiddleware()) { authorized.GET("/bookmarks", handlers.GetBookmarks) authorized.POST("/bookmarks", handlers.CreateBookmark) authorized.POST("/bookmarks/update/:id", handlers.UpdateBookmark) authorized.POST("/bookmarks/delete/:id", handlers.DeleteBookmark) authorized.GET("/logout", handlers.Logout) }
r.Run(":8080") }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package models
import ( "time" )
type User struct { ID uint `gorm:"primaryKey" json:"id"` Username string `gorm:"unique;not null" json:"username"` Email string `gorm:"unique;not null" json:"email"` Password string `gorm:"not null" json:"-"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package models
import ( "time" )
type Bookmark struct { ID uint `gorm:"primaryKey" json:"id"` UserID uint `gorm:"not null" json:"user_id"` Title string `gorm:"not null" json:"title"` URL string `gorm:"not null" json:"url"` Description string `json:"description,omitempty"` Folder string `json:"folder,omitempty"` Tags string `json:"tags,omitempty"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` }
|
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
| package utils
import ( "fmt" "log" "os"
"your_project/internal/models"
"gorm.io/driver/mysql" "gorm.io/gorm" )
var DB *gorm.DB
func InitDB() { dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_HOST"), os.Getenv("DB_NAME"), ) var err error DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { log.Fatal("Failed to connect to database: ", err) }
err = DB.AutoMigrate(&models.User{}, &models.Bookmark{}) if err != nil { log.Fatal("Failed to migrate database: ", err) } }
|
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
| package handlers
import ( "net/http" "time"
"github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" "github.com/dgrijalva/jwt-go"
"your_project/internal/models" "your_project/internal/utils" )
type RegisterInput struct { Username string `form:"username" binding:"required"` Email string `form:"email" binding:"required,email"` Password string `form:"password" binding:"required,min=6"` }
func Register(c *gin.Context) { var input RegisterInput if err := c.ShouldBind(&input); err != nil { c.HTML(http.StatusBadRequest, "register.html", gin.H{ "error": err.Error(), }) return }
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost) if err != nil { c.HTML(http.StatusInternalServerError, "register.html", gin.H{ "error": "服务器错误", }) return }
user := models.User{ Username: input.Username, Email: input.Email, Password: string(hashedPassword), }
if err := utils.DB.Create(&user).Error; err != nil { c.HTML(http.StatusBadRequest, "register.html", gin.H{ "error": "邮箱或用户名已存在", }) return }
c.Redirect(http.StatusSeeOther, "/login") }
type LoginInput struct { Email string `form:"email" binding:"required,email"` Password string `form:"password" binding:"required"` }
func Login(c *gin.Context) { var input LoginInput if err := c.ShouldBind(&input); err != nil { c.HTML(http.StatusBadRequest, "login.html", gin.H{ "error": err.Error(), }) return }
var user models.User if err := utils.DB.Where("email = ?", input.Email).First(&user).Error; err != nil { c.HTML(http.StatusUnauthorized, "login.html", gin.H{ "error": "无效的凭证", }) return }
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.Password)); err != nil { c.HTML(http.StatusUnauthorized, "login.html", gin.H{ "error": "无效的凭证", }) return }
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "user_id": user.ID, "exp": time.Now().Add(time.Hour * 72).Unix(), })
tokenString, err := token.SignedString([]byte("your_secret_key")) if err != nil { c.HTML(http.StatusInternalServerError, "login.html", gin.H{ "error": "无法生成令牌", }) return }
c.SetCookie("token", tokenString, 3600*72, "/", "", false, true)
c.Redirect(http.StatusSeeOther, "/bookmarks") }
|
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
| package middleware
import ( "fmt" "net/http"
"github.com/dgrijalva/jwt-go" "github.com/gin-gonic/gin" )
func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { tokenString, err := c.Cookie("token") if err != nil { c.Redirect(http.StatusSeeOther, "/login") c.Abort() return }
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } return []byte("your_secret_key"), nil })
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { userID := uint(claims["user_id"].(float64)) c.Set("user_id", userID) c.Next() } else { c.Redirect(http.StatusSeeOther, "/login") c.Abort() 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
| package handlers
import ( "net/http"
"github.com/gin-gonic/gin" "your_project/internal/models" "your_project/internal/utils" )
type CreateBookmarkInput struct { Title string `form:"title" binding:"required"` URL string `form:"url" binding:"required,url"` Description string `form:"description"` Folder string `form:"folder"` Tags string `form:"tags"` }
func CreateBookmark(c *gin.Context) { var input CreateBookmarkInput if err := c.ShouldBind(&input); err != nil { c.HTML(http.StatusBadRequest, "bookmarks.html", gin.H{ "error": err.Error(), }) return }
userID := c.MustGet("user_id").(uint)
bookmark := models.Bookmark{ UserID: userID, Title: input.Title, URL: input.URL, Description: input.Description, Folder: input.Folder, Tags: input.Tags, }
if err := utils.DB.Create(&bookmark).Error; err != nil { c.HTML(http.StatusInternalServerError, "bookmarks.html", gin.H{ "error": "创建书签失败", }) return }
c.Redirect(http.StatusSeeOther, "/bookmarks") }
func GetBookmarks(c *gin.Context) { userID := c.MustGet("user_id").(uint) var bookmarks []models.Bookmark if err := utils.DB.Where("user_id = ?", userID).Find(&bookmarks).Error; err != nil { c.HTML(http.StatusInternalServerError, "bookmarks.html", gin.H{ "error": "获取书签失败", }) return }
c.HTML(http.StatusOK, "bookmarks.html", gin.H{ "bookmarks": bookmarks, }) }
func UpdateBookmark(c *gin.Context) { var input CreateBookmarkInput if err := c.ShouldBind(&input); err != nil { c.HTML(http.StatusBadRequest, "bookmarks.html", gin.H{ "error": err.Error(), }) return }
userID := c.MustGet("user_id").(uint) bookmarkID := c.Param("id")
var bookmark models.Bookmark if err := utils.DB.Where("id = ? AND user_id = ?", bookmarkID, userID).First(&bookmark).Error; err != nil { c.HTML(http.StatusNotFound, "bookmarks.html", gin.H{ "error": "未找到书签", }) return }
bookmark.Title = input.Title bookmark.URL = input.URL bookmark.Description = input.Description bookmark.Folder = input.Folder bookmark.Tags = input.Tags
if err := utils.DB.Save(&bookmark).Error; err != nil { c.HTML(http.StatusInternalServerError, "bookmarks.html", gin.H{ "error": "更新书签失败", }) return }
c.Redirect(http.StatusSeeOther, "/bookmarks") }
func DeleteBookmark(c *gin.Context) { userID := c.MustGet("user_id").(uint) bookmarkID := c.Param("id")
if err := utils.DB.Where("id = ? AND user_id = ?", bookmarkID, userID).Delete(&models.Bookmark{}).Error; err != nil { c.HTML(http.StatusInternalServerError, "bookmarks.html", gin.H{ "error": "删除书签失败", }) return }
c.Redirect(http.StatusSeeOther, "/bookmarks") }
|
十、下一步建议
- 最小可行产品(MVP):先实现核心功能,如用户注册、登录和基本的书签管理,快速上线并获取用户反馈。
- 用户反馈:通过调查问卷、用户测试等方式收集用户反馈,了解他们的需求和痛点。
- 迭代开发:根据反馈持续优化和添加新功能,如高级搜索、书签分享、自动分类等。
- 市场推广:通过社交媒体、内容营销、SEO 等方式推广平台,吸引更多用户。
- 安全审计:定期进行安全审计,确保用户数据的安全和隐私。
结语
使用原生 HTML 进行前端开发可以简化项目结构,减少依赖,适合初期开发和小型项目。通过合理的服务器端渲染和模板设计,您可以构建一个功能丰富且用户友好的在线书签整理平台。保持代码的整洁和模块化,确保后续的维护和扩展更加便捷。如果在开发过程中遇到具体问题或需要进一步的指导,请随时向我咨询!