Files
lone-services/README.md
T
gjs 94e1c6f97d
CI / changes (push) Successful in 2s
CI / docker-bff (push) Has been skipped
CI / docker-product (push) Has been skipped
CI / docker-admin (push) Successful in 3m27s
fix
2026-08-13 11:32:12 +08:00

488 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.
## 常用命令
### 命令 ①:goctl 生成/更新脚手架
```bash
goctl -I. -I../pkg/third_party rpc protoc proto/order.proto --go_out=. --go-grpc_out=. --zrpc_out=.
```
### 命令 ②:生成服务端 pb / grpc
```bash
goctl rpc protoc proto/order.proto --proto_path=. --proto_path=../pkg/third_party --go_out=. --go-grpc_out=. --zrpc_out=.
```
**什么时候跑:**
- 只改 **校验规则**(必填/长度/范围等)→ **只跑这条**
- 增删改字段、增删 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** 路由
## 如何加一个新服务
以加 `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
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"`
}
```
### 4. 配置服务名与监听端口
编辑 `order/etc/order.yaml`
```yaml
Nacos:
Hosts:
- host.docker.internal:8848
NamespaceId: test
Group: LONE_SERVICES
RegisterIP: admin
ConfigID: develop-admin
```
### 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)
// Nacos配置拉取初始化
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)
}
}()
// 初始化MySQL
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)
}
// 初始化Redis
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) {
admin.RegisterAdminServer(grpcServer, server.NewAdminServer(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)
}
```
执行 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: utils.GetConfigString("mysql.prefix"),
}
}
```
### 7. 接入 Docker Compose
`deploy/docker-compose.dev.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/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
goctl rpc protoc proto/order.proto --proto_path=. --proto_path=../pkg/third_party --go_out=. --go-grpc_out=. --zrpc_out=.
```
**什么时候跑:**
- 只改 **校验规则**(必填/长度/范围等)→ **只跑这条**
- 增删改字段、增删 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 -I. -I../pkg/third_party 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/](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。