Initial commit of akmon project

This commit is contained in:
2026-01-20 08:04:15 +08:00
commit 77a2bab985
1309 changed files with 343305 additions and 0 deletions

View File

@@ -0,0 +1,326 @@
# 🚀 推送消息接收服务部署指南
这个独立的推送消息接收服务专门用于接收和存储各种推送消息到 Supabase 数据库。
## 📋 服务概述
- **目的**: 专门接收推送信息的独立服务
- **数据库**: Supabase PostgreSQL
- **端口**: 3001 (可配置)
- **功能**: 接收、存储、去重、统计、监控推送消息
## 🚀 快速部署
### 1. 准备 Supabase 项目
1. **创建 Supabase 项目**:
```
访问: https://supabase.com/dashboard
点击: "New project"
记录: Project URL 和 API Keys
```
2. **执行数据库脚本**:
- 打开 Supabase Dashboard > SQL Editor
- 复制并执行 `push-receiver-service/database/supabase-init.sql`
### 2. 本地部署
```bash
# 进入服务目录
cd push-receiver-service
# 安装依赖
npm install
# 配置环境
cp .env.supabase .env
# 编辑 .env 文件,配置您的 Supabase 信息
# 初始化数据库
npm run setup-supabase
# 启动服务
npm start
```
### 3. 使用便捷脚本
**Windows:**
```cmd
cd push-receiver-service
start.bat
```
**Linux/Mac:**
```bash
cd push-receiver-service
chmod +x start.sh
./start.sh
```
**或者从主项目目录:**
```bash
npm run push-receiver:setup # 初始化
npm run push-receiver # 启动服务
npm run push-receiver:test # 运行测试
```
## 🌐 暴露的服务地址
启动成功后,推送接收服务将暴露以下地址:
### 本地开发
```
服务信息: http://localhost:3001/
健康检查: http://localhost:3001/api/health
推送消息: http://localhost:3001/api/push/message
批量推送: http://localhost:3001/api/push/batch
```
### 生产环境部署
1. **云服务器部署**:
```
https://your-domain.com:3001/api/push/message
```
2. **反向代理 (推荐)**:
```
https://your-domain.com/api/push/message
https://push.your-domain.com/api/push/message
```
3. **Serverless 部署**:
```
https://your-function-url/api/push/message
```
## 📊 数据库设计
### 核心表结构
```sql
-- 推送消息主表
push_messages (
id, -- 消息唯一ID
push_type, -- 推送类型 (SOS, HEALTH, LOCATION, etc.)
user_id, -- 用户ID
device_id, -- 设备ID
raw_data, -- 原始数据 (JSONB)
parsed_data, -- 解析数据 (JSONB)
received_at, -- 接收时间
processing_status, -- 处理状态
is_duplicate, -- 是否重复
source_ip, -- 来源IP
latitude, longitude -- 地理位置
)
-- 其他支持表
push_types -- 推送类型配置
message_processing_logs -- 处理日志
devices -- 设备信息
users -- 用户信息
system_stats -- 系统统计
```
### 支持的推送类型
| 类型 | 说明 | 优先级 | 示例数据 |
|------|------|--------|----------|
| `SOS` | 紧急求救 | 1 | `{emergencyLevel: "HIGH"}` |
| `HEALTH` | 健康数据 | 3 | `{H: 75, O: 98, T: 36.5}` |
| `LOCATION` | 位置信息 | 4 | `{lat: 39.9042, lng: 116.4074}` |
| `ALERT` | 告警信息 | 2 | `{alertType: "fall", severity: "high"}` |
| `DEVICE_STATUS` | 设备状态 | 4 | `{status: "online", battery: 85}` |
| `ACTIVITY` | 活动数据 | 5 | `{type: "running", duration: 1800}` |
## 🔧 推送设备配置
### API 调用格式
```http
POST http://localhost:3001/api/push/message
Content-Type: application/json
X-API-Key: your-api-key
{
"pushType": "HEALTH",
"userId": "user_12345",
"deviceId": "device_001",
"H": 75,
"O": 98,
"T": 36.5
}
```
### 各种语言调用示例
**JavaScript:**
```javascript
fetch('http://localhost:3001/api/push/message', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'your-api-key'
},
body: JSON.stringify({
pushType: 'HEALTH',
userId: 'user_12345',
H: 105, O: 88
})
});
```
**Python:**
```python
import requests
requests.post('http://localhost:3001/api/push/message',
headers={'X-API-Key': 'your-api-key'},
json={'pushType': 'HEALTH', 'userId': 'user_12345', 'H': 105, 'O': 88}
)
```
**cURL:**
```bash
curl -X POST http://localhost:3001/api/push/message \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{"pushType":"HEALTH","userId":"user_12345","H":105,"O":88}'
```
## 🔒 安全配置
### 环境变量配置
```env
# Supabase 配置
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# 安全配置
API_KEY=your-secure-api-key-2025
ALLOWED_ORIGINS=https://your-app.com,https://your-admin.com
RATE_LIMIT_MAX_REQUESTS=1000
# 服务配置
PORT=3001
LOG_LEVEL=info
```
### 生产环境安全建议
1. **使用强 API 密钥**
2. **配置 CORS 白名单**
3. **启用请求频率限制**
4. **使用 HTTPS**
5. **定期备份数据**
## 📈 监控和维护
### 健康检查
```bash
curl http://localhost:3001/api/health
```
### 统计信息
```bash
curl http://localhost:3001/api/stats
```
### 日志监控
```bash
# 查看日志
tail -f push-receiver-service/logs/supabase-push-service.log
# 查看错误日志
tail -f push-receiver-service/logs/supabase-push-service-error.log
```
### 数据清理
```bash
# 清理30天前的数据
curl -X POST http://localhost:3001/api/cleanup \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{"days": 30}'
```
## 🚀 部署到生产环境
### 1. 云服务器部署
```bash
# 使用 PM2 管理进程
npm install -g pm2
pm2 start supabase-server.js --name "push-receiver"
pm2 startup
pm2 save
```
### 2. Docker 部署
```dockerfile
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install --production
EXPOSE 3001
CMD ["node", "supabase-server.js"]
```
### 3. Nginx 反向代理
```nginx
location /api/push/ {
proxy_pass http://localhost:3001/api/push/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
```
## 📞 故障排除
### 常见问题
1. **无法连接 Supabase**:
- 检查 SUPABASE_URL 和 SUPABASE_SERVICE_ROLE_KEY
- 确认网络连接正常
2. **API 调用失败**:
- 检查 API_KEY 是否正确
- 确认请求格式正确
3. **服务无法启动**:
- 检查端口 3001 是否被占用
- 查看错误日志获取详细信息
### 调试步骤
1. 运行测试脚本: `npm test`
2. 检查日志文件
3. 验证环境配置
4. 测试 Supabase 连接
---
## 📋 总结
这个推送消息接收服务提供了:
✅ **完整的推送消息接收和存储**
✅ **基于 Supabase 的可扩展数据库**
✅ **详细的日志和监控功能**
✅ **安全的 API 访问控制**
✅ **支持多种推送消息类型**
✅ **重复消息检测和处理**
✅ **批量消息处理能力**
✅ **生产级的部署支持**
推送设备或系统只需要向 `http://your-server:3001/api/push/message` 发送 POST 请求即可完成消息推送。所有消息都会被完整保存到 Supabase 数据库中,供后续业务处理使用。

View File

@@ -0,0 +1,118 @@
# 推送消息接收服务表名前缀更新完成
## 🎉 更新完成
所有推送消息接收服务相关的数据库表已成功添加 `ps_` 前缀,以避免与现有系统表的命名冲突。
## 📋 表名变更对照
| 原表名 | 新表名 | 用途 |
|--------|--------|------|
| `push_messages` | `ps_push_messages` | 推送消息主表 |
| `push_types` | `ps_push_types` | 推送类型配置表 |
| `message_processing_logs` | `ps_message_processing_logs` | 消息处理日志表 |
| `system_stats` | `ps_system_stats` | 系统统计表 |
## 🔧 更新内容
### 1. 数据库结构文件
-`database/supabase-init.sql` - 所有表定义、索引、触发器、视图、函数都已更新
### 2. 应用代码
-`lib/supabase-database.js` - 所有数据库操作方法中的表名引用已更新
-`check-config.js` - 配置检查脚本中的表名引用已更新
### 3. 文档
-`SUPABASE_ADAPTATION.md` - 适配说明文档已更新
-`README.md` - 使用说明已更新
### 4. 视图和函数
-`ps_recent_message_stats` - 消息统计视图
-`ps_active_devices_stats` - 设备活跃度统计视图
-`ps_cleanup_old_messages()` - 清理旧消息函数
-`ps_get_message_stats()` - 获取消息统计函数
### 5. 索引和约束
- ✅ 所有索引名称已更新为带 `ps_` 前缀
- ✅ 外键约束已正确关联到 `ak_users``ak_devices`
- ✅ 触发器名称已更新
## 🚀 部署步骤
### 1. 数据库初始化
```bash
# 在 Supabase SQL Editor 中执行
# 文件: database/supabase-init.sql
```
### 2. 环境配置
```bash
# 确保 .env 文件中有正确的 Supabase 配置
SUPABASE_URL=your-supabase-url
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
API_KEY=your-api-key
```
### 3. 测试服务
```bash
# 检查配置
npm run check-config
# 运行测试
npm test
# 启动服务
npm start
```
## 📊 API 使用无变化
API 接口和使用方式保持不变,只是底层数据库表名发生了变化:
```javascript
// API 调用方式不变
const response = await fetch('/api/push/message', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'your-api-key'
},
body: JSON.stringify({
type: 'HEALTH',
data: {
user_id: '123e4567-e89b-12d3-a456-426614174000',
device_id: '987fcdeb-51a2-43d7-8f9e-123456789abc',
H: 72,
O: 98,
T: 36.5
}
})
});
```
## 🛡️ 数据安全
- ✅ 所有现有数据保持不变
- ✅ 与现有 `ak_*` 表系统完全兼容
- ✅ 外键约束确保数据完整性
- ✅ RLS行级安全策略已正确配置
## 🔍 监控和维护
推送消息服务现在使用独立的 `ps_` 前缀表,可以通过以下方式监控:
- 查看 `ps_push_messages` 表了解消息接收情况
- 查看 `ps_message_processing_logs` 表了解处理日志
- 查看 `ps_system_stats` 表了解系统统计
- 使用 `ps_recent_message_stats` 视图获取实时统计
## ✅ 完成状态
- [x] 数据库表结构更新
- [x] 应用代码适配
- [x] 文档更新
- [x] 配置检查工具更新
- [x] 视图和函数更新
- [x] 索引和约束更新
服务现在已准备就绪,可以在您的 Supabase 环境中部署和使用!

View File

