新人学习记和新人作业自测报告
学习内容范围和推荐文档
编程语言Go
新人作业采用Go语言进行编写,作为只会C++,在学校学过Python,略懂Java的选手,学这门语言感觉难度不大。
推荐一个学习链接给大家:
https://www.runoob.com/go/go-tutorial.html
个人感觉,Go语言是一门非常精简的代码,写法与Python有一定的相似性,引用SDK相关的库非常方便(在GoLand的支持下可以一键引用,比C++高到不知哪里去了)。但是它也有一些非常坑爹的特性(比如定义了变量就一定要用,否则会编译错误)
MySQL和Gorm
新人作业中,文章的存储和点赞数据的存储,需要采用MySQL数据库进行存储。由于新人作业采用Go语言进行编写,这意味着我们需要采用Go语言对数据库进行一系列的操作(这需要Gorm这一工具来实现)。
鄙人浅推一个MySQL的学习链接给大家:
https://www.runoob.com/sql/sql-tutorial.html
本作业中需要用到的MySQL知识包含以下内容
1,搭建MySQL环境
2,创建MySQL数据库,创建表(Table)
3,对某个表进行增,删,查(没有改)
在掌握MySQL后,可以来学习下Gorm。Gorm是Go语言对MySQL进行交互的框架。
鄙人浅推一个文档给大家GORM 基本介绍.pdf
下文为一个简单的Gorm样例代码。
package main
import (
"fmt"
"github.com/mitchellh/mapstructure"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
//注意,这个Id,Name等元素,首字母必须大写
type User struct {
Id int32 `gorm:"column:id;primary_key"`
Name string `gorm:"column:name"`
}
func AddToDB(id int32, name string) {
user := User{
Id: id,
Name: name,
}
fmt.Println("user = ", user)
DB.Table("test_table").Create(&user)
}
var DB *gorm.DB
var err error
func RemoveFromDB() {
//支持移除多个
DB.Table("test_table").Where("id<=?", 102).Delete(nil)
}
func UpdateFromDB_new() {
var need_modify = User{104, "modify2"}
DB.Table("test_table").Where("id<=104").Updates(need_modify)
}
func main() {
DB, err = gorm.Open(mysql.New(mysql.Config{
//root表示数据库账号,:和@中间是密码(这里没有设置)
//tcp中间的地址和端口表示连接的数据库的ip地址和端口
//localtest_alpahinf表示当前选择的数据库名
//其余信息喂默认信息
DSN: "root:@tcp(127.0.0.1:3306)/localtest_alphainf?charset=utf8mb4&parseTime=True&loc=Local",
}),
)
//AddToDB(1234, "alphainf")
//AddToDB(114, "beta")
//AddToDB(104, "orzlyy")
UpdateFromDB_new()
mp := make(map[string]interface{})
DB.Table("test_table").Where("id = 114").Find(mp)
now := User{}
mapstructure.Decode(mp, &now)
fmt.Println(now)
}
Redis和GoRedis
作业要求中提到我们需要用Redis记录某文章的浏览量。
我们可以把Redis理解为一个轻量级的数据库。这个数据库可以方便地记录一些数据量较小的东西(比如说某一篇文章的访问量)。
GoRedis就是利用Go语言与Redis进行交互的框架。
鄙人推两个Redis的学习链接
https://www.runoob.com/redis/redis-commands.html
这里有一份挺详细的文档:Redis&&Goredis学习
此处奉上鄙人学习Redis/GoRedis时写下的测试代码
package main
import (
"fmt"
"github.com/go-redis/redis"
)
// 声明一个全局的rdb变量
var rdb *redis.Client
var err error
// 初始化连接
func main() {
//6379是本地Redis服务的默认端口号,默认也是没有密码的
rdb = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})
//ctx := context.Background()
rdb.Set("score", 100, 0)
rdb.Incr("score")
fmt.Println(rdb.Get("alpha").Result())
//_, err = rdb.Ping().Result()
//fmt.Println(err)
}
KiteX/IDL/Thirft
这是个什么东西呢?
和学校里不同,公司里的项目通常需要多人协作来完成,这会带来以下问题:
1,模块与模块间存在调用和被调用的关系,开发者需要知道每个模块调用的规则
2,存在调用与被调用的模块之间,可能会采用不同的语言进行编写,但语言的不同不能导致交互的不能
3,模块与模块之间,为了有较好的可维护性,在一般参数外,需要额外传递一些参数(比如日志信息,错误信息等)。但如果每次都要手动地去写,既容易漏,又容易格式不规范。
因此,为解决上述问题,我们需要用KiteX来生成代码框架
这是IDL文件的编写教程:idl语法
这是KiteX的安装/使用教程:KiteX入门实践
在下文中,你可以看到本人新手作业的IDL文件,已包含注释
数据存储方案
在mysql中,新建两个表,用practice_xiefengze_articleinfo存储文章,用practice_xiefengze_articlelikeinfo存储文章点赞信息。在Redis中存储两类信息,分别为“文章当天浏览数”和“文章最近一次被浏览的日期”
MySQL
开两个MySQL的表,分别存储文章信息和文章点赞信息
表practice_xiefengze_articleinfo
CREATE TABLE `PRACTICE_XIEFENGZE_ARTICLEINFO`(
`id` INT UNSIGNED AUTO_INCREMENT,
`title` VARCHAR(100) NOT NULL,
`content` VARCHAR(256),
`author` VARCHAR(100),
`create_time` VARCHAR(100),
PRIMARY KEY (`id`)
);
破防了,如果这个自增的id要能通过脚手架返回回来,则id必须叫做id,不能叫做什么article_id之类的东西,必须叫做id(大小写无所谓),(有bug!!!!浪费我1h)
表practice_xiefengze_articlelikeinfo
CREATE TABLE `PRACTICE_XIEFENGZE_ARTICLELIKEINFO`(
`id` INT UNSIGNED AUTO_INCREMENT,
`like_user_id` INT NOT NULL,
`article_id` INT not null,
`create_time` VARCHAR(100),
PRIMARY KEY (`id`)
);pri
Redis
存储文章浏览数,其key为$articleid$ + view ,其中$articleid$表示该文章的id,浏览数用string存储
为支持存储当天浏览数,会存储上次被浏览的时间,其key为$articleid$+time,浏览时间以“年-月-日”的形式进行存储。
代码框架
代码框架含义详见下方注释,一些答疑见下方
namespace go ve.gmp.practice
//记录文章信息的结构体,将与数据库表practice_xiefengze_articleinfo交互
//注意下方这个id必须叫做id,不能叫什么articleID之类的,否则会浪费你1h
struct Article {
1: required i64 id, //记录文章ID,作为数据库主键
2: required string title, //记录文章标题
3: required string content, //记录文章内容
4: required string author, //记录作者姓名
5: required string createTime,//以字符串的形式记录下创建时间,格式为yyyy-mm-dd hh:mm:ss
}
//记录需创建文章信息的结构体
//注意:文章的ID将会自动递增,主键将由程序自动生成
//文章的创建时间将由程序自动生成
struct CreateArticleRequest {
1: required string title, //记录文章标题
2: required string content, //记录文章内容
3: required string author, //记录作者姓名
}
//记录文章列表的结构体
struct ArticleResponse {
1: required list<Article> articles, //记录文章列表
}
//记录文章点赞信息的结构体,将于数据库表practice_xiefengze_likeinfo交互
struct LikeInfo {
1: required i64 id, //记录点赞的ID,作为数据库主键
2: required i64 articleID, //记录被点赞文章的ID
3: required i64 likeUserID, //记录点赞人的ID
4: required string createTime,//以字符串的形式记录下点赞时间,格式与struct Article相同
}
//某人给某文章点赞
//文章的id和点赞时间将由代码生成
struct LikeInfoRequest {
1: required i64 articleID, //文章的ID
2: required i64 likeUserID, //点赞的用户的ID
}
//获取某日点赞数和
struct GetLikesAndViewsResponse {
1: required i64 DayViews, //当日点击量
2: required i64 Likes, //总点赞数
}
//创建文章的返回值
struct CreateArticleResponse {}
//文章列表接口的返回值
struct GetArticleListRequest {}
//请求一篇文章的结构体
//注意,该结构体将被两个服务所使用
struct GetArticleRequest{
1: required i64 articleID, //只包含所请求的文章的ID
}
//给某文章点赞或取消点赞的返回值
struct LikeArticleResponse {}
//支持的一系列服务
service ArticleService {
CreateArticleResponse CreateArticle(1: CreateArticleRequest req) //创建文章的接口
ArticleResponse GetArticleList(1: GetArticleListRequest req) //获取文章列表的接口
Article GetArticle(1: GetArticleRequest req) //给出文章id,返回给定的文章信息,每访问一次,文章访问数将自动+1
LikeArticleResponse LikeArticle(1: LikeInfoRequest req) //给某文章点赞
LikeArticleResponse DislikeArticle(1: LikeInfoRequest req) //取消给某文章点赞
GetLikesAndViewsResponse GetLikesAndViews(1: GetArticleRequest req) //给出文章id,返回给定的文章信息
}
Q:为什么传入和传出的参数都是恰好一个结构体
这是代码规范的要求,若一个接口需要传入/传出数据,则必须通过结构体进行。
有一个特别的地方:若传入/传出的参数只有一个,甚至没有传入/传出参数,也需要对其封装结构体(可以封装空结构体)
结构体的命名规范:用于参数传入的结构体通常叫做xxxRequest,用于传出的结构体通常叫做xxxResponse
功能实现
1、实现创建文章接口,记录:标题、内容、作者、创建时间
将文章基本信息(标题,内容,作者)加入到一个结构体中,并将当前时间记录进结构体。最后将该结构体加入到数据库中,即可实现文章的保存。
加入数据库后,由于id属于自增变量,在article.id中将会返回该文章获得的id。(注意一定要叫id,否则无法直接返回自增的id)
新文章创建后,还在Redis中记录文章的访问次数和最近一次访问的日期。我们通过文章的id生成Redis中存储访问次数和日期的主键(详见下方代码第20和21行),并记录初始数据。
func (s *ArticleServiceImpl) CreateArticle(ctx context.Context, req *practice.CreateArticleRequest) (resp *practice.CreateArticleResponse, err error) {
resp = practice.NewCreateArticleResponse()
//将一篇新文章写入数据库
article := practice.Article{
Author: req.Author,
Content: req.Content,
Title: req.Title,
CreateTime: time.Now().Format("2006-01-02 15:04:05"),
}
result := ArticleDB.Create(&article)
if result.Error != nil {
logs.CtxInfo(ctx, result.Error.Error())
return nil, result.Error
}
//这个返回值弄了我1h
ArticleID := toString(article.Id)
//viewTag和timeTag为两个字符串常量,用于生成不同的Key
Rdb.Set(ArticleID+viewTag, "1", 0)
Rdb.Set(ArticleID+timeTag, time.Now().Format("2006-01-02"), 0)
return resp, nil
}
2、实现获取文章列表接口,返回文章列表接口
直接与数据库交互,将整个表中的数据导出到list中即可实现
func (s *ArticleServiceImpl) GetArticleList(ctx context.Context, req *practice.GetArticleListRequest) (resp *practice.ArticleResponse, err error) {
resp = practice.NewArticleResponse()
result := ArticleDB.Find(&resp.Articles)
if result.Error != nil {
logs.CtxInfo(ctx, result.Error.Error())
return nil, result.Error
}
return resp, nil
}
3、实现获取指定文章的详细信息接口,并使用redis记录当天浏览数
获取指定文章的方法,是基于给定的文章id,在数据库中进行查询所得到。
本代码中,若执行一次指定文章的查询,将会进行访问次数的自增。
func (s *ArticleServiceImpl) GetArticle(ctx context.Context, req *practice.GetArticleRequest) (resp *practice.Article, err error) {
resp = practice.NewArticle()
result := ArticleDB.Where("id=?", req.ArticleID).Find(resp)
//fmt.Println(resp.Id, resp.Title, resp.Author)
if result.Error != nil {
return nil, result.Error
}
updateView(req.ArticleID, &ctx)//详见下文
Rdb.Incr(toString(req.ArticleID) + viewTag)
return resp, nil
}
上文中有一个updateView函数,该函数是用于检查该文章上次访问日期的函数。若上次访问该文章不是今天,则会将访问次数清零,并更新最近访问的日期。
func updateView(articleID int64, ctx *context.Context) {
strID := toString(articleID)
lastLikeTime, err := Rdb.Get(strID + timeTag).Result()
if err != nil {
logs.CtxInfo(*ctx, err.Error())
}
nowTime := time.Now().Format("2006-01-02")
//fmt.Println(lastLikeTime, nowTime)
if lastLikeTime != nowTime {
fmt.Println("enter")
Rdb.Set(strID+timeTag, nowTime, 0)
Rdb.Set(strID+viewTag, "0", 0)
}
}
4、实现点赞文章接口,记录:点赞人、点赞时间、点赞的文章;同时支持取消点赞
和创建文章具有一定的相似性,传入的参数只有点赞人的ID和被点赞文章的ID,点赞的时间也由本模块进行生成。
和创建文章不同的是,本模块会先检查该点赞人是否已经点赞了这篇文章,若被点赞过了就会报错,防止来自前端的攻击。
func (s *ArticleServiceImpl) LikeArticle(ctx context.Context, req *practice.LikeInfoRequest) (resp *practice.LikeArticleResponse, err error) {
resp = practice.NewLikeArticleResponse()
likeinfo := practice.LikeInfo{
LikeUserID: req.LikeUserID,
ArticleID: req.ArticleID,
CreateTime: time.Now().Format("2006-01-02 15:04:05"),
}
var lastLikeCnt int64
LikeDB.Where("like_user_id=? and article_id =?", req.LikeUserID, req.ArticleID).Count(&lastLikeCnt)
if lastLikeCnt != 0 {
err := errors.New("error: already like")
logs.CtxInfo(ctx, err.Error())
fmt.Println(err.Error())
return nil, err
}
result := LikeDB.Create(&likeinfo)
if result.Error != nil {
logs.CtxInfo(ctx, result.Error.Error())
return nil, result.Error
}
return resp, nil
}
取消点赞的代码和点赞的代码较为相似,同样有非法操作排除机制。
func (s *ArticleServiceImpl) DislikeArticle(ctx context.Context, req *practice.LikeInfoRequest) (resp *practice.LikeArticleResponse, err error) {
resp = practice.NewLikeArticleResponse()
var lastLikeCnt int64
LikeDB.Where("like_user_id=? and article_id =?", req.LikeUserID, req.ArticleID).Count(&lastLikeCnt)
if lastLikeCnt == 0 {
err := errors.New("error: no like")
logs.CtxInfo(ctx, err.Error())
fmt.Println(err.Error())
return nil, err
}
result := LikeDB.Where("like_user_id=? and article_id =?", req.LikeUserID, req.ArticleID).Delete(nil)
//result := LikeDB.Delete(&likeinfo)
if result.Error != nil {
logs.CtxInfo(ctx, result.Error.Error())
return nil, result.Error
}
return resp, nil
}
5、实现获取指定文章的点赞数和当天浏览数接口
点赞数的查询,直接通过在表中查询符合条件的点赞记录的数量即可
当天浏览数,直接在Redis中查询出符合条件的内容即可(记得UpdateView)
func (s *ArticleServiceImpl) GetLikesAndViews(ctx context.Context, req *practice.GetArticleRequest) (resp *practice.GetLikesAndViewsResponse, err error) {
resp = practice.NewGetLikesAndViewsResponse()
//var likeCnt int64
LikeDB.Where("article_id =?", req.ArticleID).Count(&resp.Likes)
updateView(req.ArticleID, &ctx)
dayViewStr, err := Rdb.Get(toString(req.ArticleID) + viewTag).Result()
if err != nil {
logs.CtxInfo(ctx, err.Error())
}
fmt.Sscanln(dayViewStr, &resp.DayViews)
return resp, nil
}
模块测试
创建文章测试
表中已存有若干篇文章
加入一篇文章,代码如下:
func testAddArticle() {
ctx := context.Background()
article := practice.CreateArticleRequest{
Author: "xiefengze",
Content: "谢丰泽到此一游",
Title: "到此一游",
}
cli.CreateArticle(ctx, &article)
}
加入后情况如下:
返回文章测试
输出文章的测试代码如下:
func GetArticleListTest() {
ctx := context.Background()
resp, err := cli.GetArticleList(ctx, practice.NewGetArticleListRequest())
if err == nil {
len := len(resp.Articles)
for i := 0; i < len; i++ {
fmt.Println(resp.Articles[i])
}
}
}
MySQL中存储的文章如上图所示,该函数输出如下图所示:
获取指定文章
测试代码如下:
func GetArticleTest(articleID int64) {
ctx := context.Background()
articleRequest := practice.GetArticleRequest{
ArticleID: articleID,
}
articleResponse, err := cli.GetArticle(ctx, &articleRequest)
if err == nil {
fmt.Println(articleResponse)
}
}
运行GetArticleTest(6),输出如下:
实现点赞/取消点赞
既有点赞数据如下:
增加一条点赞数据测试代码:
func LikeArticle(articleID int64, userID int64) {
ctx := context.Background()
likeArticle := practice.LikeInfoRequest{
ArticleID: articleID,
LikeUserID: userID,
}
cli.LikeArticle(ctx, &likeArticle)
}
运行LikeArticle(6, 101),得到:
取消点赞测试代码
func DislikeArticle(articleID int64, userID int64) {
ctx := context.Background()
likeArticle := practice.LikeInfoRequest{
ArticleID: articleID,
LikeUserID: userID,
}
cli.DislikeArticle(ctx, &likeArticle)
}
运行DislikeArticle(1000, 102)
获取指定文章的点赞数和当天浏览数
测试代码如下
func GetLikeAndViewTest(articleID int64) {
ctx := context.Background()
articleRequest := practice.GetArticleRequest{
ArticleID: articleID,
}
resp, err := cli.GetLikesAndViews(ctx, &articleRequest)
// err.Error()
if err == nil {
fmt.Println(resp)
}
}
运行GetLikeAndViewTest(3),输出如下:
我们发现,以前的浏览数被自动清空了
我们进行一次访问,然后再输出试试
发现浏览的次数变为了1