gjs 418afad78a
CI / changes (push) Successful in 1s
CI / docker-bff (push) Successful in 1s
CI / docker-product (push) Failing after 1s
sms
2026-08-07 15:42:20 +08:00
2026-08-05 17:37:32 +08:00
2026-08-07 15:40:17 +08:00
2026-08-07 11:57:32 +08:00
2026-08-06 13:43:52 +08:00
2026-08-07 15:40:17 +08:00
2026-08-07 11:57:43 +08:00
2026-08-06 22:20:48 +08:00
2026-08-06 17:00:28 +08:00

如何加一个新服务

以加 order 为例(对标现有 product)。

0. 安装 goctl

go install github.com/zeromicro/go-zero/tools/goctl@latest
goctl --version
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

1. 创建 RPC 服务(goctl

在仓库根目录执行:

goctl rpc new order
cd order

2. 将 proto 移到 proto/ 目录

goctl rpc new 默认把 proto 放在服务根目录(如 order/order.proto)。统一挪到 proto/

mkdir proto
mv order.proto proto/

3. Zero 兼容 Nacos 等配置

编辑 order/internal/config/config.go

package config

import "github.com/zeromicro/go-zero/zrpc"

type Config struct {
	zrpc.RpcServerConf
	Nacos    NacosConf
	Mysql    MysqlConf
	BizRedis RedisConf
	AppLog   LogConf
}

type NacosConf struct {
	Hosts       []string
	NamespaceId string `json:",optional"`
	Group       string `json:",optional"`
	RegisterIP  string `json:",optional"`
}

type MysqlConf struct {
	Host     string
	Port     int
	User     string
	Password string
	Database string
	Charset  string `json:",default=utf8mb4"`
	Prefix   string `json:",optional"`

	ReadHost     string `json:",optional"`
	ReadPort     int    `json:",optional"`
	ReadUser     string `json:",optional"`
	ReadPassword string `json:",optional"`
	ReadDatabase string `json:",optional"`
}

type RedisConf struct {
	Host     string
	Port     int    `json:",default=6379"`
	Password string `json:",optional"`
	DB       int    `json:",optional"`
}

type LogConf struct {
	Path       string `json:",default=logs"`
	InfoFile   string `json:",default=info.log"`
	ErrorFile  string `json:",default=error.log"`
	FatalFile  string `json:",default=fatal.log"`
	MaxSize    int    `json:",default=100"`
	MaxBackups int    `json:",default=10"`
	MaxAge     int    `json:",default=30"`
}

4. 配置服务名与监听端口

编辑 order/etc/order.yaml

Name: order-service
ListenOn: 0.0.0.0:10200
Mode: dev

Log:
  ServiceName: order-service
  Mode: file              # console | file | volume
  Encoding: plain
  Level: info
  Path: logs
  KeepDays: 7
  Compress: false
  # Rotation: daily
  # MaxSize: 100
  # MaxBackups: 10

Nacos:
  Hosts:
    - rnacos:8848
  NamespaceId: test
  Group: LONE_SERVICES
  RegisterIP: order

Mysql:
  Host: mysql
  Port: 3306
  User: root
  Password: "123123"
  Database: dms-order
  Charset: utf8mb4
  Prefix:
  # ReadHost: mysql-slave
  # ReadPort: 3306
  # ReadUser: root
  # ReadPassword: "123123"
  # ReadDatabase: dms-order

BizRedis:
  Host: redis
  Port: 6379
  Password: ""
  DB: 0

AppLog:
  Path: logs
  InfoFile: info.log
  ErrorFile: error.log
  FatalFile: fatal.log
  MaxSize: 100
  MaxBackups: 10
  MaxAge: 30

5. 构建 mod

编辑 order/go.mod 增加本地包 pkg.local

go 1.26

replace pkg.local => ../pkg

6. 服务注册

编辑 order/order.go

增加 import

"pkg.local/discovery"
"pkg.local/log"
"pkg.local/modelbase"
"pkg.local/mysql"
"pkg.local/redis"
"pkg.local/validate"

增加初始化


func main() {

	flag.Parse()

	var c config.Config
	conf.MustLoad(*configFile, &c)

	if err := log.Init(log.Config{
		Path:       c.AppLog.Path,
		InfoFile:   c.AppLog.InfoFile,
		ErrorFile:  c.AppLog.ErrorFile,
		FatalFile:  c.AppLog.FatalFile,
		MaxSize:    c.AppLog.MaxSize,
		MaxBackups: c.AppLog.MaxBackups,
		MaxAge:     c.AppLog.MaxAge,
	}); err != nil {
		fmt.Fprintf(os.Stderr, "log init: %v\n", err)
		os.Exit(1)
	}

	if err := discovery.Init(discovery.Config{
		Hosts:       c.Nacos.Hosts,
		NamespaceId: c.Nacos.NamespaceId,
		Group:       c.Nacos.Group,
	}); err != nil {
		logx.Errorf("nacos init: %v", err)
	}

	registerIP := c.Nacos.RegisterIP
	port, err := listenPort(c.ListenOn)
	if err != nil {
		logx.Errorf("parse ListenOn: %v", err)
	}

	if err := discovery.Register(discovery.Instance{
		ServiceName: c.Name,
		IP:          registerIP,
		Port:        port,
		Group:       c.Nacos.Group,
	}); err != nil {
		logx.Errorf("nacos register: %v", err)
	}

	defer func() {
		if err := discovery.Deregister(); err != nil {
			logx.Errorf("nacos deregister: %v", err)
		}
	}()

	db, err := mysql.New(mysql.Config{
		Host:         c.Mysql.Host,
		Port:         c.Mysql.Port,
		User:         c.Mysql.User,
		Password:     c.Mysql.Password,
		Database:     c.Mysql.Database,
		Charset:      c.Mysql.Charset,
		Prefix:       c.Mysql.Prefix,
		ReadHost:     c.Mysql.ReadHost,
		ReadPort:     c.Mysql.ReadPort,
		ReadUser:     c.Mysql.ReadUser,
		ReadPassword: c.Mysql.ReadPassword,
		ReadDatabase: c.Mysql.ReadDatabase,
	})
	if err != nil {
		logx.Errorf("mysql init: %v", err)
	}

	if err := redis.Init(redis.Config{
		Host:     c.BizRedis.Host,
		Port:     c.BizRedis.Port,
		Password: c.BizRedis.Password,
		DB:       c.BizRedis.DB,
	}); err != nil {
		logx.Errorf("redis init: %v", err)
	}

	modelbase.Init(db, modelbase.Config{Prefix: c.Mysql.Prefix})

	ctx := svc.NewServiceContext(c, db)

	s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
		order.RegisterOrderServer(grpcServer, server.NewOrderServer(ctx))

		if c.Mode == service.DevMode || c.Mode == service.TestMode {
			reflection.Register(grpcServer)
		}
	})
	defer s.Stop()

	logx.AddWriter(logx.NewWriter(os.Stdout))

	s.AddUnaryInterceptors(validate.UnaryServerInterceptor(validate.MustNew()))

	logx.Infof("Starting rpc server at %s...", c.ListenOn)
	s.Start()
}