@@ -0,0 +1,405 @@
# 基于 Supabase 的推送消息接收服务
专门用于接收和存储各种推送消息到 Supabase 数据库的独立服务。
## 🌟 特性
-**基于 Supabase**: 使用 Supabase PostgreSQL 数据库
-**推送消息接收**: 支持多种类型的推送消息SOS、健康、位置、告警等
-**批量处理**: 支持单个和批量消息处理
-**重复检测**: 自动检测和处理重复消息
-**实时监控**: 提供详细的统计和监控功能
-**安全保护**: API 密钥验证和请求频率限制
-**完整日志**: 详细的操作日志记录
-**优雅关闭**: 支持服务的优雅启动和关闭
## 🚀 快速开始
### 1. 安装依赖
```bash
cd push-receiver-service
npm install
```
### 2. 配置 Supabase
1. **创建 Supabase 项目**:
- 访问 [https://supabase.com/dashboard](https://supabase.com/dashboard)
- 点击 "New project" 创建新项目
- 记录项目的 URL 和 API Keys
2. **⚠️ 重要:数据库适配说明**:
- 本服务已适配现有的 `ak_users``ak_devices`
- 推送消息相关表使用 `ps_` 前缀(`ps_push_messages``ps_push_types` 等)
- 请确保现有的基础表已存在于您的 Supabase 项目中
- 详细适配信息请查看 [`SUPABASE_ADAPTATION.md`](./SUPABASE_ADAPTATION.md)
3. **执行数据库初始化脚本**:
- 在 Supabase Dashboard 中打开 "SQL Editor"
- 复制 `database/supabase-init.sql` 文件内容
- 粘贴到 SQL Editor 中并执行
4. **配置环境变量**:
```bash
cp .env.supabase .env
```
然后编辑 `.env` 文件:
```env
SUPABASE_URL=https://your-project-ref.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
API_KEY=your-custom-api-key
```
### 3. 初始化数据库
```bash
node setup-supabase.js
```
### 4. 启动服务
```bash
node supabase-server.js
```
服务将在 `http://localhost:3001` 启动。
## 📋 API 端点
### 基础信息
- **服务信息**: `GET /`
- **健康检查**: `GET /api/health`
### 推送消息接收
- **单个消息**: `POST /api/push/message`
- **批量消息**: `POST /api/push/batch`
### 数据查询
- **统计信息**: `GET /api/stats`
- **消息列表**: `GET /api/messages`
- **清理数据**: `POST /api/cleanup`
## 🔧 API 使用示例
### 发送单个推送消息
```bash
curl -X POST http://localhost:3001/api/push/message \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{
"pushType": "HEALTH",
"userId": "123e4567-e89b-12d3-a456-426614174000",
"deviceId": "987fcdeb-51a2-43d7-8f9e-123456789abc",
"H": 75,
"O": 98,
"T": 36.5
}'
```
> **注意**: `userId` 和 `deviceId` 必须使用 UUID 格式,且必须是现有 `ak_users` 和 `ak_devices` 表中的有效记录。
### 发送批量推送消息
```bash
curl -X POST http://localhost:3001/api/push/batch \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{
"messages": [
{
"pushType": "SOS",
"userId": "123e4567-e89b-12d3-a456-426614174000",
"emergencyLevel": "HIGH",
"lat": 39.9042,
"lng": 116.4074
},
{
"pushType": "HEALTH",
"userId": "456e7890-e89b-12d3-a456-426614174001",
"H": 80,
"O": 95
}
]
}'
```
### JavaScript 调用示例
```javascript
// 发送单个推送消息
const response = await fetch('http://localhost:3001/api/push/message', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'your-api-key'
},
body: JSON.stringify({
pushType: 'HEALTH',
userId: '123e4567-e89b-12d3-a456-426614174000',
deviceId: '987fcdeb-51a2-43d7-8f9e-123456789abc',
H: 105,
O: 88
})
});
const result = await response.json();
console.log('推送结果:', result);
```
### Python 调用示例
```python
import requests
# 发送单个推送消息
response = requests.post('http://localhost:3001/api/push/message',
headers={
'Content-Type': 'application/json',
'X-API-Key': 'your-api-key'
},
json={
'pushType': 'HEALTH',
'userId': 'user_12345',
'H': 105,
'O': 88
}
)
result = response.json()
print('推送结果:', result)
```
## 📊 支持的推送类型
| 类型 | 说明 | 优先级 | 必需字段 |
|------|------|--------|----------|
| `SOS` | 紧急求救 | 1 (最高) | `userId` |
| `ALERT` | 告警信息 | 2 | `userId`, `alertType` |
| `HEALTH` | 健康数据 | 3 | `userId` |
| `LOCATION` | 位置信息 | 4 | `userId` |
| `DEVICE_STATUS` | 设备状态 | 4 | `deviceId` |
| `ACTIVITY` | 活动数据 | 5 | `userId` |
## 🗄️ 数据库结构
### 主要数据表
1. **push_messages**: 推送消息主表
2. **push_types**: 推送类型配置表
3. **message_processing_logs**: 消息处理日志表
4. **devices**: 设备信息表
5. **users**: 用户信息表
6. **system_stats**: 系统统计表
### 消息数据结构
```json
{
"id": "uuid",
"push_type": "HEALTH",
"user_id": "user_12345",
"device_id": "device_001",
"raw_data": { "完整的原始数据" },
"parsed_data": { "解析后的结构化数据" },
"received_at": "2025-06-25T10:30:00Z",
"processing_status": "processed",
"priority": 3,
"is_duplicate": false,
"source_ip": "192.168.1.100",
"user_agent": "Device Client/1.0"
}
```
## 🔒 安全配置
### API 密钥验证
在环境变量中设置 `API_KEY`
```env
API_KEY=your-secure-api-key
```
所有 API 请求(除健康检查外)都需要在请求头中包含:
```
X-API-Key: your-secure-api-key
```
### 请求频率限制
- 默认:每分钟最多 1000 个请求
- 可通过环境变量配置:`RATE_LIMIT_MAX_REQUESTS`
### CORS 配置
可通过 `ALLOWED_ORIGINS` 环境变量配置允许的来源。
## 📈 监控和日志
### 日志文件
- `logs/supabase-push-service.log`: 一般日志
- `logs/supabase-push-service-error.log`: 错误日志
- `logs/database.log`: 数据库操作日志
### 统计信息
访问 `/api/stats` 获取详细的统计信息:
```json
{
"success": true,
"data": {
"service": {
"uptime": 3600,
"requestCount": 150,
"messageCount": 120,
"errorCount": 2
},
"messages": [
{
"push_type": "HEALTH",
"total_count": 80,
"processed_count": 78,
"failed_count": 2,
"pending_count": 0
}
]
}
}
```
## 🔧 环境配置
### 完整的环境变量配置
```env
# 服务器配置
PORT=3001
HOST=0.0.0.0
NODE_ENV=production
# Supabase 配置
SUPABASE_URL=https://your-project-ref.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# 安全配置
API_KEY=your-secure-api-key
ALLOWED_ORIGINS=*
RATE_LIMIT_MAX_REQUESTS=1000
# 日志配置
LOG_LEVEL=info
LOG_DIR=./logs
# 消息处理配置
MAX_MESSAGE_SIZE=1048576
BATCH_SIZE_LIMIT=1000
ENABLE_DUPLICATE_CHECK=true
# 数据清理配置
AUTO_CLEANUP_ENABLED=true
AUTO_CLEANUP_DAYS=30
```
## 🚀 部署
### Docker 部署
```dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3001
CMD ["node", "supabase-server.js"]
```
### PM2 部署
```bash
npm install -g pm2
pm2 start supabase-server.js --name "supabase-push-receiver"
pm2 startup
pm2 save
```
### 云服务部署
支持部署到:
- Vercel
- Netlify
- Railway
- Heroku
- AWS Lambda
- 腾讯云函数
## 🛠️ 开发
### 项目结构
```
push-receiver-service/
├── lib/
│ └── supabase-database.js # Supabase 数据库操作类
├── database/
│ └── supabase-init.sql # 数据库初始化脚本
├── logs/ # 日志目录
├── supabase-server.js # 主服务器文件
├── setup-supabase.js # 数据库设置脚本
├── package.json
├── .env.supabase # Supabase 环境配置模板
└── README.md
```
### 运行测试
```bash
# 测试 Supabase 连接
node setup-supabase.js
# 启动开发服务器
npm run dev
```
## 🤝 业务集成
### 与现有系统集成
1. **配置推送地址**: 将设备或系统的推送地址指向此服务
2. **设置API密钥**: 确保所有推送请求都包含正确的API密钥
3. **监控日志**: 定期检查服务日志确保正常运行
4. **数据分析**: 使用 Supabase Dashboard 或 API 进行数据分析
### 数据流处理
```
设备/系统 → 推送消息 → 接收服务 → Supabase → 业务处理
实时通知/告警
```
## 📞 支持
如果您在使用过程中遇到问题:
1. 检查日志文件获取详细错误信息
2. 确认 Supabase 配置正确
3. 验证 API 密钥和网络连接
4. 查看 Supabase Dashboard 的数据库状态
---
**注意**: 这是一个专门的消息接收服务,只负责接收和存储推送消息。业务逻辑处理需要另外实现。

View File

@@ -0,0 +1,167 @@
# Supabase 数据库适配说明
## 概述
推送消息接收服务的数据库结构已适配现有的 `ak_users``ak_devices` 表结构,以避免重复创建表和数据冲突。所有推送消息相关的表都使用 `ps_` 前缀命名。
## 主要变更
### 1. 移除独立表
- ❌ 移除独立的 `devices`
- ❌ 移除独立的 `users`
- ✅ 使用现有的 `public.ak_devices`
- ✅ 使用现有的 `public.ak_users`
### 2. 表命名规范
-`ps_push_messages` - 推送消息主表
-`ps_push_types` - 推送类型配置表
-`ps_message_processing_logs` - 消息处理日志表
-`ps_system_stats` - 系统统计表
### 3. 修改关联字段
- `ps_push_messages.user_id`: VARCHAR(255) → UUID关联到 `public.ak_users(id)`
- `ps_push_messages.device_id`: VARCHAR(255) → UUID关联到 `public.ak_devices(id)`
#### `ps_push_messages` 表结构
```sql
CREATE TABLE ps_push_messages (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
message_id VARCHAR(255),
push_type VARCHAR(50) NOT NULL,
user_id UUID REFERENCES public.ak_users(id) ON DELETE SET NULL,
device_id UUID REFERENCES public.ak_devices(id) ON DELETE SET NULL,
raw_data JSONB NOT NULL,
parsed_data JSONB,
received_at TIMESTAMPTZ DEFAULT NOW(),
processing_status VARCHAR(20) DEFAULT 'pending',
-- 其他字段...
);
```
### 4. 适配表结构
#### `public.ak_users` 表结构
```sql
CREATE TABLE public.ak_users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(64) UNIQUE NOT NULL,
email VARCHAR(128) UNIQUE NOT NULL,
password_hash VARCHAR(256) NOT NULL,
gender VARCHAR(16) DEFAULT 'other',
birthday DATE,
height_cm INT,
weight_kg INT,
bio TEXT,
phone VARCHAR(32),
avatar_url TEXT,
region_id UUID REFERENCES public.ak_regions(id),
school_id UUID REFERENCES public.ak_schools(id),
grade_id UUID REFERENCES public.ak_grades(id),
class_id UUID REFERENCES public.ak_classes(id),
role VARCHAR(32) DEFAULT 'student',
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
auth_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
preferred_language UUID REFERENCES public.ak_languages(id)
);
```
#### `public.ak_devices` 表结构
```sql
CREATE TABLE public.ak_devices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES public.ak_users(id) ON DELETE CASCADE,
device_type VARCHAR(32) NOT NULL,
device_name VARCHAR(64),
device_mac VARCHAR(64),
bind_time TIMESTAMPTZ DEFAULT now(),
status VARCHAR(16) DEFAULT 'active',
extra JSONB
);
```
### 5. 数据库操作调整
#### 设备信息处理
```javascript
// 旧方式:自动创建/更新设备
await this.upsertDevice(deviceData);
// 新方式:检查并更新现有设备状态
await this.upsertDevice({
device_id: deviceData.device_id, // 必须是现有设备的 UUID
metadata: deviceData.metadata
});
```
#### 用户信息处理
```javascript
// 旧方式:自动创建/更新用户
await this.upsertUser(userData);
// 新方式:检查现有用户(不自动创建)
await this.upsertUser({
user_id: userData.user_id // 必须是现有用户的 UUID
});
```
## API 调用变更
### 推送消息接口
```javascript
// 发送推送消息时user_id 和 device_id 必须使用 UUID 格式
const response = await fetch('/api/push/message', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'your-api-key'
},
body: JSON.stringify({
type: 'HEALTH',
data: {
user_id: '123e4567-e89b-12d3-a456-426614174000', // UUID 格式
device_id: '987fcdeb-51a2-43d7-8f9e-123456789abc', // UUID 格式
H: 72, // 心率
O: 98, // 血氧
T: 36.5 // 体温
}
})
});
```
### 错误处理
如果推送消息中包含不存在的 user_id 或 device_id
- 消息仍会被存储到 `push_messages`
- 会在日志中记录警告信息
- 不会自动创建用户或设备记录
## 部署注意事项
### 1. 数据库初始化
确保在执行 `database/supabase-init.sql` 之前,现有的 `ak_users``ak_devices` 表已经存在。
### 2. 外键约束
推送消息表与现有表建立了外键关联:
- `ps_push_messages.user_id``public.ak_users(id)`
- `ps_push_messages.device_id``public.ak_devices(id)`
### 3. 权限设置
确保 Supabase Service Role 对现有表有适当的访问权限。
## 数据完整性
### 推送消息存储
- ✅ 所有推送消息都会完整存储
- ✅ 支持 user_id 或 device_id 为 NULL 的情况
- ✅ 通过外键约束保证数据一致性
### 统计和查询
- ✅ 可以通过关联查询获取用户和设备的详细信息
- ✅ 统计功能完全兼容现有表结构
## 使用建议
1. **用户和设备管理**:通过专门的用户管理和设备管理接口创建和维护用户、设备信息
2. **推送消息**:只负责接收和存储推送消息,不处理用户/设备的创建
3. **数据关联**:通过外键关联获取完整的用户和设备信息
4. **监控告警**:监控日志中关于不存在用户/设备的警告信息

View File

@@ -0,0 +1,117 @@
/**
* Supabase 连接配置检查脚本
* 帮助验证 Supabase 连接配置是否正确
*/
require('dotenv').config();
class SupabaseConfigChecker {
constructor() {
this.config = {
SUPABASE_URL: process.env.SUPABASE_URL,
SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY,
SUPABASE_SERVICE_ROLE_KEY: process.env.SUPABASE_SERVICE_ROLE_KEY,
API_KEY: process.env.API_KEY
};
}
async checkConfig() {
console.log('🔍 检查 Supabase 配置...\n');
// 检查环境变量
console.log('📋 环境变量检查:');
for (const [key, value] of Object.entries(this.config)) {
if (value) {
const displayValue = value.length > 20 ?
value.substring(0, 20) + '...' : value;
console.log(`${key}: ${displayValue}`);
} else {
console.log(`${key}: 未设置`);
}
}
console.log('\n🌐 URL 格式检查:');
if (this.config.SUPABASE_URL) {
try {
const url = new URL(this.config.SUPABASE_URL);
console.log(`✅ 协议: ${url.protocol}`);
console.log(`✅ 主机: ${url.hostname}`);
console.log(`✅ 端口: ${url.port || '默认'}`);
} catch (error) {
console.log(`❌ URL 格式错误: ${error.message}`);
}
}
// 尝试基本连接测试
if (this.config.SUPABASE_URL && this.config.SUPABASE_ANON_KEY) {
console.log('\n🔗 尝试连接测试...');
await this.testConnection();
} else {
console.log('\n⚠ 无法进行连接测试,缺少必要配置');
}
console.log('\n💡 配置建议:');
this.provideSuggestions();
}
async testConnection() {
try {
const { createClient } = require('@supabase/supabase-js');
const supabase = createClient(
this.config.SUPABASE_URL,
this.config.SUPABASE_ANON_KEY
);
// 尝试一个简单的查询
const { data, error } = await supabase
.from('ps_push_messages')
.select('count')
.limit(1);
if (error) {
console.log(`❌ 连接测试失败: ${error.message}`);
// 提供错误分析
if (error.message.includes('authentication')) {
console.log('💡 建议: 检查 SUPABASE_ANON_KEY 是否正确');
} if (error.message.includes('relation') && error.message.includes('does not exist')) {
console.log('💡 建议: 数据库中缺少 ps_push_messages 表,请先执行数据库初始化脚本');
}
if (error.message.includes('connection')) {
console.log('💡 建议: 检查 SUPABASE_URL 是否可访问');
}
} else {
console.log('✅ 基本连接测试成功');
}
} catch (error) {
console.log(`❌ 连接测试出错: ${error.message}`);
}
}
provideSuggestions() {
console.log('1. 确保 Supabase 实例正在运行并可访问');
console.log('2. 检查防火墙和网络配置');
console.log('3. 验证 API 密钥的有效性');
console.log('4. 确保数据库中存在必要的表结构');
console.log('5. 如使用自托管 Supabase确认服务状态');
console.log('\n📝 下一步操作:');
console.log('1. 更新 .env 文件中的正确配置');
console.log('2. 运行: npm run setup-supabase');
console.log('3. 运行: npm test');
console.log('4. 运行: npm start');
}
}
// 执行检查
async function main() {
const checker = new SupabaseConfigChecker();
await checker.checkConfig();
}
if (require.main === module) {
main().catch(console.error);
}
module.exports = SupabaseConfigChecker;

View File

@@ -0,0 +1,311 @@
-- 推送消息接收服务数据库设计
-- 创建数据库和基础表结构
-- 1. 创建数据库 (需要管理员权限执行)
-- CREATE DATABASE push_messages;
-- CREATE USER push_service WITH PASSWORD 'your_secure_password';
-- GRANT ALL PRIVILEGES ON DATABASE push_messages TO push_service;
-- 使用数据库
-- \c push_messages;
-- 2. 创建扩展
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
-- 3. 推送消息主表
CREATE TABLE push_messages (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
message_id VARCHAR(255) UNIQUE, -- 外部消息ID如果有
push_type VARCHAR(50) NOT NULL, -- 推送类型SOS, HEALTH, LOCATION, ALERT 等
user_id VARCHAR(255), -- 用户ID
device_id VARCHAR(255), -- 设备ID
source_ip INET, -- 来源IP地址
user_agent TEXT, -- 用户代理
-- 消息内容JSON格式存储原始数据
raw_data JSONB NOT NULL, -- 原始接收到的完整数据
parsed_data JSONB, -- 解析后的结构化数据
-- 时间戳
received_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-- 处理状态
processing_status VARCHAR(20) DEFAULT 'pending', -- pending, processed, failed, ignored
processed_at TIMESTAMP WITH TIME ZONE,
error_message TEXT,
retry_count INTEGER DEFAULT 0,
-- 优先级和分类
priority INTEGER DEFAULT 5, -- 1-10数字越小优先级越高
category VARCHAR(100), -- 消息分类
tags TEXT[], -- 标签数组
-- 验证和重复检查
checksum VARCHAR(64), -- 消息校验和,用于去重
is_duplicate BOOLEAN DEFAULT FALSE,
original_message_id UUID, -- 如果是重复消息指向原始消息ID
-- 地理位置信息(如果有)
latitude DECIMAL(10, 8),
longitude DECIMAL(11, 8),
location_accuracy FLOAT,
location_timestamp TIMESTAMP WITH TIME ZONE,
-- 索引和搜索
search_vector tsvector, -- 全文搜索向量
-- 软删除
is_deleted BOOLEAN DEFAULT FALSE,
deleted_at TIMESTAMP WITH TIME ZONE
);
-- 4. 推送类型配置表
CREATE TABLE push_types (
id SERIAL PRIMARY KEY,
type_code VARCHAR(50) UNIQUE NOT NULL,
type_name VARCHAR(100) NOT NULL,
description TEXT,
default_priority INTEGER DEFAULT 5,
validation_schema JSONB, -- JSON Schema 用于验证消息格式
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- 5. 消息处理日志表
CREATE TABLE message_processing_logs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
message_id UUID REFERENCES push_messages(id) ON DELETE CASCADE,
processing_step VARCHAR(100) NOT NULL,
status VARCHAR(20) NOT NULL, -- started, completed, failed
started_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP WITH TIME ZONE,
duration_ms INTEGER,
details JSONB,
error_details TEXT
);
-- 6. 设备信息表
CREATE TABLE devices (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
device_id VARCHAR(255) UNIQUE NOT NULL,
device_name VARCHAR(255),
device_type VARCHAR(100), -- sensor, mobile, wearable, etc.
user_id VARCHAR(255),
last_seen_at TIMESTAMP WITH TIME ZONE,
is_active BOOLEAN DEFAULT TRUE,
metadata JSONB, -- 设备元数据
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- 7. 用户信息表(简化)
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id VARCHAR(255) UNIQUE NOT NULL,
user_name VARCHAR(255),
user_type VARCHAR(50), -- student, teacher, elder, caregiver, etc.
contact_info JSONB, -- 联系方式
preferences JSONB, -- 用户偏好设置
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- 8. 系统统计表
CREATE TABLE system_stats (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
stat_date DATE DEFAULT CURRENT_DATE,
stat_hour INTEGER DEFAULT EXTRACT(HOUR FROM CURRENT_TIMESTAMP),
push_type VARCHAR(50),
message_count INTEGER DEFAULT 0,
success_count INTEGER DEFAULT 0,
error_count INTEGER DEFAULT 0,
avg_processing_time_ms FLOAT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(stat_date, stat_hour, push_type)
);
-- 9. 创建索引
CREATE INDEX idx_push_messages_received_at ON push_messages(received_at DESC);
CREATE INDEX idx_push_messages_push_type ON push_messages(push_type);
CREATE INDEX idx_push_messages_user_id ON push_messages(user_id);
CREATE INDEX idx_push_messages_device_id ON push_messages(device_id);
CREATE INDEX idx_push_messages_processing_status ON push_messages(processing_status);
CREATE INDEX idx_push_messages_priority ON push_messages(priority);
CREATE INDEX idx_push_messages_checksum ON push_messages(checksum);
CREATE INDEX idx_push_messages_location ON push_messages USING GIST(ST_POINT(longitude, latitude)) WHERE latitude IS NOT NULL AND longitude IS NOT NULL;
CREATE INDEX idx_push_messages_search_vector ON push_messages USING GIN(search_vector);
CREATE INDEX idx_push_messages_raw_data ON push_messages USING GIN(raw_data);
CREATE INDEX idx_push_messages_parsed_data ON push_messages USING GIN(parsed_data);
CREATE INDEX idx_processing_logs_message_id ON message_processing_logs(message_id);
CREATE INDEX idx_processing_logs_started_at ON message_processing_logs(started_at DESC);
CREATE INDEX idx_devices_device_id ON devices(device_id);
CREATE INDEX idx_devices_user_id ON devices(user_id);
CREATE INDEX idx_devices_last_seen_at ON devices(last_seen_at DESC);
CREATE INDEX idx_users_user_id ON users(user_id);
CREATE INDEX idx_system_stats_date_hour ON system_stats(stat_date DESC, stat_hour DESC);
CREATE INDEX idx_system_stats_push_type ON system_stats(push_type);
-- 10. 创建触发器函数
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE OR REPLACE FUNCTION update_search_vector()
RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector := to_tsvector('english',
COALESCE(NEW.push_type, '') || ' ' ||
COALESCE(NEW.user_id, '') || ' ' ||
COALESCE(NEW.device_id, '') || ' ' ||
COALESCE(NEW.category, '') || ' ' ||
COALESCE(array_to_string(NEW.tags, ' '), '') || ' ' ||
COALESCE(NEW.raw_data::text, '')
);
RETURN NEW;
END;
$$ language 'plpgsql';
-- 11. 创建触发器
CREATE TRIGGER update_push_messages_updated_at
BEFORE UPDATE ON push_messages
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_push_messages_search_vector
BEFORE INSERT OR UPDATE ON push_messages
FOR EACH ROW EXECUTE FUNCTION update_search_vector();
CREATE TRIGGER update_push_types_updated_at
BEFORE UPDATE ON push_types
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_devices_updated_at
BEFORE UPDATE ON devices
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- 12. 插入基础推送类型配置
INSERT INTO push_types (type_code, type_name, description, default_priority, validation_schema) VALUES
('SOS', '紧急求救', 'SOS紧急求救信号', 1, '{"type": "object", "required": ["userId"], "properties": {"userId": {"type": "string"}, "emergencyLevel": {"type": "string", "enum": ["LOW", "MEDIUM", "HIGH", "CRITICAL"]}}}'),
('HEALTH', '健康数据', '健康监测数据推送', 3, '{"type": "object", "required": ["userId"], "properties": {"userId": {"type": "string"}, "H": {"type": "number"}, "O": {"type": "number"}, "T": {"type": "number"}}}'),
('LOCATION', '位置信息', '位置定位数据推送', 4, '{"type": "object", "required": ["userId"], "properties": {"userId": {"type": "string"}, "lat": {"type": "number"}, "lng": {"type": "number"}, "accuracy": {"type": "number"}}}'),
('ALERT', '告警信息', '各类告警信息推送', 2, '{"type": "object", "required": ["userId", "alertType"], "properties": {"userId": {"type": "string"}, "alertType": {"type": "string"}, "severity": {"type": "string"}}}'),
('ACTIVITY', '活动数据', '运动和活动数据推送', 5, '{"type": "object", "required": ["userId"], "properties": {"userId": {"type": "string"}, "activityType": {"type": "string"}, "duration": {"type": "number"}, "calories": {"type": "number"}}}'),
('DEVICE_STATUS', '设备状态', '设备状态信息推送', 4, '{"type": "object", "required": ["deviceId"], "properties": {"deviceId": {"type": "string"}, "status": {"type": "string"}, "batteryLevel": {"type": "number"}}}');
-- 13. 创建视图
-- 最近24小时消息统计视图
CREATE VIEW recent_message_stats AS
SELECT
push_type,
COUNT(*) as total_count,
COUNT(*) FILTER (WHERE processing_status = 'processed') as processed_count,
COUNT(*) FILTER (WHERE processing_status = 'failed') as failed_count,
COUNT(*) FILTER (WHERE processing_status = 'pending') as pending_count,
AVG(EXTRACT(EPOCH FROM (processed_at - received_at)) * 1000) as avg_processing_time_ms,
MIN(received_at) as first_received,
MAX(received_at) as last_received
FROM push_messages
WHERE received_at >= CURRENT_TIMESTAMP - INTERVAL '24 hours'
AND is_deleted = FALSE
GROUP BY push_type;
-- 活跃设备统计视图
CREATE VIEW active_devices_stats AS
SELECT
device_type,
COUNT(*) as total_devices,
COUNT(*) FILTER (WHERE last_seen_at >= CURRENT_TIMESTAMP - INTERVAL '1 hour') as active_1h,
COUNT(*) FILTER (WHERE last_seen_at >= CURRENT_TIMESTAMP - INTERVAL '24 hours') as active_24h,
COUNT(*) FILTER (WHERE last_seen_at >= CURRENT_TIMESTAMP - INTERVAL '7 days') as active_7d
FROM devices
WHERE is_active = TRUE
GROUP BY device_type;
-- 14. 创建函数
-- 清理旧数据函数
CREATE OR REPLACE FUNCTION cleanup_old_messages(days_to_keep INTEGER DEFAULT 30)
RETURNS INTEGER AS $$
DECLARE
deleted_count INTEGER;
BEGIN
-- 软删除超过指定天数的消息
UPDATE push_messages
SET is_deleted = TRUE, deleted_at = CURRENT_TIMESTAMP
WHERE received_at < CURRENT_TIMESTAMP - (days_to_keep || ' days')::INTERVAL
AND is_deleted = FALSE;
GET DIAGNOSTICS deleted_count = ROW_COUNT;
-- 记录清理日志
INSERT INTO message_processing_logs (message_id, processing_step, status, details)
VALUES (NULL, 'cleanup_old_messages', 'completed',
json_build_object('days_to_keep', days_to_keep, 'deleted_count', deleted_count));
RETURN deleted_count;
END;
$$ LANGUAGE plpgsql;
-- 获取消息统计函数
CREATE OR REPLACE FUNCTION get_message_stats(hours_back INTEGER DEFAULT 24)
RETURNS TABLE(
push_type VARCHAR,
total_count BIGINT,
processed_count BIGINT,
failed_count BIGINT,
pending_count BIGINT,
avg_processing_time_ms NUMERIC
) AS $$
BEGIN
RETURN QUERY
SELECT
pm.push_type,
COUNT(*) as total_count,
COUNT(*) FILTER (WHERE pm.processing_status = 'processed') as processed_count,
COUNT(*) FILTER (WHERE pm.processing_status = 'failed') as failed_count,
COUNT(*) FILTER (WHERE pm.processing_status = 'pending') as pending_count,
AVG(EXTRACT(EPOCH FROM (pm.processed_at - pm.received_at)) * 1000) as avg_processing_time_ms
FROM push_messages pm
WHERE pm.received_at >= CURRENT_TIMESTAMP - (hours_back || ' hours')::INTERVAL
AND pm.is_deleted = FALSE
GROUP BY pm.push_type
ORDER BY total_count DESC;
END;
$$ LANGUAGE plpgsql;
-- 15. 创建定时任务需要pg_cron扩展
-- 每天凌晨2点清理30天前的数据
-- SELECT cron.schedule('cleanup-old-messages', '0 2 * * *', 'SELECT cleanup_old_messages(30);');
-- 16. 权限设置
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO push_service;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO push_service;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO push_service;
-- 17. 初始化完成标记
INSERT INTO system_stats (stat_date, stat_hour, push_type, message_count)
VALUES (CURRENT_DATE, EXTRACT(HOUR FROM CURRENT_TIMESTAMP), 'SYSTEM', 0)
ON CONFLICT (stat_date, stat_hour, push_type) DO NOTHING;
COMMENT ON DATABASE push_messages IS '推送消息接收服务数据库 - 存储所有接收到的推送消息及相关数据';
COMMENT ON TABLE push_messages IS '推送消息主表 - 存储所有接收到的推送消息';
COMMENT ON TABLE push_types IS '推送类型配置表 - 定义各种推送消息类型及其验证规则';
COMMENT ON TABLE message_processing_logs IS '消息处理日志表 - 记录消息处理过程的详细日志';
COMMENT ON TABLE devices IS '设备信息表 - 存储推送消息来源设备的信息';
COMMENT ON TABLE users IS '用户信息表 - 存储用户基本信息';
COMMENT ON TABLE system_stats IS '系统统计表 - 存储系统运行统计数据';

View File

@@ -0,0 +1,318 @@
-- 推送消息接收服务 Supabase 数据库设计
-- 在 Supabase Dashboard 的 SQL Editor 中执行此脚本
-- 适配现有的 ak_users 和 ak_devices 表结构
-- 启用必要的扩展
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- 1. 推送消息主表
CREATE TABLE IF NOT EXISTS ps_push_messages (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
message_id VARCHAR(255), -- 外部消息ID如果有
push_type VARCHAR(50) NOT NULL, -- 推送类型SOS, HEALTH, LOCATION, ALERT 等
user_id UUID REFERENCES public.ak_users(id) ON DELETE SET NULL, -- 关联到现有用户表
device_id UUID REFERENCES public.ak_devices(id) ON DELETE SET NULL, -- 关联到现有设备表
source_ip INET, -- 来源IP地址
user_agent TEXT, -- 用户代理
-- 消息内容JSON格式存储原始数据
raw_data JSONB NOT NULL, -- 原始接收到的完整数据
parsed_data JSONB, -- 解析后的结构化数据
-- 时间戳
received_at TIMESTAMPTZ DEFAULT NOW(),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
-- 处理状态
processing_status VARCHAR(20) DEFAULT 'pending', -- pending, processed, failed, ignored
processed_at TIMESTAMPTZ,
error_message TEXT,
retry_count INTEGER DEFAULT 0,
-- 优先级和分类
priority INTEGER DEFAULT 5, -- 1-10数字越小优先级越高
category VARCHAR(100), -- 消息分类
tags TEXT[], -- 标签数组
-- 验证和重复检查
checksum VARCHAR(64), -- 消息校验和,用于去重
is_duplicate BOOLEAN DEFAULT FALSE,
original_message_id UUID, -- 如果是重复消息指向原始消息ID
-- 地理位置信息(如果有)
latitude DECIMAL(10, 8),
longitude DECIMAL(11, 8),
location_accuracy FLOAT,
location_timestamp TIMESTAMPTZ,
-- 索引和搜索
search_vector TSVECTOR, -- 全文搜索向量
-- 软删除
is_deleted BOOLEAN DEFAULT FALSE,
deleted_at TIMESTAMPTZ,
-- 约束
CONSTRAINT unique_checksum UNIQUE (checksum),
CONSTRAINT fk_original_message FOREIGN KEY (original_message_id) REFERENCES ps_push_messages(id)
);
-- 2. 推送类型配置表
CREATE TABLE IF NOT EXISTS ps_push_types (
id SERIAL PRIMARY KEY,
type_code VARCHAR(50) UNIQUE NOT NULL,
type_name VARCHAR(100) NOT NULL,
description TEXT,
default_priority INTEGER DEFAULT 5,
validation_schema JSONB, -- JSON Schema 用于验证消息格式
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- 3. 消息处理日志表
CREATE TABLE IF NOT EXISTS ps_message_processing_logs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
message_id UUID REFERENCES ps_push_messages(id) ON DELETE CASCADE,
processing_step VARCHAR(100) NOT NULL,
status VARCHAR(20) NOT NULL, -- started, completed, failed
started_at TIMESTAMPTZ DEFAULT NOW(),
completed_at TIMESTAMPTZ,
duration_ms INTEGER,
details JSONB,
error_details TEXT
);
-- 4. 设备信息表 - 已存在,使用现有的 ak_devices 表
-- CREATE TABLE public.ak_devices (
-- id UUID PRIMARY KEY,
-- user_id UUID REFERENCES public.ak_users(id),
-- device_type VARCHAR(32) NOT NULL,
-- device_name VARCHAR(64),
-- device_mac VARCHAR(64),
-- bind_time TIMESTAMPTZ DEFAULT now(),
-- status VARCHAR(16) DEFAULT 'active',
-- extra JSONB
-- );
-- 5. 用户信息表 - 已存在,使用现有的 ak_users 表
-- CREATE TABLE public.ak_users (
-- id UUID PRIMARY KEY,
-- username VARCHAR(64) UNIQUE NOT NULL,
-- email VARCHAR(128) UNIQUE NOT NULL,
-- role VARCHAR(32) DEFAULT 'student',
-- created_at TIMESTAMPTZ DEFAULT now(),
-- ...其他字段
-- );
-- 6. 系统统计表
CREATE TABLE IF NOT EXISTS ps_system_stats (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
stat_date DATE DEFAULT CURRENT_DATE,
stat_hour INTEGER DEFAULT EXTRACT(HOUR FROM NOW()),
push_type VARCHAR(50),
message_count INTEGER DEFAULT 0,
success_count INTEGER DEFAULT 0,
error_count INTEGER DEFAULT 0,
avg_processing_time_ms FLOAT,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(stat_date, stat_hour, push_type)
);
-- 7. 创建索引
CREATE INDEX IF NOT EXISTS idx_ps_push_messages_received_at ON ps_push_messages(received_at DESC);
CREATE INDEX IF NOT EXISTS idx_ps_push_messages_push_type ON ps_push_messages(push_type);
CREATE INDEX IF NOT EXISTS idx_ps_push_messages_user_id ON ps_push_messages(user_id);
CREATE INDEX IF NOT EXISTS idx_ps_push_messages_device_id ON ps_push_messages(device_id);
CREATE INDEX IF NOT EXISTS idx_ps_push_messages_processing_status ON ps_push_messages(processing_status);
CREATE INDEX IF NOT EXISTS idx_ps_push_messages_priority ON ps_push_messages(priority);
CREATE INDEX IF NOT EXISTS idx_ps_push_messages_checksum ON ps_push_messages(checksum);
CREATE INDEX IF NOT EXISTS idx_ps_push_messages_search_vector ON ps_push_messages USING GIN(search_vector);
CREATE INDEX IF NOT EXISTS idx_ps_push_messages_raw_data ON ps_push_messages USING GIN(raw_data);
CREATE INDEX IF NOT EXISTS idx_ps_push_messages_parsed_data ON ps_push_messages USING GIN(parsed_data);
CREATE INDEX IF NOT EXISTS idx_ps_push_messages_location ON ps_push_messages(latitude, longitude) WHERE latitude IS NOT NULL AND longitude IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_ps_processing_logs_message_id ON ps_message_processing_logs(message_id);
CREATE INDEX IF NOT EXISTS idx_ps_processing_logs_started_at ON ps_message_processing_logs(started_at DESC);
-- 现有 ak_devices 和 ak_users 表的索引已存在,不需要重复创建
CREATE INDEX IF NOT EXISTS idx_ps_system_stats_date_hour ON ps_system_stats(stat_date DESC, stat_hour DESC);
CREATE INDEX IF NOT EXISTS idx_ps_system_stats_push_type ON ps_system_stats(push_type);
-- 8. 创建触发器函数
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION update_search_vector()
RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector := to_tsvector('english',
COALESCE(NEW.push_type, '') || ' ' ||
COALESCE(NEW.user_id, '') || ' ' ||
COALESCE(NEW.device_id, '') || ' ' ||
COALESCE(NEW.category, '') || ' ' ||
COALESCE(array_to_string(NEW.tags, ' '), '') || ' ' ||
COALESCE(NEW.raw_data::text, '')
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 9. 创建触发器
DROP TRIGGER IF EXISTS update_ps_push_messages_updated_at ON ps_push_messages;
CREATE TRIGGER update_ps_push_messages_updated_at
BEFORE UPDATE ON ps_push_messages
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
DROP TRIGGER IF EXISTS update_ps_push_messages_search_vector ON ps_push_messages;
CREATE TRIGGER update_ps_push_messages_search_vector
BEFORE INSERT OR UPDATE ON ps_push_messages
FOR EACH ROW EXECUTE FUNCTION update_search_vector();
DROP TRIGGER IF EXISTS update_ps_push_types_updated_at ON ps_push_types;
CREATE TRIGGER update_ps_push_types_updated_at
BEFORE UPDATE ON ps_push_types
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- 现有的 ak_devices 和 ak_users 表可能已有更新时间触发器,不需要重复创建
-- 10. 插入基础推送类型配置
INSERT INTO ps_push_types (type_code, type_name, description, default_priority, validation_schema) VALUES
('SOS', '紧急求救', 'SOS紧急求救信号', 1, '{"type": "object", "required": ["userId"], "properties": {"userId": {"type": "string"}, "emergencyLevel": {"type": "string", "enum": ["LOW", "MEDIUM", "HIGH", "CRITICAL"]}}}'),
('HEALTH', '健康数据', '健康监测数据推送', 3, '{"type": "object", "required": ["userId"], "properties": {"userId": {"type": "string"}, "H": {"type": "number"}, "O": {"type": "number"}, "T": {"type": "number"}}}'),
('LOCATION', '位置信息', '位置定位数据推送', 4, '{"type": "object", "required": ["userId"], "properties": {"userId": {"type": "string"}, "lat": {"type": "number"}, "lng": {"type": "number"}, "accuracy": {"type": "number"}}}'),
('ALERT', '告警信息', '各类告警信息推送', 2, '{"type": "object", "required": ["userId", "alertType"], "properties": {"userId": {"type": "string"}, "alertType": {"type": "string"}, "severity": {"type": "string"}}}'),
('ACTIVITY', '活动数据', '运动和活动数据推送', 5, '{"type": "object", "required": ["userId"], "properties": {"userId": {"type": "string"}, "activityType": {"type": "string"}, "duration": {"type": "number"}, "calories": {"type": "number"}}}'),
('DEVICE_STATUS', '设备状态', '设备状态信息推送', 4, '{"type": "object", "required": ["deviceId"], "properties": {"deviceId": {"type": "string"}, "status": {"type": "string"}, "batteryLevel": {"type": "number"}}}')
ON CONFLICT (type_code) DO NOTHING;
-- 11. 创建视图
CREATE OR REPLACE VIEW ps_recent_message_stats AS
SELECT
push_type,
COUNT(*) as total_count,
COUNT(*) FILTER (WHERE processing_status = 'processed') as processed_count,
COUNT(*) FILTER (WHERE processing_status = 'failed') as failed_count,
COUNT(*) FILTER (WHERE processing_status = 'pending') as pending_count,
AVG(EXTRACT(EPOCH FROM (processed_at - received_at)) * 1000) as avg_processing_time_ms,
MIN(received_at) as first_received,
MAX(received_at) as last_received
FROM ps_push_messages
WHERE received_at >= NOW() - INTERVAL '24 hours'
AND is_deleted = FALSE
GROUP BY push_type;
CREATE OR REPLACE VIEW ps_active_devices_stats AS
SELECT
d.device_type,
COUNT(*) as total_devices,
COUNT(*) FILTER (WHERE d.bind_time >= NOW() - INTERVAL '1 hour') as active_1h,
COUNT(*) FILTER (WHERE d.bind_time >= NOW() - INTERVAL '24 hours') as active_24h,
COUNT(*) FILTER (WHERE d.bind_time >= NOW() - INTERVAL '7 days') as active_7d
FROM public.ak_devices d
WHERE d.status = 'active'
GROUP BY d.device_type;
-- 12. 创建函数
CREATE OR REPLACE FUNCTION ps_cleanup_old_messages(days_to_keep INTEGER DEFAULT 30)
RETURNS INTEGER AS $$
DECLARE
deleted_count INTEGER;
BEGIN
-- 软删除超过指定天数的消息
UPDATE ps_push_messages
SET is_deleted = TRUE, deleted_at = NOW()
WHERE received_at < NOW() - (days_to_keep || ' days')::INTERVAL
AND is_deleted = FALSE;
GET DIAGNOSTICS deleted_count = ROW_COUNT;
-- 记录清理日志
INSERT INTO ps_message_processing_logs (message_id, processing_step, status, details)
VALUES (NULL, 'cleanup_old_messages', 'completed',
json_build_object('days_to_keep', days_to_keep, 'deleted_count', deleted_count));
RETURN deleted_count;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION ps_get_message_stats(hours_back INTEGER DEFAULT 24)
RETURNS TABLE(
push_type VARCHAR,
total_count BIGINT,
processed_count BIGINT,
failed_count BIGINT,
pending_count BIGINT,
avg_processing_time_ms NUMERIC
) AS $$
BEGIN RETURN QUERY
SELECT
pm.push_type,
COUNT(*) as total_count,
COUNT(*) FILTER (WHERE pm.processing_status = 'processed') as processed_count,
COUNT(*) FILTER (WHERE pm.processing_status = 'failed') as failed_count,
COUNT(*) FILTER (WHERE pm.processing_status = 'pending') as pending_count,
AVG(EXTRACT(EPOCH FROM (pm.processed_at - pm.received_at)) * 1000) as avg_processing_time_ms
FROM ps_push_messages pm
WHERE pm.received_at >= NOW() - (hours_back || ' hours')::INTERVAL
AND pm.is_deleted = FALSE
GROUP BY pm.push_type
ORDER BY total_count DESC;
END;
$$ LANGUAGE plpgsql;
-- 13. 启用 Row Level Security (RLS)
ALTER TABLE ps_push_messages ENABLE ROW LEVEL SECURITY;
ALTER TABLE ps_push_types ENABLE ROW LEVEL SECURITY;
ALTER TABLE ps_message_processing_logs ENABLE ROW LEVEL SECURITY;
ALTER TABLE ps_system_stats ENABLE ROW LEVEL SECURITY;
-- 现有的 ak_devices 和 ak_users 表可能已有 RLS 设置
-- 14. 创建 RLS 策略Service Role 可以访问所有数据)
CREATE POLICY "Enable all access for service role" ON ps_push_messages
FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY "Enable all access for service role" ON ps_push_types
FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY "Enable all access for service role" ON ps_message_processing_logs
FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY "Enable all access for service role" ON ps_system_stats
FOR ALL USING (auth.role() = 'service_role');
-- 现有的 ak_devices 和 ak_users 表可能已有 RLS 策略
-- 15. 创建 API 访问策略(允许匿名用户插入数据)
CREATE POLICY "Enable insert for anon users" ON ps_push_messages
FOR INSERT WITH CHECK (true);
CREATE POLICY "Enable read for anon users" ON ps_push_types
FOR SELECT USING (is_active = true);
-- 16. 初始化完成标记
INSERT INTO ps_system_stats (stat_date, stat_hour, push_type, message_count)
VALUES (CURRENT_DATE, EXTRACT(HOUR FROM NOW()), 'SYSTEM', 0)
ON CONFLICT (stat_date, stat_hour, push_type) DO NOTHING;
-- 17. 创建实时订阅(用于监控推送消息)
-- 在 Supabase Dashboard 的 Database > Replication 中启用 ps_push_messages 表的实时功能
-- 注释
COMMENT ON TABLE ps_push_messages IS '推送消息主表 - 存储所有接收到的推送消息';
COMMENT ON TABLE ps_push_types IS '推送类型配置表 - 定义各种推送消息类型及其验证规则';
COMMENT ON TABLE ps_message_processing_logs IS '消息处理日志表 - 记录消息处理过程的详细日志';
COMMENT ON TABLE ps_system_stats IS '系统统计表 - 存储系统运行统计数据';
-- 现有表的注释:
-- public.ak_devices: 设备表 - 存储用户绑定的各种设备信息
-- public.ak_users: 用户表 - 存储系统用户的基本信息和配置

View File

@@ -0,0 +1,22 @@
手表imei:860100005876385
{
"appId": "NS4yMDQ2NDAzNA==",
"appKey": "9BC1BB6DF0993D167C6E0327CBDF5F1E",
"deviceList": "860100005876385"
}
forever start -c "npm start" .
forever start -c "npm start" /path/to/your/project
forever list
forever stop <index|script>
forever logs
# 或直接查看日志文件
cat ~/.forever/*.log

View File

@@ -0,0 +1,427 @@
/**
* 数据库连接和操作类
* 提供 PostgreSQL 数据库的连接池和基础操作方法
*/
const { Pool } = require('pg');
const winston = require('winston');
require('dotenv').config();
class DatabaseManager {
constructor() {
this.pool = null;
this.logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'logs/database.log' })
]
});
this.initializePool();
}
/**
* 初始化数据库连接池
*/
initializePool() {
try {
this.pool = new Pool({
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
database: process.env.DB_NAME || 'push_messages',
user: process.env.DB_USER || 'push_service',
password: process.env.DB_PASSWORD,
ssl: process.env.DB_SSL === 'true',
max: parseInt(process.env.DB_MAX_CONNECTIONS) || 20,
idleTimeoutMillis: parseInt(process.env.DB_IDLE_TIMEOUT) || 30000,
connectionTimeoutMillis: parseInt(process.env.DB_CONNECTION_TIMEOUT) || 5000,
});
// 监听连接池事件
this.pool.on('connect', (client) => {
this.logger.info('数据库连接已建立', {
totalCount: this.pool.totalCount,
idleCount: this.pool.idleCount,
waitingCount: this.pool.waitingCount
});
});
this.pool.on('error', (err, client) => {
this.logger.error('数据库连接池错误', { error: err.message, stack: err.stack });
});
this.logger.info('数据库连接池初始化完成');
} catch (error) {
this.logger.error('数据库连接池初始化失败', { error: error.message, stack: error.stack });
throw error;
}
}
/**
* 测试数据库连接
*/
async testConnection() {
try {
const client = await this.pool.connect();
const result = await client.query('SELECT NOW() as current_time, version() as version');
client.release();
this.logger.info('数据库连接测试成功', {
currentTime: result.rows[0].current_time,
version: result.rows[0].version.split(' ')[0]
});
return {
success: true,
currentTime: result.rows[0].current_time,
version: result.rows[0].version
};
} catch (error) {
this.logger.error('数据库连接测试失败', { error: error.message, stack: error.stack });
throw error;
}
}
/**
* 插入推送消息
*/
async insertPushMessage(messageData) {
const startTime = Date.now();
const client = await this.pool.connect();
try {
await client.query('BEGIN');
// 生成消息校验和用于去重
const checksum = this.generateChecksum(messageData);
// 检查是否存在重复消息
const duplicateCheck = await client.query(
'SELECT id FROM push_messages WHERE checksum = $1 AND is_deleted = FALSE LIMIT 1',
[checksum]
);
let messageId;
let isDuplicate = false;
if (duplicateCheck.rows.length > 0) {
// 处理重复消息
isDuplicate = true;
const originalMessageId = duplicateCheck.rows[0].id;
const insertQuery = `
INSERT INTO push_messages (
message_id, push_type, user_id, device_id, source_ip, user_agent,
raw_data, parsed_data, priority, category, tags, checksum,
is_duplicate, original_message_id, latitude, longitude,
location_accuracy, location_timestamp
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18
) RETURNING id
`;
const result = await client.query(insertQuery, [
messageData.message_id || null,
messageData.push_type || messageData.pushType,
messageData.user_id || messageData.userId,
messageData.device_id || messageData.deviceId,
messageData.source_ip,
messageData.user_agent,
JSON.stringify(messageData.raw_data || messageData),
JSON.stringify(messageData.parsed_data || messageData),
messageData.priority || 5,
messageData.category,
messageData.tags,
checksum,
true,
originalMessageId,
messageData.latitude || messageData.lat,
messageData.longitude || messageData.lng,
messageData.location_accuracy || messageData.accuracy,
messageData.location_timestamp || null
]);
messageId = result.rows[0].id;
} else {
// 插入新消息
const insertQuery = `
INSERT INTO push_messages (
message_id, push_type, user_id, device_id, source_ip, user_agent,
raw_data, parsed_data, priority, category, tags, checksum,
is_duplicate, latitude, longitude, location_accuracy, location_timestamp
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17
) RETURNING id
`;
const result = await client.query(insertQuery, [
messageData.message_id || null,
messageData.push_type || messageData.pushType,
messageData.user_id || messageData.userId,
messageData.device_id || messageData.deviceId,
messageData.source_ip,
messageData.user_agent,
JSON.stringify(messageData.raw_data || messageData),
JSON.stringify(messageData.parsed_data || messageData),
messageData.priority || 5,
messageData.category,
messageData.tags,
checksum,
false,
messageData.latitude || messageData.lat,
messageData.longitude || messageData.lng,
messageData.location_accuracy || messageData.accuracy,
messageData.location_timestamp || null
]);
messageId = result.rows[0].id;
}
// 记录处理日志
await client.query(
`INSERT INTO message_processing_logs (message_id, processing_step, status, started_at, completed_at, duration_ms, details)
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
[
messageId,
'message_received',
'completed',
new Date(startTime),
new Date(),
Date.now() - startTime,
JSON.stringify({ isDuplicate, checksum })
]
);
// 更新设备信息
if (messageData.device_id || messageData.deviceId) {
await this.upsertDevice(client, {
device_id: messageData.device_id || messageData.deviceId,
device_name: messageData.device_name,
device_type: messageData.device_type,
user_id: messageData.user_id || messageData.userId,
metadata: messageData.device_metadata
});
}
// 更新用户信息
if (messageData.user_id || messageData.userId) {
await this.upsertUser(client, {
user_id: messageData.user_id || messageData.userId,
user_name: messageData.user_name,
user_type: messageData.user_type
});
}
await client.query('COMMIT');
this.logger.info('推送消息插入成功', {
messageId,
pushType: messageData.push_type || messageData.pushType,
userId: messageData.user_id || messageData.userId,
isDuplicate,
processingTime: Date.now() - startTime
});
return {
success: true,
messageId,
isDuplicate,
processingTime: Date.now() - startTime
};
} catch (error) {
await client.query('ROLLBACK');
this.logger.error('推送消息插入失败', { error: error.message, stack: error.stack });
throw error;
} finally {
client.release();
}
}
/**
* 批量插入推送消息
*/
async insertPushMessagesBatch(messagesData) {
const startTime = Date.now();
const client = await this.pool.connect();
try {
await client.query('BEGIN');
const results = [];
for (const messageData of messagesData) {
try {
const result = await this.insertPushMessage(messageData);
results.push(result);
} catch (error) {
results.push({
success: false,
error: error.message,
messageData: messageData
});
}
}
await client.query('COMMIT');
const successCount = results.filter(r => r.success).length;
const failureCount = results.filter(r => !r.success).length;
this.logger.info('批量推送消息插入完成', {
totalCount: messagesData.length,
successCount,
failureCount,
processingTime: Date.now() - startTime
});
return {
success: true,
totalCount: messagesData.length,
successCount,
failureCount,
results,
processingTime: Date.now() - startTime
};
} catch (error) {
await client.query('ROLLBACK');
this.logger.error('批量推送消息插入失败', { error: error.message, stack: error.stack });
throw error;
} finally {
client.release();
}
}
/**
* 更新或插入设备信息
*/
async upsertDevice(client, deviceData) {
const query = `
INSERT INTO devices (device_id, device_name, device_type, user_id, last_seen_at, metadata)
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, $5)
ON CONFLICT (device_id)
DO UPDATE SET
device_name = COALESCE(EXCLUDED.device_name, devices.device_name),
device_type = COALESCE(EXCLUDED.device_type, devices.device_type),
user_id = COALESCE(EXCLUDED.user_id, devices.user_id),
last_seen_at = CURRENT_TIMESTAMP,
metadata = COALESCE(EXCLUDED.metadata, devices.metadata),
updated_at = CURRENT_TIMESTAMP
`;
await client.query(query, [
deviceData.device_id,
deviceData.device_name,
deviceData.device_type,
deviceData.user_id,
JSON.stringify(deviceData.metadata || {})
]);
}
/**
* 更新或插入用户信息
*/
async upsertUser(client, userData) {
const query = `
INSERT INTO users (user_id, user_name, user_type)
VALUES ($1, $2, $3)
ON CONFLICT (user_id)
DO UPDATE SET
user_name = COALESCE(EXCLUDED.user_name, users.user_name),
user_type = COALESCE(EXCLUDED.user_type, users.user_type),
updated_at = CURRENT_TIMESTAMP
`;
await client.query(query, [
userData.user_id,
userData.user_name,
userData.user_type
]);
}
/**
* 生成消息校验和
*/
generateChecksum(data) {
const crypto = require('crypto');
const normalizedData = JSON.stringify(data, Object.keys(data).sort());
return crypto.createHash('sha256').update(normalizedData).digest('hex');
}
/**
* 获取消息统计信息
*/
async getMessageStats(hoursBack = 24) {
try {
const query = 'SELECT * FROM get_message_stats($1)';
const result = await this.pool.query(query, [hoursBack]);
return result.rows;
} catch (error) {
this.logger.error('获取消息统计失败', { error: error.message, stack: error.stack });
throw error;
}
}
/**
* 获取系统健康状态
*/
async getHealthStatus() {
try {
const poolStats = {
totalCount: this.pool.totalCount,
idleCount: this.pool.idleCount,
waitingCount: this.pool.waitingCount
};
const dbStats = await this.pool.query(`
SELECT
COUNT(*) as total_messages,
COUNT(*) FILTER (WHERE received_at >= CURRENT_TIMESTAMP - INTERVAL '1 hour') as messages_last_hour,
COUNT(*) FILTER (WHERE processing_status = 'pending') as pending_messages,
COUNT(*) FILTER (WHERE processing_status = 'failed') as failed_messages
FROM push_messages
WHERE is_deleted = FALSE
`);
return {
database: {
connected: true,
pool: poolStats,
stats: dbStats.rows[0]
},
timestamp: new Date().toISOString()
};
} catch (error) {
this.logger.error('获取系统健康状态失败', { error: error.message, stack: error.stack });
return {
database: {
connected: false,
error: error.message
},
timestamp: new Date().toISOString()
};
}
}
/**
* 关闭数据库连接池
*/
async close() {
if (this.pool) {
await this.pool.end();
this.logger.info('数据库连接池已关闭');
}
}
}
module.exports = DatabaseManager;

View File

@@ -0,0 +1,571 @@
/**
* Supabase 数据库连接和操作类
* 提供基于 Supabase 的数据库操作方法
*/
const { createClient } = require('@supabase/supabase-js');
const winston = require('winston');
const crypto = require('crypto');
require('dotenv').config();
class SupabaseDatabaseManager {
constructor() {
this.supabase = null;
this.logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'logs/database.log' })
]
});
this.initializeClient();
}
/**
* 初始化 Supabase 客户端
*/
initializeClient() {
try {
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (!supabaseUrl || !supabaseKey) {
throw new Error('Supabase URL 和 Service Role Key 必须在环境变量中设置');
}
const eldercare_user = process.env.ELDERCARE_USER;
const eldercare_password = process.env.ELDERCARE_PASSWORD;
this.supabase = createClient(supabaseUrl, supabaseKey, {
auth: {
autoRefreshToken: true,
persistSession: true
},
db: {
schema: 'public'
}
});
this.supabase.auth.signInWithPassword({
email: eldercare_user,
password: eldercare_password
}).then(({ data, error }) => {
if (error) {
this.logger.error('Supabase 用户认证失败', {
error: error.message,
stack: error.stack
});
throw error;
}
});
this.logger.info('Supabase 客户端初始化完成', {
url: supabaseUrl,
hasServiceKey: !!supabaseKey
});
} catch (error) {
this.logger.error('Supabase 客户端初始化失败', {
error: error.message,
stack: error.stack
});
throw error;
}
}
/**
* 测试数据库连接
*/
async testConnection() {
try {
const { data, error } = await this.supabase
.from('ps_push_types')
.select('count')
.limit(1);
if (error) {
throw error;
}
this.logger.info('Supabase 数据库连接测试成功');
return {
success: true,
message: 'Supabase 数据库连接正常',
timestamp: new Date().toISOString()
};
} catch (error) {
this.logger.error('Supabase 数据库连接测试失败', {
error: error.message,
stack: error.stack
});
throw error;
}
}
/**
* 插入推送消息
*/
async insertPushMessage(messageData) {
const startTime = Date.now();
try {
// 生成消息校验和用于去重
const checksum = this.generateChecksum(messageData);
// 检查是否存在重复消息
const { data: duplicateData } = await this.supabase
.from('ps_push_msg_raw')
.select('id')
.eq('checksum', checksum)
.eq('is_deleted', false)
.limit(1);
let isDuplicate = false;
let originalMessageId = null;
if (duplicateData && duplicateData.length > 0) {
isDuplicate = true;
originalMessageId = duplicateData[0].id;
}
console.log('isDuplicate', isDuplicate, 'originalMessageId', originalMessageId);
// 准备插入数据
const insertData = {
checksum: checksum,
push_type: messageData.push_type || messageData.pushType,
source_ip: messageData.source_ip,
user_agent: messageData.user_agent,
raw_data: messageData.raw_data || messageData
};
console.log('insertData', insertData);
// 插入推送消息
const { data: messageResult, error: messageError } = await this.supabase
.from('ps_push_msg_raw')
.insert(insertData)
.select('id')
.single();
console.log('messageResult', messageResult);
// 检查插入结果
if (messageError) {
this.logger.error('插入推送消息失败', {
error: messageError.message,
data: insertData
});
throw messageError;
}
const messageId = messageResult.id;
// 记录处理日志
await this.supabase
.from('message_processing_logs')
.insert({
message_id: messageId,
processing_step: 'message_received',
status: 'completed',
started_at: new Date(startTime).toISOString(),
completed_at: new Date().toISOString(),
duration_ms: Date.now() - startTime,
details: { isDuplicate, checksum }
});
// 更新设备信息
if (messageData.device_id || messageData.deviceId) {
await this.upsertDevice({
device_id: messageData.device_id || messageData.deviceId,
device_name: messageData.device_name,
device_type: messageData.device_type,
user_id: messageData.user_id || messageData.userId,
metadata: messageData.device_metadata || {}
});
}
// 更新用户信息
if (messageData.user_id || messageData.userId) {
await this.upsertUser({
user_id: messageData.user_id || messageData.userId,
user_name: messageData.user_name,
user_type: messageData.user_type
});
}
this.logger.info('推送消息插入成功', {
messageId,
pushType: messageData.push_type || messageData.pushType,
userId: messageData.user_id || messageData.userId,
isDuplicate,
processingTime: Date.now() - startTime
});
return {
success: true,
messageId,
isDuplicate,
processingTime: Date.now() - startTime
};
} catch (error) {
this.logger.error('推送消息插入失败', {
error: error.message,
stack: error.stack,
messageData: messageData
});
throw error;
}
}
/**
* 批量插入推送消息
*/
async insertPushMessagesBatch(messagesData) {
const startTime = Date.now();
try {
const results = [];
// 使用事务批量插入
for (const messageData of messagesData) {
try {
const result = await this.insertPushMessage(messageData);
results.push(result);
} catch (error) {
results.push({
success: false,
error: error.message,
messageData: messageData
});
}
}
const successCount = results.filter(r => r.success).length;
const failureCount = results.filter(r => !r.success).length;
this.logger.info('批量推送消息插入完成', {
totalCount: messagesData.length,
successCount,
failureCount,
processingTime: Date.now() - startTime
});
return {
success: true,
totalCount: messagesData.length,
successCount,
failureCount,
results,
processingTime: Date.now() - startTime
};
} catch (error) {
this.logger.error('批量推送消息插入失败', {
error: error.message,
stack: error.stack
});
throw error;
}
}
/**
* 更新或插入设备信息 - 适配现有 ak_devices 表
*/
async upsertDevice(deviceData) {
try {
// 检查设备是否存在
const { data: existingDevice, error: selectError } = await this.supabase
.from('ak_devices')
.select('id')
.eq('id', deviceData.device_id)
.single();
if (selectError && selectError.code !== 'PGRST116') { // PGRST116 是没有找到记录的错误
throw selectError;
}
// 如果设备存在,更新状态;如果不存在,记录日志(设备应该通过其他接口创建)
if (existingDevice) {
const { error } = await this.supabase
.from('ak_devices')
.update({
status: 'active',
extra: {
...deviceData.metadata,
last_push_at: new Date().toISOString()
}
})
.eq('id', deviceData.device_id);
if (error) {
throw error;
}
this.logger.info('设备状态更新成功', { device_id: deviceData.device_id });
} else {
this.logger.warn('推送消息关联的设备不存在', {
device_id: deviceData.device_id,
message: '设备需要通过设备管理接口先创建'
});
}
} catch (error) {
this.logger.error('设备信息更新失败', {
error: error.message,
deviceData: deviceData
});
}
}
/**
* 更新或插入用户信息 - 适配现有 ak_users 表
*/
async upsertUser(userData) {
try {
// 检查用户是否存在
const { data: existingUser, error: selectError } = await this.supabase
.from('ak_users')
.select('id')
.eq('id', userData.user_id)
.single();
if (selectError && selectError.code !== 'PGRST116') { // PGRST116 是没有找到记录的错误
throw selectError;
}
// 如果用户存在,记录活跃状态;如果不存在,记录日志(用户应该通过其他接口创建)
if (existingUser) {
this.logger.info('用户推送消息活跃', { user_id: userData.user_id });
} else {
this.logger.warn('推送消息关联的用户不存在', {
user_id: userData.user_id,
message: '用户需要通过用户管理接口先创建'
});
}
} catch (error) {
this.logger.error('用户信息检查失败', {
error: error.message,
userData: userData
});
}
}
/**
* 生成消息校验和
*/
generateChecksum(data) {
const normalizedData = JSON.stringify(data, Object.keys(data).sort());
return crypto.createHash('sha256').update(normalizedData).digest('hex');
}
/**
* 获取消息统计信息
*/
async getMessageStats(hoursBack = 24) {
try {
const { data, error } = await this.supabase
.rpc('get_message_stats', { hours_back: hoursBack });
if (error) {
throw error;
}
return data || [];
} catch (error) {
this.logger.error('获取消息统计失败', {
error: error.message,
stack: error.stack
});
throw error;
}
}
/**
* 获取系统健康状态
*/
async getHealthStatus() {
try { // 获取消息统计
const { data: messageStats, error: statsError } = await this.supabase
.from('ps_push_messages')
.select('processing_status', { count: 'exact' })
.gte('received_at', new Date(Date.now() - 60 * 60 * 1000).toISOString())
.eq('is_deleted', false);
if (statsError) {
throw statsError;
}
// 统计不同状态的消息数量
const stats = {
total_messages: 0,
messages_last_hour: messageStats?.length || 0,
pending_messages: 0,
failed_messages: 0,
processed_messages: 0
};
// 获取总消息数
const { count: totalCount } = await this.supabase
.from('ps_push_messages')
.select('*', { count: 'exact', head: true })
.eq('is_deleted', false);
stats.total_messages = totalCount || 0;
// 获取待处理和失败的消息数
const { count: pendingCount } = await this.supabase
.from('ps_push_messages')
.select('*', { count: 'exact', head: true })
.eq('processing_status', 'pending')
.eq('is_deleted', false);
const { count: failedCount } = await this.supabase
.from('ps_push_messages')
.select('*', { count: 'exact', head: true })
.eq('processing_status', 'failed')
.eq('is_deleted', false);
stats.pending_messages = pendingCount || 0;
stats.failed_messages = failedCount || 0;
stats.processed_messages = stats.total_messages - stats.pending_messages - stats.failed_messages;
return {
database: {
connected: true,
provider: 'Supabase',
stats: stats
},
timestamp: new Date().toISOString()
};
} catch (error) {
this.logger.error('获取系统健康状态失败', {
error: error.message,
stack: error.stack
});
return {
database: {
connected: false,
provider: 'Supabase',
error: error.message
},
timestamp: new Date().toISOString()
};
}
}
/**
* 获取推送消息列表
*/
async getPushMessages(options = {}) {
try {
const {
limit = 50,
offset = 0,
pushType = null,
userId = null,
startDate = null,
endDate = null,
status = null
} = options;
let query = this.supabase
.from('ps_push_messages')
.select(`
id,
push_type,
user_id,
device_id,
received_at,
processing_status,
priority,
raw_data,
is_duplicate
`)
.eq('is_deleted', false)
.order('received_at', { ascending: false })
.range(offset, offset + limit - 1);
if (pushType) {
query = query.eq('push_type', pushType);
}
if (userId) {
query = query.eq('user_id', userId);
}
if (status) {
query = query.eq('processing_status', status);
}
if (startDate) {
query = query.gte('received_at', startDate);
}
if (endDate) {
query = query.lte('received_at', endDate);
}
const { data, error } = await query;
if (error) {
throw error;
}
return data || [];
} catch (error) {
this.logger.error('获取推送消息列表失败', {
error: error.message,
stack: error.stack
});
throw error;
}
}
/**
* 清理旧数据
*/
async cleanupOldMessages(daysToKeep = 30) {
try {
const cutoffDate = new Date(Date.now() - daysToKeep * 24 * 60 * 60 * 1000).toISOString();
const { data, error } = await this.supabase
.from('ps_push_messages')
.update({
is_deleted: true,
deleted_at: new Date().toISOString()
})
.lt('received_at', cutoffDate)
.eq('is_deleted', false)
.select('id');
if (error) {
throw error;
}
const deletedCount = data?.length || 0;
this.logger.info('清理旧数据完成', {
daysToKeep,
deletedCount,
cutoffDate
});
return deletedCount;
} catch (error) {
this.logger.error('清理旧数据失败', {
error: error.message,
stack: error.stack
});
throw error;
}
}
/**
* 关闭连接Supabase 客户端不需要显式关闭)
*/
async close() {
this.logger.info('Supabase 客户端连接已关闭');
}
}
module.exports = SupabaseDatabaseManager;

1772
push-receiver-service/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
{
"name": "push-receiver-service",
"version": "1.0.0",
"description": "专门接收推送消息的独立服务",
"main": "server.js",
"scripts": {
"start": "node supabase-server.js",
"start:pg": "node server.js",
"dev": "nodemon supabase-server.js",
"dev:pg": "nodemon server.js",
"setup-supabase": "node setup-supabase.js",
"setup-pg": "node setup-database.js",
"test": "node test.js",
"check-config": "node check-config.js"
},
"dependencies": {
"express": "^5.1.0",
"cors": "^2.8.5",
"helmet": "^8.1.0",
"express-rate-limit": "^7.5.1",
"winston": "^3.17.0",
"dotenv": "^16.5.0",
"uuid": "^11.1.0",
"joi": "^17.13.3",
"compression": "^1.8.0",
"express-validator": "^7.2.1",
"@supabase/supabase-js": "^2.50.2",
"crypto": "^1.0.1"
},
"devDependencies": {
"nodemon": "^3.1.10"
},
"keywords": [
"push",
"receiver",
"service",
"api",
"database"
],
"author": "Your Name",
"license": "MIT"
}

View File

@@ -0,0 +1,556 @@
/**
* 推送消息接收服务主服务器
* 专门用于接收和存储各种推送消息
*/
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const rateLimit = require('express-rate-limit');
const { body, validationResult } = require('express-validator');
const winston = require('winston');
const path = require('path');
const fs = require('fs');
require('dotenv').config();
const DatabaseManager = require('./lib/database');
class PushReceiverService {
constructor() {
this.app = express();
this.port = process.env.PORT || 3001;
this.host = process.env.HOST || '0.0.0.0';
this.db = new DatabaseManager();
this.setupLogger();
this.setupMiddleware();
this.setupRoutes();
this.setupErrorHandling();
// 统计信息
this.stats = {
startTime: new Date(),
requestCount: 0,
messageCount: 0,
errorCount: 0
};
}
/**
* 设置日志系统
*/
setupLogger() {
// 确保日志目录存在
const logDir = process.env.LOG_DIR || './logs';
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
this.logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.File({
filename: path.join(logDir, 'push-service-error.log'),
level: 'error',
maxsize: parseInt(process.env.LOG_FILE_MAX_SIZE) || 10485760,
maxFiles: parseInt(process.env.LOG_FILE_MAX_FILES) || 5
}),
new winston.transports.File({
filename: path.join(logDir, 'push-service.log'),
maxsize: parseInt(process.env.LOG_FILE_MAX_SIZE) || 10485760,
maxFiles: parseInt(process.env.LOG_FILE_MAX_FILES) || 5
}),
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
})
]
});
}
/**
* 设置中间件
*/
setupMiddleware() {
// 安全中间件
this.app.use(helmet({
contentSecurityPolicy: false
}));
// 压缩中间件
this.app.use(compression());
// CORS 配置
const corsOptions = {
origin: (origin, callback) => {
const allowedOrigins = process.env.ALLOWED_ORIGINS;
if (!allowedOrigins || allowedOrigins === '*') {
callback(null, true);
} else {
const origins = allowedOrigins.split(',');
if (origins.includes(origin) || !origin) {
callback(null, true);
} else {
callback(new Error('不允许的CORS来源'));
}
}
},
credentials: true,
optionsSuccessStatus: 200
};
this.app.use(cors(corsOptions));
// 请求体解析
this.app.use(express.json({
limit: process.env.MAX_MESSAGE_SIZE || '1mb'
}));
this.app.use(express.urlencoded({ extended: true }));
// API密钥验证中间件
this.app.use('/api', (req, res, next) => {
if (req.path === '/health') {
return next(); // 健康检查不需要验证
}
const apiKey = req.headers['x-api-key'] || req.headers['authorization'];
const expectedApiKey = process.env.API_KEY;
if (expectedApiKey && (!apiKey || apiKey !== expectedApiKey)) {
this.stats.errorCount++;
return res.status(401).json({
success: false,
error: 'UNAUTHORIZED',
message: 'API密钥无效或缺失'
});
}
next();
});
// 速率限制
const limiter = rateLimit({
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 60000,
max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 1000,
message: {
success: false,
error: 'RATE_LIMIT_EXCEEDED',
message: process.env.RATE_LIMIT_MESSAGE || '请求过于频繁,请稍后再试'
},
standardHeaders: true,
legacyHeaders: false
});
this.app.use('/api', limiter);
// 请求日志中间件
this.app.use((req, res, next) => {
const startTime = Date.now();
this.stats.requestCount++;
res.on('finish', () => {
const duration = Date.now() - startTime;
this.logger.info('HTTP Request', {
method: req.method,
url: req.url,
status: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userAgent: req.get('User-Agent'),
contentLength: req.get('Content-Length') || 0
});
});
next();
});
}
/**
* 设置路由
*/
setupRoutes() {
// 根路径 - 服务信息
this.app.get('/', (req, res) => {
res.json({
name: 'Push Receiver Service',
version: '1.0.0',
description: '专门接收推送消息的独立服务',
status: 'running',
uptime: Math.floor((Date.now() - this.stats.startTime.getTime()) / 1000),
stats: {
...this.stats,
uptime: Math.floor((Date.now() - this.stats.startTime.getTime()) / 1000)
},
endpoints: {
pushMessage: 'POST /api/push/message',
pushBatch: 'POST /api/push/batch',
health: 'GET /api/health',
stats: 'GET /api/stats'
},
timestamp: new Date().toISOString()
});
});
// 健康检查
this.app.get('/api/health', async (req, res) => {
try {
const dbHealth = await this.db.getHealthStatus();
res.json({
status: 'OK',
service: 'Push Receiver Service',
version: '1.0.0',
timestamp: new Date().toISOString(),
uptime: Math.floor((Date.now() - this.stats.startTime.getTime()) / 1000),
database: dbHealth.database,
stats: this.stats
});
} catch (error) {
this.logger.error('健康检查失败', { error: error.message, stack: error.stack });
res.status(500).json({
status: 'ERROR',
service: 'Push Receiver Service',
error: error.message,
timestamp: new Date().toISOString()
});
}
});
// 接收单个推送消息
this.app.post('/api/push/message',
[
body('pushType').notEmpty().withMessage('推送类型不能为空'),
body('userId').optional().isString().withMessage('用户ID必须是字符串'),
body('deviceId').optional().isString().withMessage('设备ID必须是字符串')
],
async (req, res) => {
try {
// 验证请求数据
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: 'VALIDATION_ERROR',
message: '请求数据验证失败',
details: errors.array()
});
}
// 准备消息数据
const messageData = {
...req.body,
source_ip: req.ip,
user_agent: req.get('User-Agent'),
raw_data: req.body,
parsed_data: req.body,
received_at: new Date()
};
// 保存到数据库
const result = await this.db.insertPushMessage(messageData);
this.stats.messageCount++;
this.logger.info('推送消息接收成功', {
messageId: result.messageId,
pushType: req.body.pushType,
userId: req.body.userId,
deviceId: req.body.deviceId,
isDuplicate: result.isDuplicate,
processingTime: result.processingTime
});
res.json({
success: true,
message: '推送消息接收成功',
data: {
messageId: result.messageId,
isDuplicate: result.isDuplicate,
processingTime: result.processingTime,
timestamp: new Date().toISOString()
}
});
} catch (error) {
this.stats.errorCount++;
this.logger.error('推送消息接收失败', {
error: error.message,
stack: error.stack,
requestBody: req.body
});
res.status(500).json({
success: false,
error: 'MESSAGE_PROCESSING_ERROR',
message: '推送消息处理失败',
details: error.message
});
}
}
);
// 批量接收推送消息
this.app.post('/api/push/batch',
[
body('messages').isArray().withMessage('messages必须是数组'),
body('messages').custom((messages) => {
const maxBatchSize = parseInt(process.env.BATCH_SIZE_LIMIT) || 1000;
if (messages.length > maxBatchSize) {
throw new Error(`批量消息数量不能超过${maxBatchSize}`);
}
return true;
})
],
async (req, res) => {
try {
// 验证请求数据
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: 'VALIDATION_ERROR',
message: '请求数据验证失败',
details: errors.array()
});
}
const { messages } = req.body;
// 准备批量消息数据
const messagesData = messages.map(msg => ({
...msg,
source_ip: req.ip,
user_agent: req.get('User-Agent'),
raw_data: msg,
parsed_data: msg,
received_at: new Date()
}));
// 批量保存到数据库
const result = await this.db.insertPushMessagesBatch(messagesData);
this.stats.messageCount += result.successCount;
this.stats.errorCount += result.failureCount;
this.logger.info('批量推送消息接收完成', {
totalCount: result.totalCount,
successCount: result.successCount,
failureCount: result.failureCount,
processingTime: result.processingTime
});
res.json({
success: true,
message: `批量推送消息处理完成,成功${result.successCount}条,失败${result.failureCount}`,
data: {
totalCount: result.totalCount,
successCount: result.successCount,
failureCount: result.failureCount,
processingTime: result.processingTime,
timestamp: new Date().toISOString()
}
});
} catch (error) {
this.stats.errorCount++;
this.logger.error('批量推送消息接收失败', {
error: error.message,
stack: error.stack,
requestBody: req.body
});
res.status(500).json({
success: false,
error: 'BATCH_PROCESSING_ERROR',
message: '批量推送消息处理失败',
details: error.message
});
}
}
);
// 获取统计信息
this.app.get('/api/stats', async (req, res) => {
try {
const hoursBack = parseInt(req.query.hours) || 24;
const messageStats = await this.db.getMessageStats(hoursBack);
res.json({
success: true,
data: {
service: {
uptime: Math.floor((Date.now() - this.stats.startTime.getTime()) / 1000),
...this.stats
},
messages: messageStats,
period: `最近${hoursBack}小时`
},
timestamp: new Date().toISOString()
});
} catch (error) {
this.logger.error('获取统计信息失败', { error: error.message, stack: error.stack });
res.status(500).json({
success: false,
error: 'STATS_ERROR',
message: '获取统计信息失败',
details: error.message
});
}
});
}
/**
* 设置错误处理
*/
setupErrorHandling() {
// 404 处理
this.app.use((req, res) => {
res.status(404).json({
success: false,
error: 'NOT_FOUND',
message: '请求的端点不存在',
path: req.path,
method: req.method,
availableEndpoints: [
'GET /',
'GET /api/health',
'POST /api/push/message',
'POST /api/push/batch',
'GET /api/stats'
]
});
});
// 全局错误处理
this.app.use((error, req, res, next) => {
this.stats.errorCount++;
this.logger.error('未处理的错误', {
error: error.message,
stack: error.stack,
url: req.url,
method: req.method
});
res.status(500).json({
success: false,
error: 'INTERNAL_SERVER_ERROR',
message: '服务器内部错误',
details: process.env.NODE_ENV === 'development' ? error.message : undefined
});
});
}
/**
* 启动服务器
*/
async start() {
try {
// 测试数据库连接
await this.db.testConnection();
// 启动HTTP服务器
this.server = this.app.listen(this.port, this.host, () => {
console.log('🚀 推送消息接收服务启动成功!');
console.log(`📍 服务器地址: http://${this.host}:${this.port}`);
console.log('');
console.log('📋 API 端点:');
console.log(` - 服务信息: http://${this.host}:${this.port}/`);
console.log(` - 健康检查: http://${this.host}:${this.port}/api/health`);
console.log(` - 推送消息: http://${this.host}:${this.port}/api/push/message`);
console.log(` - 批量推送: http://${this.host}:${this.port}/api/push/batch`);
console.log(` - 统计信息: http://${this.host}:${this.port}/api/stats`);
console.log('');
console.log('🗄️ 数据库连接: ✅ 正常');
console.log('🔒 API密钥保护: ' + (process.env.API_KEY ? '✅ 启用' : '❌ 未设置'));
console.log('📊 请求速率限制: ✅ 启用');
console.log('');
console.log('✅ 服务已准备就绪,等待接收推送消息...');
console.log('💡 按 Ctrl+C 停止服务');
this.logger.info('推送消息接收服务启动完成', {
port: this.port,
host: this.host,
nodeEnv: process.env.NODE_ENV,
apiKeyEnabled: !!process.env.API_KEY
});
});
// 设置优雅关闭
this.setupGracefulShutdown();
} catch (error) {
this.logger.error('服务启动失败', { error: error.message, stack: error.stack });
process.exit(1);
}
}
/**
* 设置优雅关闭
*/
setupGracefulShutdown() {
const shutdown = async (signal) => {
console.log(`\n📴 收到${signal}信号,正在优雅关闭服务...`);
this.logger.info(`收到${signal}信号,开始关闭服务`);
// 停止接收新请求
if (this.server) {
this.server.close(async () => {
console.log('🌐 HTTP服务器已关闭');
this.logger.info('HTTP服务器已关闭');
// 关闭数据库连接
try {
await this.db.close();
console.log('🗄️ 数据库连接已关闭');
this.logger.info('数据库连接已关闭');
} catch (error) {
console.error('❌ 关闭数据库连接失败:', error.message);
this.logger.error('关闭数据库连接失败', { error: error.message });
}
console.log('✅ 服务已安全关闭');
this.logger.info('服务已安全关闭');
process.exit(0);
});
// 设置强制关闭超时
setTimeout(() => {
console.error('❌ 强制关闭服务(超时)');
this.logger.error('强制关闭服务(超时)');
process.exit(1);
}, 30000);
}
};
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
// 捕获未处理的异常
process.on('uncaughtException', (error) => {
console.error('💥 未捕获的异常:', error);
this.logger.error('未捕获的异常', { error: error.message, stack: error.stack });
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('💥 未处理的 Promise 拒绝:', reason);
this.logger.error('未处理的 Promise 拒绝', { reason: reason?.message || reason });
process.exit(1);
});
}
}
// 启动服务
if (require.main === module) {
const service = new PushReceiverService();
service.start();
}
module.exports = PushReceiverService;

