From a02c95b527631bcc9cd47f2c31a54a8a06d9be62 Mon Sep 17 00:00:00 2001 From: lbb00 Date: Fri, 11 Sep 2026 14:44:41 +0800 Subject: [PATCH 1/2] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=E5=BC=95?= =?UTF-8?q?=E7=94=A8=E7=A4=BA=E4=BE=8B=E5=92=8C=E7=8A=B6=E6=80=81=20badge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e90be5b..b48aa2d 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,24 @@ # qdmp-server-sdk -[千岛小程序开放平台 OpenAPI](https://open.qiandao.com/docs/api/auth-token) 官方 Server SDK,[Node.js](#nodejs)、[Java](#java)、[Go](#go) 三端实现,类型安全的业务接口封装 + 应用凭证自动缓存。 +[![npm](https://img.shields.io/npm/v/@qdmp/qdmp-server-sdk?label=npm)](https://www.npmjs.com/package/@qdmp/qdmp-server-sdk) +[![Go Reference](https://pkg.go.dev/badge/github.com/EchoTechFE/qdmp-server-sdk/go.svg)](https://pkg.go.dev/github.com/EchoTechFE/qdmp-server-sdk/go) +[![Node CI](https://github.com/EchoTechFE/qdmp-server-sdk/actions/workflows/node-ci.yml/badge.svg?branch=main)](https://github.com/EchoTechFE/qdmp-server-sdk/actions/workflows/node-ci.yml) +[![Java CI](https://github.com/EchoTechFE/qdmp-server-sdk/actions/workflows/java-ci.yml/badge.svg?branch=main)](https://github.com/EchoTechFE/qdmp-server-sdk/actions/workflows/java-ci.yml) +[![Go CI](https://github.com/EchoTechFE/qdmp-server-sdk/actions/workflows/go-ci.yml/badge.svg?branch=main)](https://github.com/EchoTechFE/qdmp-server-sdk/actions/workflows/go-ci.yml) +[![License](https://img.shields.io/github/license/EchoTechFE/qdmp-server-sdk)](./LICENSE) + +[千岛小程序开放平台 OpenAPI](https://open.qiandao.com/docs/api) 官方 Server SDK,提供 [Node.js](#nodejs)、[Java](#java) 和 [Go](#go) 三端实现,包括类型安全的业务接口和应用凭证自动缓存。 ## Node.js +### 安装 + ```bash npm install @qdmp/qdmp-server-sdk ``` +### 引用 + ```ts import { QdmpClient, QdmpApiError, QdmpValidationError } from '@qdmp/qdmp-server-sdk' @@ -46,6 +57,8 @@ const appCredential = await qdmp.auth.getAppAccessToken() ## Java +Java 包尚未发布到 Maven Central。当前可从仓库的 `java/` 目录构建;发布后的 Gradle 坐标如下: + ```gradle dependencies { implementation("io.github.echotechfe:qdmp-server-sdk:") @@ -53,6 +66,14 @@ dependencies { ``` ```java +import io.github.echotechfe.qdmp.QdmpClient; +import io.github.echotechfe.qdmp.QdmpClientConfig; +import io.github.echotechfe.qdmp.QdmpContext; +import io.github.echotechfe.qdmp.auth.AppAccessTokenResult; +import io.github.echotechfe.qdmp.auth.RefreshTokenResult; +import io.github.echotechfe.qdmp.auth.UserAccessTokenResult; +import io.github.echotechfe.qdmp.generated.MarkAddRequest; + QdmpClient qdmp = new QdmpClient( QdmpClientConfig.builder() .appId(System.getenv("QDMP_APP_ID")) @@ -77,11 +98,22 @@ AppAccessTokenResult appCredential = qdmp.auth().getAppAccessToken(); ## Go +### 安装 + ```bash go get github.com/EchoTechFE/qdmp-server-sdk/go ``` +### 引用 + ```go +import ( + "os" + + qdmp "github.com/EchoTechFE/qdmp-server-sdk/go" + "github.com/EchoTechFE/qdmp-server-sdk/go/generated" +) + client, err := qdmp.NewClient(qdmp.ClientOptions{ AppID: os.Getenv("QDMP_APP_ID"), AppSecret: os.Getenv("QDMP_APP_SECRET"), From 6fcaa677f9df77e093543b5cba25a021a9ad62dd Mon Sep 17 00:00:00 2001 From: lbb00 Date: Fri, 11 Sep 2026 14:53:48 +0800 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20=E9=87=8D=E6=9E=84=20README=20?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=8C=87=E5=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 214 ++++++++++++++++++++++++++---------------------------- 1 file changed, 101 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index b48aa2d..a2d645a 100644 --- a/README.md +++ b/README.md @@ -7,71 +7,114 @@ [![Go CI](https://github.com/EchoTechFE/qdmp-server-sdk/actions/workflows/go-ci.yml/badge.svg?branch=main)](https://github.com/EchoTechFE/qdmp-server-sdk/actions/workflows/go-ci.yml) [![License](https://img.shields.io/github/license/EchoTechFE/qdmp-server-sdk)](./LICENSE) -[千岛小程序开放平台 OpenAPI](https://open.qiandao.com/docs/api) 官方 Server SDK,提供 [Node.js](#nodejs)、[Java](#java) 和 [Go](#go) 三端实现,包括类型安全的业务接口和应用凭证自动缓存。 +[千岛小程序开放平台 OpenAPI](https://open.qiandao.com/docs/api) 官方服务端 SDK,提供 Node.js、Go 和 Java 实现。三端共用一份接口定义,请求参数和响应结果都有对应类型。 -## Node.js +## SDK 状态 -### 安装 +| 语言 | 环境要求 | 获取方式 | +| --- | --- | --- | +| Node.js | Node.js 22+ | [npm](https://www.npmjs.com/package/@qdmp/qdmp-server-sdk) | +| Go | Go 1.24+ | [Go Reference](https://pkg.go.dev/github.com/EchoTechFE/qdmp-server-sdk/go) | +| Java | Java 11+ | 暂未发布到 Maven Central,可从 [`java/`](./java/) 构建 | + +## Node.js 快速开始 + +安装: ```bash npm install @qdmp/qdmp-server-sdk ``` -### 引用 +初始化客户端后,用前端 `qd.login()` 返回的一次性授权码换取用户凭证: ```ts -import { QdmpClient, QdmpApiError, QdmpValidationError } from '@qdmp/qdmp-server-sdk' +import { QdmpClient } from '@qdmp/qdmp-server-sdk' const qdmp = new QdmpClient({ appId: process.env.QDMP_APP_ID!, appSecret: process.env.QDMP_APP_SECRET!, }) -// 前端 qd.login() 拿到的一次性授权码传到服务端,换用户授权凭证 const credential = await qdmp.auth.getUserAccessToken(code) -// => { accessToken, refreshToken, expiresAt, openId },自己按 openId 存起来 +const context = { accessToken: credential.accessToken } -// 调业务接口:accessToken 每次显式传进去 -const me = await qdmp.user.me({ accessToken: credential.accessToken }) -await qdmp.mark.add({ accessToken: credential.accessToken }, { spuId: '123', rating: { value: 5 } }) -await qdmp.mark.batchAdd({ accessToken: credential.accessToken }, { spuIds: ['123', '456'] }) -await qdmp.comment.create({ accessToken: credential.accessToken }, { postId: '123', content: '很喜欢' }) +const me = await qdmp.user.me(context) +console.log(me) +await qdmp.mark.add(context, { + spuId: '123', + rating: { value: 5 }, +}) +``` -// accessToken 过期了,自己拿 refreshToken 换新的(SDK 不代管,也不自动重试) +access token 过期后,由业务代码发起续期并保存新结果: + +```ts const fresh = await qdmp.auth.refreshToken(credential.refreshToken) await qdmp.user.me({ accessToken: fresh.accessToken }) +``` + +## Go 快速开始 + +安装: + +```bash +go get github.com/EchoTechFE/qdmp-server-sdk/go +``` + +下面的代码放在已有 `context.Context` 和授权码 `code` 的处理函数中: + +```go +import ( + "os" + + qdmp "github.com/EchoTechFE/qdmp-server-sdk/go" + "github.com/EchoTechFE/qdmp-server-sdk/go/generated" +) + +client, err := qdmp.NewClient(qdmp.ClientOptions{ + AppID: os.Getenv("QDMP_APP_ID"), + AppSecret: os.Getenv("QDMP_APP_SECRET"), +}) +if err != nil { + return err +} + +credential, err := client.Auth.GetUserAccessToken(ctx, code) +if err != nil { + return err +} -try { - await qdmp.wishspu.list({ accessToken: fresh.accessToken }, { offset: '0', limit: '20' }) -} catch (err) { - if (err instanceof QdmpApiError) console.error(err.code, err.message, err.httpStatus) - else if (err instanceof QdmpValidationError) console.error('本地参数错误:', err.message) +requestContext := qdmp.Context{AccessToken: credential.AccessToken} +_, err = client.User.Me(ctx, requestContext) +if err != nil { + return err } -// 应用凭证,开发调试/后台任务才需要 -const appCredential = await qdmp.auth.getAppAccessToken() -// => { accessToken, expiresAt, refreshToken, openId }(openId 恒为空串) +_, err = client.Mark.Add(ctx, requestContext, generated.MarkAddJSONBody{SpuId: "123"}) +if err != nil { + return err +} + +return nil ``` -失败抛三类错误:`QdmpApiError`(业务失败,响应体 `code` 非 `'0'`)、`QdmpValidationError`(本地参数校验失败,不发请求)、`QdmpTransportError`(传输层问题,如收到重定向、响应体超过 10MB)。底层 `fetch` 本身的网络错误原样透出。 +续期时调用 `client.Auth.RefreshToken(ctx, credential.RefreshToken)`,然后保存并使用返回的新 access token。 ## Java -Java 包尚未发布到 Maven Central。当前可从仓库的 `java/` 目录构建;发布后的 Gradle 坐标如下: +Java SDK 暂未发布到 Maven Central。当前版本可在仓库中构建: -```gradle -dependencies { - implementation("io.github.echotechfe:qdmp-server-sdk:") -} +```bash +cd java +./gradlew build ``` +API 用法如下: + ```java import io.github.echotechfe.qdmp.QdmpClient; import io.github.echotechfe.qdmp.QdmpClientConfig; import io.github.echotechfe.qdmp.QdmpContext; -import io.github.echotechfe.qdmp.auth.AppAccessTokenResult; -import io.github.echotechfe.qdmp.auth.RefreshTokenResult; -import io.github.echotechfe.qdmp.auth.UserAccessTokenResult; import io.github.echotechfe.qdmp.generated.MarkAddRequest; QdmpClient qdmp = new QdmpClient( @@ -80,111 +123,56 @@ QdmpClient qdmp = new QdmpClient( .appSecret(System.getenv("QDMP_APP_SECRET")) .build()); -UserAccessTokenResult credential = qdmp.auth().getUserAccessToken(code); - -// QdmpContext.of(accessToken) 是唯一入口,空/非法 token 在构造期就失败 -QdmpContext ctx = QdmpContext.of(credential.getAccessToken()); -UserMe200ResponseAllOfData me = qdmp.user().me(ctx); -qdmp.mark().add(ctx, new MarkAddRequest().spuId("123")); - -// 过期了自己换,SDK 不代管 -RefreshTokenResult fresh = qdmp.auth().refreshToken(credential.getRefreshToken()); -qdmp.user().me(QdmpContext.of(fresh.getAccessToken())); +var credential = qdmp.auth().getUserAccessToken(code); +var context = QdmpContext.of(credential.getAccessToken()); -AppAccessTokenResult appCredential = qdmp.auth().getAppAccessToken(); +var me = qdmp.user().me(context); +qdmp.mark().add(context, new MarkAddRequest().spuId("123")); ``` -业务失败抛 `QdmpApiError`,传输层异常抛 `QdmpTransportException`,`auth.*` 的参数校验失败抛 `QdmpValidationError`(均在 `io.github.echotechfe.qdmp.errors` 包下)。`QdmpContext.of()` 的 token 校验按 Java 惯例抛 `NullPointerException`(null)/ `IllegalArgumentException`(空白或含不能进 HTTP 头的字符)。 +续期时调用 `qdmp.auth().refreshToken(credential.getRefreshToken())`,然后用返回的新 access token 创建 `QdmpContext`。 -## Go +## 凭证怎么用 -### 安装 +平台有两种凭证,SDK 对它们的处理方式不同。 -```bash -go get github.com/EchoTechFE/qdmp-server-sdk/go -``` - -### 引用 - -```go -import ( - "os" +### 用户授权凭证 - qdmp "github.com/EchoTechFE/qdmp-server-sdk/go" - "github.com/EchoTechFE/qdmp-server-sdk/go/generated" -) +用户授权凭证代表当前登录用户。服务端拿到前端 `qd.login()` 返回的授权码后,调用 `getUserAccessToken` 换取 access token、refresh token 和过期时间。返回结果可以按 `openId` 保存。 -client, err := qdmp.NewClient(qdmp.ClientOptions{ - AppID: os.Getenv("QDMP_APP_ID"), - AppSecret: os.Getenv("QDMP_APP_SECRET"), -}) +SDK 不保存或自动续期用户凭证。每次业务调用都要显式传入 access token;过期后调用 `refreshToken`,由业务代码保存新的 access token。该接口不会更换 refresh token。 -credential, err := client.Auth.GetUserAccessToken(ctx, code) +SDK 也不会自动重试失败的业务请求。遇到 HTTP 401 和错误码 `10005`、`10006` 时,是否续期并重试由业务代码决定。续期接口返回错误码 `10007`、`10008` 时,需要让用户重新授权。 -// 调业务接口:accessToken 每次显式传进去(ctx 仍是标准的 context.Context,管超时和取消) -qdmpCtx := qdmp.Context{AccessToken: credential.AccessToken} -me, err := client.User.Me(ctx, qdmpCtx) -_, err = client.Mark.Add(ctx, qdmpCtx, generated.MarkAddJSONBody{SpuId: "123"}) +### 应用凭证 -// 过期了自己换,SDK 不代管 -fresh, err := client.Auth.RefreshToken(ctx, credential.RefreshToken) -me, err = client.User.Me(ctx, qdmp.Context{AccessToken: fresh.AccessToken}) +应用凭证代表应用本身,适合开发调试、服务端联调和不需要用户身份的后台任务。以 Node.js 为例: -appCredential, err := client.Auth.GetAppAccessToken(ctx) +```ts +const credential = await qdmp.auth.getAppAccessToken() ``` -业务失败返回 `*qdmp.QdmpApiError`,缺凭证返回哨兵错误 `qdmp.ErrAccessTokenRequired`(`errors.Is` 判断),均是普通 `error`,不 panic。 - -## 凭证生命周期模型 +`getAppAccessToken` 会缓存凭证,并在到期前 300 秒重新获取。默认缓存在当前进程;多实例部署可以实现 `TokenStore`,改用 Redis 等共享存储。 -- **用户授权凭证由调用方自己管**:SDK 不缓存、不代管、不自动续期。每次业务调用把 accessToken 显式传进去。 - 一个进程要服务海量终端用户,哪次调用属于哪个用户、凭证该存到哪、什么时候该续期,只有你自己知道; - 你也可以完全不用 `getUserAccessToken`,自己实现拿凭证那一步,SDK 照样能用。 -- **续期**:`auth.refreshToken(refreshToken)` 换一个新的 accessToken,一次性调用,SDK 不缓存结果。 - `/auth/v1/refresh` 只返回新的 accessToken 和 expiresAt,**refreshToken 保持不变**。 - 它本身失效时返回 HTTP 200 + `10007`/`10008`,此时凭证已无法挽救,需要重新走一次「拿授权码 → 换凭证」。 -- **SDK 不做任何自动重试**:业务调用撞上 HTTP 401 + `10005`/`10006`(access_token 失效/过期)时, - 直接把 `QdmpApiError` 抛/返回给你,由你决定是否续期后重试。 -- **应用凭证**:`getAppAccessToken()` 自动缓存 + 到期前 300 秒重新换取 + 单飞锁防并发重复换取, - 持久化走可插拔的 `TokenStore`(默认内存实现,多实例部署可换 Redis 等共享存储)。 - 它返回的是完整凭证 `{accessToken, expiresAt, refreshToken, openId}`——应用凭证响应里同样带 refreshToken, - 只是 `openId` 恒为空串。SDK 自己不拿它续期(到期直接重换),但如实交给你,要不要自己续期由你决定。 -- **成功判定只看响应体 `code === '0'`,不看 HTTP 状态码**——refreshToken 过期是 HTTP 200 + `10008`, - access_token 失效才是 HTTP 401 + `10006`。 +## 接口需要哪种凭证 -## 各分组凭证要求 +| 接口分组 | 凭证要求 | +| --- | --- | +| `auth.*` | 不需要额外凭证,调用时使用 appId、appSecret 或 refresh token | +| `user.me`、`mark.*`、`wishspu.*`、`post.*`、`comment.*` | 必须传用户授权凭证;缺少凭证时不会发出请求 | +| `island.*`、`spu.*`、`tag.*`、`genai.*` | 显式传入 access token;应用凭证可用 | -| 分组 | 要求 | -|---|---| -| `auth.*` | 不需要凭证(用 appId/appSecret 或 refreshToken) | -| `user.me` / `mark.*` / `wishspu.*` / `post.*` / `comment.*` | 必须是用户授权凭证,缺失时本地直接报错,不发请求 | -| `island.*` / `spu.*` / `tag.*` / `genai.*` | 应用凭证也实测调通过,但 SDK 不做静默 fallback,传哪种由调用方决定 | +接口是否成功以响应体的 `code === '0'` 为准,不只看 HTTP 状态码。 -`x-echo-qdmp-version` 头只在 `standard` 鉴权方案下发送,`genai` 分组用 `x-openapi-access-token` + `x-openapi-app-id` 另一对头,`auth` 分组不需要——三端都从 `shared/generated/route-meta.json` 读取每个 operation 的 `authScheme`/`tokenRequired`。 +## 错误处理 -## 底层依赖 - -| 语言 | HTTP 传输 | JSON | -|---|---|---| -| Node.js | 内置全局 `fetch`(`undici` 仅作为开发依赖,提供测试用的 `MockAgent`) | 内置 `JSON` | -| Java | OkHttp(`okhttp3`) | Jackson | -| Go | 标准库 `net/http` | 标准库 `encoding/json` | - -类型/DTO 由 OpenAPI 3.0 spec(`shared/openapi.yaml`)生成,三端分别用 `openapi-typescript`、`openapi-generator-cli`、`oapi-codegen`——只生成类型,不生成完整 client,业务方法和请求逻辑都是手写的。 - -## 目录结构 - -``` -qdmp-server-sdk/ -├─ shared/ # 单一真源 openapi.yaml + 生成的路由元数据/错误码表 -├─ node/ # npm: @qdmp/qdmp-server-sdk -├─ java/ # Maven: io.github.echotechfe:qdmp-server-sdk -└─ go/ # module: github.com/EchoTechFE/qdmp-server-sdk/go -``` +- Node.js:业务错误为 `QdmpApiError`,本地参数错误为 `QdmpValidationError`,重定向或响应体过大等传输错误为 `QdmpTransportError`。底层 `fetch` 的网络错误会原样抛出。 +- Java:业务错误为 `QdmpApiError`,参数错误为 `QdmpValidationError`,传输错误为 `QdmpTransportException`,都在 `io.github.echotechfe.qdmp.errors` 包中。 +- Go:业务错误为 `*qdmp.QdmpApiError`;缺少凭证时返回 `qdmp.ErrAccessTokenRequired`,可用 `errors.Is` 判断。SDK 返回普通 `error`,不会 panic。 ## 开发 -本地环境搭建、代码生成、构建/测试/覆盖率命令见 [DEVELOPMENT.md](./DEVELOPMENT.md)。 +`shared/openapi.yaml` 是三端共用的接口定义。代码生成、构建、测试和覆盖率命令见 [DEVELOPMENT.md](./DEVELOPMENT.md)。 ## License