zzw b86e5013fb
CI / changes (push) Successful in 1m43s
CI / ad (push) Successful in 0s
CI / admin (push) Successful in 0s
CI / analysis (push) Successful in 0s
CI / bff (push) Successful in 0s
CI / chore (push) Successful in 0s
CI / equipment (push) Successful in 0s
CI / express (push) Successful in 0s
CI / factory (push) Successful in 0s
CI / message (push) Successful in 0s
CI / order (push) Successful in 0s
CI / payment (push) Successful in 1s
CI / percutaneous (push) Successful in 0s
CI / product (push) Successful in 0s
CI / sale (push) Successful in 0s
CI / record (push) Successful in 0s
CI / store (push) Successful in 1s
CI / support (push) Successful in 0s
CI / user (push) Successful in 0s
CI / task (push) Successful in 0s
CI / wecom (push) Successful in 1s
CI / water (push) Successful in 0s
CI / ws (push) Successful in 17s
chore: ws git cache
2026-10-23 15:02:03 +08:00
2026-10-21 13:29:03 +08:00
2026-10-21 13:29:03 +08:00
2026-10-23 15:02:03 +08:00
2026-08-11 17:12:37 +08:00
2026-10-07 09:50:59 +08:00
ws
2026-10-08 14:14:39 +08:00
2026-09-22 16:47:38 +08:00
2026-10-13 11:05:09 +08:00

常用命令

D:\Git\bin\bash.exe rpc/scripts/gen-rpc.sh order
或者
rpc/scripts/gen-rpc.sh order
1.加一个服务需要在bff/etc/bff.yaml 加 Upstreams
2.docker.compose.prod.yaml 加服务
3.nacos 加配置
4.增加 etc/order.yaml 配置文件
5.Dockerfile 文件
6.changes 里的 SERVICES="..."
  matrix.name 加一行 order

如何加一个新服务

以加 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)

在仓库根目录执行:

cd services
goctl rpc new order
cd ../rpc
mkdir order

2. 将 proto 移到 rpc/ 目录

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

cd ../services/order
mv order.proto ../../rpc/order/

编辑 rpc/order/order.proto,把 go_package 改成本仓库模块路径:


import "google/api/annotations.proto";
option go_package = "lone-services/rpc/order";

3. 生成pb 文件

windows 用 git 的 bash 跑

D:\Git\bin\bash.exe rpc/scripts/gen-rpc.sh order

4. Zero 兼容 Nacos 等配置

编辑 order/internal/config/config.go:

package config

type Config struct {
	Nacos NacosConf
}

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

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

编辑 order/etc/order.yaml:

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

6. 服务注册

增加初始化 order "lone-services/rpc/order/pb"


func main() {
	flag.Parse()
	var c config.Config
	conf.MustLoad(*configFile, &c)

	nacosParam := utils.NacosConfig{
		Hosts:       c.Nacos.Hosts,
		NamespaceId: c.Nacos.NamespaceId,
		Group:       c.Nacos.Group,
		ConfigID:    c.Nacos.ConfigID,
	}
	utils.InitConfig(nacosParam)

	logConf := logx.LogConf{
		ServiceName: utils.GetConfigString("log.serviceName"),
		Mode:        utils.GetConfigString("log.mode"),
		Encoding:    utils.GetConfigString("log.encoding"),
		Level:       utils.GetConfigString("log.level"),
		Path:        utils.GetConfigString("log.path"),
		KeepDays:    utils.GetConfigInt("log.keepDays"),
		MaxSize:     utils.GetConfigInt("log.maxSize"),
		MaxBackups:  utils.GetConfigInt("log.maxBackups"),
		Compress:    utils.GetConfigBool("log.compress"),
	}
	logx.SetUp(logConf)
	logx.AddWriter(logx.NewWriter(os.Stdout))

	listenOn := utils.GetConfigString("base.listenOn")
	mode := utils.GetConfigString("base.mode")
	serviceName := utils.GetConfigString("base.name")

	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)
		os.Exit(1)
	}

	port, err := listenPort(listenOn)
	if err != nil {
		logx.Errorf("parse ListenOn: %v", err)
		os.Exit(1)
	}

	if err := discovery.Register(discovery.Instance{
		ServiceName: serviceName,
    IP:          c.Nacos.RegisterIP,
		Port:        port,
		Group:       c.Nacos.Group,
	}); err != nil {
		logx.Errorf("nacos register: %v", err)
		os.Exit(1)
	}
  logx.Infof("服务注册成功: %s:%d", c.Nacos.RegisterIP, port)

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

	db, err := mysql.New(mysql.Config{
		Host:         utils.GetConfigString("mysql.host"),
		Port:         utils.GetConfigInt("mysql.port"),
		User:         utils.GetConfigString("mysql.user"),
		Password:     utils.GetConfigString("mysql.password"),
		Database:     utils.GetConfigString("mysql.database"),
		Charset:      utils.GetConfigString("mysql.charset"),
		Prefix:       utils.GetConfigString("mysql.prefix"),
		ReadHost:     utils.GetConfigString("mysql_read.host"),
		ReadPort:     utils.GetConfigInt("mysql_read.port"),
		ReadUser:     utils.GetConfigString("mysql_read.user"),
		ReadPassword: utils.GetConfigString("mysql_read.password"),
		ReadDatabase: utils.GetConfigString("mysql_read.database"),
	})
	if err != nil {
		logx.Errorf("mysql init: %v", err)
		os.Exit(1)
	}

	if err := redis.Init(redis.Config{
		Host:     utils.GetConfigString("redis.host"),
		Port:     utils.GetConfigInt("redis.port"),
		Password: utils.GetConfigString("redis.password"),
		DB:       utils.GetConfigInt("redis.db"),
	}); err != nil {
		logx.Errorf("redis init: %v", err)
		os.Exit(1)
	}

	debug := utils.GetConfigBool("mysql.debug")
	modelbase.Init(db, modelbase.Config{Prefix: utils.GetConfigString("mysql.prefix"), Debug: debug})

	rpcConf := zrpc.RpcServerConf{
		ListenOn: listenOn,
	}
	rpcConf.Mode = mode

	ctx := svc.NewServiceContext(c, db)

	s := zrpc.MustNewServer(rpcConf, func(grpcServer *grpc.Server) {
		order.RegisterOrderServer(grpcServer, server.NewOrderServer(ctx))
		if mode == service.DevMode || mode == service.TestMode {
			reflection.Register(grpcServer)
		}
	})
	defer s.Stop()

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

	logx.Infof("Starting rpc server at %s...", 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)
}