View File

@@ -0,0 +1,219 @@
/**
* 数据库初始化脚本
* 自动创建数据库表结构和初始数据
*/
const DatabaseManager = require('./lib/database');
const fs = require('fs');
const path = require('path');
require('dotenv').config();
class DatabaseSetup {
constructor() {
this.db = new DatabaseManager();
}
async setup() {
try {
console.log('🔧 开始数据库初始化...');
// 测试数据库连接
console.log('📡 测试数据库连接...');
await this.db.testConnection();
console.log('✅ 数据库连接成功');
// 读取并执行初始化SQL
console.log('📋 执行数据库初始化脚本...');
const sqlPath = path.join(__dirname, 'database', 'init.sql');
if (fs.existsSync(sqlPath)) {
const initSQL = fs.readFileSync(sqlPath, 'utf8');
// 将SQL分割成多个语句执行
const statements = initSQL
.split(';')
.filter(stmt => stmt.trim().length > 0)
.map(stmt => stmt.trim() + ';');
const client = await this.db.pool.connect();
try {
for (let i = 0; i < statements.length; i++) {
const statement = statements[i];
if (statement.trim() === ';') continue;
try {
await client.query(statement);
console.log(`✅ 执行语句 ${i + 1}/${statements.length}`);
} catch (error) {
// 跳过一些可能的错误(如表已存在等)
if (error.message.includes('already exists') ||
error.message.includes('duplicate key')) {
console.log(`⚠️ 跳过语句 ${i + 1}/${statements.length}: ${error.message}`);
} else {
throw error;
}
}
}
} finally {
client.release();
}
console.log('✅ 数据库初始化脚本执行完成');
} else {
console.log('❌ 未找到初始化SQL文件:', sqlPath);
}
// 验证表结构
console.log('🔍 验证数据库表结构...');
await this.verifyTables();
// 插入测试数据
console.log('📝 插入测试数据...');
await this.insertTestData();
console.log('🎉 数据库初始化完成!');
} catch (error) {
console.error('❌ 数据库初始化失败:', error.message);
throw error;
} finally {
await this.db.close();
}
}
async verifyTables() {
const expectedTables = [
'ps_push_messages',
'ps_push_types',
'message_processing_logs',
'devices',
'users',
'system_stats'
];
const client = await this.db.pool.connect();
try {
const result = await client.query(`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
ORDER BY table_name
`);
const existingTables = result.rows.map(row => row.table_name);
console.log('📊 现有数据表:');
existingTables.forEach(table => {
const exists = expectedTables.includes(table);
console.log(` ${exists ? '✅' : '❓'} ${table}`);
});
const missingTables = expectedTables.filter(table => !existingTables.includes(table));
if (missingTables.length > 0) {
console.log('❌ 缺少数据表:', missingTables.join(', '));
throw new Error(`缺少必要的数据表: ${missingTables.join(', ')}`);
}
console.log('✅ 所有必要数据表已存在');
} finally {
client.release();
}
}
async insertTestData() {
const client = await this.db.pool.connect();
try {
// 插入测试推送消息
const testMessages = [
{
push_type: 'HEALTH',
user_id: 'test_user_001',
device_id: 'device_001',
raw_data: {
pushType: 'HEALTH',
userId: 'test_user_001',
H: 75,
O: 98,
T: 36.5
}
},
{
push_type: 'SOS',
user_id: 'test_user_002',
device_id: 'device_002',
raw_data: {
pushType: 'SOS',
userId: 'test_user_002',
emergencyLevel: 'HIGH',
location: {
lat: 39.9042,
lng: 116.4074
}
}
},
{
push_type: 'LOCATION',
user_id: 'test_user_003',
device_id: 'device_003',
raw_data: {
pushType: 'LOCATION',
userId: 'test_user_003',
lat: 31.2304,
lng: 121.4737,
accuracy: 10
}
}
];
for (const message of testMessages) {
const checksum = require('crypto')
.createHash('sha256')
.update(JSON.stringify(message.raw_data))
.digest('hex');
await client.query(`
INSERT INTO ps_push_messages (
push_type, user_id, device_id, raw_data, parsed_data,
checksum, priority, processing_status
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (checksum) DO NOTHING
`, [
message.push_type,
message.user_id,
message.device_id,
JSON.stringify(message.raw_data),
JSON.stringify(message.raw_data),
checksum,
message.push_type === 'SOS' ? 1 : 5,
'processed'
]);
}
console.log('✅ 测试数据插入完成');
} finally {
client.release();
}
}
}
// 如果直接运行此脚本
if (require.main === module) {
const setup = new DatabaseSetup();
setup.setup()
.then(() => {
console.log('🎉 数据库设置完成,可以启动服务了!');
process.exit(0);
})
.catch((error) => {
console.error('❌ 数据库设置失败:', error.message);
process.exit(1);
});
}
module.exports = DatabaseSetup;

