模拟第三方信息存入数据库并进行消息推送

This commit is contained in:
not-like-juvenile
2026-03-09 17:27:56 +08:00
parent 436b7b251f
commit ee9fabd806
5 changed files with 413 additions and 29 deletions

View File

@@ -124,6 +124,35 @@ UNIPUSH 集成注意事项
- 如果 `/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`)以限制谁能发送通知。

View File

@@ -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 触发器;我可以协助把轮询替换为实时订阅的示例实现。
---

View File

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