const express = require('express') const bodyParser = require('body-parser') const cors = require('cors') const fs = require('fs').promises const path = require('path') 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') async function ensureDataDir() { try { await fs.mkdir(DATA_DIR, { recursive: true }) try { await fs.access(DEVICES_FILE) } catch (e) { await fs.writeFile(DEVICES_FILE, '[]', 'utf8') } } catch (e) { console.error('无法创建数据目录:', e) process.exit(1) } } async function readDevices() { try { const txt = await fs.readFile(DEVICES_FILE, 'utf8') return JSON.parse(txt || '[]') } catch (e) { return [] } } async function writeDevices(devices) { await fs.writeFile(DEVICES_FILE, JSON.stringify(devices, null, 2), 'utf8') } async function start() { await ensureDataDir() const app = express() app.use(cors()) app.use(bodyParser.json()) app.get('/health', (req, res) => res.json({ ok: true })) // 注册或更新设备 app.post('/api/v1/push/register', async (req, res) => { const { cid, user_id, platform } = req.body || {} if (!cid) return res.status(400).json({ error: 'cid required' }) const devices = await readDevices() let found = devices.find(d => d.cid === cid) const now = new Date().toISOString() if (found) { found.user_id = user_id ?? found.user_id found.platform = platform ?? found.platform found.updated_at = now found.active = true } else { found = { cid, user_id: user_id ?? null, platform: platform ?? null, created_at: now, updated_at: now, active: true } devices.push(found) } await writeDevices(devices) return res.json({ ok: true, cid }) }) // 注销设备(可选移除或置为 inactive) app.post('/api/v1/push/unregister', async (req, res) => { const { cid, user_id } = req.body || {} if (!cid && !user_id) return res.status(400).json({ error: 'cid or user_id required' }) let devices = await readDevices() if (cid) { devices = devices.map(d => d.cid === cid ? Object.assign({}, d, { active: false, updated_at: new Date().toISOString() }) : d) } else if (user_id) { devices = devices.map(d => d.user_id === user_id ? Object.assign({}, d, { active: false, updated_at: new Date().toISOString() }) : d) } await writeDevices(devices) 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 }) }) // 发送推送(mock 或代理到真实 provider,当环境变量 PUSH_PROXY_URL 设置时会代理请求) app.post('/api/v1/push/send', async (req, res) => { const { cids, user_id, notification, payload } = req.body || {} if ((!cids || cids.length === 0) && !user_id) return res.status(400).json({ error: 'cids or user_id required' }) 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) } // 如果配置了 PUSH_PROXY_URL 则转发到外部推送服务(例如 uni-push2 的服务端 API) const proxyUrl = process.env.PUSH_PROXY_URL || '' const proxyToken = process.env.PUSH_PROXY_TOKEN || '' if (proxyUrl) { try { const resp = await fetch(proxyUrl, { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, proxyToken ? { Authorization: `Bearer ${proxyToken}` } : {}), body: JSON.stringify({ targets, notification, payload }) }) const data = await resp.text() return res.json({ ok: true, proxied: true, status: resp.status, response: data }) } catch (e) { console.warn('代理推送失败:', e) return res.status(500).json({ ok: false, error: String(e) }) } } // 否则仅记录并返回模拟结果 console.log('Mock push to', targets.length, 'clients') console.log('notification:', notification) console.log('payload:', payload) return res.json({ ok: true, mocked: true, sent: targets.length }) }) app.listen(PORT, () => { console.log(`Push server listening on http://localhost:${PORT}`) }) } start().catch(e => { console.error('启动失败:', e) process.exit(1) })