返回列表 发布新帖

[玩法教程] 【青龙面板】地震预警+企微推送脚本

401 0
发表于 2026-2-11 14:47:25 | 查看全部 阅读模式 IP:–贵州–遵义

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # @Time    : 2025
  4. # @Script  : 地震预警监控脚本(青龙面板版-无频率限制)
  5. # @Description: 自动获取最优IP接口,支持地区选择推送,仅做频率提醒

  6. import requests
  7. import json
  8. import time
  9. import os
  10. import sys
  11. import hashlib
  12. import urllib3
  13. from datetime import datetime
  14. from typing import Dict, Any, Optional, Union, List

  15. # 抑制SSL警告(IP接口需要关闭SSL验证)
  16. urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

  17. """
  18. 接口盒子:提供各种免费API接口,集群服务器保障服务稳定。
  19. 官网:https://www.apihz.cn/

  20. 【青龙面板环境变量配置】
  21. export QUAKE_APIHZ_ID="88888888"              # 必填:接口盒子用户ID
  22. export QUAKE_APIHZ_KEY="88888888"             # 必填:接口盒子通讯秘钥
  23. export QUAKE_WECOM_KEY="xxxxxxxx-xxxx-xxxx"   # 必填:企业微信机器人KEY
  24. export QUAKE_MIN_MAG="3.0"                    # 可选:最小推送震级(默认3.0)
  25. export QUAKE_MENTIONED_MOBILE=""              # 可选:需要@的手机号(多个用逗号分隔)
  26. export QUAKE_ENABLED="true"                   # 可选:是否启用推送(true/false,默认true)
  27. export QUAKE_REGIONS=""                       # 可选:推送地区(多个用逗号分隔,如"四川,云南",留空则全国)

  28. 【重要提醒】
  29. 接口盒子免费用户限制:每分钟调用不能超过10次!
  30. 建议青龙定时规则设置为:*/6 * * * * (每6分钟一次)
  31. 如果设置过于频繁(如每分钟1次),可能导致API被封禁!
  32. """

  33. # -----------------从环境变量读取配置-----------------
  34. def load_config_from_env():
  35.     """从青龙环境变量加载配置"""
  36.     config = {
  37.         'apihz': {
  38.             'id': os.getenv('QUAKE_APIHZ_ID', ''),
  39.             'key': os.getenv('QUAKE_APIHZ_KEY', ''),
  40.             'getapi_url': 'https://api.apihz.cn/getapi.php',
  41.             'fallback_url': 'https://cn.apihz.cn/api/tianqi/dizhen.php',
  42.             'api_path': '/api/tianqi/dizhen.php',
  43.         },
  44.         'wecom': {
  45.             'enabled': os.getenv('QUAKE_ENABLED', 'true').lower() == 'true',
  46.             'webhook_url': '',
  47.             'mentioned_mobile': [],
  48.             'filter': {
  49.                 'min_magnitude': 3.0,
  50.                 'regions': [],
  51.             }
  52.         },
  53.         'storage': {
  54.             'record_file': 'quake_apihz_records.json',
  55.             'max_records': 200,
  56.         }
  57.     }
  58.    
  59.     # 处理企业微信KEY
  60.     wecom_key = os.getenv('QUAKE_WECOM_KEY', '')
  61.     if wecom_key:
  62.         config['wecom']['webhook_url'] = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={wecom_key}"
  63.    
  64.     # 处理最小震级
  65.     try:
  66.         min_mag = float(os.getenv('QUAKE_MIN_MAG', '3.0'))
  67.         config['wecom']['filter']['min_magnitude'] = min_mag
  68.     except:
  69.         config['wecom']['filter']['min_magnitude'] = 3.0
  70.    
  71.     # 处理地区过滤
  72.     regions = os.getenv('QUAKE_REGIONS', '')
  73.     if regions:
  74.         import re
  75.         region_list = [r.strip() for r in re.split('[,,、]', regions) if r.strip()]
  76.         config['wecom']['filter']['regions'] = region_list
  77.    
  78.     # 处理@手机号
  79.     mentioned = os.getenv('QUAKE_MENTIONED_MOBILE', '')
  80.     if mentioned:
  81.         config['wecom']['mentioned_mobile'] = [phone.strip() for phone in mentioned.split(',') if phone.strip()]
  82.    
  83.     return config

  84. # -----------------原封不动的getpost函数-----------------
  85. def getpost(config: Dict[str, Any]) -> Union[str, Dict[str, Any]]:
  86.     """
  87.     通用GET/POST请求函数(保持原代码不变)
  88.     """
  89.     if not config.get('url'):
  90.         return json.dumps({'code': 400, 'msg': '请求地址不能为空!'}, ensure_ascii=False)
  91.    
  92.     url = config['url']
  93.     method = 'POST' if config.get('type', 0) == 1 else 'GET'
  94.     params = {}
  95.     data = {}
  96.    
  97.     request_data = config.get('data', {})
  98.     if request_data and isinstance(request_data, dict):
  99.         if method == 'GET':
  100.             params = request_data
  101.         else:
  102.             data = request_data
  103.    
  104.     headers = config.get('headers', {}).copy()
  105.     if config.get('user_agent'):
  106.         headers['User-Agent'] = config['user_agent']
  107.    
  108.     if method == 'POST' and 'Content-Type' not in headers and data:
  109.         headers['Content-Type'] = 'application/x-www-form-urlencoded'
  110.    
  111.     cookies = config.get('cookies')
  112.     if isinstance(cookies, str):
  113.         cookies_dict = {}
  114.         for cookie in cookies.split(';'):
  115.             if '=' in cookie:
  116.                 key, value = cookie.strip().split('=', 1)
  117.                 cookies_dict[key] = value
  118.         cookies = cookies_dict
  119.    
  120.     request_options = config.get('request_options', {})
  121.     proxies = request_options.get('proxies')
  122.     proxy_auth = request_options.get('proxy_auth')
  123.    
  124.     if proxy_auth and proxies:
  125.         for proxy_type in ['http', 'https']:
  126.             if proxy_type in proxies and isinstance(proxies[proxy_type], str):
  127.                 proxy_url = proxies[proxy_type]
  128.                 if proxy_url.startswith('http://'):
  129.                     proxy_url = proxy_url.replace('http://', f'http://{proxy_auth[0]}:{proxy_auth[1]}@')
  130.                 proxies[proxy_type] = proxy_url
  131.    
  132.     request_kwargs = {
  133.         'url': url, 'method': method, 'headers': headers,
  134.         'params': params, 'data': data, 'cookies': cookies,
  135.         'timeout': (request_options.get('connect_timeout', 10), request_options.get('timeout', 30)),
  136.         'verify': request_options.get('verify', True),
  137.         'allow_redirects': request_options.get('allow_redirects', True),
  138.         'stream': request_options.get('stream', False),
  139.         'proxies': proxies,
  140.     }
  141.     request_kwargs = {k: v for k, v in request_kwargs.items() if v is not None}
  142.    
  143.     try:
  144.         response = requests.request(**request_kwargs)
  145.         if config.get('encoding'):
  146.             response.encoding = config['encoding']
  147.         return response.text
  148.     except requests.exceptions.Timeout as e:
  149.         return json.dumps({'code': 400, 'msg': f'请求超时: {str(e)}'}, ensure_ascii=False)
  150.     except requests.exceptions.ConnectionError as e:
  151.         return json.dumps({'code': 400, 'msg': f'连接错误: {str(e)}'}, ensure_ascii=False)
  152.     except requests.exceptions.RequestException as e:
  153.         return json.dumps({'code': 400, 'msg': f'请求失败: {str(e)}'}, ensure_ascii=False)
  154.     except Exception as e:
  155.         return json.dumps({'code': 400, 'msg': f'未知错误: {str(e)}'}, ensure_ascii=False)

  156. # -----------------辅助函数-----------------
  157. def get_ql_path(filename: str) -> str:
  158.     """获取青龙面板脚本目录路径"""
  159.     ql_data_path = '/ql/data/scripts/'
  160.     ql_old_path = '/ql/scripts/'
  161.     current_path = os.path.dirname(os.path.abspath(__file__))
  162.    
  163.     if '/ql/' in current_path:
  164.         return os.path.join(current_path, filename)
  165.     if os.path.exists(ql_data_path):
  166.         return os.path.join(ql_data_path, filename)
  167.     elif os.path.exists(ql_old_path):
  168.         return os.path.join(ql_old_path, filename)
  169.     else:
  170.         return os.path.join(os.getcwd(), filename)

  171. def load_pushed_records(filename: str) -> List[str]:
  172.     """加载已推送记录"""
  173.     filepath = get_ql_path(filename)
  174.     if not os.path.exists(filepath):
  175.         return []
  176.     try:
  177.         with open(filepath, 'r', encoding='utf-8') as f:
  178.             return json.load(f)
  179.     except Exception as e:
  180.         print(f"⚠️ 读取历史记录失败:{e}")
  181.         return []

  182. def save_pushed_records(filename: str, records: List[str], max_records: int = 200):
  183.     """保存推送记录"""
  184.     filepath = get_ql_path(filename)
  185.     if len(records) > max_records:
  186.         records = records[-max_records:]
  187.     try:
  188.         with open(filepath, 'w', encoding='utf-8') as f:
  189.             json.dump(records, f, ensure_ascii=False)
  190.     except Exception as e:
  191.         print(f"⚠️ 保存记录失败:{e}")

  192. def get_optimal_api_url(api_config: Dict) -> str:
  193.     """获取当前最优IP接口地址(带容错),并拼接接口路径"""
  194.     getapi_config = {
  195.         'url': api_config['getapi_url'],
  196.         'type': 0,
  197.         'data': {
  198.             'id': api_config['id'],
  199.             'key': api_config['key'],
  200.         },
  201.         'request_options': {
  202.             'timeout': 10,
  203.             'connect_timeout': 5,
  204.             'verify': False,
  205.             'allow_redirects': True,
  206.         },
  207.         'headers': {
  208.             'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
  209.             'Accept': 'application/json',
  210.         },
  211.     }
  212.    
  213.     print("🌐 正在获取最优IP接口地址...")
  214.    
  215.     try:
  216.         response = getpost(getapi_config)
  217.         
  218.         try:
  219.             result = json.loads(response) if isinstance(response, str) else response
  220.             
  221.             if isinstance(result, dict) and result.get('code') == 200:
  222.                 # 优先使用api字段
  223.                 if 'api' in result and result['api']:
  224.                     base_url = result['api'].strip()
  225.                     if base_url.endswith('/'):
  226.                         optimal_url = base_url + api_config['api_path'].lstrip('/')
  227.                     else:
  228.                         optimal_url = base_url + api_config['api_path']
  229.                     print(f"✅ 获取最优接口成功:{optimal_url}")
  230.                     return optimal_url
  231.                 elif 'url' in result and result['url']:
  232.                     optimal_url = result['url'].strip()
  233.                     print(f"✅ 获取最优接口成功(url字段):{optimal_url}")
  234.                     return optimal_url
  235.                 elif 'data' in result and isinstance(result['data'], dict):
  236.                     if 'url' in result['data']:
  237.                         optimal_url = result['data']['url'].strip()
  238.                         print(f"✅ 获取最优接口成功(data.url字段):{optimal_url}")
  239.                         return optimal_url
  240.                     elif 'api' in result['data']:
  241.                         optimal_url = result['data']['api'].strip()
  242.                         print(f"✅ 获取最优接口成功(data.api字段):{optimal_url}")
  243.                         return optimal_url
  244.                
  245.                 print(f"⚠️ 返回格式异常,使用默认域名接口")
  246.                 return api_config['fallback_url']
  247.             else:
  248.                 error_msg = result.get('msg', '未知错误') if isinstance(result, dict) else '解析失败'
  249.                 print(f"⚠️ 获取最优接口失败:{error_msg},将使用默认域名接口")
  250.                 return api_config['fallback_url']
  251.                
  252.         except json.JSONDecodeError:
  253.             potential_url = response.strip() if isinstance(response, str) else ""
  254.             if potential_url.startswith('http'):
  255.                 if potential_url.endswith('/'):
  256.                     optimal_url = potential_url + api_config['api_path'].lstrip('/')
  257.                 else:
  258.                     optimal_url = potential_url + api_config['api_path']
  259.                 print(f"✅ 获取最优接口成功(文本模式):{optimal_url}")
  260.                 return optimal_url
  261.             else:
  262.                 print(f"⚠️ 返回非有效URL,使用默认域名接口")
  263.                 return api_config['fallback_url']
  264.                
  265.     except Exception as e:
  266.         print(f"⚠️ 获取最优接口异常:{str(e)},将使用默认域名接口")
  267.         return api_config['fallback_url']

  268. def generate_quake_id(quake: Dict) -> str:
  269.     """生成唯一ID"""
  270.     unique_str = f"{quake.get('weizhi', '')}_{quake.get('addtime', '')}_{quake.get('leve', '')}"
  271.     return hashlib.md5(unique_str.encode('utf-8')).hexdigest()

  272. def send_wecom_notification(title: str, content: str, wecom_config: Dict) -> bool:
  273.     """发送企业微信消息"""
  274.     if not wecom_config['enabled']:
  275.         print("ℹ️ 推送已禁用,跳过")
  276.         return False
  277.    
  278.     webhook_url = wecom_config['webhook_url']
  279.     if not webhook_url:
  280.         print("❌ 错误:未配置企业微信机器人KEY")
  281.         return False
  282.    
  283.     message = {
  284.         "msgtype": "markdown",
  285.         "markdown": {"content": f"**{title}**\n\n{content}"}
  286.     }
  287.     if wecom_config['mentioned_mobile']:
  288.         message["mentioned_mobile_list"] = wecom_config['mentioned_mobile']
  289.    
  290.     try:
  291.         resp = requests.post(
  292.             webhook_url,
  293.             headers={'Content-Type': 'application/json'},
  294.             data=json.dumps(message, ensure_ascii=False).encode('utf-8'),
  295.             timeout=10
  296.         )
  297.         result = resp.json()
  298.         if result.get('errcode') == 0:
  299.             print(f"✅ 推送成功:{title}")
  300.             return True
  301.         else:
  302.             print(f"❌ 推送失败:{result.get('errmsg')}")
  303.             return False
  304.     except Exception as e:
  305.         print(f"❌ 推送异常:{str(e)}")
  306.         return False

  307. def format_quake_message(quake: Dict) -> str:
  308.     """格式化地震信息"""
  309.     location = quake.get('weizhi', '未知地点')
  310.     magnitude = float(quake.get('leve', 0))
  311.     depth = float(quake.get('shendu', 0))
  312.     latitude = float(quake.get('weidu', 0))
  313.     longitude = float(quake.get('jingdu', 0))
  314.     occur_time = quake.get('addtime', '未知时间')
  315.     cache_time = quake.get('hctime', '未知时间')
  316.    
  317.     lat_dir = "N" if latitude >= 0 else "S"
  318.     lon_dir = "E" if longitude >= 0 else "W"
  319.     coord_str = f"{abs(latitude):.2f}°{lat_dir} {abs(longitude):.2f}°{lon_dir}"
  320.    
  321.     if depth < 10:
  322.         depth_desc = "浅源地震(破坏力较强)"
  323.     elif depth < 30:
  324.         depth_desc = "中源地震"
  325.     else:
  326.         depth_desc = "深源地震(破坏力较弱)"
  327.    
  328.     content = f"""**🎯 震中位置**:{location}
  329. **📈 地震等级**:{magnitude}级
  330. **🕳️ 震源深度**:{depth}km({depth_desc})
  331. **🌍 震中坐标**:{coord_str}
  332. **🕐 发生时间**:{occur_time}
  333. **📊 数据同步**:{cache_time}
  334. """
  335.    
  336.     if magnitude >= 6.0:
  337.         content += "\n> 🔴 **严重警报**:强震级别,请注意安全!"
  338.     elif magnitude >= 5.0:
  339.         content += "\n> 🟠 **重要提醒**:显著地震,震感强烈!"
  340.     elif magnitude >= 4.0:
  341.         content += "\n> 🟡 **温馨提示**:有感地震,注意悬挂物晃动。"
  342.    
  343.     return content

  344. def should_push_quake(quake: Dict, filter_config: Dict, pushed_records: List[str]) -> tuple:
  345.     """
  346.     判断是否推送
  347.     返回: (是否推送: bool, 原因: str)
  348.     """
  349.     unique_id = generate_quake_id(quake)
  350.     location = quake.get('weizhi', '')
  351.    
  352.     # 1. 检查是否已推送
  353.     if unique_id in pushed_records:
  354.         return False, "已推送过"
  355.    
  356.     # 2. 震级检查
  357.     try:
  358.         magnitude = float(quake.get('leve', 0))
  359.         if magnitude < filter_config['min_magnitude']:
  360.             return False, f"震级{magnitude}低于阈值{filter_config['min_magnitude']}"
  361.     except:
  362.         return False, "震级数据异常"
  363.    
  364.     # 3. 地区检查
  365.     regions = filter_config.get('regions', [])
  366.     if regions:
  367.         matched = False
  368.         for region in regions:
  369.             if region in location:
  370.                 matched = True
  371.                 break
  372.         
  373.         if not matched:
  374.             return False, f"地区[{location}]不在监控列表{regions}中"
  375.    
  376.     return True, "符合推送条件"

  377. # -----------------主程序-----------------
  378. def main():
  379.     print(f"🌍 地震预警监控启动(青龙面板版)")
  380.     print(f"⏰ 当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
  381.     print("-" * 50)
  382.    
  383.     # ⚠️ 重要频率提醒
  384.     print("⚠️  【重要提醒】接口盒子免费用户限制:每分钟调用不能超过10次!")
  385.     print("⚠️  建议青龙定时规则设置为:*/6 * * * * (每6分钟一次)")
  386.     print("⚠️  过于频繁(如每分钟1次)可能导致API被封禁!")
  387.     print("-" * 50)
  388.    
  389.     # 加载配置
  390.     config = load_config_from_env()
  391.    
  392.     # 验证必要配置
  393.     if not config['apihz']['id'] or not config['apihz']['key']:
  394.         print("❌ 错误:缺少必要环境变量 QUAKE_APIHZ_ID 或 QUAKE_APIHZ_KEY")
  395.         sys.exit(1)
  396.    
  397.     if not config['wecom']['webhook_url']:
  398.         print("❌ 错误:缺少必要环境变量 QUAKE_WECOM_KEY")
  399.         sys.exit(1)
  400.    
  401.     # 显示配置信息
  402.     print(f"📊 配置信息:")
  403.     print(f"   用户ID:{config['apihz']['id']}")
  404.     print(f"   最小震级:{config['wecom']['filter']['min_magnitude']}级")
  405.     if config['wecom']['filter']['regions']:
  406.         print(f"   监控地区:{', '.join(config['wecom']['filter']['regions'])}(仅推送这些地区)")
  407.     else:
  408.         print(f"   监控地区:全国(未配置地区限制)")
  409.     print(f"   推送状态:{'启用' if config['wecom']['enabled'] else '禁用'}")
  410.     if config['wecom']['mentioned_mobile']:
  411.         print(f"   @手机号:{', '.join(config['wecom']['mentioned_mobile'])}")
  412.     print("-" * 50)
  413.    
  414.     # 1. 动态获取最优IP接口
  415.     optimal_url = get_optimal_api_url(config['apihz'])
  416.     print(f"🎯 实际请求地址:{optimal_url}")
  417.     print("-" * 50)
  418.    
  419.     # 2. 构建地震API配置
  420.     quake_config = {
  421.         'url': optimal_url,
  422.         'type': 0,
  423.         'data': {
  424.             'id': config['apihz']['id'],
  425.             'key': config['apihz']['key'],
  426.         },
  427.         'request_options': {
  428.             'timeout': 30,
  429.             'connect_timeout': 10,
  430.             'verify': False if 'http://' in optimal_url else True,
  431.             'allow_redirects': True,
  432.         },
  433.         'headers': {
  434.             'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
  435.             'Accept': 'application/json',
  436.         },
  437.     }
  438.    
  439.     # 3. 加载历史记录
  440.     record_file = config['storage']['record_file']
  441.     pushed_records = load_pushed_records(record_file)
  442.     print(f"📋 已加载 {len(pushed_records)} 条历史推送记录")
  443.    
  444.     # 4. 请求地震数据(⚠️ 无频率限制,用户自行控制)
  445.     print("🔍 正在请求地震数据...")
  446.     response = getpost(quake_config)
  447.    
  448.     # 5. 解析响应
  449.     try:
  450.         result = json.loads(response) if isinstance(response, str) else response
  451.         
  452.         if isinstance(result, dict) and result.get('code') == 200:
  453.             quake_list = result.get('data', [])
  454.             print(f"✅ 成功获取 {len(quake_list)} 条地震记录")
  455.         else:
  456.             error_msg = result.get('msg', '未知错误') if isinstance(result, dict) else str(response)[:200]
  457.             print(f"❌ API错误:{error_msg}")
  458.             sys.exit(1)
  459.     except Exception as e:
  460.         print(f"❌ 解析失败:{str(e)}")
  461.         sys.exit(1)
  462.    
  463.     # 6. 处理数据
  464.     new_records = []
  465.     push_count = 0
  466.     skip_count = 0
  467.     skip_reasons = {}
  468.    
  469.     # 去重
  470.     unique_quakes = {}
  471.     for quake in quake_list:
  472.         key = f"{quake.get('weizhi')}_{quake.get('addtime')}_{quake.get('leve')}"
  473.         if key not in unique_quakes:
  474.             unique_quakes[key] = quake
  475.    
  476.     filtered_list = list(unique_quakes.values())
  477.    
  478.     for quake in filtered_list:
  479.         unique_id = generate_quake_id(quake)
  480.         location = quake.get('weizhi', '未知地点')
  481.         magnitude = float(quake.get('leve', 0))
  482.         
  483.         should_push, reason = should_push_quake(quake, config['wecom']['filter'], pushed_records)
  484.         
  485.         if should_push:
  486.             if magnitude >= 6.0:
  487.                 title = f"🔴🔴🔴 强震警报:{location}发生{magnitude}级地震"
  488.             elif magnitude >= 5.0:
  489.                 title = f"🔴 强震预警:{location}发生{magnitude}级地震"
  490.             elif magnitude >= 4.0:
  491.                 title = f"🟠 显著地震:{location}发生{magnitude}级地震"
  492.             else:
  493.                 title = f"🟡 地震信息:{location}发生{magnitude}级地震"
  494.             
  495.             content = format_quake_message(quake)
  496.             
  497.             if send_wecom_notification(title, content, config['wecom']):
  498.                 push_count += 1
  499.             
  500.             new_records.append(unique_id)
  501.         else:
  502.             skip_count += 1
  503.             skip_reasons[reason] = skip_reasons.get(reason, 0) + 1
  504.             
  505.             if unique_id not in pushed_records:
  506.                 new_records.append(unique_id)
  507.    
  508.     # 7. 保存记录
  509.     if new_records:
  510.         all_records = pushed_records + new_records
  511.         save_pushed_records(record_file, all_records, config['storage']['max_records'])
  512.    
  513.     print("-" * 50)
  514.     print(f"✅ 执行完成:推送 {push_count} 条,跳过 {skip_count} 条")
  515.    
  516.     if skip_reasons:
  517.         print("📊 跳过原因统计:")
  518.         for reason, count in skip_reasons.items():
  519.             print(f"   - {reason}: {count}条")
  520.    
  521.     if push_count == 0 and len(filtered_list) > 0:
  522.         print("ℹ️ 本次运行无符合条件的地震推送")
  523.    
  524.     print(f"🏠 记录文件:{get_ql_path(record_file)}")
  525.    
  526.     # 再次提醒频率限制
  527.     print("-" * 50)
  528.     print("⚠️  提醒:本次请求已计入频次,免费用户每分钟不能超过10次!")
  529.     print("⚠️  请确保青龙定时设置合理,避免API被封禁!")

  530. if __name__ == '__main__':
  531.     main()
复制代码


评论

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Copyright © 2026 绿联NAS私有云社区 版权所有 All Rights Reserved. 粤公网安备44030002002555号| 粤ICP备12028978号
关灯 在本版发帖
联系技术支持
返回顶部
快速回复 返回顶部 返回列表