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'' if img else ""
+ rows += f"""
+
+
+ """
+
+ return f"""
+ {img_tag}{name}
+ {price} 洛克贝
+ {limit}
+ {end_time}
+ 📦 远行商人 - {period}
+
数据时间: {timestamp} · 共 {len(items)} 件商品
+| 商品 | +价格 | +限购 | +下架时间 | +
|---|
洛克王国远行商人商品推送
+ """ + + +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