编辑 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: utils.GetConfigString("mysql.prefix"),
	}
}

编辑调用其它服务 internal/svc/servicecontext.go:

type rpcClientCacheEntry struct {
	cli      zrpc.Client
	target   string
	expireAt time.Time
}

const rpcClientCacheTTL = 30 * time.Second //缓存过期时间

var (
	rpcCacheLock sync.Mutex
	rpcCache     = make(map[string]*rpcClientCacheEntry)
)

func GetRpcClient(serviceName string) (zrpc.Client, error) {
	if serviceName == utils.StringEmpty {
		err := fmt.Errorf("rpc serviceName is empty")
		logx.Error(err)
		return nil, err
	}

	cacheKey := serviceName

	rpcCacheLock.Lock()
	entry, ok := rpcCache[cacheKey]
	if ok && time.Now().Before(entry.expireAt) {
		rpcCacheLock.Unlock()
		return entry.cli, nil
	}
	delete(rpcCache, cacheKey)
	rpcCacheLock.Unlock()

	inst, err := discovery.Pick(serviceName)
	if err != nil {
		err = fmt.Errorf("discovery pick %s failed: %w", serviceName, err)
		logx.Error(err)
		return nil, err
	}

	target := net.JoinHostPort(inst.IP, strconv.FormatUint(inst.Port, utils.NumberTen))

	cli := zrpc.MustNewClient(zrpc.RpcClientConf{
		Target:  target,
		Timeout: utils.RpcTimeOut, //rpc调用超时5s
	})

	rpcCacheLock.Lock()
	rpcCache[cacheKey] = &rpcClientCacheEntry{
		cli:      cli,
		target:   target,
		expireAt: time.Now().Add(rpcClientCacheTTL),
	}
	rpcCacheLock.Unlock()

	return cli, nil
}

type ServiceContext struct {
	Config         config.Config
	DB             *gorm.DB
	Prefix         string
	ProductSvcName string //服务名
	ExpressSvcName string
	ChoreSvcName   string
}

func NewServiceContext(c config.Config, db *gorm.DB) *ServiceContext {
	//启动仅读取配置,不建立rpc连接 多个服务就多个
	productSvc := utils.GetConfigString("services.product")
	ExpressSvc := utils.GetConfigString("services.express")
	ChoreSvc := utils.GetConfigString("services.chore")

	if productSvc == utils.StringEmpty {
		logx.Error("config services.product empty")
	}
	if ExpressSvc == utils.StringEmpty {
		logx.Error("config services.express empty")
	}
	if ChoreSvc == utils.StringEmpty {
		logx.Error("config services.chore empty")
	}

	svcCtx := &ServiceContext{
		Config:         c,
		DB:             db,
		Prefix:         utils.GetConfigString("mysql.prefix"),
		ProductSvcName: productSvc,
		ExpressSvcName: ExpressSvc,
		ChoreSvcName:   ChoreSvc,
	}

	return svcCtx
}

7. 接入 Docker Compose

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

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

8. 在 BFF 配 Upstream(HTTP 路由写在 proto)

编辑 bff/etc/bff.yaml,在 Upstreams 增加一段:

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

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 310 MiB
Languages
Go 82.7%
PureBasic 16.1%
Dockerfile 0.8%
Lua 0.3%
Shell 0.1%