【Go学习实战】03-3-文章评论及写文章

【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.Writingwr := 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.Titlewr.CdnURL = config.Cfg.System.CdnURLcategorys, err := dao.GetAllCategory()if err != nil {log.Printf("查询分类异常: %v", err)return}wr.Categorys = categorysreturn
}

查看页面

因为要加载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.Methodswitch 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(&params)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 := 0if 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.Posterr := 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.PathpIdStr := 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 := 0if params["type"] != nil {postType, _ = params["type"].(int)}post.CategoryId = categoryIdpost.Content = contentpost.Markdown = markdownpost.Slug = slugpost.Title = titlepost.Type = postTypepost.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) {// 上传成功,回调并回显 URLcb(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 apiimport ("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 IDaccessKeySecret = config.Cfg.System.AccessKeySecret // 你的 AccessKey Secretendpoint        = config.Cfg.System.Endpoint        // OSS EndpointbucketName      = 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 Bucketbucket, err := client.Bucket(bucketName)if err != nil {http.Error(w, "无法访问 OSS Bucket", http.StatusInternalServerError)return}// 解析请求中的表单数据err = r.ParseMultipartForm(10 << 20) // 限制最大文件大小为 10MBif 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)// 将文件上传到 OSSerr = bucket.PutObject(fileName, file)if err != nil {http.Error(w, "文件上传到 OSS 失败", http.StatusInternalServerError)return}// 生成文件的 URLimageURL := 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)
}

测试

成功

在这里插入图片描述

查看阿里云,阿里云也上传成功

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/32645.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

02 | 快速部署 fastgo 项目

提示&#xff1a; 所有体系课见专栏&#xff1a;Go 项目开发极速入门实战课&#xff1b;欢迎加入 云原生 AI 实战 星球&#xff0c;12 高质量体系课、20 高质量实战项目助你在 AI 时代建立技术竞争力&#xff08;聚焦于 Go、云原生、AI Infra&#xff09;。 在学习一个开源项目…

[Linux] 3588开发准备工作

背景需求 在3588上开发软件系统&#xff0c;用于视频流读取&#xff0c;处理&#xff0c;推流等操作。一般来说&#xff0c;会先买对应型号的开发板进行开发测试。同步制作硬件&#xff0c;等到硬件回来之后&#xff0c;可将代码进行烧录到嵌入式板端&#xff0c;能够执行相应…

stm32-RTC时实时钟

1. 时间戳的基本概念 定义&#xff1a; 时间戳是一个表示时间的标记&#xff0c;它通常以数字的形式出现&#xff0c;代表某一时刻距离参考时刻的时间差。最常见的是 Unix 时间戳&#xff0c;它表示从1970年1月1日 00:00:00 UTC 到某个时刻所经过的秒数&#xff08;或毫秒、微秒…

【操作系统安全】任务1:操作系统部署

目录 一、VMware Workstation Pro 17 部署 二、VMware Workstation 联网方式 三、VMware 虚拟机安装流程 四、操作系统介绍 五、Kali 操作系统安装 六、Windows 系统安装 七、Windows 系统网络配置 八、Linux 网络配置 CSDN 原创主页&#xff1a;不羁https://blog.csd…

JavaScript读取当前URL字符串中参数的两种方法

方法1 使用location.search属性&#xff0c;location.search属性会返回URL的问号后面的参数字符串 const querystr window.location.search; const params new URLSearchParams(querystr); //例如&#xff1a;URL为 index.html?Id123&Nametest //要获取参数名Id的值…

DeepSeek-进阶版部署(Linux+GPU)

前面几个小节讲解的Win和Linux部署DeepSeek的比较简单的方法&#xff0c;而且采用的模型也是最小的&#xff0c;作为测试体验使用是没问题的。如果要在生产环境使用还是需要用到GPU来实现&#xff0c;下面我将以有一台带上GPU显卡的Linux机器来部署DeepSeek。这里还只是先体验单…

MySQL的安装与建表

目录 一&#xff0c;MySQL 安装部署 1.1 版本及下载 1.2安装过程 二&#xff0c;数据库的建立 1&#xff0c;employees 表的建立 2&#xff0c;orders表的建立​ 3&#xff0c;involces表的建立 一&#xff0c;MySQL 安装部署 1.1 版本及下载 MySQL官网地址&#xff1a…

Spring 框架学习

技术体系结构 总体技术体系 单一架构 一个项目&#xff0c;一个工程&#xff0c;导出为一个 war 包&#xff0c;在一个 Tomcat 上运行&#xff0c;也叫 all in one。 单一架构&#xff0c;项目主要应用技术框架为&#xff1a;Spring、SpringMVC 、Mybatis。 分布式架构 一个…