View File

@@ -0,0 +1,249 @@
/**
* Supabase 数据库初始化脚本
* 自动检查并初始化 Supabase 数据库表结构
*/
const SupabaseDatabaseManager = require('./lib/supabase-database');
const fs = require('fs');
const path = require('path');
require('dotenv').config();
class SupabaseDatabaseSetup {
constructor() {
this.db = new SupabaseDatabaseManager();
}
async setup() {
try {
console.log('🔧 开始 Supabase 数据库初始化...');
// 测试 Supabase 连接
console.log('📡 测试 Supabase 数据库连接...');
await this.db.testConnection();
console.log('✅ Supabase 数据库连接成功');
// 检查环境变量
console.log('🔍 检查环境配置...');
this.checkEnvironmentVariables();
// 验证表结构
console.log('🔍 验证数据库表结构...');
await this.verifyTables();
// 插入测试数据
console.log('📝 插入测试数据...');
await this.insertTestData();
// 测试基本功能
console.log('🧪 测试基本功能...');
await this.testBasicFunctionality();
console.log('🎉 Supabase 数据库初始化完成!');
} catch (error) {
console.error('❌ Supabase 数据库初始化失败:', error.message);
throw error;
} finally {
await this.db.close();
}
}
checkEnvironmentVariables() {
const requiredEnvVars = [
'SUPABASE_URL',
'SUPABASE_SERVICE_ROLE_KEY'
];
const missingVars = requiredEnvVars.filter(varName => !process.env[varName]);
if (missingVars.length > 0) {
throw new Error(`缺少必要的环境变量: ${missingVars.join(', ')}`);
}
console.log('✅ 环境配置检查通过');
console.log(` - Supabase URL: ${process.env.SUPABASE_URL}`);
console.log(` - Service Role Key: ${'*'.repeat(20)}${process.env.SUPABASE_SERVICE_ROLE_KEY.slice(-10)}`);
}
async verifyTables() {
const expectedTables = [
'ps_push_messages',
'ps_push_types',
'message_processing_logs',
'devices',
'users',
'system_stats'
];
try {
// 检查每个表是否存在
for (const tableName of expectedTables) {
const { data, error } = await this.db.supabase
.from(tableName)
.select('*')
.limit(1);
if (error) {
console.log(`❌ 数据表 ${tableName} 不存在或无法访问`);
console.log(` 错误: ${error.message}`);
console.log(` 📋 请在 Supabase Dashboard 的 SQL Editor 中执行 database/supabase-init.sql`);
throw new Error(`数据表 ${tableName} 不可用: ${error.message}`);
} else {
console.log(`✅ 数据表 ${tableName} 可用`);
}
}
console.log('✅ 所有必要数据表验证通过');
} catch (error) {
throw new Error(`数据表验证失败: ${error.message}`);
}
}
async insertTestData() {
try {
// 检查是否已有测试数据
const { data: existingData } = await this.db.supabase
.from('ps_push_messages')
.select('id')
.eq('user_id', 'test_user_supabase')
.limit(1);
if (existingData && existingData.length > 0) {
console.log(' 测试数据已存在,跳过插入');
return;
}
// 插入测试推送消息
const testMessages = [
{
pushType: 'HEALTH',
userId: 'test_user_supabase',
deviceId: 'device_supabase_001',
H: 75,
O: 98,
T: 36.5,
source_ip: '127.0.0.1',
user_agent: 'Supabase Test Client'
},
{
pushType: 'SOS',
userId: 'test_user_supabase_002',
deviceId: 'device_supabase_002',
emergencyLevel: 'HIGH',
lat: 39.9042,
lng: 116.4074,
source_ip: '127.0.0.1',
user_agent: 'Supabase Test Client'
},
{
pushType: 'LOCATION',
userId: 'test_user_supabase_003',
deviceId: 'device_supabase_003',
lat: 31.2304,
lng: 121.4737,
accuracy: 10,
source_ip: '127.0.0.1',
user_agent: 'Supabase Test Client'
}
];
for (const message of testMessages) {
await this.db.insertPushMessage(message);
}
console.log('✅ 测试数据插入完成');
} catch (error) {
console.warn('⚠️ 测试数据插入失败:', error.message);
// 不阻止初始化过程
}
}
async testBasicFunctionality() {
try {
// 测试插入消息
const testMessage = {
pushType: 'TEST',
userId: 'setup_test_user',
deviceId: 'setup_test_device',
testData: 'Supabase setup test',
source_ip: '127.0.0.1',
user_agent: 'Setup Test Client'
};
const insertResult = await this.db.insertPushMessage(testMessage);
console.log('✅ 消息插入测试通过');
// 测试获取统计信息
const stats = await this.db.getMessageStats(1);
console.log('✅ 统计信息获取测试通过');
// 测试获取健康状态
const health = await this.db.getHealthStatus();
console.log('✅ 健康状态检查测试通过');
// 测试获取消息列表
const messages = await this.db.getPushMessages({ limit: 5 });
console.log('✅ 消息列表获取测试通过');
console.log('✅ 所有基本功能测试通过');
} catch (error) {
throw new Error(`基本功能测试失败: ${error.message}`);
}
}
async showSetupInstructions() {
console.log('\n📋 Supabase 数据库设置说明:');
console.log('');
console.log('1. 创建 Supabase 项目:');
console.log(' - 访问 https://supabase.com/dashboard');
console.log(' - 点击 "New project" 创建新项目');
console.log(' - 记录项目的 URL 和 API Keys');
console.log('');
console.log('2. 执行数据库初始化脚本:');
console.log(' - 在 Supabase Dashboard 中打开 "SQL Editor"');
console.log(' - 复制 database/supabase-init.sql 文件内容');
console.log(' - 粘贴到 SQL Editor 中并执行');
console.log('');
console.log('3. 配置环境变量:');
console.log(' - 复制 .env.supabase 为 .env');
console.log(' - 更新 SUPABASE_URL 为您的项目 URL');
console.log(' - 更新 SUPABASE_SERVICE_ROLE_KEY 为您的 Service Role Key');
console.log('');
console.log('4. 启用实时功能 (可选):');
console.log(' - 在 Database > Replication 中');
console.log(' - 启用 push_messages 表的实时复制');
console.log('');
}
}
// 如果直接运行此脚本
if (require.main === module) {
const setup = new SupabaseDatabaseSetup();
// 检查是否有必要的环境变量
if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_ROLE_KEY) {
console.log('⚠️ 缺少 Supabase 环境配置');
setup.showSetupInstructions();
process.exit(1);
}
setup.setup()
.then(() => {
console.log('🎉 Supabase 数据库设置完成,可以启动服务了!');
console.log('');
console.log('🚀 启动服务命令:');
console.log(' node supabase-server.js');
console.log('');
process.exit(0);
})
.catch((error) => {
console.error('❌ Supabase 数据库设置失败:', error.message);
setup.showSetupInstructions();
process.exit(1);
});
}
module.exports = SupabaseDatabaseSetup;

