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 生成

  1. 创建目录 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 中,但是需要区分 esmysql两种实体。如果直接叫 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 文件,内容如下

  1. 生成 proto 文件模板
kratos proto add api/review/review.proto
  1. 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 {}

  1. 执行 make api 生成对应的pb.go 文件
  2. 生成 service 文件
 kratos proto server api/review/review.proto -t internal/service
  1. 将新增的服务,注册到 http server 中,gpr 同理
    image
  2. 在 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
}
  1. 执行 kratos run 跑起来,使用 postman 测试一下接口
    其实在上文中,执行 make api的时候,就自动生成一个 swagger的文件,可以用 postman import 对应文件后,直接使用。效果如下
    image

image

支持图片上传到 minio

正如上面说的,kratos 使用 proto 来定义文件上传接口,不太友好,所有需要自定义的 http 接口,具体实现,参考 kratos官方文档给的例子 https://github.com/go-kratos/examples/blob/main/http/upload/main.go

  1. 定义一个传文件的 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")
}

  1. 把 上述方法,注册到 http 的指定路由中
    image
  2. data 层中实现往 minio中存数据,这个看代码吧
  3. 使用 postman 测试如下
    image
posted @ 2025-10-24 06:06  沧海一声笑rush  阅读(4)  评论(0)    收藏  举报