kratos 框架编写一个评价系统
保证本机安装 kratos 的前提下, 创建一个模板
kratos new review-service -r https://gitee.com/go-kratos/kratos-layout.git
在 .gitignore中,添加如下一行,就可以把这个仓库,用自己的git托管了。(因为.pb.go文件,其实可以使用 make 生成,个人感觉,不需要使用git托管)
*.pb.go
数据库
设计数据库并使用 ORM 自动生成数据库
在生产环境,一般是使用 sql 文件在数据库跑;本文比较简单,直接用 orm 生成
- 创建目录 internal/model/mysqlmodel,代码如下:
package mysqlmodel
import (
"encoding/json"
"fmt"
"time"
"gorm.io/gorm"
)
type ReviewStatus uint8
// 2. 定义常量对应枚举值(与 GORM 注释中的 1/2/3 对应)
const (
// ReviewStatusPending 待审核(状态值 1)
ReviewStatusPending ReviewStatus = 1
// ReviewStatusApproved 已审核(状态值 2)
ReviewStatusApproved ReviewStatus = 2
// ReviewStatusRejected 已拒绝絕(状态值 3)
ReviewStatusRejected ReviewStatus = 3
)
func (s *ReviewStatus) UnmarshalJSON(data []byte) error {
var statusStr string
if err := json.Unmarshal(data, &statusStr); err != nil {
return err
}
// 根据字符串反推枚举值
switch statusStr {
case "待审核":
*s = ReviewStatusPending
case "已审核":
*s = ReviewStatusApproved
case "已拒绝":
*s = ReviewStatusRejected
default:
return fmt.Errorf("无效的审核状态: %s", statusStr)
}
return nil
}
type ReviewInfo struct {
ID uint `gorm:"primaryKey,autoIncrement"`
// 默认可以为空,非空,要显示设置;
UserID uint `gorm:"comment:创建方表示;not null"`
// varchar(512) 这里的 512是字符数,不是字节数,这里能存 512个汉字
Content string `gorm:"type:varchar(512);comment:评论内容;not null"`
ShopID uint `gorm:"comment:店铺ID;not null"`
ShopScore uint8 `gorm:"type:tinyint(4);comment:商家评分;not null"`
LogisticsScore uint8 `gorm:"type:tinyint(4);comment:物流评分;not null"`
HavePicture bool `gorm:"not null;comment:是否有图片,默认没有,有的话,去ReviewImage表中查询"`
Status ReviewStatus `gorm:"type:tinyint(4);comment:审核状态,1:待审核,2:已审核,3:已拒绝;not null;default:0"`
OptIdea string `gorm:"type:varchar(216);comment:审核建议,如果拒绝,写拒绝的原因;not null"`
CreatedAt time.Time `gorm:"comment:创建时间,自动生成"`
UpdatedAt time.Time `gorm:"comment:更新时间,自动生成"`
DeletedAt gorm.DeletedAt `gorm:"index;comment:逻辑删除"`
}
// 用于执行 mino中的 url
type ReviewImage struct {
ID uint `gorm:"primaryKey,autoIncrement"`
// 评论和评论回复图片都要存储,所以这里要区分
// 在查询过程中,如果差图片,肯定要指定实体类型,所有创建联合索引,更合理
// gorm中,两个字段使用同一个索引名将创建复合索引
// 优先级 priority 定义,在查询过程中,先根据实体类型查询,再根据实体ID查询
EntityType uint8 `gorm:"index:idx_type_review_id,priority:1;type:tinyint(4);comment:实体类型,1:订单,2:评价"`
EntityID uint `gorm:"index:idx_type_review_id,priority:2;comment:评论ID,用于关联ReviewInfo,创建索引"`
URL string `gorm:"type:varchar(256);comment:图片URL"`
CreatedAt time.Time `gorm:"comment:创建时间,自动生成"`
UpdatedAt time.Time `gorm:"comment:更新时间,自动生成"`
DeletedAt gorm.DeletedAt `gorm:"index;comment:逻辑删除"`
}
// 回复评论的列表
type ReviewReployInfo struct {
ID uint `gorm:"primaryKey,autoIncrement"`
ReviewID uint `gorm:"index:idx_review_id;comment:评论ID,用于关联ReviewInfo,创建索引"`
Content string `gorm:"type:varchar(512);comment:评论内容;not null"`
HavePicture bool `gorm:"not null;comment:是否有图片,默认没有,有的话,去ReviewImage表中查询"`
Status uint8 `gorm:"comment:审核状态,1:待审核,2:已审核,3:已拒绝;not null;default:0"`
CreatedAt time.Time `gorm:"comment:创建时间,自动生成"`
UpdatedAt time.Time `gorm:"comment:更新时间,自动生成"`
DeletedAt gorm.DeletedAt `gorm:"index;comment:逻辑删除"`
}
针对上表,为何如此设计,有几个问题,需要考虑
1. 为何目录为 internal/model/mysqlmodel ?
模型结构体,应该定义在 model 中,但是需要区分 es 和 mysql两种实体。如果直接叫 internal/model/mysql,根据go语言规范,package也要叫 mysql会和 data 层面的大量关键字冲突,自己写的代码,还能起个别名,但本文用的gen会自动生成文档,这个不好起别名,故包名 myqlmodel
2. 范式问题
因为 review 表中的图片,是存放在 ReviewImage 中的,而且 ReviewImage 表中的数据,会很多。那么是否可以在reviewInfo表中,冗余一个imageId组成的数组,直接在reviewImage表根据主键查询呢? 答案是不行,这明显不符合第一范式,会给后续的增删改查,都买下了大雷,至于考虑的提高性能的问题,大哥,考虑的有点多了。不要给自己找麻烦,才是正经。性能考虑是dba的事。
3. 组合索引问题
因为 评论和回复评论,都存在 reviewImage中了,所以查找之前,一定会声明类型(是 review 还是 reviewReply?)。所以这里要组合索引。再想想,他是否是唯一索引呢?(答案:不是)
4. gorm的使用技巧
具体看看注释吧,或者看看 gorm 的官方文档。https://gorm.io/zh_CN/docs/index.html
数据库声明的 varchar(256) 字段,256 是指的字符不是字节,如果使用UTF8MB4字符集,表示能存 256 个汉字,如果超出了,可能会截断,也可能会报错。可以根据需要设置
枚举类ReviewStatus的使用。注意反序列化时的操作
5. 模型定义好了,怎么链接数据库,自动迁移呢?
随便创建个文件空文件夹,起package名为main,用下面代码,跑一下就行(一个go.mod 项目,可以有多个 main 函数,但是main函数必须在main包中)
package main
import (
"context"
"review-service/internal/data/mysqlquery"
"review-service/internal/model/mysqlmodel"
"gorm.io/driver/mysql"
"gorm.io/gen"
"gorm.io/gorm"
)
func autoMigrae(db *gorm.DB) {
// Migrate the schema
if err := db.AutoMigrate(&mysqlmodel.ReviewInfo{}, &mysqlmodel.ReviewImage{}, &mysqlmodel.ReviewReployInfo{}); err != nil {
panic(err)
}
g := gen.NewGenerator(gen.Config{
OutPath: "../data/mysqlquery",
Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface, // generate mode
})
// gormdb, _ := gorm.Open(mysql.Open("root:@(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=True&loc=Local"))
g.UseDB(db) // reuse your gorm db
// Generate basic type-safe DAO API for struct `model.User` following conventions
g.ApplyBasic(mysqlmodel.ReviewInfo{}, mysqlmodel.ReviewImage{}, mysqlmodel.ReviewReployInfo{})
// Generate Type Safe API with Dynamic SQL defined on Querier interface for `model.User` and `model.Company`
// g.ApplyInterface(func(Querier) {}, model.User{}, model.Company{})
// Generate the code
g.Execute()
}
func main() {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "root:123456@tcp(127.0.0.1:3306)/review?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic(err)
}
// 自动迁移
autoMigrae(db)
// 插入数据
mysqlquery.SetDefault(db)
ctx := context.Background()
reviewDemo := mysqlmodel.ReviewInfo{
UserID: 8,
Content: "外卖口味不错,值得购买",
ShopID: 13,
ShopScore: 18,
LogisticsScore: 66,
HavePicture: false,
Status: mysqlmodel.ReviewStatusPending,
OptIdea: "",
}
if err := mysqlquery.ReviewInfo.WithContext(ctx).Create(&reviewDemo); err != nil {
panic(err)
}
// 查询一下数据
review, err := mysqlquery.ReviewInfo.WithContext(ctx).Where(mysqlquery.ReviewInfo.UserID.Eq(8)).First()
if err != nil {
panic(err)
}
// 打印一下数据
println(review.Content)
}
如果感觉,自动生成的代码,有点多。可以把 *.gen.go放到 .gitignore 中,在 makefile 文件中,加上如下一行,每次拉取后,都自动生成
autoMigrate:
cd ./internal/model && go run generate_mysql.go
注意,我这里,是为了看代码的时候,防止太乱,所有就把所有自动生成的代码,过滤掉了。每次都重新生成。实际项目中不建议这么搞,避免版本变动,导致新生成的代码和老代码,对不上。。。
service 层
写一个比较简单的例子,支持创建评价。创建评价时能上传图片。上传图片和提交表单用两个接口,逻辑为前端先传图片给后端,后端存储后,将url返回给前端,第二个提交表格时,将该 url带上。先以简单的提交表单为例
定义接口文档
kratos 定义 api 接口,是使用的 proto,
vscore中对 proto 格式化,可查看 https://www.cnblogs.com/rush-peng/p/15345891.html
使用如下命令,创建新的 proto 文件,内容如下
- 生成 proto 文件模板
kratos proto add api/review/review.proto
- api定义如下,这里只定义了创建评价的接口,kratos 对于使用 proto 定义二进制流上传,不太支持,所有用自己定义的接口实现,参考https://juejin.cn/post/7393172946803785769
syntax = "proto3";
package api.review;
import "google/api/annotations.proto";
option go_package = "review-service/api/review;review";
option java_multiple_files = true;
option java_package = "api.review";
service Review {
rpc CreateReview (CreateReviewRequest) returns (CreateReviewReply) {
option (google.api.http) = {
post: "/review/create"
body: "*"
};
};
}
// 审核状态枚举定义
enum ReviewStatus {
REVIEW_STATUS_UNKNOWN = 0; // 默认值
REVIEW_STATUS_PENDING = 1; // 待审核
REVIEW_STATUS_APPROVED = 2; // 已审核
REVIEW_STATUS_REJECTED = 3; // 已拒绝
}
message CreateReviewRequest {
uint32 user_id = 1; // 创建方表示
string content = 2; // 评论内容 (最多 512 个字符,可包含汉字)
uint32 shop_id = 3; // 店铺ID
uint32 shop_score = 4; // 商家评分
uint32 logistics_score = 5; // 物流评分
bool have_picture = 6; // 是否有图片,默认没有,有的话去 ReviewImage 表中查询
string picture_url = 7; // 图片URL,若 have_picture 为 true 则必填
}
message CreateReviewReply {}
- 执行
make api生成对应的pb.go文件 - 生成 service 文件
kratos proto server api/review/review.proto -t internal/service
- 将新增的服务,注册到 http
server中,gpr 同理