View File

@@ -0,0 +1,76 @@
@echo off
REM 推送消息接收服务启动脚本 (Windows)
echo 🚀 启动推送消息接收服务...
REM 检查 Node.js 是否安装
node --version >nul 2>&1
if %errorlevel% neq 0 (
echo ❌ Node.js 未安装,请先安装 Node.js
pause
exit /b 1
)
REM 检查是否在正确的目录
if not exist "package.json" (
echo ❌ 请在 push-receiver-service 目录中运行此脚本
pause
exit /b 1
)
REM 安装依赖
echo 📦 安装依赖包...
npm install
if %errorlevel% neq 0 (
echo ❌ 依赖安装失败
pause
exit /b 1
)
REM 检查环境配置
if not exist ".env" (
echo ⚠️ 未找到 .env 文件,复制模板...
if exist ".env.supabase" (
copy .env.supabase .env
echo 📋 已复制 .env.supabase 为 .env请编辑配置
echo - SUPABASE_URL: 您的 Supabase 项目 URL
echo - SUPABASE_SERVICE_ROLE_KEY: 您的 Service Role Key
echo - API_KEY: 自定义 API 密钥
set /p confirm="是否已配置完成环境变量?(y/n): "
if /i not "%confirm%"=="y" (
echo ❌ 请先配置环境变量再启动服务
pause
exit /b 1
)
) else (
echo ❌ 未找到环境配置模板文件
pause
exit /b 1
)
)
REM 初始化数据库
echo 🗄️ 初始化 Supabase 数据库...
node setup-supabase.js
if %errorlevel% neq 0 (
echo ❌ 数据库初始化失败,请检查配置
pause
exit /b 1
)
REM 运行测试
echo 🧪 运行基础测试...
node test.js
if %errorlevel% neq 0 (
echo ⚠️ 测试有错误,但继续启动服务...
)
REM 启动服务
echo 🚀 启动推送消息接收服务...
echo 📍 服务将在 http://localhost:3001 启动
echo 💡 按 Ctrl+C 停止服务
echo.
node supabase-server.js
pause

