feat(api): 支持 gRPC 用户服务集成
- 更新路由以支持 gRPC 服务器地址配置 - 修改 YMTUsersHandler 以使用 gRPC 客户端进行用户数据查询 - 添加 gRPC 连接失败的降级处理逻辑 - 引入 gRPC 生成的代码以支持用户服务调用
This commit is contained in:
parent
c02f051cb8
commit
fde87fde96
|
|
@ -0,0 +1,84 @@
|
|||
# gRPC 集成说明
|
||||
|
||||
## 已完成的工作
|
||||
|
||||
1. ✅ 更新了配置结构,添加了 `grpc_server` 配置支持
|
||||
2. ✅ 添加了 gRPC 依赖到 `go.mod`
|
||||
3. ✅ 创建了 gRPC 客户端包装器 (`server/internal/grpc/user_client.go`)
|
||||
4. ✅ 修改了 `/api/ymt/users` 接口使用 gRPC 调用 `SimpleListAllUser`
|
||||
5. ✅ 更新了路由和主程序传递 gRPC 配置
|
||||
|
||||
## 需要完成的步骤
|
||||
|
||||
### 1. 生成 Proto 文件
|
||||
|
||||
在项目根目录运行:
|
||||
|
||||
```bash
|
||||
cd grpc
|
||||
make generate
|
||||
```
|
||||
|
||||
这将生成 `grpc/user/userv1/*.pb.go` 文件。
|
||||
|
||||
### 2. 下载 Go 依赖
|
||||
|
||||
在 `server` 目录运行:
|
||||
|
||||
```bash
|
||||
cd server
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
这将下载 `google.golang.org/grpc` 和 `google.golang.org/protobuf` 等依赖。
|
||||
|
||||
### 3. 配置 gRPC 服务器地址
|
||||
|
||||
确保 `server/config.yaml` 中配置了 gRPC 服务器地址:
|
||||
|
||||
```yaml
|
||||
app:
|
||||
grpc_server:
|
||||
host: "121.41.108.37"
|
||||
port: "30900"
|
||||
```
|
||||
|
||||
或者通过环境变量设置:
|
||||
|
||||
```bash
|
||||
export GRPC_SERVER_HOST=121.41.108.37
|
||||
export GRPC_SERVER_PORT=30900
|
||||
```
|
||||
|
||||
### 4. 编译和运行
|
||||
|
||||
```bash
|
||||
cd server
|
||||
go build -o server ./cmd/server/main.go
|
||||
./server
|
||||
```
|
||||
|
||||
## 接口变更
|
||||
|
||||
`/api/ymt/users` 接口现在通过 gRPC 调用获取数据,而不是直接查询数据库。
|
||||
|
||||
**请求参数:**
|
||||
- `limit` (可选): 限制返回数量,默认 2000,最大 10000
|
||||
- `q` (可选): 搜索关键词
|
||||
|
||||
**响应格式:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "用户名(1)"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 如果 gRPC 服务器地址未配置,接口会返回错误
|
||||
- gRPC 连接使用单例模式,整个应用共享一个连接
|
||||
- 连接包含 keepalive 配置,自动维护连接健康
|
||||
|
||||
|
|
@ -77,7 +77,14 @@ func main() {
|
|||
} else {
|
||||
resellerDB = marketing // fallback
|
||||
}
|
||||
r := api.NewRouter(meta, marketing, marketingAuth, resellerDB, ymt)
|
||||
// 获取 gRPC 服务器地址
|
||||
grpcAddr := cfg.GRPCServer.Address()
|
||||
if grpcAddr != "" {
|
||||
log.Println("gRPC server address:", grpcAddr)
|
||||
} else {
|
||||
log.Println("warning: gRPC server address not configured, /api/ymt/users will not work")
|
||||
}
|
||||
r := api.NewRouter(meta, marketing, marketingAuth, resellerDB, ymt, grpcAddr)
|
||||
addr := ":" + func() string {
|
||||
s := cfg.Port
|
||||
if s == "" {
|
||||
|
|
|
|||
|
|
@ -21,3 +21,6 @@ require (
|
|||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
)
|
||||
|
||||
// 使用本地 grpc 生成的代码
|
||||
replace grpc/user/userv1 => ../grpc/user/userv1
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import (
|
|||
"os"
|
||||
)
|
||||
|
||||
func NewRouter(metaDB *sql.DB, marketingDB *sql.DB, marketingAuthDB *sql.DB, resellerDB *sql.DB, ymtDB *sql.DB) http.Handler {
|
||||
func NewRouter(metaDB *sql.DB, marketingDB *sql.DB, marketingAuthDB *sql.DB, resellerDB *sql.DB, ymtDB *sql.DB, grpcAddr string) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/api/templates", withAccess(withTrace(TemplatesHandler(metaDB, marketingDB))))
|
||||
mux.Handle("/api/templates/", withAccess(withTrace(TemplatesHandler(metaDB, marketingDB))))
|
||||
|
|
@ -21,8 +21,8 @@ func NewRouter(metaDB *sql.DB, marketingDB *sql.DB, marketingAuthDB *sql.DB, res
|
|||
mux.Handle("/api/resellers/", withAccess(withTrace(ResellersHandler(resellerDB))))
|
||||
mux.Handle("/api/plans", withAccess(withTrace(PlansHandler(marketingDB))))
|
||||
mux.Handle("/api/plans/", withAccess(withTrace(PlansHandler(marketingDB))))
|
||||
mux.Handle("/api/ymt/users", withAccess(withTrace(YMTUsersHandler(ymtDB))))
|
||||
mux.Handle("/api/ymt/users/", withAccess(withTrace(YMTUsersHandler(ymtDB))))
|
||||
mux.Handle("/api/ymt/users", withAccess(withTrace(YMTUsersHandler(grpcAddr))))
|
||||
mux.Handle("/api/ymt/users/", withAccess(withTrace(YMTUsersHandler(grpcAddr))))
|
||||
mux.Handle("/api/ymt/merchants", withAccess(withTrace(YMTMerchantsHandler(ymtDB))))
|
||||
mux.Handle("/api/ymt/merchants/", withAccess(withTrace(YMTMerchantsHandler(ymtDB))))
|
||||
mux.Handle("/api/ymt/activities", withAccess(withTrace(YMTActivitiesHandler(ymtDB))))
|
||||
|
|
|
|||
|
|
@ -1,19 +1,35 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/internal/grpc"
|
||||
)
|
||||
|
||||
type YMTUsersAPI struct {
|
||||
ymt *sql.DB
|
||||
grpcClient *grpc.UserClient
|
||||
}
|
||||
|
||||
func YMTUsersHandler(ymt *sql.DB) http.Handler {
|
||||
api := &YMTUsersAPI{ymt: ymt}
|
||||
func YMTUsersHandler(grpcAddr string) http.Handler {
|
||||
var api *YMTUsersAPI
|
||||
if grpcAddr != "" {
|
||||
client, err := grpc.NewUserClient(grpcAddr)
|
||||
if err != nil {
|
||||
// 如果 gRPC 连接失败,记录错误但继续运行(降级处理)
|
||||
// 在实际调用时会返回错误
|
||||
} else {
|
||||
api = &YMTUsersAPI{grpcClient: client}
|
||||
}
|
||||
}
|
||||
if api == nil {
|
||||
// 如果没有 gRPC 客户端,创建一个空的 API(会返回错误)
|
||||
api = &YMTUsersAPI{}
|
||||
}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p := strings.TrimPrefix(r.URL.Path, "/api/ymt/users")
|
||||
if r.Method == http.MethodGet && p == "" {
|
||||
|
|
@ -25,6 +41,11 @@ func YMTUsersHandler(ymt *sql.DB) http.Handler {
|
|||
}
|
||||
|
||||
func (a *YMTUsersAPI) list(w http.ResponseWriter, r *http.Request) {
|
||||
if a.grpcClient == nil {
|
||||
fail(w, r, http.StatusInternalServerError, "gRPC client not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
limit := 2000
|
||||
if limitStr != "" {
|
||||
|
|
@ -32,34 +53,45 @@ func (a *YMTUsersAPI) list(w http.ResponseWriter, r *http.Request) {
|
|||
limit = n
|
||||
}
|
||||
}
|
||||
sql1 := "SELECT id, name FROM user WHERE id IS NOT NULL"
|
||||
args := []interface{}{}
|
||||
sql1 += " ORDER BY id ASC LIMIT ?"
|
||||
args = append(args, limit)
|
||||
rows, err := a.ymt.Query(sql1, args...)
|
||||
|
||||
// 获取搜索关键词(如果有)
|
||||
keyword := r.URL.Query().Get("q")
|
||||
|
||||
// 创建带超时的 context
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 调用 gRPC 服务
|
||||
resp, err := a.grpcClient.SimpleListAllUser(ctx, keyword)
|
||||
if err != nil {
|
||||
fail(w, r, http.StatusInternalServerError, err.Error())
|
||||
fail(w, r, http.StatusInternalServerError, fmt.Sprintf("gRPC call failed: %v", err))
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// 转换响应格式
|
||||
out := []map[string]interface{}{}
|
||||
for rows.Next() {
|
||||
var id sql.NullInt64
|
||||
var name sql.NullString
|
||||
if err := rows.Scan(&id, &name); err != nil {
|
||||
for i, user := range resp.List {
|
||||
if i >= limit {
|
||||
break
|
||||
}
|
||||
if user == nil {
|
||||
continue
|
||||
}
|
||||
if !id.Valid {
|
||||
continue
|
||||
// 构建显示名称:realname(id)或 username(id)
|
||||
displayName := user.Realname
|
||||
if displayName == "" {
|
||||
displayName = user.Username
|
||||
}
|
||||
n := strings.TrimSpace(name.String)
|
||||
if n == "" {
|
||||
n = strconv.FormatInt(id.Int64, 10)
|
||||
if displayName == "" {
|
||||
displayName = strconv.FormatInt(int64(user.Id), 10)
|
||||
}
|
||||
display := fmt.Sprintf("%s(%d)", n, id.Int64)
|
||||
display := fmt.Sprintf("%s(%d)", displayName, user.Id)
|
||||
|
||||
out = append(out, map[string]interface{}{"id": id.Int64, "name": display})
|
||||
out = append(out, map[string]interface{}{
|
||||
"id": user.Id,
|
||||
"name": display,
|
||||
})
|
||||
}
|
||||
|
||||
ok(w, r, out)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ import (
|
|||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
|
||||
|
||||
// 使用生成的 proto 代码
|
||||
// 注意:需要先运行 cd grpc && make generate 生成代码
|
||||
userv1 "server/grpc/user/userv1"
|
||||
userv1 "grpc/user/userv1"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -90,4 +90,3 @@ func (c *UserClient) Close() error {
|
|||
func (c *UserClient) GetConn() *grpc.ClientConn {
|
||||
return c.conn
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue