v4: 改用本地IndexTTS2 + 新增飞书推送

- TTS从小爱音箱云API改为本地IndexTTS2模型
- 新增飞书群Webhook推送
- 新增notify_all统一通知(企业微信+飞书)
- 新增local_tts.py本地TTS封装模块
- 新增run.bat一键启动脚本
- 新增README.md项目说明
- 新增.gitignore排除敏感文件
- 更新CLAUDE.md文档
This commit is contained in:
2026-05-12 17:06:49 +08:00
commit f0bfd9dbbe
10 changed files with 912 additions and 0 deletions
+307
View File
@@ -0,0 +1,307 @@
"""
丰享订单监控 v4
- 优先使用 Cookie 文件请求 API,无需每次都打开浏览器
- Cookie 过期时自动打开浏览器提示重新登录
- 晚上 21:00 到早上 7:40 自动暂停
- 高峰期缩短间隔,闲时拉长间隔
"""
import json
import random
import time
import datetime
import requests
from pathlib import Path
from playwright.sync_api import sync_playwright
from local_tts import speak as _local_speak
# ============ 配置 ============
LOGIN_URL = "https://fspass.szfx.com/ucenter/userlogin?platform=saas&redirect_url=https%3A%2F%2Ffs.szfx.com%2FMMS%23%2F&return_url=https%3A%2F%2Ffs.szfx.com%2Fsaasmerchant%2Fsetstoken"
DATA_API = "https://fs.szfx.com/saasmerchant/pcweb/order/quickpayorder/list"
SHOP_ID = "20434543575189"
WEBHOOK_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=644ab6d9-3b66-4166-88e9-5a8a89e3731d"
FEISHU_WEBHOOK_URL = "https://open.feishu.cn/open-apis/bot/v2/hook/oc_d5ef8b1abf66842c28ef57e1658dc096"
COOKIES_FILE = "cookies.json"
# 本地 TTS 配置(使用 IndexTTS2,无需额外配置)
# 高峰时段配置
PEAK_HOURS = [(11, 13), (17, 19)]
POLL_INTERVAL_MIN = 5
POLL_INTERVAL_MAX = 10
PEAK_PAGE_SIZE = 20
IDLE_PAGE_SIZE = 5
# 夜间暂停:21:00 ~ 07:40 不轮询
NIGHT_START_H = 21
NIGHT_END_H = 7
NIGHT_END_M = 40
KEEPALIVE_INTERVAL = 300 # 每5分钟保活一次
# ==============================
def fmt_money(cents):
return f"{cents / 100:.2f}" if cents else "0.00元"
def log(msg):
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{now}] {msg}")
def is_peak_hour():
hour = datetime.datetime.now().hour
return any(start <= hour < end for start, end in PEAK_HOURS)
def is_night_time():
"""是否在夜间暂停时段 (21:00 ~ 07:40)"""
now = datetime.datetime.now()
minutes = now.hour * 60 + now.minute
return minutes >= NIGHT_START_H * 60 or minutes < NIGHT_END_H * 60 + NIGHT_END_M
def seconds_until_morning():
"""距离明天早上 07:40 还有多少秒"""
now = datetime.datetime.now()
target = now.replace(hour=NIGHT_END_H, minute=NIGHT_END_M, second=0, microsecond=0)
if now.hour >= NIGHT_START_H:
target += datetime.timedelta(days=1)
return max(1, int((target - now).total_seconds()))
def get_poll_config():
if is_peak_hour():
return POLL_INTERVAL_MIN, POLL_INTERVAL_MAX, PEAK_PAGE_SIZE
return POLL_INTERVAL_MIN, POLL_INTERVAL_MAX, IDLE_PAGE_SIZE
def format_msg(order):
return f"【丰享丰食】订单收款成功,收款{fmt_money(order['amountPayable'])}"
def notify_all(text):
"""统一发送通知到企业微信 + 飞书"""
send_to_wecom(text)
send_to_feishu(text)
def send_to_wecom(text):
payload = {"msgtype": "text", "text": {"content": text}}
try:
resp = requests.post(WEBHOOK_URL, json=payload, timeout=10)
result = resp.json()
if result.get("errcode") == 0:
log("企业微信推送成功")
else:
log(f"企业微信推送失败: {result}")
except Exception as e:
log(f"企业微信推送异常: {e}")
def send_to_feishu(text):
payload = {"msg_type": "text", "content": {"text": text}}
try:
resp = requests.post(FEISHU_WEBHOOK_URL, json=payload, timeout=10)
result = resp.json()
if result.get("code") == 0:
log("飞书推送成功")
else:
log(f"飞书推送失败: {result}")
except Exception as e:
log(f"飞书推送异常: {e}")
def speak(text):
"""使用本地 IndexTTS2 语音播报"""
try:
_local_speak(text)
log("语音播报成功")
except Exception as e:
log(f"语音播报异常: {e}")
def load_cookies():
"""加载本地 Cookie 文件"""
cookie_path = Path(__file__).parent / COOKIES_FILE
if cookie_path.exists():
with open(cookie_path, "r", encoding="utf-8") as f:
return json.load(f)
return None
def save_cookies(cookiejar):
"""把浏览器 Cookie 列表保存为 dict 到文件"""
cookie_dict = {c["name"]: c["value"] for c in cookiejar}
cookie_path = Path(__file__).parent / COOKIES_FILE
with open(cookie_path, "w", encoding="utf-8") as f:
json.dump(cookie_dict, f, ensure_ascii=False, indent=2)
log("Cookie 已保存到 cookies.json")
def fetch_orders_via_requests(cookies, page_size=5):
"""通过 requests + Cookie 拉取订单列表,返回 (订单列表 / SESSION_EXPIRED / None)"""
headers = {
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Referer": "https://fs.szfx.com/MMS",
"Origin": "https://fs.szfx.com",
}
payload = {"shopId": SHOP_ID, "page": 1, "pageSize": page_size}
try:
resp = requests.post(DATA_API, json=payload, headers=headers, cookies=cookies, timeout=30)
data = resp.json()
if data.get("errno") == 0:
return data["data"]["list"]
elif data.get("errno") in (110003, 10009):
return "SESSION_EXPIRED"
else:
log(f"API 异常: errno={data.get('errno')}, errmsg={data.get('errmsg')}")
return None
except Exception as e:
log(f"请求异常: {e}")
return None
def browser_login():
"""打开浏览器让用户手动登录,返回 Cookie dict"""
log("启动浏览器,请手动登录...")
pw = sync_playwright().start()
browser = pw.chromium.launch(headless=False, args=["--start-maximized"])
context = browser.new_context(no_viewport=True)
page = context.new_page()
page.goto(LOGIN_URL)
log("等待登录完成(10分钟超时)...")
try:
page.wait_for_url("**/MMS**", timeout=600_000)
log("登录成功!提取 Cookie...")
except Exception:
log("登录超时")
browser.close()
pw.stop()
return None
time.sleep(2)
cookiejar = context.cookies()
save_cookies(cookiejar)
browser.close()
pw.stop()
log("浏览器已关闭")
return {c["name"]: c["value"] for c in cookiejar}
def main():
# 1. 尝试加载已有 Cookie
cookies = load_cookies()
if cookies:
log("检测到已有 Cookie,验证有效性...")
test_result = fetch_orders_via_requests(cookies, page_size=1)
if test_result == "SESSION_EXPIRED" or test_result is None:
log("Cookie 已过期,需要重新登录")
cookies = None
else:
log("Cookie 有效,无需打开浏览器")
# 2. 没有有效 Cookie → 打开浏览器登录
if not cookies:
notify_all("【监控通知】需要登录丰享平台,请查看电脑浏览器完成登录")
cookies = browser_login()
if not cookies:
log("登录失败,退出")
return
# 3. 获取初始最新订单号
log("获取初始订单...")
_, _, page_size = get_poll_config()
last_order_id = None
orders = fetch_orders_via_requests(cookies, page_size)
if orders and orders != "SESSION_EXPIRED" and len(orders) > 0:
last_order_id = orders[0]["orderId"]
log(f"当前最新订单号: {last_order_id}")
last_keepalive = time.time()
mode = "高峰期" if is_peak_hour() else ("夜间暂停" if is_night_time() else "闲时")
log(f"开始监控(当前模式: {mode}")
try:
while True:
# 夜间暂停:到点就睡,天亮再起
if is_night_time():
sleep_secs = seconds_until_morning()
until = datetime.datetime.now() + datetime.timedelta(seconds=sleep_secs)
log(f"夜间模式,暂停监控至 {until.strftime('%H:%M:%S')}(约 {sleep_secs // 60} 分钟)")
time.sleep(sleep_secs)
log("恢复监控...")
# 验证 Cookie 是否还活着
test = fetch_orders_via_requests(cookies, page_size=1)
if test == "SESSION_EXPIRED":
log("Cookie 在夜间过期,需要重新登录")
notify_all("【监控通知】Cookie 过期,请查看电脑浏览器完成登录")
cookies = browser_login()
if not cookies:
log("登录失败,退出")
return
last_keepalive = time.time()
continue
# 轮询
interval_min, interval_max, page_size = get_poll_config()
wait = random.randint(interval_min, interval_max)
mode = "高峰期" if is_peak_hour() else "闲时"
log(f"[{mode}] 等待 {wait} 秒,拉取 {page_size} 条...")
time.sleep(wait)
# 定期保活(一次 API 请求即可维持 Cookie)
if time.time() - last_keepalive > KEEPALIVE_INTERVAL:
fetch_orders_via_requests(cookies, page_size=1)
last_keepalive = time.time()
# 拉取订单
orders = fetch_orders_via_requests(cookies, page_size)
# Cookie 过期 → 浏览器重新登录
if orders == "SESSION_EXPIRED":
log("Cookie 已过期!")
notify_all("【监控异常】Cookie 已过期,请查看电脑浏览器完成登录")
cookies = browser_login()
if not cookies:
notify_all("登录超时,监控已退出。请手动重新运行。")
return
send_to_wecom("重新登录成功,监控已恢复")
last_keepalive = time.time()
orders = fetch_orders_via_requests(cookies, page_size)
if orders and orders != "SESSION_EXPIRED" and len(orders) > 0:
last_order_id = orders[0]["orderId"]
continue
if not orders or orders == "SESSION_EXPIRED":
continue
# 找出所有新订单(orderId > last_order_id
new_orders = []
for o in orders:
if o["orderId"] == last_order_id:
break
new_orders.append(o)
if new_orders:
log(f"发现 {len(new_orders)} 条新订单!")
# 从旧到新依次推送
for o in reversed(new_orders):
msg = format_msg(o)
notify_all(msg)
speak(msg)
time.sleep(0.5)
last_order_id = new_orders[0]["orderId"]
else:
log(f"暂无新订单(最新: {last_order_id}")
except KeyboardInterrupt:
log("监控已停止")
if __name__ == "__main__":
main()