博客标签功能是实现文章分类展示的核心模块,通过标签可以将同主题的文章聚合在一起,方便用户快速定位感兴趣的内容。使用Golang实现该功能需要处理好文章与标签的多对多关系,同时完成对应的接口开发和前端展示逻辑。

数据库表结构设计
文章和标签是典型的多对多关系,一篇文章可以属于多个标签,一个标签也可以关联多篇文章,因此需要设计三张表:
- 文章表:存储文章的基础信息
- 标签表:存储标签的基础信息
- 文章标签关联表:存储文章和标签的对应关系
下面是具体的表结构SQL定义:
-- 文章表
CREATE TABLE article (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(200) NOT NULL COMMENT '文章标题',
content TEXT NOT NULL COMMENT '文章内容',
create_time DATETIME NOT NULL COMMENT '创建时间'
);
-- 标签表
CREATE TABLE tag (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL UNIQUE COMMENT '标签名称'
);
-- 文章标签关联表
CREATE TABLE article_tag (
article_id INT NOT NULL COMMENT '文章ID',
tag_id INT NOT NULL COMMENT '标签ID',
PRIMARY KEY (article_id, tag_id),
FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tag(id) ON DELETE CASCADE
);
核心结构体定义
在Golang中需要定义对应的结构体来映射数据库表,同时方便后续的数据操作:
package model
import "time"
// 文章结构体
type Article struct {
ID int `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
CreateTime time.Time `json:"create_time"`
Tags []Tag `json:"tags"` // 关联标签列表
}
// 标签结构体
type Tag struct {
ID int `json:"id"`
Name string `json:"name"`
}
// 文章标签关联结构体
type ArticleTag struct {
ArticleID int `json:"article_id"`
TagID int `json:"tag_id"`
}
标签与文章关联逻辑实现
新增文章时绑定标签
新增文章时需要同时处理标签的创建和关联关系,逻辑如下:先插入文章记录,再处理标签,如果标签不存在则先创建,最后插入关联记录。
package service
import (
"database/sql"
"errors"
"time"
"your_project/model"
)
type ArticleService struct {
DB *sql.DB
}
// 新增文章并绑定标签
func (s *ArticleService) CreateArticle(title, content string, tagNames []string) (int, error) {
// 开启事务
tx, err := s.DB.Begin()
if err != nil {
return 0, err
}
defer tx.Rollback()
// 插入文章
result, err := tx.Exec("INSERT INTO article (title, content, create_time) VALUES (?, ?, ?)",
title, content, time.Now())
if err != nil {
return 0, err
}
articleID, err := result.LastInsertId()
if err != nil {
return 0, err
}
// 处理标签和关联
for _, tagName := range tagNames {
// 查询标签是否存在
var tagID int
err := tx.QueryRow("SELECT id FROM tag WHERE name = ?", tagName).Scan(&tagID)
if err != nil && err != sql.ErrNoRows {
return 0, err
}
// 标签不存在则创建
if err == sql.ErrNoRows {
res, err := tx.Exec("INSERT INTO tag (name) VALUES (?)", tagName)
if err != nil {
return 0, err
}
tagID64, err := res.LastInsertId()
if err != nil {
return 0, err
}
tagID = int(tagID64)
}
// 插入关联记录
_, err = tx.Exec("INSERT INTO article_tag (article_id, tag_id) VALUES (?, ?)", articleID, tagID)
if err != nil {
return 0, err
}
}
// 提交事务
if err := tx.Commit(); err != nil {
return 0, err
}
return int(articleID), nil
}
根据标签查询文章列表
查询某个标签下的所有文章时,需要通过关联表进行连表查询:
// 根据标签ID查询文章列表
func (s *ArticleService) GetArticlesByTagID(tagID int) ([]model.Article, error) {
rows, err := s.DB.Query(`
SELECT a.id, a.title, a.content, a.create_time
FROM article a
JOIN article_tag at ON a.id = at.article_id
WHERE at.tag_id = ?
ORDER BY a.create_time DESC
`, tagID)
if err != nil {
return nil, err
}
defer rows.Close()
var articles []model.Article
for rows.Next() {
var article model.Article
err := rows.Scan(&article.ID, &article.Title, &article.Content, &article.CreateTime)
if err != nil {
return nil, err
}
// 查询文章对应的所有标签
tagRows, err := s.DB.Query(`
SELECT t.id, t.name
FROM tag t
JOIN article_tag at ON t.id = at.tag_id
WHERE at.article_id = ?
`, article.ID)
if err != nil {
return nil, err
}
defer tagRows.Close()
for tagRows.Next() {
var tag model.Tag
tagRows.Scan(&tag.ID, &tag.Name)
article.Tags = append(article.Tags, tag)
}
articles = append(articles, article)
}
return articles, nil
}
接口开发与前端展示
后端接口实现
使用Golang标准库的net/http包开发简单的接口,提供标签文章查询能力:
package main
import (
"encoding/json"
"net/http"
"strconv"
"your_project/service"
)
func main() {
db := initDB() // 初始化数据库连接的函数,需自行实现
articleService := &service.ArticleService{DB: db}
// 根据标签ID查询文章的接口
http.HandleFunc("/api/articles/by-tag", func(w http.ResponseWriter, r *http.Request) {
tagIDStr := r.URL.Query().Get("tag_id")
if tagIDStr == "" {
http.Error(w, "缺少tag_id参数", http.StatusBadRequest)
return
}
tagID, err := strconv.Atoi(tagIDStr)
if err != nil {
http.Error(w, "tag_id参数格式错误", http.StatusBadRequest)
return
}
articles, err := articleService.GetArticlesByTagID(tagID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(articles)
})
http.ListenAndServe(":8080", nil)
}
前端展示逻辑
前端可以通过请求接口获取标签对应的文章列表,然后渲染到页面中,示例代码如下:
// 获取URL中的标签ID参数
function getTagID() {
const params = new URLSearchParams(window.location.search);
return params.get('tag_id');
}
// 请求接口并渲染文章列表
async function loadArticlesByTag() {
const tagID = getTagID();
if (!tagID) {
document.getElementById('article-list').innerHTML = '未指定标签';
return;
}
try {
const res = await fetch(`/api/articles/by-tag?tag_id=${tagID}`);
const articles = await res.json();
const listContainer = document.getElementById('article-list');
if (articles.length === 0) {
listContainer.innerHTML = '该标签下暂无文章';
return;
}
let html = '';
articles.forEach(article => {
html += `
<div class="article-item">
<h3>${article.title}</h3>
<p>发布时间:${new Date(article.create_time).toLocaleString()}</p>
<p>所属标签:${article.tags.map(tag => tag.name).join('、')}</p>
<a href="/article?id=${article.id}">查看详情</a>
</div>
`;
});
listContainer.innerHTML = html;
} catch (err) {
console.error('加载文章失败:', err);
}
}
// 页面加载完成后执行
window.onload = loadArticlesByTag;
注意事项
- 操作文章和标签的关联时建议使用事务,避免数据不一致的问题
- 标签名称建议添加唯一索引,防止重复创建相同标签
- 查询文章列表时如果数据量较大,需要添加分页逻辑,避免一次性返回过多数据
- 前端展示时可以对标签进行高亮处理,提升用户体验