View File

@@ -0,0 +1,67 @@
#!/bin/bash
# 推送消息接收服务启动脚本
echo "🚀 启动推送消息接收服务..."
# 检查 Node.js 是否安装
if ! command -v node &> /dev/null; then
echo "❌ Node.js 未安装,请先安装 Node.js"
exit 1
fi
# 检查是否在正确的目录
if [ ! -f "package.json" ]; then
echo "❌ 请在 push-receiver-service 目录中运行此脚本"
exit 1
fi
# 安装依赖
echo "📦 安装依赖包..."
npm install
# 检查环境配置
if [ ! -f ".env" ]; then
echo "⚠️ 未找到 .env 文件,复制模板..."
if [ -f ".env.supabase" ]; then
cp .env.supabase .env
echo "📋 已复制 .env.supabase 为 .env请编辑配置"
echo " - SUPABASE_URL: 您的 Supabase 项目 URL"
echo " - SUPABASE_SERVICE_ROLE_KEY: 您的 Service Role Key"
echo " - API_KEY: 自定义 API 密钥"
read -p "是否已配置完成环境变量?(y/n): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "❌ 请先配置环境变量再启动服务"
exit 1
fi
else
echo "❌ 未找到环境配置模板文件"
exit 1
fi
fi
# 初始化数据库
echo "🗄️ 初始化 Supabase 数据库..."
node setup-supabase.js
if [ $? -ne 0 ]; then
echo "❌ 数据库初始化失败,请检查配置"
exit 1
fi
# 运行测试
echo "🧪 运行基础测试..."
node test.js
if [ $? -ne 0 ]; then
echo "⚠️ 测试有错误,但继续启动服务..."
fi
# 启动服务
echo "🚀 启动推送消息接收服务..."
echo "📍 服务将在 http://localhost:3001 启动"
echo "💡 按 Ctrl+C 停止服务"
echo ""
node supabase-server.js

