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