统一推送数据库表
This commit is contained in:
23
server/README.md
Normal file
23
server/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Push Server (开发用)
|
||||
|
||||
这是一个用于本地开发与调试的轻量 Node.js 推送后端(mock)。
|
||||
|
||||
功能:
|
||||
- 注册/更新设备:`POST /api/v1/push/register` { cid, user_id, platform }
|
||||
- 注销设备:`POST /api/v1/push/unregister` { cid | user_id }
|
||||
- 列出设备:`GET /api/v1/push/devices?user_id=...&active=true|false`
|
||||
- 发送推送(模拟):`POST /api/v1/push/send` { cids:[], user_id, notification, payload }
|
||||
|
||||
如果你有真实的推送服务端 API,可以设置环境变量 `PUSH_PROXY_URL`(和可选的 `PUSH_PROXY_TOKEN`),服务器会将 `/api/v1/push/send` 请求代理到该 URL。
|
||||
|
||||
快速使用:
|
||||
|
||||
```bash
|
||||
cd server
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
默认监听端口:`7301`,可通过 `PORT` 环境变量修改。
|
||||
|
||||
设备存储在 `server/data/push_devices.json`,用于本地持久化。
|
||||
1
server/data/push_devices.json
Normal file
1
server/data/push_devices.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
15
server/package.json
Normal file
15
server/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "mall-push-server",
|
||||
"version": "0.1.0",
|
||||
"description": "Local push backend for development (register devices, mock send)",
|
||||
"main": "push-server.js",
|
||||
"scripts": {
|
||||
"start": "node push-server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"body-parser": "^1.20.2",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"node-fetch": "^2.6.12"
|
||||
}
|
||||
}
|
||||
135
server/push-server.js
Normal file
135
server/push-server.js
Normal file
@@ -0,0 +1,135 @@
|
||||
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)
|
||||
})
|
||||
Reference in New Issue
Block a user