G-Star 公益行起航,挥动开源技术点亮公益!

公益组织&#xff0c;一直是社会温暖的传递者&#xff0c;但在数字化浪潮中&#xff0c;也面临着诸多比大众想象中复杂的挑战&#xff1a;项目管理如何更高效&#xff1f;志愿者管理又该如何创新&#xff1f;宣传推广怎么才能更有影响力&#xff1f;内部管理和技术支持又该如何…

STM32-Unix时间戳

一&#xff1a;什么是时间戳 Unix时间戳&#xff08;Unix Timestamp&#xff09;是一个计数器数值&#xff0c;这个数值表示的是一个从1970年1月1日0时0分0秒开始到现在所经过的秒数&#xff0c;不考虑闰秒。 时间戳存储在一个秒计数器里&#xff0c;秒计数器为32位/64位的整…

zsh: command not found: adb 报错问题解决

哈喽小伙伴们大家好&#xff0c;我是小李&#xff0c;今天&#xff0c;我满怀信心想要在本地跑一下pda,然而&#xff0c; what? 居然报错了&#xff01;&#xff01;别逗我啊&#xff01; 好吧&#xff0c;究其原因&#xff1a;没有配置好sdk 那就配呗。 首先&#xff0c;…

嵌入式八股C语言---面向对象篇

面向对象与面向过程 面向过程 就是把整个业务逻辑分成多个步骤,每步或每一个功能都可以使用一个函数来实现面向对象 对象是类的实例化,此时一个类就内部有属性和相应的方法 封装 在C语言里实现封装就是实现一个结构体,里面包括的成员变量和函数指针,然后在构造函数中,为结构体…

Linux_17进程控制

前提回顾&#xff1a; 页表可以将无序的物理地址映射为有序的; 通过进程地址空间&#xff0c;避免将内存直接暴漏给操作系统&#xff1b; cr3寄存器存放的有当前运行进程的页表的物理地址&#xff1b; 一、查看命令行参数和环境变量的地址 因为命令行参数和环境变量都是字符…

NVIDIA k8s-device-plugin源码分析与安装部署

在《kubernetes Device Plugin原理与源码分析》一文中&#xff0c;我们从源码层面了解了kubelet侧关于device plugin逻辑的实现逻辑&#xff0c;本文以nvidia管理GPU的开源github项目k8s-device-plugin为例&#xff0c;来看看设备插件侧的实现示例。 一、Kubernetes Device Pl…

MySql索引下推(ICP)是什么?有什么用?

目录 基本介绍为什么需要索引下推&#xff1f;未引入ICP&#xff08;x&#xff09;引入ICP&#xff08;√&#xff09; 如何指导sql优化适用场景sql优化 基本介绍 索引下推&#xff08;Index Condition Pushdown, ICP&#xff09;&#xff0c;是MySQL5.6 引入的优化技术&#…

用户可免费体验!国家超算互联网平台上线阿里开源推理模型接口服

近日&#xff0c;国家超算互联网平台上线阿里巴巴开源推理模型QwQ-32B API接口服务&#xff0c;现在用户可获得免费的100万Tokens。基于国产深算智能加速卡以及全国一体化算力网&#xff0c;平台支持海量用户便捷调用QwQ-32B、DeepSeek-R1等国产开源大模型的接口服务。 了解QwQ…

大数据学习(63)- Zookeeper详解

&&大数据学习&& &#x1f525;系列专栏&#xff1a; &#x1f451;哲学语录: 用力所能及&#xff0c;改变世界。 &#x1f496;如果觉得博主的文章还不错的话&#xff0c;请点赞&#x1f44d;收藏⭐️留言&#x1f4dd;支持一下博主哦&#x1f91e; &#x1f…

【蓝桥杯python研究生组备赛】003 贪心

题目1 股票买卖 给定一个长度为 N 的数组&#xff0c;数组中的第 i 个数字表示一个给定股票在第 i 天的价格。 设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易&#xff08;多次买卖一支股票&#xff09;。 注意&#xff1a;你不能同时参与多笔交易&…

mmdet3d.models.utils的clip_sigmoid理解

Sigmoid 函数 标准的 sigmoid 函数定义为&#xff1a; 容易得出结论&#xff1a; 取值范围(0, 1) clip_sigmoid 是在标准的 sigmoid 函数基础上进行 裁剪&#xff08;clip&#xff09;&#xff0c;即对 sigmoid 输出的结果加以限制&#xff0c;避免其超出特定范围。 import …