- 在 review 的 service 中,定义一个如下的
ReviewService的结构体,别忘了讲NewReviewService使用wire托管,实现依赖注入。
package service
import (
"context"
"github.com/go-kratos/kratos/v2/log"
pb "review-service/api/review"
"review-service/internal/data"
"review-service/internal/model/mysqlmodel"
)
type ReviewService struct {
pb.UnimplementedReviewServer
rep *data.ReviewRepo
log *log.Helper
}
func NewReviewService(rep *data.ReviewRepo, logger log.Logger) *ReviewService {
return &ReviewService{rep: rep, log: log.NewHelper(logger)}
}
func (s *ReviewService) CreateReview(ctx context.Context, req *pb.CreateReviewRequest) (*pb.CreateReviewReply, error) {
reviewInfo := &mysqlmodel.ReviewInfo{
UserID: uint(req.UserId),
Content: req.Content,
ShopID: uint(req.ShopId),
ShopScore: uint8(req.ShopScore),
LogisticsScore: uint8(req.LogisticsScore),
HavePicture: req.HavePicture,
Status: mysqlmodel.ReviewStatusPending, // 默认待审核
}
s.log.Infof("CreateReview req: %v", req)
if err := s.rep.SaveReviewInfo(ctx, reviewInfo); err != nil {
s.log.Errorf("CreateReview failed, err: %v", err)
return nil, err
}
s.log.Infof("CreateReview success, reviewID: %d", reviewInfo.ID)
// 如果有图片的话,更新 url
return &pb.CreateReviewReply{}, nil
}
- 执行
kratos run跑起来,使用postman测试一下接口
其实在上文中,执行make api的时候,就自动生成一个swagger的文件,可以用postmanimport 对应文件后,直接使用。效果如下


