263 lines
9.0 KiB
Python
263 lines
9.0 KiB
Python
"""
|
||
APNs 推送相关的数据库模型和 API 端点
|
||
将以下代码添加到 app.py 中
|
||
|
||
=== 步骤 1: 添加数据库模型(放在其他模型定义附近)===
|
||
"""
|
||
|
||
# ==================== 数据库模型 ====================
|
||
|
||
class UserDeviceToken(db.Model):
|
||
"""用户设备推送 Token"""
|
||
__tablename__ = 'user_device_tokens'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) # 可选,未登录也能注册
|
||
device_token = db.Column(db.String(255), unique=True, nullable=False)
|
||
platform = db.Column(db.String(20), default='ios') # ios / android
|
||
app_version = db.Column(db.String(20), nullable=True)
|
||
is_active = db.Column(db.Boolean, default=True)
|
||
|
||
# 推送订阅设置
|
||
subscribe_all = db.Column(db.Boolean, default=True) # 订阅所有事件
|
||
subscribe_important = db.Column(db.Boolean, default=True) # 订阅重要事件 (S/A级)
|
||
subscribe_types = db.Column(db.Text, nullable=True) # 订阅的事件类型,JSON 数组
|
||
|
||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||
|
||
user = db.relationship('User', backref='device_tokens')
|
||
|
||
|
||
"""
|
||
=== 步骤 2: 添加 API 端点(放在其他 API 定义附近)===
|
||
"""
|
||
|
||
# ==================== Device Token API ====================
|
||
|
||
@app.route('/api/users/device-token', methods=['POST'])
|
||
def register_device_token():
|
||
"""
|
||
注册设备推送 Token
|
||
App 启动时调用,保存设备 Token 用于推送
|
||
"""
|
||
try:
|
||
data = request.get_json()
|
||
device_token = data.get('device_token')
|
||
platform = data.get('platform', 'ios')
|
||
app_version = data.get('app_version', '1.0.0')
|
||
|
||
if not device_token:
|
||
return jsonify({'success': False, 'message': '缺少 device_token'}), 400
|
||
|
||
# 查找是否已存在
|
||
existing = UserDeviceToken.query.filter_by(device_token=device_token).first()
|
||
|
||
if existing:
|
||
# 更新现有记录
|
||
existing.platform = platform
|
||
existing.app_version = app_version
|
||
existing.is_active = True
|
||
existing.updated_at = datetime.now()
|
||
|
||
# 如果用户已登录,关联用户
|
||
if current_user.is_authenticated:
|
||
existing.user_id = current_user.id
|
||
else:
|
||
# 创建新记录
|
||
new_token = UserDeviceToken(
|
||
device_token=device_token,
|
||
platform=platform,
|
||
app_version=app_version,
|
||
user_id=current_user.id if current_user.is_authenticated else None,
|
||
is_active=True
|
||
)
|
||
db.session.add(new_token)
|
||
|
||
db.session.commit()
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Device token 注册成功'
|
||
})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
print(f"[API] 注册 device token 失败: {e}")
|
||
return jsonify({'success': False, 'message': str(e)}), 500
|
||
|
||
|
||
@app.route('/api/users/device-token', methods=['DELETE'])
|
||
def unregister_device_token():
|
||
"""
|
||
取消注册设备 Token(用户登出时调用)
|
||
"""
|
||
try:
|
||
data = request.get_json()
|
||
device_token = data.get('device_token')
|
||
|
||
if not device_token:
|
||
return jsonify({'success': False, 'message': '缺少 device_token'}), 400
|
||
|
||
token_record = UserDeviceToken.query.filter_by(device_token=device_token).first()
|
||
|
||
if token_record:
|
||
token_record.is_active = False
|
||
token_record.updated_at = datetime.now()
|
||
db.session.commit()
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Device token 已取消注册'
|
||
})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
print(f"[API] 取消注册 device token 失败: {e}")
|
||
return jsonify({'success': False, 'message': str(e)}), 500
|
||
|
||
|
||
@app.route('/api/users/push-subscription', methods=['POST'])
|
||
def update_push_subscription():
|
||
"""
|
||
更新推送订阅设置
|
||
"""
|
||
try:
|
||
data = request.get_json()
|
||
device_token = data.get('device_token')
|
||
|
||
if not device_token:
|
||
return jsonify({'success': False, 'message': '缺少 device_token'}), 400
|
||
|
||
token_record = UserDeviceToken.query.filter_by(device_token=device_token).first()
|
||
|
||
if not token_record:
|
||
return jsonify({'success': False, 'message': 'Device token 不存在'}), 404
|
||
|
||
# 更新订阅设置
|
||
if 'subscribe_all' in data:
|
||
token_record.subscribe_all = data['subscribe_all']
|
||
if 'subscribe_important' in data:
|
||
token_record.subscribe_important = data['subscribe_important']
|
||
if 'subscribe_types' in data:
|
||
token_record.subscribe_types = json.dumps(data['subscribe_types'])
|
||
|
||
token_record.updated_at = datetime.now()
|
||
db.session.commit()
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': '订阅设置已更新'
|
||
})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
print(f"[API] 更新推送订阅失败: {e}")
|
||
return jsonify({'success': False, 'message': str(e)}), 500
|
||
|
||
|
||
"""
|
||
=== 步骤 3: 修改 broadcast_new_event 函数,添加 APNs 推送 ===
|
||
"""
|
||
|
||
# 在文件顶部导入
|
||
from apns_push import send_event_notification
|
||
|
||
# 修改 broadcast_new_event 函数
|
||
def broadcast_new_event_with_apns(event):
|
||
"""
|
||
广播新事件到所有订阅的客户端(WebSocket + APNs)
|
||
"""
|
||
try:
|
||
# === 原有的 WebSocket 推送代码 ===
|
||
print(f'\n[WebSocket DEBUG] ========== 广播新事件 ==========')
|
||
print(f'[WebSocket DEBUG] 事件ID: {event.id}')
|
||
print(f'[WebSocket DEBUG] 事件标题: {event.title}')
|
||
print(f'[WebSocket DEBUG] 重要性: {event.importance}')
|
||
|
||
event_data = {
|
||
'id': event.id,
|
||
'title': event.title,
|
||
'importance': event.importance,
|
||
'event_type': event.event_type,
|
||
'created_at': event.created_at.isoformat() if event.created_at else None,
|
||
'source': event.source,
|
||
'related_avg_chg': event.related_avg_chg,
|
||
'related_max_chg': event.related_max_chg,
|
||
'hot_score': event.hot_score,
|
||
'keywords': event.keywords_list if hasattr(event, 'keywords_list') else event.keywords,
|
||
}
|
||
|
||
# 发送到 WebSocket 房间
|
||
socketio.emit('new_event', event_data, room='events_all', namespace='/')
|
||
|
||
if event.event_type:
|
||
room_name = f"events_{event.event_type}"
|
||
socketio.emit('new_event', event_data, room=room_name, namespace='/')
|
||
|
||
# === 新增:APNs 推送 ===
|
||
# 只推送重要事件 (S/A 级)
|
||
if event.importance in ['S', 'A']:
|
||
try:
|
||
# 获取所有活跃的设备 token
|
||
# 可以根据订阅设置过滤
|
||
device_tokens_query = db.session.query(UserDeviceToken.device_token).filter(
|
||
UserDeviceToken.is_active == True,
|
||
db.or_(
|
||
UserDeviceToken.subscribe_all == True,
|
||
UserDeviceToken.subscribe_important == True
|
||
)
|
||
).all()
|
||
|
||
tokens = [t[0] for t in device_tokens_query]
|
||
|
||
if tokens:
|
||
print(f'[APNs] 准备推送到 {len(tokens)} 个设备')
|
||
result = send_event_notification(event, tokens)
|
||
print(f'[APNs] 推送结果: 成功 {result["success"]}, 失败 {result["failed"]}')
|
||
else:
|
||
print('[APNs] 没有活跃的设备 token')
|
||
|
||
except Exception as apns_error:
|
||
print(f'[APNs ERROR] 推送失败: {apns_error}')
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
# 清除事件列表缓存
|
||
clear_events_cache()
|
||
|
||
print(f'[WebSocket DEBUG] ========== 广播完成 ==========\n')
|
||
|
||
except Exception as e:
|
||
print(f'[WebSocket ERROR] 推送新事件失败: {e}')
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
|
||
"""
|
||
=== 步骤 4: 创建数据库表(在 MySQL 中执行)===
|
||
"""
|
||
|
||
SQL_CREATE_TABLE = """
|
||
CREATE TABLE IF NOT EXISTS user_device_tokens (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT NULL,
|
||
device_token VARCHAR(255) NOT NULL UNIQUE,
|
||
platform VARCHAR(20) DEFAULT 'ios',
|
||
app_version VARCHAR(20) NULL,
|
||
is_active BOOLEAN DEFAULT TRUE,
|
||
subscribe_all BOOLEAN DEFAULT TRUE,
|
||
subscribe_important BOOLEAN DEFAULT TRUE,
|
||
subscribe_types TEXT NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||
INDEX idx_device_token (device_token),
|
||
INDEX idx_user_id (user_id),
|
||
INDEX idx_is_active (is_active)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
"""
|
||
|
||
print("请在 MySQL 中执行以下 SQL 创建表:")
|
||
print(SQL_CREATE_TABLE)
|