## 常用命令 ```bash D:\Git\bin\bash.exe rpc/scripts/gen-rpc.sh order 或者 rpc/scripts/gen-rpc.sh order ``` ```bash 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 ```bash 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) 在仓库根目录执行: ```bash cd services goctl rpc new order cd ../rpc mkdir order ``` ### 2. 将 proto 移到 `rpc/` 目录 `goctl rpc new` 默认把 proto 放在服务根目录(如 `order/order.proto`)。统一挪到 `rpc/`: ```bash cd ../services/order mv order.proto ../../rpc/order/ ``` 编辑 `rpc/order/order.proto`,把 `go_package` 改成本仓库模块路径: ```protobuf option go_package = "lone-services/rpc/order"; ``` ### 3. 生成pb 文件 windows 用 git 的 bash 跑 ```bash D:\Git\bin\bash.exe rpc/scripts/gen-rpc.sh order ``` ### 4. Zero 兼容 Nacos 等配置 编辑 `order/internal/config/config.go`: ```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`: ```yaml Nacos: Hosts: - rnacos:8848 NamespaceId: test Group: LONE_SERVICES RegisterIP: order ConfigID: order ``` ### 6. 服务注册 增加初始化 ```go 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`: ```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`: ```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` 增加服务: ```yaml 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 注册路由): ```yaml - Name: order Grpc: Target: order-service # 必须等于 order.yaml 的 Name Timeout: 5000 ProtoSets: - rpc/order/order.pb ``` ## Proto 常用校验规则(protovalidate) 文档:[https://protovalidate.com/schemas/standard-rules/](https://protovalidate.com/schemas/standard-rules/) 依赖:`import "buf/validate/validate.proto";`,服务启动挂 `pkg.local/validate` 拦截器。 ### 必填字符串 ```protobuf string name = 1 [(buf.validate.field).string = {min_len: 1, max_len: 256}]; ``` `min_len: 1` 表示不能为空串。 ### 可选字符串(仅限长度) ```protobuf string subhead = 2 [(buf.validate.field).string = {max_len: 255}]; ``` 空串可通过;有内容时限制最大长度。 ### 必填数值(含 0 合法) ```protobuf double price = 6 [(buf.validate.field).double = {gte: 0}]; int32 period_validity = 16 [(buf.validate.field).int32 = {gte: 0}]; ``` ### 非必填数值(0 表示未传,跳过校验) ```protobuf double store_price = 7 [(buf.validate.field) = { ignore: IGNORE_IF_ZERO_VALUE, double: {gte: 0} }]; ``` 有值时仍要求 `>= 0`;未传/为 0 不校验。 ### 枚举 / 固定取值(0 表示未传) ```protobuf 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 ```protobuf int64 id = 1 [(buf.validate.field).int64 = {gt: 0}]; ``` ### 无规则字段 ```protobuf uint32 number = 20; // 不做 protovalidate ``` ### 删除字段编号(避免复用) ```protobuf // reserved 2; ``` 业务条件校验(如「普通商品必须带齐价格」)仍写在 `internal/logic`,不要全塞进 proto。