227 lines
7.4 KiB
Markdown
227 lines
7.4 KiB
Markdown
# 华为统一推送(HMS Push)服务端集成方案
|
||
|
||
> pi(API 维护方)编写 | 2026-09-07
|
||
|
||
---
|
||
|
||
## 一、背景
|
||
|
||
当前 Gateway 通过 SSE(Server-Sent Events)向在线客户端实时推送新邮件。但当客户端在后台或被系统杀死时,SSE 断开,用户无法收到新邮件通知。华为鸿蒙统一推送(HMS Push)可在设备关闭应用时仍推送通知——点击通知打开应用并跳转对应邮件。
|
||
|
||
**目标**:Gateway 在 SSE 之外增加推送管道,当 `new_mail` 到达时,对所有已注册推送 token 的设备发送 HMS Push 通知。
|
||
|
||
---
|
||
|
||
## 二、HMS Push API 工作方式
|
||
|
||
```
|
||
客户端 (鸿蒙) 服务端 (Gateway) 华为 Push API
|
||
│ │ │
|
||
│── 注册 HMS Push,获取 token ──→ │ │
|
||
│ │ │
|
||
│── POST /me/devices/push-token ──→│ 存储 token │
|
||
│ │ │
|
||
│ ...应用关闭/进入后台... │ │
|
||
│ │ │
|
||
│ ← new_mail SSE 断开 ────────── │ │
|
||
│ │ │
|
||
│ │── 调用 HMS Push API ────────→│
|
||
│ │ (POST v1/messages:send) │
|
||
│ │ │
|
||
│ │ ← 推送到设备 ──│
|
||
│ ← 通知栏弹出 ────────────────── │ │
|
||
│── 用户点击通知 ──→ 打开应用 ──────│ │ │
|
||
```
|
||
|
||
**HMS Push API 核心接口**:
|
||
```
|
||
POST https://push-api.cloud.huawei.com/v1/{app_id}/messages:send
|
||
Authorization: Bearer {access_token}
|
||
Body: {
|
||
"validate_only": false,
|
||
"message": {
|
||
"token": ["push_token_1", ...],
|
||
"notification": {
|
||
"title": "新邮件:{subject}",
|
||
"body": "发件人:{from_name}"
|
||
},
|
||
"data": {
|
||
"mail_id": "...",
|
||
"session_id": "...",
|
||
"action": "open_mail"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**认证**:需要华为开发者账号的 App ID + App Secret → 获取 access_token(有效期 1 小时,需刷新)。
|
||
|
||
---
|
||
|
||
## 三、服务端架构
|
||
|
||
### 3.1 数据模型
|
||
|
||
新增 `push_tokens` 表(SQLite + PostgreSQL 通用):
|
||
|
||
```sql
|
||
CREATE TABLE push_tokens (
|
||
user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
|
||
push_token TEXT NOT NULL, -- HMS Push token(设备唯一)
|
||
platform TEXT NOT NULL DEFAULT 'harmonyos', -- 标识来源(未来可扩展)
|
||
device_id TEXT NOT NULL DEFAULT '', -- 客户端生成的设备唯一 ID
|
||
created_at DATETIME DEFAULT (datetime('now')),
|
||
updated_at DATETIME DEFAULT (datetime('now')),
|
||
PRIMARY KEY (user_id, push_token)
|
||
);
|
||
```
|
||
|
||
- 每个用户可注册多个设备(手机、平板)
|
||
- 同一设备的 token 更新时 upsert(`device_id` 去重)
|
||
- 用户删除时级联清理
|
||
|
||
### 3.2 API 端点
|
||
|
||
```
|
||
POST /me/devices/push-token 注册/更新推送 token
|
||
DELETE /me/devices/push-token 注销推送 token
|
||
GET /me/devices/push-tokens 查询已注册的设备列表
|
||
```
|
||
|
||
请求体(注册):
|
||
```json
|
||
{
|
||
"push_token": "CAHPxxx...",
|
||
"device_id": "device-uuid-xxx",
|
||
"platform": "harmonyos"
|
||
}
|
||
```
|
||
|
||
### 3.3 推送触发点
|
||
|
||
在 `notify/mail.go` 的 `Recipients()` 函数末尾增加推送调用:
|
||
|
||
```go
|
||
// 现有 SSE 推送
|
||
sse.Default.SendToRecipient(m.To.Name, "new_mail", payload(...))
|
||
|
||
// 新增:HMS Push 推送(异步,不阻塞 SSE)
|
||
go pushToHMS(m.To.Name, payload(...))
|
||
```
|
||
|
||
`pushToHMS` 逻辑:
|
||
1. 查询该用户的所有 `push_tokens`
|
||
2. 组装 HMS Push 请求(title + body + data payload)
|
||
3. 调用 HMS Push API(带 access_token 刷新逻辑)
|
||
4. 失败静默(SSE 已保证在线设备收到,Push 是补充通道)
|
||
|
||
### 3.4 access_token 管理
|
||
|
||
HMS Push API 需要 OAuth 2.0 access_token(有效期 ~3600s):
|
||
|
||
```go
|
||
type HMSTokenManager struct {
|
||
appID string
|
||
appSecret string
|
||
token string
|
||
expiresAt time.Time
|
||
}
|
||
|
||
func (m *HMSTokenManager) getToken() (string, error) {
|
||
if time.Now().Before(m.expiresAt) {
|
||
return m.token, nil
|
||
}
|
||
// POST https://oauth-login.cloud.huawei.com/oauth2/v3/token
|
||
// grant_type=client_credentials&client_id=appID&client_secret=appSecret
|
||
// ...
|
||
return newToken, nil
|
||
}
|
||
```
|
||
|
||
### 3.5 推送内容模板
|
||
|
||
```
|
||
标题:新邮件
|
||
正文:{from_name}:{subject}
|
||
数据:{ mail_id, session_id, from_name, subject, action: "open_mail" }
|
||
```
|
||
|
||
客户端收到通知后,打开应用并路由到对应邮件详情页。
|
||
|
||
---
|
||
|
||
## 四、配置项
|
||
|
||
```bash
|
||
# HMS Push 配置(环境变量)
|
||
HMS_APP_ID=10xxxxxx # 华为开发者 App ID
|
||
HMS_APP_SECRET=xxxxxx # App Secret
|
||
HMS_ENABLED=true # 开关(false 时只走 SSE 不推 HMS)
|
||
```
|
||
|
||
`HMS_ENABLED=false` 时 `pushToHMS` 直接 return,零开销。
|
||
|
||
---
|
||
|
||
## 五、鸿蒙客户端侧需要做的事
|
||
|
||
1. **集成 HMS Push SDK**:`@hms.push` 包
|
||
2. **注册 token**:App 启动时调用 HMS Push 获取 token → `POST /me/devices/push-token`
|
||
3. **接收通知**:收到通知后解析 `data` 字段 → 路由到对应邮件
|
||
4. **点击通知**:`action: "open_mail"` → 跳转到 `mail_id` 对应的邮件详情
|
||
|
||
---
|
||
|
||
## 六、时机与限制
|
||
|
||
| 场景 | 行为 |
|
||
|---|---|
|
||
| 客户端在线(SSE 连接) | SSE 已推送,**Push 不发**(避免重复通知) |
|
||
| 客户端后台/SSE 断开 | Push 发送,唤醒设备 |
|
||
| 用户已禁用通知 | 服务端不知情,Push 发送但被系统拦截(正常行为) |
|
||
| 同一用户多设备 | 所有注册的 token 都推送 |
|
||
| 推送频率限制 | HMS Push 免费版有 QPS 限制(按 App 而定),大量并发时需队列化 |
|
||
|
||
**SSE 与 Push 的切换判断**:最简实现是**无条件推送**(SSE 在线时 Push 也会发,但客户端收到 SSE 后可以忽略 Push)。如果要精确判断,客户端在线时上报一个 `push_active` 标记,服务端据此跳过。但最简方案更可靠——依赖客户端状态判断有延迟风险。
|
||
|
||
---
|
||
|
||
## 七、鸿蒙端集成要点(供 dsh 参考)
|
||
|
||
```typescript
|
||
// 鸿蒙端伪代码
|
||
import push from '@hms.push';
|
||
|
||
// App 启动时
|
||
async function registerPushToken() {
|
||
const token = await push.getToken({ scope: 'message' });
|
||
if (token) {
|
||
await fetch(`${gateway}/me/devices/push-token`, {
|
||
method: 'POST',
|
||
headers: { Authorization: `Bearer ${userKey}` },
|
||
body: JSON.stringify({ push_token: token, device_id: deviceId })
|
||
});
|
||
}
|
||
}
|
||
|
||
// 收到推送
|
||
push.onMessage((data) => {
|
||
if (data.action === 'open_mail') {
|
||
navigateToMail(data.mail_id);
|
||
}
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## 八、实施阶段
|
||
|
||
| 阶段 | 内容 | 估时 |
|
||
|---|---|---|
|
||
| **P1** | `push_tokens` 表 + API 端点 + HMS access_token 管理 | 1 天 |
|
||
| **P2** | `notify/mail.go` 接入 + 推送触发逻辑 | 0.5 天 |
|
||
| **P3** | 鸿蒙客户端集成(dsh 侧) | 由 dsh 实施 |
|
||
| **P4** | 测试 + 边界(token 刷新失败、推送频率、多设备) | 0.5 天 |
|
||
|
||
总计 Gateway 侧 **1.5 天**,可与多账号方案并行推进。
|