【Go学习实战】03-3-文章评论及写文章
- 文章评论
- 注册valine
- 获取凭证
- 加载评论页面
- 写文章
- 修改cdn位置
- 完善功能
- 查看页面
- 发布文章
- POST发布文章
- 发布文章测试
- 查询文章详情
- 查询详情测试
- 修改文章
- 修改文章测试
- 写文章图片上传
- 前端
- 后端逻辑
- 测试
文章评论
这里我们的博客因为是个轻量级的博客,所以评论系统我们选择Valine来作为我们的评论系统,我们只需要配置好对应的appid及appkey,之后调用api就可以进行使用。
注册valine
获取凭证
创建应用
点击应用凭证
将其配置到我们的config.toml中
配置好后重新加载
记得修改html页面的valine的cdn配置,使用官方的cdn
<script src="https://cdn.jsdelivr.net/npm/valine@latest/dist/Valine.min.js"></script>
加载评论页面
成功加载到评论,我们再新添几个评论
这里我们可以学习到评论一般都是放在mongodb的,因为这种评论是非结构化的,可能有图片,可能有文字,甚至可能有语音或者视频,并且评论的增长量也是非常恐怖的,所以我们可以把评论放在mongodb中进行存储。
写文章
修改cdn位置
在write.html中
<script src="{{.CdnURL}}/js/cos-js-sdk-v5.min.js"></script>
改为
<script src="https://cdn.jsdelivr.net/npm/cos-js-sdk-v5/dist/cos-js-sdk-v5.min.js"></script>
完善功能
分配路由:
写作请求的url是http://localhost:8080/writing,我们也要对其分配路由,因为是页面,所以是view下的
http.HandleFunc("/writing", views.HTML.Writing)
在views中完善我们的功能
创建对应的解析路径和接口
接口
type HTMLRenderer interface {
Index(w http.ResponseWriter, r *http.Request)
Category(w http.ResponseWriter, r *http.Request)
Login(w http.ResponseWriter, r *http.Request)
Detail(w http.ResponseWriter, r *http.Request)
Writing(w http.ResponseWriter, r *http.Request)
}
解析路径,创建writing.go
func (*HTMLApi) Writing(w http.ResponseWriter, r *http.Request) {
writing := common.Template.Writing
wr := service.Writing()
writing.WriteData(w, wr)
}
这个数据我们要定义一下,我们对照写的前端完善一下要传回取页面的数据,在models/article.go中
type WriteRes struct {
Title string `json:"title"`
CdnURL string `json:"cdnURL"`
Categorys []Category `json:"categorys"`
}
完善service层,在service/detail.go中
func Writing() (wr models.WriteRes) {
wr.Title = config.Cfg.Viewer.Title
wr.CdnURL = config.Cfg.System.CdnURL
categorys, err := dao.GetAllCategory()
if err != nil {
log.Printf("查询分类异常: %v", err)
return
}
wr.Categorys = categorys
return
}
查看页面
因为要加载cdn中的css样式文件,我们使用国内的镜像cdn
// 使用国外的CDN,加载速度有时会很慢,或者自定义URL
// You can custom KaTeX load url.
editormd.katexURL = {
css : "//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min",
js : "//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min"
};
修改为
editormd.katexURL = {
css: "https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.css",
js: "https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.js"
};
因为css样式文件都在本地,所以我们直接把CDN的url指向本地就好,url为resource,然后路由到public/resource就好
[system]
CdnURL = "/resource"
我们的编辑器使用的markdown
发布文章
我们写完了之后点击发布请求的url是http://localhost:8080/api/v1/post,我们也要对其分配路由
http.HandleFunc("/api/v1/post", api.API.SaveAndUpdatePost)
接口
type APIResponder interface {
SaveAndUpdatePost(w http.ResponseWriter, r *http.Request)
Login(w http.ResponseWriter, r *http.Request)
}
因为是返回json,所以是api下的
因为我们后期不仅有发布文章POST还是修改文章PUT,我们要分别处理
因为我们不管在什么时候都要验证下用户是否登录,所以要检查token
//判断用户是否登录
token := r.Header.Get("Authorization")
if token == "" {
log.Printf("SaveAndUpdatePost的token为空")
return
}
//解析token
_,claim,err:=utils.ParseToken(token)
if err!=nil{
common.ErrorResult(w,errors.New("token解析失败"))
return
}
uid:=claim.Uid
所以我们通过对r *http.Request的解析获取他的Method,再根据是什么样的请求类型分别进行处理
method := r.Method
switch method {
case http.MethodPost:
params, err := common.GetRequestJsonParam(r)
if err != nil {
log.Printf("SaveAndUpdatePost的POST解析请求参数异常:%v", err)
return
}
POST发布文章
因为发起的是post请求,所以我们还是要解析表单,就要用到我们之前在common写的解析表单的方法
func GetRequestJsonParam(r *http.Request) (map[string]interface{}, error) {
var params map[string]interface{}
// 使用 json.NewDecoder 来逐步解码请求体
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(¶ms)
if err != nil {
log.Printf("解析请求参数失败:%v", err)
return nil, err
}
return params, nil
}
我们一般保存后都会返回一个文章的pid
func SavePost(post *models.Post) {
res, err := DB.Exec("insert into blog_post(title, content, markdown, category_id, user_id, view_count, type, slug,create_at,update_at) values(?,?,?,?,?,?,?,?,?,?)",
post.Title, post.Content, post.Markdown, post.CategoryId, post.UserId, post.ViewCount, post.Type, post.Slug, post.CreateAt, post.UpdateAt)
if err != nil {
log.Printf("保存文章失败: %v", err)
}
id, _ := res.LastInsertId()
post.Pid = int(id)
}
service层
func SavePost(post *models.Post) {
dao.SavePost(post)
}
再上层
case http.MethodPost:
params, err := common.GetRequestJsonParam(r)
if err != nil {
log.Printf("SaveAndUpdatePost的POST解析请求参数异常:%v", err)
return
}
cId := params["categoryId"].(string)
categoryId, _ := strconv.Atoi(cId)
content := params["content"].(string)
markdown := params["markdown"].(string)
slug := params["slug"].(string)
title := params["title"].(string)
postType := 0
if params["type"] != nil {
postType, _ = params["type"].(int)
}
post := &models.Post{
CategoryId: categoryId,
Content: content,
Markdown: markdown,
Slug: slug,
Title: title,
Type: postType,
UserId: uid,
Pid: -1,
ViewCount: 0,
CreateAt: time.Now(),
UpdateAt: time.Now(),
}
service.SavePost(post)
common.SuccessResult(w, post)
发布文章测试
成功插入数据库
查询文章详情
我们写完了之后点击发布请求的url是http://localhost:8080/api/v1/post/29,是个GET请求,我们也要对其分配路由
http.HandleFunc("/api/v1/post/", api.API.GetPost)
接口
type APIResponder interface {
SaveAndUpdatePost(w http.ResponseWriter, r *http.Request)
Login(w http.ResponseWriter, r *http.Request)
GetPost(w http.ResponseWriter, r *http.Request)
}
查看其js
function getArticleItem(id) {
$.ajax({
url: "/api/v1/post/" + id,
type: "GET",
contentType: "application/json",
success: function (res) {
if (res.code != 200) {
initEditor();
return alert(res.error);
}
ArticleItem = res.data || {};
initActive();
initEditor();
},
beforeSend: setAjaxToken,
});
}
我们最后直接返回post到ArticleItem就可以了
dao层我们已经写过了
func GetPostById(pid int) (*models.Post, error) {
row := DB.QueryRow("select * from blog_post where pid = ?", pid)
var post models.Post
err := row.Scan(&post.Pid, &post.Title, &post.Content, &post.Markdown, &post.CategoryId, &post.UserId, &post.ViewCount, &post.Type, &post.Slug, &post.CreateAt, &post.UpdateAt)
if err != nil {
return nil, err
}
return &post, nil
}
service
func GetPostById(pid int) (*models.Post, error) {
return dao.GetPostById(pid)
}
api,因为这是个get请求,我们直接截断找id就好
func (*Api) GetPost(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
pIdStr := strings.TrimPrefix(path, "/api/v1/post/")
pid, err := strconv.Atoi(pIdStr)
if err != nil {
common.ErrorResult(w, errors.New("GetPost不识别此请求路径"))
return
}
post, err := service.GetPostById(pid)
if err != nil {
common.ErrorResult(w, errors.New("GetPost查询出错"))
return
}
common.SuccessResult(w, post)
}
查询详情测试
修改文章
如果在已存在的文章进行修改,那么我们这请求就变成了PUT,所以也要对其进行路由,因为我们之前已经把post写了,我们这里只用完善下put就好
在api/post.go#SaveAndUpdatePost中
case http.MethodPut:
params, err := common.GetRequestJsonParam(r)
if err != nil {
log.Printf("SaveAndUpdatePost的PUT解析请求参数异常:%v", err)
return
}
pid := params["pid"].(float64)
pId := int(pid)
post, _ := service.GetPostById(pId)
if post == nil {
common.ErrorResult(w, errors.New("SaveAndUpdatePost查询不到文章"))
return
}
cId := params["categoryId"].(string)
categoryId, _ := strconv.Atoi(cId)
content := params["content"].(string)
markdown := params["markdown"].(string)
slug := params["slug"].(string)
title := params["title"].(string)
postType := 0
if params["type"] != nil {
postType, _ = params["type"].(int)
}
post.CategoryId = categoryId
post.Content = content
post.Markdown = markdown
post.Slug = slug
post.Title = title
post.Type = postType
post.UpdateAt = time.Now()
service.UpdatePost(post)
common.SuccessResult(w, post)
dao层
func UpdatePost(post *models.Post) {
_, err := DB.Exec("update blog_post set title=?, content=?, markdown=?, category_id=?, user_id=?, view_count=?, type=?, slug=?, update_at=? where pid=?",
post.Title, post.Content, post.Markdown, post.CategoryId, post.UserId, post.ViewCount, post.Type, post.Slug, post.UpdateAt, post.Pid)
if err != nil {
log.Printf("更新文章失败: %v", err)
}
}
service
func UpdatePost(post *models.Post) {
dao.UpdatePost(post)
}
修改文章测试
查看数据库
写文章图片上传
前端
修改我们的前端符合我们的要求
imageUploadCalback: function (files, cb) {
let formData = new FormData();
formData.append("file", files[0]);
fetch("/api/v1/upload/oss", { // 这里是后端的文件上传接口
method: "POST",
body: formData
})
.then(response => response.json())
.then(data => {
if (data.code === 200) {
// 上传成功,回调并回显 URL
cb(data.data.url); // 将返回的 URL 传递给编辑器
// 可以根据需求在其他地方显示该图片
console.log("上传成功,图片 URL:", data.data.url);
} else {
// 上传失败,处理错误
alert("上传失败:" + (data.error || "未知错误"));
}
})
.catch(error => {
console.error("上传失败", error);
alert("上传失败,请重试!");
});
},
后端逻辑
因为我们请求的地址是/api/v1/upload/oss,所以也要做路由
http.HandleFunc("/api/v1/upload/oss", api.API.UploadImage)
接口
type APIResponder interface {
SaveAndUpdatePost(w http.ResponseWriter, r *http.Request)
Login(w http.ResponseWriter, r *http.Request)
GetPost(w http.ResponseWriter, r *http.Request)
UploadImage(w http.ResponseWriter, r *http.Request)
}
接下来写我们api的逻辑
package api
import (
"fmt"
"math/rand"
"myWeb/common"
"myWeb/config"
"net/http"
"time"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
)
// 阿里云 OSS 配置
var (
accessKeyID = config.Cfg.System.AccessKeyID // 你的 AccessKey ID
accessKeySecret = config.Cfg.System.AccessKeySecret // 你的 AccessKey Secret
endpoint = config.Cfg.System.Endpoint // OSS Endpoint
bucketName = config.Cfg.System.BucketName // 你的 Bucket 名称
)
// uploadImage 处理文件上传到阿里云 OSS
func (*Api) UploadImage(w http.ResponseWriter, r *http.Request) {
// 创建 OSS 客户端
client, err := oss.New(endpoint, accessKeyID, accessKeySecret)
if err != nil {
http.Error(w, "无法创建 OSS 客户端", http.StatusInternalServerError)
return
}
// 获取 OSS Bucket
bucket, err := client.Bucket(bucketName)
if err != nil {
http.Error(w, "无法访问 OSS Bucket", http.StatusInternalServerError)
return
}
// 解析请求中的表单数据
err = r.ParseMultipartForm(10 << 20) // 限制最大文件大小为 10MB
if err != nil {
http.Error(w, "解析表单数据失败", http.StatusBadRequest)
return
}
// 获取文件
file, _, err := r.FormFile("file") // 获取表单中名为 "file" 的文件
if err != nil {
http.Error(w, "获取文件失败", http.StatusBadRequest)
return
}
defer file.Close()
// 获取文件的扩展名
ext := ".jpg" // 默认使用 .jpg
// 假设获取文件扩展名,这里可以根据实际情况修改
if fileHeader, _, err := r.FormFile("file"); err == nil {
ext = getFileExtension(fileHeader)
}
// 生成唯一的文件名,使用时间戳和文件扩展名
fileName := fmt.Sprintf("uploads/%d_%s", time.Now().Unix(), generateRandomString(8)+ext)
// 将文件上传到 OSS
err = bucket.PutObject(fileName, file)
if err != nil {
http.Error(w, "文件上传到 OSS 失败", http.StatusInternalServerError)
return
}
// 生成文件的 URL
imageURL := fmt.Sprintf("https://%s.%s/%s", bucketName, endpoint, fileName)
// 使用 SuccessResult 返回响应数据
common.SuccessResult(w, map[string]string{"url": imageURL})
}
// 获取文件扩展名
func getFileExtension(file interface{}) string {
// 根据实际需要从文件获取扩展名
return ".jpg" // 示例返回 jpg 后缀
}
// 生成随机字符串,用于文件名
func generateRandomString(n int) string {
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
result := make([]byte, n)
for i := range result {
result[i] = letters[rand.Intn(len(letters))]
}
return string(result)
}
测试
成功
查看阿里云,阿里云也上传成功