feat(api): 支持 gRPC 用户服务集成

- 更新路由以支持 gRPC 服务器地址配置
- 修改 YMTUsersHandler 以使用 gRPC 客户端进行用户数据查询
- 添加 gRPC 连接失败的降级处理逻辑
- 引入 gRPC 生成的代码以支持用户服务调用
This commit is contained in:
zhouyonggao 2025-12-20 14:11:28 +08:00
parent c02f051cb8
commit fde87fde96
6 changed files with 154 additions and 29 deletions

84
server/GRPC_SETUP.md Normal file
View File

@ -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 配置,自动维护连接健康

View File

@ -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 == "" {

View File

@ -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

View File

@ -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))))

View File

@ -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
// 构建显示名称realnameid或 usernameid
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)
}

View File

@ -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
}