消息推送和第三方接收后台搭建,未测试

This commit is contained in:
not-like-juvenile
2026-02-25 09:38:01 +08:00
parent 1ad4d05ffb
commit 0762857434
11 changed files with 1571 additions and 24 deletions

View File

@@ -8,6 +8,10 @@ const fetch = require('node-fetch')
const PORT = process.env.PORT || 7301
const DATA_DIR = path.join(__dirname, 'data')
const DEVICES_FILE = path.join(DATA_DIR, 'push_devices.json')
const SUPA_URL = process.env.SUPA_URL || ''
// 支持两种环境变量名SUPA_KEY和 SERVICE_ROLE_KEY常用于自托管 .env
const SUPA_KEY = process.env.SUPA_KEY || process.env.SERVICE_ROLE_KEY || ''
const SUPA_SCHEMA = process.env.SUPA_SCHEMA || 'public'
async function ensureDataDir() {
try {
@@ -36,6 +40,52 @@ async function writeDevices(devices) {
await fs.writeFile(DEVICES_FILE, JSON.stringify(devices, null, 2), 'utf8')
}
async function supaFetch(path, opts = {}) {
const url = `${SUPA_URL.replace(/\/$/, '')}/rest/v1/${path}`
const headers = Object.assign({}, opts.headers || {}, {
apikey: SUPA_KEY,
Authorization: `Bearer ${SUPA_KEY}`,
Accept: 'application/json'
})
try {
console.log('supaFetch ->', url)
const resp = await fetch(url, Object.assign({}, opts, { headers }))
if (!resp.ok) {
let txt = ''
try { txt = await resp.text() } catch (e) { txt = `<unable to read body: ${e}>` }
console.warn('supaFetch failed', resp.status, url, txt)
} else {
// optional: peek at small JSON responses for debugging
try {
const clone = resp.clone()
const maybeText = await clone.text()
if (maybeText && maybeText.length < 2000) console.log('supaFetch response preview:', maybeText)
} catch (e) {
// ignore preview errors
}
}
return resp
} catch (e) {
console.warn('supaFetch exception', e, url)
throw e
}
}
async function getDevicesFromSupabase({ user_id, active } = {}) {
let q = 'push_devices'
const params = []
if (user_id) params.push(`user_id=eq.${encodeURIComponent(user_id)}`)
if (active != null) params.push(`is_active=eq.${encodeURIComponent(active ? 'true' : 'false')}`)
const suffix = params.length ? `?${params.join('&')}` : ''
const resp = await supaFetch(`${q}${suffix}`, { method: 'GET' })
if (!resp.ok) {
const txt = await resp.text()
throw new Error(`supabase get devices failed ${resp.status} ${txt}`)
}
const data = await resp.json()
return data
}
async function start() {
await ensureDataDir()
const app = express()
@@ -61,6 +111,33 @@ async function start() {
devices.push(found)
}
await writeDevices(devices)
// 如果配置了 Supabase则尝试写入 supabase 表(异步记录错误)
if (SUPA_URL && SUPA_KEY) {
(async () => {
try {
const body = {
cid: found.cid,
user_id: found.user_id,
platform: found.platform,
appid: found.appid || 'default',
is_active: true,
last_seen_at: found.updated_at
}
const resp = await supaFetch(`push_devices?on_conflict=cid`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Prefer: 'return=representation' },
body: JSON.stringify(body)
})
if (!resp.ok) {
const txt = await resp.text()
console.warn('Supabase upsert push_device failed:', resp.status, txt)
}
} catch (e) {
console.warn('Supabase upsert exception:', e)
}
})()
}
return res.json({ ok: true, cid })
})
@@ -75,16 +152,49 @@ async function start() {
devices = devices.map(d => d.user_id === user_id ? Object.assign({}, d, { active: false, updated_at: new Date().toISOString() }) : d)
}
await writeDevices(devices)
// 同步到 Supabase如果可用
if (SUPA_URL && SUPA_KEY) {
(async () => {
try {
const now = new Date().toISOString()
if (cid) {
await supaFetch(`push_devices?cid=eq.${encodeURIComponent(cid)}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', Prefer: 'return=representation' },
body: JSON.stringify({ is_active: false, updated_at: now })
})
} else if (user_id) {
await supaFetch(`push_devices?user_id=eq.${encodeURIComponent(user_id)}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', Prefer: 'return=representation' },
body: JSON.stringify({ is_active: false, updated_at: now })
})
}
} catch (e) {
console.warn('Supabase unregister exception:', e)
}
})()
}
return res.json({ ok: true })
})
// 列出设备
app.get('/api/v1/push/devices', async (req, res) => {
const { user_id, active } = req.query
let devices = await readDevices()
if (user_id) devices = devices.filter(d => String(d.user_id) === String(user_id))
if (active != null) devices = devices.filter(d => String(!!d.active) === String(active === 'true'))
res.json({ ok: true, total: devices.length, data: devices })
try {
if (SUPA_URL && SUPA_KEY) {
const devices = await getDevicesFromSupabase({ user_id, active: active == null ? undefined : (active === 'true') })
return res.json({ ok: true, total: devices.length, data: devices })
}
let devices = await readDevices()
if (user_id) devices = devices.filter(d => String(d.user_id) === String(user_id))
if (active != null) devices = devices.filter(d => String(!!d.active) === String(active === 'true'))
res.json({ ok: true, total: devices.length, data: devices })
} catch (e) {
console.warn('list devices failed:', e)
res.status(500).json({ ok: false, error: String(e) })
}
})
// 发送推送mock 或代理到真实 provider当环境变量 PUSH_PROXY_URL 设置时会代理请求)
@@ -95,8 +205,19 @@ async function start() {
let targets = []
if (cids && cids.length > 0) targets = cids
else if (user_id) {
const devices = await readDevices()
targets = devices.filter(d => String(d.user_id) === String(user_id) && d.active).map(d => d.cid)
if (SUPA_URL && SUPA_KEY) {
try {
const devices = await getDevicesFromSupabase({ user_id, active: true })
targets = devices.map(d => d.cid)
} catch (e) {
console.warn('Supabase query devices failed, falling back to local:', e)
const devices = await readDevices()
targets = devices.filter(d => String(d.user_id) === String(user_id) && d.active).map(d => d.cid)
}
} else {
const devices = await readDevices()
targets = devices.filter(d => String(d.user_id) === String(user_id) && d.active).map(d => d.cid)
}
}
// 如果配置了 PUSH_PROXY_URL 则转发到外部推送服务(例如 uni-push2 的服务端 API
@@ -124,8 +245,8 @@ async function start() {
return res.json({ ok: true, mocked: true, sent: targets.length })
})
app.listen(PORT, () => {
console.log(`Push server listening on http://localhost:${PORT}`)
app.listen(PORT, '0.0.0.0', () => {
console.log(`Push server listening on http://0.0.0.0:${PORT}`)
})
}