func listenPort(listenOn string) (uint64, error) {
	_, portStr, err := net.SplitHostPort(listenOn)
	if err != nil {
		return 0, err
	}
	return strconv.ParseUint(portStr, 10, 64)
}

执行 tidy

go mod tidy

编辑 internal/svc/servicecontext.go


type ServiceContext struct {
	Config config.Config
	DB     *gorm.DB
	Prefix string
}

func NewServiceContext(c config.Config, db *gorm.DB) *ServiceContext {
	return &ServiceContext{
		Config: c,
		DB:     db,
		Prefix: c.Mysql.Prefix,
	}
}

7. 接入 Docker Compose

deploy/docker-compose.override.yml 增加服务:

  order:
    image: golang:1.26.5
    volumes:
      - ..:/src
      - go-mod-cache:/go/pkg/mod
    working_dir: /src/order
    command: go run . -f etc/order.yaml
    ports:
      - "10200:10200"
    environment:
      NACOS_SERVER_ADDR: rnacos:8848
    depends_on:
      - rnacos

8. 生成 ProtoSet 给 BFF


protoc --include_imports --proto_path=proto --descriptor_set_out=../bff/etc/order.pb order.proto

9. 在 BFF 配 UpstreamHTTP 路由写在 proto

在对应 RPC 上声明 google.api.http(需 import "google/api/annotations.proto";):

