develop
CI / changes (push) Successful in 13s
CI / ad (push) Successful in 0s
CI / admin (push) Successful in 0s
CI / bff (push) Successful in 0s
CI / chore (push) Successful in 0s
CI / express (push) Successful in 0s
CI / product (push) Successful in 0s
CI / sale (push) Successful in 0s
CI / user (push) Successful in 0s
CI / wecom (push) Successful in 32s
常用命令
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 改成本仓库模块路径:
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. 服务注册
增加初始化
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"),
}
}
编辑2调用其它服务 internal/svc/servicecontext.go:
import (
"fmt"
"net"
"strconv"
"sync"
"time"
"lone-services/pkg/discovery"
"lone-services/pkg/utils"
"lone-services/services/order/internal/config"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/zrpc"
"gorm.io/gorm"
)
// 缓存条目
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
ExpressSvcName string //服务名
}
func NewServiceContext(c config.Config, db *gorm.DB) *ServiceContext {
//启动仅读取配置,不建立rpc连接 多个服务就多个
expressSvc := utils.GetConfigString("services.express")
if expressSvc == utils.StringEmpty {
logx.Error("config services.express empty")
}
svcCtx := &ServiceContext{
Config: c,
DB: db,
Prefix: utils.GetConfigString("mysql.prefix"),
ExpressSvcName: expressSvc,
}
return svcCtx
}
调用方法logic中
//调用其它服务实例
//cli, err := svc.GetRpcClient(l.svcCtx.ExpressSvcName)
//if err != nil {
// l.Logger.Errorf("get express rpc err: %v", err)
// //降级逻辑
// return l.out(utils.ErrorInternalServer, "express"+utils.ErrorInternalServer.Msg)
//}
//expClient := express.NewExpressClient(cli.Conn())
//res, _ := expClient.Ping(context.Background(), &express.Request{})
//l.Logger.Error("res: %v", res)
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 增加一段(不必写 Mappings,gateway 会从 ProtoSet 里的 http option 注册路由):
- 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。
Description
Languages
Go
75.7%
PureBasic
22.3%
Dockerfile
1%
Lua
0.7%
Shell
0.3%