支持图片上传到 minio
正如上面说的,kratos 使用 proto 来定义文件上传接口,不太友好,所有需要自定义的 http 接口,具体实现,参考 kratos官方文档给的例子 https://github.com/go-kratos/examples/blob/main/http/upload/main.go
- 定义一个传文件的 service
先定义一个ImageService,然后给该方法,定义一个UploadFile方法
package service
import (
"fmt"
"path/filepath"
"review-service/internal/data"
"review-service/internal/utils"
"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/transport/http"
)
type ImageService struct {
rep *data.ImageRepo
log *log.Helper
}
func NewImageService(rep *data.ImageRepo, logger log.Logger) *ImageService {
return &ImageService{
rep: rep,
log: log.NewHelper(logger),
}
}
func (s *ImageService) UploadFile(ctx http.Context) error {
req := ctx.Request()
fileName := req.FormValue("name")
file, handler, err := req.FormFile("file")
s.log.Infof("UploadFile fileName: %s", fileName)
if err != nil {
s.log.Errorf("UploadFile fileName: %s, err: %v", fileName, err)
return err
}
defer file.Close()
newName := fmt.Sprintf("%s%s", utils.GetRandName(), filepath.Ext(handler.Filename))
if err := s.rep.UploadImage(ctx, file, handler.Size, newName); err != nil {
s.log.Errorf("UploadFile fileName: %s, err: %v", fileName, err)
return err
}
s.log.Infof("save fileName to minio successful: %s, newName: %s", fileName, newName)
return ctx.String(200, "File "+newName+" Uploaded successfully")
}
- 把 上述方法,注册到 http 的指定路由中

- 在
data层中实现往minio中存数据,这个看代码吧 - 使用 postman 测试如下

浙公网安备 33010602011771号