Files
lone-services/README.md
T
2026-08-06 17:00:28 +08:00

476 lines
9.8 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.
## 如何加一个新服务
以加 `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 配 UpstreamHTTP 路由写在 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** 增加一段(不必写 Mappingsgateway 会从 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。