View File

@@ -0,0 +1,661 @@
/**
* 基于 Supabase 的推送消息接收服务主服务器
* 专门用于接收和存储各种推送消息到 Supabase 数据库
*/
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const rateLimit = require('express-rate-limit');
const { body, validationResult } = require('express-validator');
const winston = require('winston');
const path = require('path');
const fs = require('fs');
require('dotenv').config();
const SupabaseDatabaseManager = require('./lib/supabase-database');
class SupabasePushReceiverService {
constructor() {
this.app = express();
this.port = process.env.PORT || 3001;
this.host = process.env.HOST || '0.0.0.0';
this.db = new SupabaseDatabaseManager();
this.setupLogger();
this.setupMiddleware();
this.setupRoutes();
this.setupErrorHandling();
// 统计信息
this.stats = {
startTime: new Date(),
requestCount: 0,
messageCount: 0,
errorCount: 0,
lastMessageTime: null
};
}
/**
* 设置日志系统
*/
setupLogger() {
// 确保日志目录存在
const logDir = process.env.LOG_DIR || './logs';
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
this.logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.File({
filename: path.join(logDir, 'supabase-push-service-error.log'),
level: 'error',
maxsize: parseInt(process.env.LOG_FILE_MAX_SIZE) || 10485760,
maxFiles: parseInt(process.env.LOG_FILE_MAX_FILES) || 5
}),
new winston.transports.File({
filename: path.join(logDir, 'supabase-push-service.log'),
maxsize: parseInt(process.env.LOG_FILE_MAX_SIZE) || 10485760,
maxFiles: parseInt(process.env.LOG_FILE_MAX_FILES) || 5
}),
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
})
]
});
}
/**
* 设置中间件
*/
setupMiddleware() {
// 安全中间件
this.app.use(helmet({
contentSecurityPolicy: false
}));
// 压缩中间件
this.app.use(compression());
// CORS 配置
const corsOptions = {
origin: (origin, callback) => {
const allowedOrigins = process.env.ALLOWED_ORIGINS;
if (!allowedOrigins || allowedOrigins === '*') {
callback(null, true);
} else {
const origins = allowedOrigins.split(',');
if (origins.includes(origin) || !origin) {
callback(null, true);
} else {
callback(new Error('不允许的CORS来源'));
}
}
},
credentials: true,
optionsSuccessStatus: 200
};
this.app.use(cors(corsOptions));
// 请求体解析
this.app.use(express.json({
limit: process.env.MAX_MESSAGE_SIZE || '1mb'
}));
this.app.use(express.urlencoded({ extended: true }));
// API密钥验证中间件
this.app.use('/api', (req, res, next) => {
if (req.path === '/health') {
return next(); // 健康检查不需要验证
}
// const apiKey = req.headers['x-api-key'] || req.headers['authorization'];
// const expectedApiKey = process.env.API_KEY;
// if (expectedApiKey && (!apiKey || apiKey !== expectedApiKey)) {
// this.stats.errorCount++;
// return res.status(401).json({
// success: false,
// error: 'UNAUTHORIZED',
// message: 'API密钥无效或缺失'
// });
// }
next();
});
// 速率限制
const limiter = rateLimit({
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 60000,
max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 1000,
message: {
success: false,
error: 'RATE_LIMIT_EXCEEDED',
message: process.env.RATE_LIMIT_MESSAGE || '请求过于频繁,请稍后再试'
},
standardHeaders: true,
legacyHeaders: false
});
this.app.use('/api', limiter);
// 请求日志中间件
this.app.use((req, res, next) => {
const startTime = Date.now();
this.stats.requestCount++;
res.on('finish', () => {
const duration = Date.now() - startTime;
this.logger.info('HTTP Request', {
method: req.method,
url: req.url,
status: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userAgent: req.get('User-Agent'),
contentLength: req.get('Content-Length') || 0
});
});
next();
});
}
/**
* 设置路由
*/
setupRoutes() {
// 根路径 - 服务信息
this.app.get('/', (req, res) => {
res.json({
name: 'Supabase Push Receiver Service',
version: '1.0.0',
description: '基于 Supabase 的推送消息接收服务',
status: 'running',
database: 'Supabase PostgreSQL',
uptime: Math.floor((Date.now() - this.stats.startTime.getTime()) / 1000),
stats: {
...this.stats,
uptime: Math.floor((Date.now() - this.stats.startTime.getTime()) / 1000)
},
endpoints: {
pushMessage: 'POST /api/push/message',
pushBatch: 'POST /api/push/batch',
health: 'GET /api/health',
stats: 'GET /api/stats',
messages: 'GET /api/messages'
},
supabase: {
url: process.env.SUPABASE_URL,
hasServiceKey: !!process.env.SUPABASE_SERVICE_ROLE_KEY
},
timestamp: new Date().toISOString()
});
});
// 健康检查
this.app.get('/api/health', async (req, res) => {
try {
const dbHealth = await this.db.getHealthStatus();
res.json({
status: 'OK',
service: 'Supabase Push Receiver Service',
version: '1.0.0',
database: 'Supabase PostgreSQL',
timestamp: new Date().toISOString(),
uptime: Math.floor((Date.now() - this.stats.startTime.getTime()) / 1000),
supabase: dbHealth.database,
stats: this.stats
});
} catch (error) {
this.logger.error('健康检查失败', { error: error.message, stack: error.stack });
res.status(500).json({
status: 'ERROR',
service: 'Supabase Push Receiver Service',
database: 'Supabase PostgreSQL',
error: error.message,
timestamp: new Date().toISOString()
});
}
});
// 接收单个推送消息
this.app.post('/api/push/message',
[
body('pushType').notEmpty().withMessage('推送类型不能为空'),
body('MID').optional().isString().withMessage('设备ID必须是字符串')
],
async (req, res) => {
console.log('📥 接收到推送消息:', req.body);
try {
// 验证请求数据
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: 'VALIDATION_ERROR',
message: '请求数据验证失败',
details: errors.array()
});
}
// 准备消息数据
const messageData = {
message_id: req.body.aaa || null,
user_id: req.body.user_id || null,
device_id: req.body.device_id || null,
push_type: req.body.pushType,
source_ip: req.ip,
user_agent: req.get('User-Agent'),
raw_data: req.body,
parsed_data: req.body,
received_at: new Date().toISOString()
};
// 保存到 Supabase
const result = await this.db.insertPushMessage(messageData);
this.stats.messageCount++;
this.stats.lastMessageTime = new Date();
this.logger.info('推送消息接收成功', {
messageId: result.messageId,
pushType: req.body.pushType,
userId: req.body.userId,
deviceId: req.body.deviceId,
isDuplicate: result.isDuplicate,
processingTime: result.processingTime
});
res.json({
success: true,
message: '推送消息接收成功',
data: {
messageId: result.messageId,
isDuplicate: result.isDuplicate,
processingTime: result.processingTime,
timestamp: new Date().toISOString(),
database: 'Supabase'
}
});
} catch (error) {
this.stats.errorCount++;
this.logger.error('推送消息接收失败', {
error: error.message,
stack: error.stack,
requestBody: req.body
});
res.status(500).json({
success: false,
error: 'MESSAGE_PROCESSING_ERROR',
message: '推送消息处理失败',
details: error.message,
database: 'Supabase'
});
}
}
);
// 批量接收推送消息
this.app.post('/api/push/batch',
[
body('messages').isArray().withMessage('messages必须是数组'),
body('messages').custom((messages) => {
const maxBatchSize = parseInt(process.env.BATCH_SIZE_LIMIT) || 1000;
if (messages.length > maxBatchSize) {
throw new Error(`批量消息数量不能超过${maxBatchSize}`);
}
return true;
})
],
async (req, res) => {
try {
// 验证请求数据
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: 'VALIDATION_ERROR',
message: '请求数据验证失败',
details: errors.array()
});
}
const { messages } = req.body;
// 准备批量消息数据
const messagesData = messages.map(msg => ({
...msg,
source_ip: req.ip,
user_agent: req.get('User-Agent'),
raw_data: msg,
parsed_data: msg,
received_at: new Date().toISOString()
}));
// 批量保存到 Supabase
const result = await this.db.insertPushMessagesBatch(messagesData);
this.stats.messageCount += result.successCount;
this.stats.errorCount += result.failureCount;
this.stats.lastMessageTime = new Date();
this.logger.info('批量推送消息接收完成', {
totalCount: result.totalCount,
successCount: result.successCount,
failureCount: result.failureCount,
processingTime: result.processingTime
});
res.json({
success: true,
message: `批量推送消息处理完成,成功${result.successCount}条,失败${result.failureCount}`,
data: {
totalCount: result.totalCount,
successCount: result.successCount,
failureCount: result.failureCount,
processingTime: result.processingTime,
timestamp: new Date().toISOString(),
database: 'Supabase'
}
});
} catch (error) {
this.stats.errorCount++;
this.logger.error('批量推送消息接收失败', {
error: error.message,
stack: error.stack,
requestBody: req.body
});
res.status(500).json({
success: false,
error: 'BATCH_PROCESSING_ERROR',
message: '批量推送消息处理失败',
details: error.message,
database: 'Supabase'
});
}
}
);
// 获取统计信息
this.app.get('/api/stats', async (req, res) => {
try {
const hoursBack = parseInt(req.query.hours) || 24;
const messageStats = await this.db.getMessageStats(hoursBack);
res.json({
success: true,
data: {
service: {
uptime: Math.floor((Date.now() - this.stats.startTime.getTime()) / 1000),
database: 'Supabase PostgreSQL',
...this.stats
},
messages: messageStats,
period: `最近${hoursBack}小时`
},
timestamp: new Date().toISOString()
});
} catch (error) {
this.logger.error('获取统计信息失败', { error: error.message, stack: error.stack });
res.status(500).json({
success: false,
error: 'STATS_ERROR',
message: '获取统计信息失败',
details: error.message,
database: 'Supabase'
});
}
});
// 获取消息列表
this.app.get('/api/messages', async (req, res) => {
try {
const options = {
limit: parseInt(req.query.limit) || 50,
offset: parseInt(req.query.offset) || 0,
pushType: req.query.pushType || null,
userId: req.query.userId || null,
startDate: req.query.startDate || null,
endDate: req.query.endDate || null,
status: req.query.status || null
};
const messages = await this.db.getPushMessages(options);
res.json({
success: true,
data: {
messages,
count: messages.length,
options,
database: 'Supabase'
},
timestamp: new Date().toISOString()
});
} catch (error) {
this.logger.error('获取消息列表失败', { error: error.message, stack: error.stack });
res.status(500).json({
success: false,
error: 'MESSAGES_ERROR',
message: '获取消息列表失败',
details: error.message,
database: 'Supabase'
});
}
});
// 清理旧数据
this.app.post('/api/cleanup', async (req, res) => {
try {
const daysToKeep = parseInt(req.body.days) || 30;
const deletedCount = await this.db.cleanupOldMessages(daysToKeep);
res.json({
success: true,
message: `清理完成,删除了${deletedCount}条旧消息`,
data: {
deletedCount,
daysToKeep,
database: 'Supabase'
},
timestamp: new Date().toISOString()
});
} catch (error) {
this.logger.error('清理旧数据失败', { error: error.message, stack: error.stack });
res.status(500).json({
success: false,
error: 'CLEANUP_ERROR',
message: '清理旧数据失败',
details: error.message,
database: 'Supabase'
});
}
});
}
/**
* 设置错误处理
*/
setupErrorHandling() {
// 404 处理
this.app.use((req, res) => {
res.status(404).json({
success: false,
error: 'NOT_FOUND',
message: '请求的端点不存在',
path: req.path,
method: req.method,
availableEndpoints: [
'GET /',
'GET /api/health',
'POST /api/push/message',
'POST /api/push/batch',
'GET /api/stats',
'GET /api/messages',
'POST /api/cleanup'
],
database: 'Supabase'
});
});
// 全局错误处理
this.app.use((error, req, res, next) => {
this.stats.errorCount++;
this.logger.error('未处理的错误', {
error: error.message,
stack: error.stack,
url: req.url,
method: req.method
});
res.status(500).json({
success: false,
error: 'INTERNAL_SERVER_ERROR',
message: '服务器内部错误',
details: process.env.NODE_ENV === 'development' ? error.message : undefined,
database: 'Supabase'
});
});
}
/**
* 启动服务器
*/
async start() {
try {
// 测试 Supabase 连接
console.log('🔧 测试 Supabase 数据库连接...');
await this.db.testConnection();
console.log('✅ Supabase 数据库连接成功');
// 启动HTTP服务器
this.server = this.app.listen(this.port, this.host, () => {
console.log('🚀 Supabase 推送消息接收服务启动成功!');
console.log(`📍 服务器地址: http://${this.host}:${this.port}`);
console.log('');
console.log('📋 API 端点:');
console.log(` - 服务信息: http://${this.host}:${this.port}/`);
console.log(` - 健康检查: http://${this.host}:${this.port}/api/health`);
console.log(` - 推送消息: http://${this.host}:${this.port}/api/push/message`);
console.log(` - 批量推送: http://${this.host}:${this.port}/api/push/batch`);
console.log(` - 统计信息: http://${this.host}:${this.port}/api/stats`);
console.log(` - 消息列表: http://${this.host}:${this.port}/api/messages`);
console.log(` - 清理数据: http://${this.host}:${this.port}/api/cleanup`);
console.log('');
console.log('🗄️ 数据库: ✅ Supabase PostgreSQL');
console.log('🔗 Supabase URL:', process.env.SUPABASE_URL);
console.log('🔒 API密钥保护: ' + (process.env.API_KEY ? '✅ 启用' : '❌ 未设置'));
console.log('📊 请求速率限制: ✅ 启用');
console.log('');
console.log('✅ 服务已准备就绪,等待接收推送消息...');
console.log('💡 按 Ctrl+C 停止服务');
this.logger.info('Supabase 推送消息接收服务启动完成', {
port: this.port,
host: this.host,
nodeEnv: process.env.NODE_ENV,
apiKeyEnabled: !!process.env.API_KEY,
supabaseUrl: process.env.SUPABASE_URL
});
});
// 监听 server 的 error 事件
this.server.on('error', (err) => {
this.logger.error('HTTP服务器启动失败', { error: err.message, stack: err.stack });
console.error('❌ HTTP服务器启动失败:', err.message);
process.exit(1);
});
// 设置优雅关闭
this.setupGracefulShutdown();
} catch (error) {
this.logger.error('服务启动失败', { error: error.message, stack: error.stack });
console.error('❌ 服务启动失败:', error.message);
process.exit(1);
}
}
/**
* 设置优雅关闭
*/
setupGracefulShutdown() {
const shutdown = async (signal) => {
console.log(`\n📴 收到${signal}信号,正在优雅关闭服务...`);
this.logger.info(`收到${signal}信号,开始关闭服务`);
// 停止接收新请求
if (this.server) {
this.server.close(async () => {
console.log('🌐 HTTP服务器已关闭');
this.logger.info('HTTP服务器已关闭');
// 关闭数据库连接
try {
await this.db.close();
console.log('🗄️ Supabase 连接已关闭');
this.logger.info('Supabase 连接已关闭');
} catch (error) {
console.error('❌ 关闭 Supabase 连接失败:', error.message);
this.logger.error('关闭 Supabase 连接失败', { error: error.message });
}
console.log('✅ 服务已安全关闭');
this.logger.info('服务已安全关闭');
process.exit(0);
});
// 设置强制关闭超时
setTimeout(() => {
console.error('❌ 强制关闭服务(超时)');
this.logger.error('强制关闭服务(超时)');
process.exit(1);
}, 30000);
}
};
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
// 捕获未处理的异常
process.on('uncaughtException', (error) => {
console.error('💥 未捕获的异常:', error);
this.logger.error('未捕获的异常', { error: error.message, stack: error.stack });
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('💥 未处理的 Promise 拒绝:', reason);
this.logger.error('未处理的 Promise 拒绝', { reason: reason?.message || reason });
process.exit(1);
});
}
}
// 启动服务
if (require.main === module) {
const service = new SupabasePushReceiverService();
service.start();
}
module.exports = SupabasePushReceiverService;

