本文基于 apps-scheduler 和 cat-led 两个实际项目,介绍懒猫微服 Go-SDK 的使用方法。
SDK 简介
懒猫微服 Go-SDK(gitee.com/linakesi/lzc-sdk)是懒猫微服平台提供的 Go 语言 SDK,允许应用通过 gRPC 与懒猫微服系统交互。SDK 封装了以下核心能力:
- 应用管理(PkgManager):查询、启动、暂停应用
- 用户管理(Users):查询用户信息
- 设备管理(Box):查询设备信息、控制 LED、关机/重启
项目结构
使用 Go-SDK 开发的懒猫应用推荐采用如下项目结构:
your-app/
├── cmd/
│ └── your-app/
│ └── main.go # 应用入口
├── internal/
│ ├── web/
│ │ └── server.go # Web 服务器配置与路由
│ ├── handlers/
│ │ ├── app.go # SDK 调用相关 handler
│ │ └── userinfo.go # 用户信息 handler
│ ├── biz/
│ │ └── usecase.go # 业务逻辑与数据库操作
│ ├── auth/
│ │ └── oidc.go # OIDC 认证
│ └── ent/
│ └── schema/ # ent ORM schema 定义
├── go.mod
├── manifest.yml # 懒猫应用清单
├── lzc-deploy-params.yml # 部署参数配置(可选)
├── lzc-build.yml # 构建配置
└── icon.png # 应用图标
依赖配置
在 go.mod 中添加 SDK 依赖:
module your-app
go 1.24.0
require (
gitee.com/linakesi/lzc-sdk v0.0.0-20250307093731-41fc0a4beab9
google.golang.org/grpc v1.63.2
)
SDK 通过 gRPC 与系统通信,因此也需要 google.golang.org/grpc 依赖。
核心概念:APIGateway
SDK 的所有功能都通过 APIGateway 对象来访问。使用模式固定为三步:创建 → 调用 → 关闭。
import (
gohelper "gitee.com/linakesi/lzc-sdk/lang/go"
"google.golang.org/grpc/metadata"
)
// 1. 准备 Context,附加用户标识
ctx := context.Background()
ctx = metadata.AppendToOutgoingContext(ctx, "x-hc-user-id", userID)
// 2. 创建 APIGateway
gw, err := gohelper.NewAPIGateway(ctx)
if err != nil {
return err
}
defer gw.Close() // 3. 确保关闭
// 通过 gw 调用各种服务
// gw.PkgManager - 应用管理
// gw.Users - 用户管理
// gw.Box - 设备管理
每次调用 SDK 都需要创建新的
APIGateway实例并在使用完毕后关闭。务必使用defer gw.Close()避免资源泄漏。
Context Metadata
SDK 使用 gRPC metadata 传递用户身份信息。最关键的是 x-hc-user-id:
import "google.golang.org/grpc/metadata"
ctx = metadata.AppendToOutgoingContext(ctx, "x-hc-user-id", userID)
如果不传递 x-hc-user-id,某些需要用户上下文的 API 可能无法正常工作。
应用管理 API
查询应用列表
import "gitee.com/linakesi/lzc-sdk/lang/go/sys"
resp, err := gw.PkgManager.QueryApplication(ctx, &sys.QueryApplicationRequest{})
if err != nil {
return err
}
for _, info := range resp.InfoList {
fmt.Println("应用ID:", info.Appid)
fmt.Println("状态:", info.Status.String()) // Installed, NotInstalled 等
fmt.Println("实例状态:", info.InstanceStatus.String()) // Running, Stopped, Starting 等
// 注意:Title, Icon, Version, Builtin 是指针类型,可能为 nil
if info.Title != nil {
fmt.Println("标题:", *info.Title)
}
if info.Icon != nil {
fmt.Println("图标:", *info.Icon)
}
if info.Version != nil {
fmt.Println("版本:", *info.Version)
}
fmt.Println("是否多实例:", info.MultiInstance)
if info.Builtin != nil {
fmt.Println("是否内置:", *info.Builtin)
}
}
也可以指定查询特定应用:
resp, err := gw.PkgManager.QueryApplication(ctx, &sys.QueryApplicationRequest{
AppidList: []string{"com.example.app1", "com.example.app2"},
})
启动应用
_, err = gw.PkgManager.Resume(ctx, &sys.AppInstance{
Appid: appID,
Uid: userID,
})
暂停应用
_, err = gw.PkgManager.Pause(ctx, &sys.AppInstance{
Appid: appID,
Uid: userID,
})
启动前检查状态(推荐)
直接调用 Resume/Pause 前,先查询应用状态可以避免不必要的错误:
func resumeApp(ctx context.Context, appID, userID string) error {
ctx = metadata.AppendToOutgoingContext(ctx, "x-hc-user-id", userID)
gw, err := gohelper.NewAPIGateway(ctx)
if err != nil {
return err
}
defer gw.Close()
// 查询当前状态
resp, err := gw.PkgManager.QueryApplication(ctx, &sys.QueryApplicationRequest{
AppidList: []string{appID},
})
if err != nil {
return err
}
if len(resp.InfoList) == 0 {
return fmt.Errorf("application %s not found", appID)
}
appInfo := resp.InfoList[0]
// 检查是否已安装
if appInfo.Status != sys.AppStatus_Installed {
return fmt.Errorf("application %s is not installed (status: %s)", appID, appInfo.Status.String())
}
// 已运行则直接返回(幂等)
if appInfo.InstanceStatus == sys.InstanceStatus_Status_Running {
return nil
}
// 启动中则等待
if appInfo.InstanceStatus == sys.InstanceStatus_Status_Starting {
return nil
}
// 调用 Resume
_, err = gw.PkgManager.Resume(ctx, &sys.AppInstance{
Appid: appID,
Uid: userID,
})
return err
}
用户管理 API
查询用户信息
import "gitee.com/linakesi/lzc-sdk/lang/go/common"
userInfo, err := gw.Users.QueryUserInfo(ctx, &common.UserID{Uid: userID})
if err == nil && userInfo != nil {
fmt.Println("昵称:", userInfo.Nickname)
fmt.Println("头像:", userInfo.Avatar)
}
设备管理 API
查询设备信息
boxInfo, err := gw.Box.QueryInfo(ctx, nil)
if err != nil {
return err
}
fmt.Println("LED 状态:", boxInfo.PowerLed)
控制 LED
import users "gitee.com/linakesi/lzc-sdk/lang/go/common"
// 开启 LED
_, err = gw.Box.ChangePowerLed(ctx, &users.ChangePowerLedRequest{
PowerLed: true,
})
// 关闭 LED
_, err = gw.Box.ChangePowerLed(ctx, &users.ChangePowerLedRequest{
PowerLed: false,
})
设备关机/重启
// 关机
_, err = gw.Box.Shutdown(ctx, &users.ShutdownRequest{
Action: users.ShutdownRequest_Poweroff,
})
// 重启
_, err = gw.Box.Shutdown(ctx, &users.ShutdownRequest{
Action: users.ShutdownRequest_Reboot,
})
OIDC 认证
懒猫微服为每个应用注入 OIDC 认证信息。应用需要实现 OIDC 认证流程来获取用户身份。
环境变量
懒猫微服会自动注入以下环境变量:
| 环境变量 | 说明 |
|---|---|
LAZYCAT_AUTH_OIDC_CLIENT_ID |
OIDC Client ID |
LAZYCAT_AUTH_OIDC_CLIENT_SECRET |
OIDC Client Secret |
LAZYCAT_AUTH_OIDC_AUTH_URI |
OIDC 授权端点 |
LAZYCAT_AUTH_OIDC_TOKEN_URI |
OIDC Token 端点 |
LAZYCAT_AUTH_OIDC_USERINFO_URI |
OIDC 用户信息端点 |
LAZYCAT_APP_DOMAIN |
应用域名 |
获取用户身份
有两种方式获取用户身份:
方式一:通过 HTTP Header(系统注入)
懒猫微服网关会自动在请求头中注入 x-hc-user-id 和 x-hc-user-role。这是最简单的方式:
// Echo 框架
func GetUserID(c echo.Context) string {
if userID := c.Request().Header.Get("x-hc-user-id"); userID != "" {
return userID
}
// 回退到 session
if userID := c.Get("user_id"); userID != nil {
return userID.(string)
}
return ""
}
// Gin 框架
func GetUserID(c *gin.Context) string {
if userID := c.GetHeader("x-hc-user-id"); userID != "" {
return userID
}
if sessionUserID, exists := c.Get("user_id"); exists && sessionUserID != "" {
return sessionUserID.(string)
}
return ""
}
方式二:通过 OIDC 认证流程
当 Header 中没有用户信息时(如用户直接通过浏览器访问),需要走 OIDC 认证:
import (
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)
type OIDCProvider struct {
config *oauth2.Config
userInfoURL string
}
func NewOIDCProvider() (*OIDCProvider, error) {
clientID := os.Getenv("LAZYCAT_AUTH_OIDC_CLIENT_ID")
clientSecret := os.Getenv("LAZYCAT_AUTH_OIDC_CLIENT_SECRET")
authURL := os.Getenv("LAZYCAT_AUTH_OIDC_AUTH_URI")
tokenURL := os.Getenv("LAZYCAT_AUTH_OIDC_TOKEN_URI")
userInfoURL := os.Getenv("LAZYCAT_AUTH_OIDC_USERINFO_URI")
if clientID == "" || clientSecret == "" || authURL == "" || tokenURL == "" {
return nil, errors.New("missing required OIDC configuration")
}
domain := os.Getenv("LAZYCAT_APP_DOMAIN")
redirectURL := fmt.Sprintf("https://%s/auth/oidc/callback", domain)
oauth2Config := &oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: redirectURL,
Scopes: []string{oidc.ScopeOpenID, "profile", "email", "groups"},
Endpoint: oauth2.Endpoint{
AuthURL: authURL,
TokenURL: tokenURL,
},
}
return &OIDCProvider{
config: oauth2Config,
userInfoURL: userInfoURL,
}, nil
}
认证中间件
中间件的核心逻辑是:优先读 Header → 回退到 Session → 未认证则重定向登录页。
// Echo 版本
func AuthMiddleware(oidcProvider *OIDCProvider) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// 优先检查系统注入的 Header
if userID := c.Request().Header.Get("x-hc-user-id"); userID != "" {
return next(c)
}
// 检查 Session
if userID := c.Get("user_id"); userID != nil && userID.(string) != "" {
c.Response().Header().Set("x-hc-user-id", userID.(string))
return next(c)
}
// 放行公共路径
if isPublicPath(c.Request().URL.Path) {
return next(c)
}
// 重定向到登录
return c.Redirect(http.StatusFound, "/login")
}
}
}
// Gin 版本
func AuthMiddleware(oidcProvider *OIDCProvider) gin.HandlerFunc {
return func(c *gin.Context) {
if userID := c.GetHeader("x-hc-user-id"); userID != "" {
c.Next()
return
}
if sessionUserID, exists := c.Get("user_id"); exists && sessionUserID != "" {
c.Header("x-hc-user-id", sessionUserID.(string))
c.Next()
return
}
if isPublicPath(c.Request.URL.Path) {
c.Next()
return
}
c.Redirect(http.StatusFound, "/login")
c.Abort()
}
}
manifest.yml 配置
使用 SDK 开发的应用通过 manifest.yml 声明应用信息和运行方式。与 Docker 应用不同,SDK 应用使用 backend_launch_command 直接运行二进制文件:
name: 应用定时管家
package: community.lazycat.app.czyt.apps-scheduler
version: 1.0.0
min_os_version: 1.3.8
description: "定时启动和停止懒猫微服上的应用"
license: mit/
author:
application:
subdomain: apps-scheduler
oidc_redirect_path: /auth/oidc/callback
public_path:
- /
upstreams:
- location: /
backend: http://127.0.0.1:8080/
backend_launch_command: /lzcapp/pkg/content/apps-scheduler
environment:
- TZ={{.U.timezone}}
- LAZYCAT_AUTH_OIDC_CLIENT_ID=${LAZYCAT_AUTH_OIDC_CLIENT_ID}
- LAZYCAT_AUTH_OIDC_CLIENT_SECRET=${LAZYCAT_AUTH_OIDC_CLIENT_SECRET}
- LAZYCAT_AUTH_OIDC_AUTH_URI=${LAZYCAT_AUTH_OIDC_AUTH_URI}
- LAZYCAT_AUTH_OIDC_TOKEN_URI=${LAZYCAT_AUTH_OIDC_TOKEN_URI}
- LAZYCAT_AUTH_OIDC_USERINFO_URI=${LAZYCAT_AUTH_OIDC_USERINFO_URI}
- LAZYCAT_APP_DOMAIN=${LAZYCAT_APP_DOMAIN}
- DB_PATH=/lzcapp/var/data/apps-scheduler.db
- LOG_DIR=/lzcapp/var/data/logs
locales:
zh:
name: "应用定时管家"
description: "定时启动和停止懒猫微服上的应用"
en:
name: "App Scheduler"
description: "Schedule start and stop operations for apps on LazyCat"
关键配置说明:
backend_launch_command:指定可执行文件路径,懒猫会直接运行该二进制。打包的二进制文件位于/lzcapp/pkg/content/目录下。oidc_redirect_path:OIDC 回调路径,需与代码中的路由一致。environment:${...}是系统运行时注入的变量,{{.U.xxx}}是用户部署时配置的参数。
lzc-build.yml 配置
SDK 应用的构建配置与 Docker 应用不同,需要指定构建脚本和内容目录:
# 构建脚本路径
buildscript: ./build.sh
# manifest 文件路径
manifest: ./manifest.yml
# 打包到 lpk 的内容目录(编译产物放这里)
contentdir: ./dist
# lpk 包输出路径
pkgout: ./
# 应用图标
icon: ./icon.png
# 开发环境配置
devshell:
routes:
- /=http://127.0.0.1:5173
dependencies:
- nodejs
- npm
- go
setupscript: |
export npm_config_registry=https://registry.npmmirror.com
export GOPROXY=https://goproxy.cn,direct
devshell 部分用于 lzc-cli project devshell 开发调试,可以指定开发依赖和路由规则。
数据库配置
两个项目都使用 ent ORM 配合 SQLite。SQLite 数据库文件存储在 /lzcapp/var/data/ 目录下,确保数据持久化。
import (
"your-app/internal/ent"
_ "github.com/lib-x/entsqlite"
)
func NewUseCase(dbPath string) (*UseCase, error) {
dataSourceName := fmt.Sprintf(
"file:%s?cache=shared&_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_pragma=synchronous(NORMAL)&_pragma=busy_timeout(10000)",
dbPath,
)
client, err := ent.Open("sqlite3", dataSourceName)
if err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
// 自动迁移
if err := client.Schema.Create(context.Background()); err != nil {
client.Close()
return nil, fmt.Errorf("failed to create schema: %w", err)
}
return &UseCase{client: client}, nil
}
推荐的 SQLite 配置:
cache=shared:共享缓存模式journal_mode(WAL):WAL 模式提升并发性能synchronous(NORMAL):平衡安全性和性能busy_timeout(10000):10 秒忙等待,避免database is locked错误
完整示例:应用入口
package main
import (
"os"
"os/signal"
"path/filepath"
"syscall"
"your-app/internal/biz"
"your-app/internal/web"
)
const (
defaultDBPath = "/lzcapp/var/data/your-app.db"
defaultLogDir = "/lzcapp/var/data/logs"
)
func main() {
// 初始化日志
logDir := os.Getenv("LOG_DIR")
if logDir == "" {
logDir = defaultLogDir
}
os.MkdirAll(logDir, 0755)
// 数据库路径
dbPath := os.Getenv("DB_PATH")
if dbPath == "" {
dbPath = defaultDBPath
}
os.MkdirAll(filepath.Dir(dbPath), 0755)
// 初始化数据库
useCase, err := biz.NewUseCase(dbPath)
if err != nil {
log.Fatal().Err(err).Msg("Failed to initialize database")
}
defer useCase.Close()
// 启动 Web 服务
addr := os.Getenv("LISTEN_ADDR")
if addr == "" {
addr = ":8080"
}
server := web.NewServer(useCase)
// 优雅关闭
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
server.Shutdown()
}()
server.Start(addr)
}
完整示例:Web 服务配置(Echo)
package web
import (
"embed"
"io/fs"
"net/http"
"your-app/internal/auth"
"your-app/internal/biz"
"your-app/internal/handlers"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
//go:embed public/*
var publicFS embed.FS
type Server struct {
echo *echo.Echo
useCase *biz.UseCase
}
func NewServer(useCase *biz.UseCase) *Server {
e := echo.New()
e.HideBanner = true
oidcProvider, _ := auth.NewOIDCProvider()
publicContent, _ := fs.Sub(publicFS, "public")
server := &Server{echo: e, useCase: useCase}
// 中间件
e.Use(middleware.Recover())
e.Use(auth.SessionMiddleware())
// 静态文件
staticHandler := http.FileServer(http.FS(publicContent))
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)))
// OIDC 路由
if oidcProvider != nil {
e.GET("/auth/oidc/login", oidcProvider.HandleLogin)
e.GET("/auth/oidc/callback", oidcProvider.HandleCallback)
}
// 需认证的路由
protected := e.Group("")
protected.Use(auth.AuthMiddleware(oidcProvider))
// API
api := protected.Group("/api")
appHandler := handlers.NewAppHandler()
api.GET("/apps", appHandler.ListApps)
api.POST("/apps/:appId/resume", appHandler.ResumeApp)
api.POST("/apps/:appId/pause", appHandler.PauseApp)
return server
}
完整示例:在 Handler 中调用 SDK
查询应用列表(Echo)
package handlers
import (
"context"
"net/http"
"your-app/internal/auth"
gohelper "gitee.com/linakesi/lzc-sdk/lang/go"
"gitee.com/linakesi/lzc-sdk/lang/go/sys"
"github.com/labstack/echo/v4"
"google.golang.org/grpc/metadata"
)
type AppInfo struct {
AppID string `json:"appId"`
Title string `json:"title"`
Icon string `json:"icon"`
Version string `json:"version"`
Status string `json:"status"`
InstanceStatus string `json:"instanceStatus"`
}
func (h *AppHandler) ListApps(c echo.Context) error {
ctx := context.Background()
userID := auth.GetUserID(c)
ctx = metadata.AppendToOutgoingContext(ctx, "x-hc-user-id", userID)
gw, err := gohelper.NewAPIGateway(ctx)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to connect to gateway",
})
}
defer gw.Close()
resp, err := gw.PkgManager.QueryApplication(ctx, &sys.QueryApplicationRequest{})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to query applications",
})
}
apps := make([]AppInfo, 0, len(resp.InfoList))
for _, info := range resp.InfoList {
if info.Status != sys.AppStatus_Installed {
continue
}
// 跳过内置应用和多实例应用
if info.Builtin != nil && *info.Builtin {
continue
}
if info.MultiInstance {
continue
}
app := AppInfo{
AppID: info.Appid,
Status: info.Status.String(),
InstanceStatus: info.InstanceStatus.String(),
}
if info.Title != nil {
app.Title = *info.Title
} else {
app.Title = info.Appid
}
if info.Icon != nil {
app.Icon = *info.Icon
}
if info.Version != nil {
app.Version = *info.Version
}
apps = append(apps, app)
}
return c.JSON(http.StatusOK, apps)
}
控制 LED(Gin)
package handlers
import (
"net/http"
"sync"
gohelper "gitee.com/linakesi/lzc-sdk/lang/go"
users "gitee.com/linakesi/lzc-sdk/lang/go/common"
"github.com/gin-gonic/gin"
)
var (
ledStatus bool
ledMutex sync.Mutex
)
func GetLedStatus(c *gin.Context) {
ctx := c.Request.Context()
gw, err := gohelper.NewAPIGateway(ctx)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
defer gw.Close()
boxInfo, err := gw.Box.QueryInfo(ctx, nil)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
ledMutex.Lock()
ledStatus = boxInfo.PowerLed
ledMutex.Unlock()
c.JSON(http.StatusOK, gin.H{"status": ledStatus})
}
func LedControl(c *gin.Context) {
ctx := c.Request.Context()
gw, err := gohelper.NewAPIGateway(ctx)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
defer gw.Close()
boxInfo, err := gw.Box.QueryInfo(ctx, nil)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
newStatus := !boxInfo.PowerLed
ledMutex.Lock()
defer ledMutex.Unlock()
_, err = gw.Box.ChangePowerLed(ctx, &users.ChangePowerLedRequest{
PowerLed: newStatus,
})
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
ledStatus = newStatus
c.JSON(http.StatusOK, gin.H{"status": ledStatus})
}
后台定时任务
对于需要后台定时执行 SDK 操作的场景(如定时启停应用、定时控制 LED),可以使用 goroutine + ticker 的模式:
type Scheduler struct {
useCase *UseCase
stopCh chan struct{}
wg sync.WaitGroup
}
func NewScheduler(useCase *UseCase) *Scheduler {
return &Scheduler{
useCase: useCase,
stopCh: make(chan struct{}),
}
}
func (s *Scheduler) Start() {
s.wg.Add(1)
go s.run()
}
func (s *Scheduler) Stop() {
close(s.stopCh)
s.wg.Wait()
}
func (s *Scheduler) run() {
defer s.wg.Done()
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
s.checkAndExecute() // 启动时立即检查一次
for {
select {
case <-ticker.C:
s.checkAndExecute()
case <-s.stopCh:
return
}
}
}
func (s *Scheduler) checkAndExecute() {
ctx := context.Background()
now := time.Now()
weekday := int(now.Weekday())
schedules, err := s.useCase.GetEnabledSchedules(ctx)
if err != nil {
return
}
for _, sch := range schedules {
if sch.Hour == now.Hour() && sch.Minute == now.Minute() && containsDay(sch.WeekDays, weekday) {
go s.executeSchedule(ctx, sch)
}
}
}
后台定时任务需要在 manifest.yml 中声明 background_task: true,防止系统因为不活跃而自动停止应用。
SDK 导入路径参考
| 包路径 | 别名 | 用途 |
|---|---|---|
gitee.com/linakesi/lzc-sdk/lang/go |
gohelper |
创建 APIGateway |
gitee.com/linakesi/lzc-sdk/lang/go/sys |
sys |
应用管理(PkgManager)相关类型 |
gitee.com/linakesi/lzc-sdk/lang/go/common |
common 或 users |
用户管理和设备管理相关类型 |
google.golang.org/grpc/metadata |
metadata |
传递 gRPC 上下文信息 |
常见问题
Gateway 创建失败
确保应用运行在懒猫微服环境中。SDK 通过特定的 gRPC 端点与系统通信,本地开发环境中无法直接使用,需要通过 lzc-cli project devshell 进入开发环境调试。
指针字段的处理
SDK 返回的很多字段(如 Title、Icon、Version、Builtin)是指针类型。使用前必须判空:
// 正确
if info.Title != nil {
title = *info.Title
}
// 错误 - 可能 panic
title = *info.Title
用户身份传递
调用 SDK API 前,确保 Context 中包含 x-hc-user-id metadata。部分 API(如 Resume/Pause)需要通过 userID 确定操作权限。
数据存储路径
- 持久数据:
/lzcapp/var/data/ - 缓存数据:
/lzcapp/cache/ - 包内容(只读):
/lzcapp/pkg/content/ - 日志文件:
/lzcapp/var/data/logs/
参考项目
- apps-scheduler - 应用定时管家,展示了应用管理 API 的使用
- cat-led - 懒猫小灯,展示了设备管理 API 的使用
- 懒猫微服开发者文档