From ea094dc949960f9f58bf7c56e5c68eb5c3e02aa1 Mon Sep 17 00:00:00 2001 From: Akatsuki-Misaki Date: Thu, 4 Jun 2026 15:04:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E6=B4=9B?= =?UTF-8?q?=E5=85=8B=E7=8E=8B=E5=9B=BD=E8=BF=9C=E8=A1=8C=E5=95=86=E4=BA=BA?= =?UTF-8?q?=E5=95=86=E5=93=81=E5=AE=9A=E6=97=B6=E9=82=AE=E4=BB=B6=E6=8E=A8?= =?UTF-8?q?=E9=80=81=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加项目基础文件: - 新增依赖配置requirements.txt - 配置git忽略规则.gitignore - 添加示例配置文件config.example.ini - 实现核心主程序main.py,包含API请求、邮件发送和定时调度功能 --- .gitignore | 14 +++ config.example.ini | 23 +++++ main.py | 227 +++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + 4 files changed, 266 insertions(+) create mode 100644 .gitignore create mode 100644 config.example.ini create mode 100644 main.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..14ea684 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +dist/ +build/ +*.egg +.env +config.ini +venv/ +.venv/ +.vscode/ +.idea/ +*.log diff --git a/config.example.ini b/config.example.ini new file mode 100644 index 0000000..8e613fa --- /dev/null +++ b/config.example.ini @@ -0,0 +1,23 @@ +[smtp] +# SMTP 服务器地址 +host = smtp.qq.com +# SMTP 端口(SSL 通常为 465,STARTTLS 通常为 587) +port = 465 +# 是否使用 SSL +use_ssl = true +# 发件人邮箱 +sender = your_email@qq.com +# 发件人邮箱密码或授权码 +password = your_authorization_code +# 收件人邮箱(多个用逗号分隔) +recipients = recipient1@example.com, recipient2@example.com + +[api] +# API 地址 +url = https://rocom-api.ovofish.com/api/shop +# API Key +key = sk-f2141ba4be3a9832106d2dc4042454666e354414d3ed0ce9 + +[schedule] +# 是否在每个时段开始时自动推送(true/false) +auto_push = true diff --git a/main.py b/main.py new file mode 100644 index 0000000..8500a64 --- /dev/null +++ b/main.py @@ -0,0 +1,227 @@ +""" +洛克王国远行商人商品定时邮件推送 + +每天4个时段自动获取商品数据并通过邮件通知: + 上午场 08:00 - 12:00 + 下午场 12:00 - 16:00 + 傍晚场 16:00 - 20:00 + 夜间场 20:00 - 24:00 + +用法: + 1. 复制 config.example.ini 为 config.ini,填写配置 + 2. pip install requests schedule + 3. python main.py +""" + +import configparser +import datetime +import json +import smtplib +import time +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText + +import requests +import schedule + +# --------------------------------------------------------------------------- +# 配置加载 +# --------------------------------------------------------------------------- + +config = configparser.ConfigParser() +config.read("config.ini", encoding="utf-8") + +SMTP_HOST = config.get("smtp", "host") +SMTP_PORT = config.getint("smtp", "port") +SMTP_SSL = config.getboolean("smtp", "use_ssl") +SMTP_SENDER = config.get("smtp", "sender") +SMTP_PASSWORD = config.get("smtp", "password") +SMTP_RECIPIENTS = [r.strip() for r in config.get("smtp", "recipients").split(",")] + +API_URL = config.get("api", "url") +API_KEY = config.get("api", "key") + +# --------------------------------------------------------------------------- +# 时段定义 +# --------------------------------------------------------------------------- + +PERIODS = [ + {"name": "上午场", "start": "08:00"}, + {"name": "下午场", "start": "12:00"}, + {"name": "傍晚场", "start": "16:00"}, + {"name": "夜间场", "start": "20:00"}, +] + +# --------------------------------------------------------------------------- +# API 请求 +# --------------------------------------------------------------------------- + + +MAX_RETRIES = 30 # 最大重试次数(每次间隔1分钟,最多重试30次即30分钟) + + +def fetch_shop_data(): + """获取当前时段商品数据""" + headers = {"X-API-Key": API_KEY} + for attempt in range(1, MAX_RETRIES + 1): + try: + resp = requests.get(API_URL, headers=headers, timeout=30) + resp.raise_for_status() + data = resp.json() + except Exception as e: + print(f"[ERROR] 第 {attempt} 次请求失败: {e}") + else: + status = data.get("status", "unknown") + items = data.get("data", []) + if status == "success" and items: + return data + print(f"[INFO] 第 {attempt} 次尝试: status={status}, 商品数={len(items)},1分钟后重试...") + + if attempt < MAX_RETRIES: + time.sleep(60) + + print("[ERROR] 达到最大重试次数,放弃本次推送") + return None + + +# --------------------------------------------------------------------------- +# 邮件构建与发送 +# --------------------------------------------------------------------------- + + +def build_html(data): + """根据 API 返回数据构建邮件 HTML(仅在有商品数据时调用)""" + period = data.get("period", "未知") + timestamp = data.get("timestamp", "") + items = data.get("data", []) + + rows = "" + for item in items: + name = item.get("name", "未知") + price = item.get("price", "0") + limit = item.get("limit", "不限购") + end_time = item.get("end_time", "") + img = item.get("image_url", "") + img_tag = f'{name}' if img else "" + rows += f""" + + {img_tag}{name} + {price} 洛克贝 + {limit} + {end_time} + + """ + + return f""" +