View File

@@ -0,0 +1,154 @@
/**
* 推送消息接收服务测试脚本
*/
const SupabaseDatabaseManager = require('./lib/supabase-database');
require('dotenv').config();
async function testService() {
console.log('🧪 开始测试推送消息接收服务...');
const db = new SupabaseDatabaseManager();
try {
// 测试 1: 数据库连接
console.log('\n📡 测试 1: 数据库连接');
await db.testConnection();
console.log('✅ 数据库连接测试通过');
// 测试 2: 插入健康数据消息
console.log('\n💊 测试 2: 健康数据消息');
const healthMessage = {
pushType: 'HEALTH',
userId: 'test_user_001',
deviceId: 'test_device_001',
H: 75,
O: 98,
T: 36.5,
source_ip: '127.0.0.1',
user_agent: 'Test Client'
};
const healthResult = await db.insertPushMessage(healthMessage);
console.log('✅ 健康数据消息插入成功:', healthResult.messageId);
// 测试 3: 插入 SOS 消息
console.log('\n🆘 测试 3: SOS 紧急消息');
const sosMessage = {
pushType: 'SOS',
userId: 'test_user_002',
deviceId: 'test_device_002',
emergencyLevel: 'HIGH',
lat: 39.9042,
lng: 116.4074,
source_ip: '127.0.0.1',
user_agent: 'Emergency Device'
};
const sosResult = await db.insertPushMessage(sosMessage);
console.log('✅ SOS 消息插入成功:', sosResult.messageId);
// 测试 4: 批量插入消息
console.log('\n📦 测试 4: 批量消息插入');
const batchMessages = [
{
pushType: 'LOCATION',
userId: 'test_user_003',
deviceId: 'test_device_003',
lat: 31.2304,
lng: 121.4737,
accuracy: 10,
source_ip: '127.0.0.1',
user_agent: 'GPS Device'
},
{
pushType: 'DEVICE_STATUS',
userId: 'test_user_004',
deviceId: 'test_device_004',
status: 'online',
batteryLevel: 85,
source_ip: '127.0.0.1',
user_agent: 'IoT Device'
},
{
pushType: 'ACTIVITY',
userId: 'test_user_005',
deviceId: 'test_device_005',
activityType: 'running',
duration: 1800,
calories: 250,
source_ip: '127.0.0.1',
user_agent: 'Fitness Tracker'
}
];
const batchResult = await db.insertPushMessagesBatch(batchMessages);
console.log('✅ 批量消息插入成功:', `${batchResult.successCount}/${batchResult.totalCount}`);
// 测试 5: 重复消息检测
console.log('\n🔄 测试 5: 重复消息检测');
const duplicateResult = await db.insertPushMessage(healthMessage);
console.log('✅ 重复消息检测:', duplicateResult.isDuplicate ? '检测到重复' : '未检测到重复');
// 测试 6: 获取统计信息
console.log('\n📊 测试 6: 统计信息获取');
const stats = await db.getMessageStats(24);
console.log('✅ 统计信息获取成功:', stats.length, '种消息类型');
stats.forEach(stat => {
console.log(` - ${stat.push_type}: ${stat.total_count} 条消息`);
});
// 测试 7: 获取健康状态
console.log('\n🏥 测试 7: 系统健康状态');
const health = await db.getHealthStatus();
console.log('✅ 系统健康状态:', health.database.connected ? '正常' : '异常');
console.log(` - 总消息数: ${health.database.stats.total_messages}`);
console.log(` - 最近1小时: ${health.database.stats.messages_last_hour}`);
// 测试 8: 获取消息列表
console.log('\n📋 测试 8: 消息列表获取');
const messages = await db.getPushMessages({ limit: 5 });
console.log('✅ 消息列表获取成功:', messages.length, '条消息');
// 测试 9: HTTP API 测试
console.log('\n🌐 测试 9: HTTP API 测试');
await testHttpAPI();
console.log('\n🎉 所有测试完成!');
} catch (error) {
console.error('❌ 测试失败:', error.message);
throw error;
} finally {
await db.close();
}
}
async function testHttpAPI() {
try {
// 测试健康检查端点
const healthResponse = await fetch('http://localhost:3001/api/health');
if (healthResponse.ok) {
console.log('✅ 健康检查 API 可用');
} else {
console.log('❌ 健康检查 API 不可用');
}
} catch (error) {
console.log(' HTTP API 测试跳过 (服务未运行)');
}
}
// 运行测试
if (require.main === module) {
testService()
.then(() => {
console.log('\n✅ 测试完成,服务运行正常!');
process.exit(0);
})
.catch((error) => {
console.error('\n❌ 测试失败:', error.message);
process.exit(1);
});
}
module.exports = { testService };