rpc Ping(Request) returns (Response) {
  option (google.api.http) = {
    post: "/admin/v3/order/ping"
    body: "*"
  };
}

编辑 bff/etc/bff.yaml,在 Upstreams 增加一段(不必写 Mappingsgateway 会从 ProtoSet 里的 http option 注册路由):

  - Name: order
    Grpc:
      Target: order-service   # 必须等于 order.yaml 的 Name
      Timeout: 5000
    ProtoSets:
      - etc/order.pb

编写业务须知

只改业务逻辑(internal/logic/)不必跑 goctl。 改 proto/order.proto(新增/改名 RPC、改字段、改 http option 等)后,按文末「三条命令何时用」处理;

若 HTTP 路径有变,改 proto 里的 google.api.http 后重新跑命令 ② 生成 ProtoSet。


命令 ①:生成服务端 pb / grpc

protoc -I. -I../pkg/third_party --go_out=. --go-grpc_out=. proto/order.proto

什么时候跑:

  • 只改 校验规则(必填/长度/范围等)→ 只跑这条
  • 增删改字段、增删 RPC → 也要跑(服务端描述符要更新)

命令 ②:生成 BFF ProtoSet

protoc -I. -I../pkg/third_party --descriptor_set_out=../bff/etc/order.pb --include_imports proto/order.proto

什么时候跑:

  • 增删改 字段BFF gateway 编解码需要)
  • 增删 RPC
  • google.api.http 路由

命令 ③:goctl 生成/更新脚手架

goctl rpc protoc proto/order.proto --go_out=. --go-grpc_out=. --zrpc_out=.

什么时候跑:

  • 新增 RPC:需要生成 internal/logic、更新 server / orderclient
  • 注意:可能覆盖已改过的 order.go 等,生成后对比合并;也可用手写 logic/server 代替

Proto 常用校验规则(protovalidate

文档:https://protovalidate.com/schemas/standard-rules/ 依赖:import "buf/validate/validate.proto";,服务启动挂 pkg.local/validate 拦截器。

必填字符串

string name = 1 [(buf.validate.field).string = {min_len: 1, max_len: 256}];

min_len: 1 表示不能为空串。

可选字符串(仅限长度)

string subhead = 2 [(buf.validate.field).string = {max_len: 255}];

空串可通过;有内容时限制最大长度。

必填数值(含 0 合法)

double price = 6 [(buf.validate.field).double = {gte: 0}];
int32 period_validity = 16 [(buf.validate.field).int32 = {gte: 0}];

非必填数值(0 表示未传,跳过校验)

double store_price = 7 [(buf.validate.field) = {
  ignore: IGNORE_IF_ZERO_VALUE,
  double: {gte: 0}
}];

有值时仍要求 >= 0;未传/为 0 不校验。

枚举 / 固定取值(0 表示未传)

uint32 type = 19 [(buf.validate.field) = {
  ignore: IGNORE_IF_ZERO_VALUE,
  uint32: {in: [1, 2, 3]}
}];

uint32 sales_model = 4 [(buf.validate.field) = {
  ignore: IGNORE_IF_ZERO_VALUE,
  uint32: {in: [1, 2]}
}];

ID 必须大于 0

int64 id = 1 [(buf.validate.field).int64 = {gt: 0}];

无规则字段

uint32 number = 20;  // 不做 protovalidate

删除字段编号(避免复用)

// reserved 2;

业务条件校验(如「普通商品必须带齐价格」)仍写在 internal/logic,不要全塞进 proto。

S
Description
No description provided
Readme 215 MiB
Languages
Go 76.4%
PureBasic 21.6%
Dockerfile 1%
Lua 0.7%
Shell 0.3%