📦 远行商人 - {period}

+

数据时间: {timestamp} · 共 {len(items)} 件商品

+ + + + + + + + + + {rows} +
商品价格限购下架时间
+

洛克王国远行商人商品推送

+ """ + + +def send_email(html_content, period_name): + """发送邮件""" + msg = MIMEMultipart("alternative") + msg["Subject"] = f"洛克王国远行商人 - {period_name} 商品提醒" + msg["From"] = SMTP_SENDER + msg["To"] = ", ".join(SMTP_RECIPIENTS) + + msg.attach(MIMEText(html_content, "html", "utf-8")) + + try: + if SMTP_SSL: + server = smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT) + else: + server = smtplib.SMTP(SMTP_HOST, SMTP_PORT) + server.starttls() + + server.login(SMTP_SENDER, SMTP_PASSWORD) + server.sendmail(SMTP_SENDER, SMTP_RECIPIENTS, msg.as_string()) + server.quit() + print(f"[OK] 邮件已发送 -> {', '.join(SMTP_RECIPIENTS)}") + except Exception as e: + print(f"[ERROR] 邮件发送失败: {e}") + + +# --------------------------------------------------------------------------- +# 定时任务 +# --------------------------------------------------------------------------- + + +def job(period_name): + """获取数据并发送邮件""" + now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + print(f"\n[{now}] 执行任务: {period_name}") + + data = fetch_shop_data() + if data is None: + print(f"[SKIP] {period_name} 未获取到商品数据,跳过发送") + return + + print(f"[INFO] API 状态: {data.get('status')} · 商品数: {data.get('count', 0)}") + + html = build_html(data) + send_email(html, period_name) + + +def main(): + print("=" * 50) + print("洛克王国远行商人商品定时邮件推送") + print("=" * 50) + print(f"SMTP 服务器 : {SMTP_HOST}:{SMTP_PORT}") + print(f"发件人 : {SMTP_SENDER}") + print(f"收件人 : {', '.join(SMTP_RECIPIENTS)}") + print(f"API 地址 : {API_URL}") + print("-" * 50) + + # 为每个时段创建定时任务(延迟2分钟,等待API数据更新) + for p in PERIODS: + h, m = map(int, p["start"].split(":")) + m += 2 + if m >= 60: + h += 1 + m -= 60 + schedule_time = f"{h:02d}:{m:02d}" + schedule_name = p["name"] + schedule.every().day.at(schedule_time).do(job, period_name=schedule_name) + print(f"已注册定时任务: {schedule_name} @ 每天 {schedule_time}(延迟2分钟)") + + # 启动时如果处于营业时段且已过2分钟缓冲,立即执行一次 + now = datetime.datetime.now() + current_hour = now.hour + current_minute = now.minute + if 8 <= current_hour < 24: + for p in PERIODS: + start_h, start_m = map(int, p["start"].split(":")) + next_start = start_h + 4 + # 判断是否在该时段内,且已过2分钟缓冲期 + if start_h <= current_hour < next_start: + if current_hour > start_h or current_minute >= 2: + print(f"\n当前处于 {p['name']},立即执行一次推送...") + job(p["name"]) + else: + print(f"\n当前处于 {p['name']} 前2分钟缓冲期,跳过本次等待定时任务触发") + break + else: + print("\n当前为未营业时段(00:00-08:00),等待 08:02 开始推送") + + print("\n[RUNNING] 定时任务运行中,按 Ctrl+C 退出...\n") + + while True: + schedule.run_pending() + time.sleep(30) + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8cde3ef --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests>=2.28.0 +schedule>=1.2.0