This commit is contained in:
2026-01-13 15:58:04 +08:00
parent 45d5debead
commit 257f1cae69
11 changed files with 1079 additions and 23 deletions

175
app.py
View File

@@ -1382,6 +1382,28 @@ class UserSubscription(db.Model):
}
class UserDeviceToken(db.Model):
"""用户设备推送 Token - 用于 APNs/FCM 推送通知"""
__tablename__ = 'user_device_tokens'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.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=beijing_now)
updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now)
user = db.relationship('User', backref='device_tokens')
class SubscriptionPlan(db.Model):
"""订阅套餐表"""
__tablename__ = 'subscription_plans'
@@ -4528,6 +4550,129 @@ def unbind_email():
return jsonify({'error': '解绑失败,请重试'}), 500
# ==================== Device Token API (APNs 推送) ====================
@app.route('/api/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 = beijing_now()
# 如果用户已登录,关联用户
if session.get('logged_in'):
existing.user_id = session.get('user_id')
else:
# 创建新记录
new_token = UserDeviceToken(
device_token=device_token,
platform=platform,
app_version=app_version,
user_id=session.get('user_id') if session.get('logged_in') 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/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 = beijing_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/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 = beijing_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
@app.route('/api/auth/register/email', methods=['POST'])
def register_with_email():
"""邮箱注册 - 使用Session"""
@@ -13530,6 +13675,36 @@ def broadcast_new_event(event):
# 清除事件列表缓存,确保用户刷新页面时获取最新数据
clear_events_cache()
# === APNs 推送(只推送重要事件 S/A 级)===
if event.importance in ['S', 'A']:
try:
from apns_push import send_event_notification
# 获取所有活跃的设备 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 ImportError as e:
print(f'[APNs WARN] apns_push 模块未安装或配置: {e}')
except Exception as apns_error:
print(f'[APNs ERROR] 推送失败: {apns_error}')
import traceback
traceback.print_exc()
print(f'[WebSocket DEBUG] ========== 广播完成 ==========\n')
except Exception as e: