476 lines
9.8 KiB
Markdown
476 lines
9.8 KiB
Markdown
## 如何加一个新服务
|
||
|
||
以加 `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
|
||
goctl rpc new order
|
||
cd order
|
||
```
|
||
|
||
### 2. 将 proto 移到 `proto/` 目录
|
||
|
||
`goctl rpc new` 默认把 proto 放在服务根目录(如 `order/order.proto`)。统一挪到 `proto/`:
|
||
|
||
```bash
|
||
mkdir proto
|
||
mv order.proto proto/
|
||
```
|
||
|
||
### 3. Zero 兼容 Nacos 等配置
|
||
|
||
编辑 `order/internal/config/config.go`:
|
||
|
||
```go
|
||
package config
|
||
|
||
import "github.com/zeromicro/go-zero/zrpc"
|
||
|
||
type Config struct {
|
||
zrpc.RpcServerConf
|
||
Nacos NacosConf
|
||
Mysql MysqlConf
|
||
BizRedis RedisConf
|
||
AppLog LogConf
|
||
}
|
||
|
||
type NacosConf struct {
|
||
Hosts []string
|
||
NamespaceId string `json:",optional"`
|
||
Group string `json:",optional"`
|
||
RegisterIP string `json:",optional"`
|
||
}
|
||
|
||
type MysqlConf struct {
|
||
Host string
|
||
Port int
|
||
User string
|
||
Password string
|
||
Database string
|
||
Charset string `json:",default=utf8mb4"`
|
||
Prefix string `json:",optional"`
|
||
|
||
ReadHost string `json:",optional"`
|
||
ReadPort int `json:",optional"`
|
||
ReadUser string `json:",optional"`
|
||
ReadPassword string `json:",optional"`
|
||
ReadDatabase string `json:",optional"`
|
||
}
|
||
|
||
type RedisConf struct {
|
||
Host string
|
||
Port int `json:",default=6379"`
|
||
Password string `json:",optional"`
|
||
DB int `json:",optional"`
|
||
}
|
||
|
||
type LogConf struct {
|
||
Path string `json:",default=logs"`
|
||
InfoFile string `json:",default=info.log"`
|
||
ErrorFile string `json:",default=error.log"`
|
||
FatalFile string `json:",default=fatal.log"`
|
||
MaxSize int `json:",default=100"`
|
||
MaxBackups int `json:",default=10"`
|
||
MaxAge int `json:",default=30"`
|
||
}
|
||
|
||
```
|
||
|
||
### 4. 配置服务名与监听端口
|
||
|
||
编辑 `order/etc/order.yaml`:
|
||
|
||
```yaml
|
||
Name: order-service
|
||
ListenOn: 0.0.0.0:10200
|
||
Mode: dev
|
||
|
||
Log:
|
||
ServiceName: order-service
|
||
Mode: file # console | file | volume
|
||
Encoding: plain
|
||
Level: info
|
||
Path: logs
|
||
KeepDays: 7
|
||
Compress: false
|
||
# Rotation: daily
|
||
# MaxSize: 100
|
||
# MaxBackups: 10
|
||
|
||
Nacos:
|
||
Hosts:
|
||
- rnacos:8848
|
||
NamespaceId: test
|
||
Group: LONE_SERVICES
|
||
RegisterIP: order
|
||
|
||
Mysql:
|
||
Host: mysql
|
||
Port: 3306
|
||
User: root
|
||
Password: "123123"
|
||
Database: dms-order
|
||
Charset: utf8mb4
|
||
Prefix:
|
||
# ReadHost: mysql-slave
|
||
# ReadPort: 3306
|
||
# ReadUser: root
|
||
# ReadPassword: "123123"
|
||
# ReadDatabase: dms-order
|
||
|
||
BizRedis:
|
||
Host: redis
|
||
Port: 6379
|
||
Password: ""
|
||
DB: 0
|
||
|
||
AppLog:
|
||
Path: logs
|
||
InfoFile: info.log
|
||
ErrorFile: error.log
|
||
FatalFile: fatal.log
|
||
MaxSize: 100
|
||
MaxBackups: 10
|
||
MaxAge: 30
|
||
|
||
```
|
||
|
||
### 5. 构建 mod
|
||
|
||
编辑 `order/go.mod` 增加本地包 pkg.local
|
||
|
||
```go
|
||
go 1.26
|
||
|
||
replace pkg.local => ../pkg
|
||
```
|
||
|
||
### 6. 服务注册
|
||
|
||
编辑 `order/order.go`:
|
||
|
||
增加 import
|
||
|
||
```go
|
||
"pkg.local/discovery"
|
||
"pkg.local/log"
|
||
"pkg.local/modelbase"
|
||
"pkg.local/mysql"
|
||
"pkg.local/redis"
|
||
"pkg.local/validate"
|
||
```
|
||
|
||
增加初始化
|
||
```go
|
||
|
||
func main() {
|
||
|
||
flag.Parse()
|
||
|
||
var c config.Config
|
||
conf.MustLoad(*configFile, &c)
|
||
|
||
if err := log.Init(log.Config{
|
||
Path: c.AppLog.Path,
|
||
InfoFile: c.AppLog.InfoFile,
|
||
ErrorFile: c.AppLog.ErrorFile,
|
||
FatalFile: c.AppLog.FatalFile,
|
||
MaxSize: c.AppLog.MaxSize,
|
||
MaxBackups: c.AppLog.MaxBackups,
|
||
MaxAge: c.AppLog.MaxAge,
|
||
}); err != nil {
|
||
fmt.Fprintf(os.Stderr, "log init: %v\n", err)
|
||
os.Exit(1)
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
registerIP := c.Nacos.RegisterIP
|
||
port, err := listenPort(c.ListenOn)
|
||
if err != nil {
|
||
logx.Errorf("parse ListenOn: %v", err)
|
||
}
|
||
|
||
if err := discovery.Register(discovery.Instance{
|
||
ServiceName: c.Name,
|
||
IP: registerIP,
|
||
Port: port,
|
||
Group: c.Nacos.Group,
|
||
}); err != nil {
|
||
logx.Errorf("nacos register: %v", err)
|
||
}
|
||
|
||
defer func() {
|
||
if err := discovery.Deregister(); err != nil {
|
||
logx.Errorf("nacos deregister: %v", err)
|
||
}
|
||
}()
|
||
|
||
db, err := mysql.New(mysql.Config{
|
||
Host: c.Mysql.Host,
|
||
Port: c.Mysql.Port,
|
||
User: c.Mysql.User,
|
||
Password: c.Mysql.Password,
|
||
Database: c.Mysql.Database,
|
||
Charset: c.Mysql.Charset,
|
||
Prefix: c.Mysql.Prefix,
|
||
ReadHost: c.Mysql.ReadHost,
|
||
ReadPort: c.Mysql.ReadPort,
|
||
ReadUser: c.Mysql.ReadUser,
|
||
ReadPassword: c.Mysql.ReadPassword,
|
||
ReadDatabase: c.Mysql.ReadDatabase,
|
||
})
|
||
if err != nil {
|
||
logx.Errorf("mysql init: %v", err)
|
||
}
|
||
|
||
if err := redis.Init(redis.Config{
|
||
Host: c.BizRedis.Host,
|
||
Port: c.BizRedis.Port,
|
||
Password: c.BizRedis.Password,
|
||
DB: c.BizRedis.DB,
|
||
}); err != nil {
|
||
logx.Errorf("redis init: %v", err)
|
||
}
|
||
|
||
modelbase.Init(db, modelbase.Config{Prefix: c.Mysql.Prefix})
|
||
|
||
ctx := svc.NewServiceContext(c, db)
|
||
|
||
s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
|
||
order.RegisterOrderServer(grpcServer, server.NewOrderServer(ctx))
|
||
|
||
if c.Mode == service.DevMode || c.Mode == service.TestMode {
|
||
reflection.Register(grpcServer)
|
||
}
|
||
})
|
||
defer s.Stop()
|
||
|
||
logx.AddWriter(logx.NewWriter(os.Stdout))
|
||
|
||
s.AddUnaryInterceptors(validate.UnaryServerInterceptor(validate.MustNew()))
|
||
|
||
logx.Infof("Starting rpc server at %s...", c.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)
|
||
}
|
||
|
||
```
|
||
|
||
执行 tidy
|
||
|
||
```bash
|
||
go mod tidy
|
||
```
|
||
|
||
编辑 `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: c.Mysql.Prefix,
|
||
}
|
||
}
|
||
|
||
```
|
||
|
||
### 7. 接入 Docker Compose
|
||
|
||
在 `deploy/docker-compose.override.yml` 增加服务:
|
||
|
||
```yaml
|
||
order:
|
||
image: golang:1.26.5
|
||
volumes:
|
||
- ..:/src
|
||
- go-mod-cache:/go/pkg/mod
|
||
working_dir: /src/order
|
||
command: go run . -f etc/order.yaml
|
||
ports:
|
||
- "10200:10200"
|
||
environment:
|
||
NACOS_SERVER_ADDR: rnacos:8848
|
||
depends_on:
|
||
- rnacos
|
||
```
|
||
|
||
### 8. 生成 ProtoSet 给 BFF
|
||
|
||
```bash
|
||
|
||
protoc --include_imports --proto_path=proto --descriptor_set_out=../bff/etc/order.pb order.proto
|
||
```
|
||
|
||
### 9. 在 BFF 配 Upstream(HTTP 路由写在 proto)
|
||
|
||
在对应 RPC 上声明 `google.api.http`(需 `import "google/api/annotations.proto";`):
|
||
|
||
```protobuf
|
||
rpc Ping(Request) returns (Response) {
|
||
option (google.api.http) = {
|
||
post: "/admin/v3/order/ping"
|
||
body: "*"
|
||
};
|
||
}
|
||
```
|
||
|
||
编辑 `bff/etc/bff.yaml`,在 **Upstreams** 增加一段(不必写 Mappings,gateway 会从 ProtoSet 里的 http option 注册路由):
|
||
|
||
```yaml
|
||
- Name: order
|
||
Grpc:
|
||
Target: order-service # 必须等于 order.yaml 的 Name
|
||
Timeout: 5000
|
||
ProtoSets:
|
||
- etc/order.pb
|
||
```
|
||
|
||
### 编写业务须知
|
||
|
||
只改业务逻辑(`internal/logic/`)不必跑 goctl。
|
||
改 `proto/order.proto`(新增/改名 RPC、改字段、改 http option 等)后,按文末「三条命令何时用」处理;
|
||
|
||
若 HTTP 路径有变,改 proto 里的 `google.api.http` 后重新跑命令 ② 生成 ProtoSet。
|
||
|
||
---
|
||
|
||
### 命令 ①:生成服务端 pb / grpc
|
||
|
||
```bash
|
||
protoc -I. -I../pkg/third_party --go_out=. --go-grpc_out=. proto/order.proto
|
||
```
|
||
|
||
**什么时候跑:**
|
||
- 只改 **校验规则**(必填/长度/范围等)→ **只跑这条**
|
||
- 增删改字段、增删 RPC → 也要跑(服务端描述符要更新)
|
||
|
||
### 命令 ②:生成 BFF ProtoSet
|
||
|
||
```bash
|
||
protoc -I. -I../pkg/third_party --descriptor_set_out=../bff/etc/order.pb --include_imports proto/order.proto
|
||
```
|
||
|
||
**什么时候跑:**
|
||
|
||
- 增删改 **字段**(BFF gateway 编解码需要)
|
||
- 增删 **RPC**
|
||
- 改 **google.api.http** 路由
|
||
|
||
### 命令 ③:goctl 生成/更新脚手架
|
||
|
||
```bash
|
||
goctl rpc protoc proto/order.proto --go_out=. --go-grpc_out=. --zrpc_out=.
|
||
```
|
||
|
||
**什么时候跑:**
|
||
|
||
- **新增 RPC**:需要生成 `internal/logic`、更新 `server` / `orderclient` 等
|
||
- 注意:可能覆盖已改过的 `order.go` 等,生成后对比合并;也可用手写 logic/server 代替
|
||
|
||
## Proto 常用校验规则(protovalidate)
|
||
|
||
文档: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。
|