466 lines
10 KiB
Markdown
466 lines
10 KiB
Markdown
## 常用命令
|
||
|
||
```bash
|
||
D:\Git\bin\bash.exe rpc/scripts/gen-rpc.sh order
|
||
或者
|
||
rpc/scripts/gen-rpc.sh order
|
||
```
|
||
|
||
加一个服务需要在bff/etc/bff.yaml 加 Upstreams ,docker.compose.prod.yaml 加服务,nacos 加配置,增加 etc/order.yaml 配置文件,Dockerfile 文件
|
||
|
||
## 如何加一个新服务
|
||
|
||
以加 `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。 |