Files
vf_react/apns_models_and_api.py
2026-01-13 15:58:04 +08:00

263 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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)