模拟第三方信息存入数据库并进行消息推送
This commit is contained in:
@@ -124,6 +124,35 @@ UNI‑PUSH 集成注意事项
|
||||
- 如果 `/rest/v1/push_devices` 返回 404:确认表名在 `public` schema 中并加载,或调整请求前缀。
|
||||
- 查看 push-server 控制台输出中的 `supaFetch` warn 和 proxy 响应体以获取具体错误信息。
|
||||
|
||||
- 如果 `/api/v1/push/send` 返回 HTTP 200 但 body 为 `{ errCode: 400, errMsg: "push_clientid required" }`:
|
||||
- 含义:云函数没有拿到 `push_clientid`,因此没有真正执行 uni-push 下发。
|
||||
- 常见原因:`CLOUD_FUNC_URL` 指向 HTTP 触发云函数(cloudbasefunction.cn 等),请求体通常在 `event.body`(字符串)中。
|
||||
- 云函数侧最小兼容写法:先把 `event.body` 解析为 JSON,再从解析结果解构 `push_clientid/title/content`。
|
||||
|
||||
- 如果手机通知标题/内容变成 `????`:
|
||||
- 含义:中文在发出请求前已被错误编码(Windows PowerShell 5.1 常见)。
|
||||
- 解决:用 UTF-8 字节发送 JSON(并设置 `application/json; charset=utf-8`)。示例:
|
||||
|
||||
```powershell
|
||||
$bodyObj = @{
|
||||
cids = @('YOUR_DEVICE_CID')
|
||||
notification = @{ title = '测试标题'; body = '测试内容' }
|
||||
payload = @{ order_id = '123' }
|
||||
}
|
||||
|
||||
$json = $bodyObj | ConvertTo-Json -Depth 10
|
||||
$bytes = [System.Text.Encoding]::UTF8.GetBytes($json)
|
||||
|
||||
Invoke-RestMethod -Method Post -Uri 'http://127.0.0.1:7301/api/v1/push/send' `
|
||||
-ContentType 'application/json; charset=utf-8' `
|
||||
-Body $bytes | ConvertTo-Json -Depth 20
|
||||
```
|
||||
|
||||
- 备注:也可用 `curl.exe` 发送 JSON,避免 PowerShell 的编码差异。
|
||||
|
||||
- 如果你在云函数里加了“回显 recv 并提前 return”用于排查乱码:
|
||||
- 排查完成后务必移除该提前 return,否则云函数不会执行 `uniPush.sendMessage()`,手机将收不到通知。
|
||||
|
||||
后续建议(可选实现)
|
||||
- 将 `express_notifications` 增加 `attempts`、`error`、`sent_at` 字段以支持重试与错误记录;可实现后台 worker 或 pg_notify+listener 做可靠投递与重试。
|
||||
- 为 `/api/v1/push/send` 与 `/api/v1/notifications` 添加管理员鉴权(例如 `PUSH_ADMIN_KEY`)以限制谁能发送通知。
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
}
|
||||
```
|
||||
|
||||
- 成功判定:云函数应返回 HTTP 2xx(服务会把非 2xx 或网络错误视为失败并按重试策略重试或标记失败)。
|
||||
- 成功判定:
|
||||
- HTTP 层面:云函数应返回 HTTP 2xx(服务会把非 2xx 或网络错误视为失败并按重试策略重试或标记失败)。
|
||||
- 业务层面(推荐约定):若云函数返回 JSON 且包含 `ok:false` 或 `errCode!=0`,push-server 会视为失败并写入 `last_error`。
|
||||
|
||||
- 快速启用示例(PowerShell):
|
||||
|
||||
@@ -94,6 +96,53 @@ limit 10;
|
||||
- 常见现象解释:
|
||||
- push-server 日志里 `supaFetch response preview: []`:表示当前没有 pending/retrying 且到期的记录可处理(队列为空)。
|
||||
|
||||
## 常见问题:手机没通知/显示 ????
|
||||
|
||||
### 1) `/api/v1/push/send` 返回 `errCode: 400, errMsg: "push_clientid required"`
|
||||
|
||||
含义:云函数没有拿到 `push_clientid`,因此没有真正调用 uni-push 下发。
|
||||
|
||||
常见原因:`CLOUD_FUNC_URL` 指向的是“HTTP 触发”云函数(例如 cloudbasefunction.cn)。这类云函数往往把 JSON 请求体放在 `event.body`(字符串)里,而不是直接放在 `event.push_clientid`。
|
||||
|
||||
云函数侧的最小兼容写法(示例):
|
||||
|
||||
```js
|
||||
let input = event || {}
|
||||
if (input && typeof input.body === 'string') {
|
||||
try { input = JSON.parse(input.body) } catch (e) { input = {} }
|
||||
} else if (input && input.body && typeof input.body === 'object') {
|
||||
input = input.body
|
||||
}
|
||||
const { token, push_clientid, title, content, payload } = input
|
||||
```
|
||||
|
||||
### 2) 手机通知标题/内容变成 `????`
|
||||
|
||||
含义:中文在“发出请求前”已被错误编码成问号(常见于 Windows PowerShell 5.1 发送 JSON)。
|
||||
|
||||
推荐做法:用 UTF-8 字节发送请求体(并带上 charset):
|
||||
|
||||
```powershell
|
||||
$bodyObj = @{
|
||||
cids = @('YOUR_DEVICE_CID')
|
||||
notification = @{ title = '测试标题'; body = '测试内容' }
|
||||
payload = @{ order_id = '123' }
|
||||
}
|
||||
|
||||
$json = $bodyObj | ConvertTo-Json -Depth 10
|
||||
$bytes = [System.Text.Encoding]::UTF8.GetBytes($json)
|
||||
|
||||
Invoke-RestMethod -Method Post -Uri 'http://127.0.0.1:7301/api/v1/push/send' `
|
||||
-ContentType 'application/json; charset=utf-8' `
|
||||
-Body $bytes | ConvertTo-Json -Depth 20
|
||||
```
|
||||
|
||||
也可以使用 `curl.exe`(更接近 Linux curl 行为)来避免 PowerShell 的编码坑。
|
||||
|
||||
### 3) 云函数“回显 recv”后反而收不到消息
|
||||
|
||||
如果你为了排查乱码在云函数里加了 `return { recv: ... }` 之类的提前返回,云函数会在执行 `uniPush.sendMessage()` 之前就退出,自然不会真的推送。排查完成后请移除/注释该提前 return。
|
||||
|
||||
- 注意:如果你需要“插入时立即触发云函数”的实时行为,可考虑将轮询改为 Supabase Realtime 订阅或使用 Supabase 的 Edge Function / webhook 触发器;我可以协助把轮询替换为实时订阅的示例实现。
|
||||
|
||||
---
|
||||
|
||||
@@ -161,15 +161,29 @@ async function start() {
|
||||
async function claimNotification(id) {
|
||||
try {
|
||||
const body = { status_code: 'processing', updated_at: new Date().toISOString() }
|
||||
// 仅当当前仍为 pending 或 retrying 且到期时,才抢占
|
||||
// 仅当当前仍为 pending 或 retrying 且到期时,才抢占。
|
||||
// 为了避免在 PATCH 中使用复杂 or= 逻辑树导致匹配失败,这里拆成两次尝试。
|
||||
const now = new Date().toISOString()
|
||||
const path = `express_notifications?id=eq.${encodeURIComponent(id)}&or=(status_code.is.null,and(status_code.eq.retrying,or(next_attempt_at.is.null,next_attempt_at.lte.${encodeURIComponent(now)})))`
|
||||
const resp = await supaFetch(path, { method: 'PATCH', headers: { 'Content-Type': 'application/json', Prefer: 'return=representation' }, body: JSON.stringify(body) })
|
||||
if (!resp.ok) return null
|
||||
const j = await resp.json().catch(() => null)
|
||||
if (Array.isArray(j)) return j.length > 0 ? j[0] : null
|
||||
if (!j || !j.id) return null
|
||||
return j
|
||||
const attempts = [
|
||||
// pending: status_code IS NULL
|
||||
`express_notifications?id=eq.${encodeURIComponent(id)}&status_code=is.null`,
|
||||
// retrying: status_code='retrying' and next_attempt_at is null or <= now
|
||||
`express_notifications?id=eq.${encodeURIComponent(id)}&status_code=eq.retrying&or=(next_attempt_at.is.null,next_attempt_at.lte.${encodeURIComponent(now)})`
|
||||
]
|
||||
|
||||
for (const path of attempts) {
|
||||
const resp = await supaFetch(path, { method: 'PATCH', headers: { 'Content-Type': 'application/json', Prefer: 'return=representation' }, body: JSON.stringify(body) })
|
||||
if (!resp.ok) continue
|
||||
const j = await resp.json().catch(() => null)
|
||||
if (Array.isArray(j)) {
|
||||
if (j.length > 0) return j[0]
|
||||
// debug hint: matched 0 rows
|
||||
try { console.log('claimNotification: updated 0 rows for', path) } catch (e) {}
|
||||
continue
|
||||
}
|
||||
if (j && j.id) return j
|
||||
}
|
||||
return null
|
||||
} catch (e) {
|
||||
console.warn('claimNotification error', e)
|
||||
return null
|
||||
@@ -192,7 +206,17 @@ async function start() {
|
||||
const txt = await resp.text().catch(() => '')
|
||||
let json
|
||||
try { json = JSON.parse(txt) } catch (e) { json = { statusText: txt } }
|
||||
return { ok: resp.ok, status: resp.status, body: json }
|
||||
// Treat explicit business-level failures as errors even when HTTP is 2xx.
|
||||
// Many cloud functions return `{ ok: false, ... }` with HTTP 200.
|
||||
const businessOk = (() => {
|
||||
if (!json || typeof json !== 'object') return true
|
||||
if (json.ok === false) return false
|
||||
// uniCloud/uni-push demo often returns { errCode: 0|..., errMsg: '...' }
|
||||
if (typeof json.errCode === 'number') return json.errCode === 0
|
||||
if (typeof json.errCode === 'string' && json.errCode.trim() !== '') return json.errCode === '0'
|
||||
return true
|
||||
})()
|
||||
return { ok: resp.ok && businessOk, httpOk: resp.ok, status: resp.status, body: json }
|
||||
} catch (e) {
|
||||
return { ok: false, error: String(e) }
|
||||
}
|
||||
@@ -239,7 +263,13 @@ async function start() {
|
||||
if (CLOUD_FUNC_URL) {
|
||||
const calls = await Promise.all(targets.map(cid => invokeCloudFuncForCid(CLOUD_FUNC_URL, PUSH_TOKEN, cid, notification && notification.title, notification && notification.body, payload)))
|
||||
for (const c of calls) {
|
||||
if (!c.ok) { allOk = false; lastNote = (c.error || JSON.stringify(c.body)).toString().substring(0, 1000); break }
|
||||
if (!c.ok) {
|
||||
allOk = false
|
||||
const err = c.error ? String(c.error) : ''
|
||||
const bodyStr = c.body ? JSON.stringify(c.body) : ''
|
||||
lastNote = `cloudfunc failed: status=${c.status || ''} httpOk=${c.httpOk === true} ${err} ${bodyStr}`.trim().substring(0, 1000)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
allOk = false
|
||||
|
||||
Reference in New Issue
Block a user