Files
lone-services/README.md
T
2026-08-28 15:05:44 +08:00

466 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 常用命令
```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 配 UpstreamHTTP 路由写在 proto
编辑 `bff/etc/bff.yaml`,在 **Upstreams** 增加一段(不必写 Mappingsgateway 会从 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。