From f0bfd9dbbefcf72d14779425e7d43209bd267eaa Mon Sep 17 00:00:00 2001 From: houhuan Date: Tue, 12 May 2026 17:06:49 +0800 Subject: [PATCH] =?UTF-8?q?v4:=20=E6=94=B9=E7=94=A8=E6=9C=AC=E5=9C=B0Index?= =?UTF-8?q?TTS2=20+=20=E6=96=B0=E5=A2=9E=E9=A3=9E=E4=B9=A6=E6=8E=A8?= =?UTF-8?q?=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TTS从小爱音箱云API改为本地IndexTTS2模型 - 新增飞书群Webhook推送 - 新增notify_all统一通知(企业微信+飞书) - 新增local_tts.py本地TTS封装模块 - 新增run.bat一键启动脚本 - 新增README.md项目说明 - 新增.gitignore排除敏感文件 - 更新CLAUDE.md文档 --- .gitignore | 12 ++ CLAUDE.md | 82 ++++++++++++ README.md | 64 +++++++++ fengxiang_scraper.py | 114 ++++++++++++++++ fetch_orders.py | 52 ++++++++ local_tts.py | 67 ++++++++++ order_monitor.py | 307 +++++++++++++++++++++++++++++++++++++++++++ push_latest_order.py | 91 +++++++++++++ run.bat | 40 ++++++ setup_xiaomi.py | 83 ++++++++++++ 10 files changed, 912 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 fengxiang_scraper.py create mode 100644 fetch_orders.py create mode 100644 local_tts.py create mode 100644 order_monitor.py create mode 100644 push_latest_order.py create mode 100644 run.bat create mode 100644 setup_xiaomi.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..307a57c --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Sensitive +cookies.json +xiaomi_config.json +xiaomi_raw_cookies.json +order_data.json + +# Python +__pycache__/ +*.pyc + +# IDE +.claude/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..94f2933 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,82 @@ +# 丰享订单监控项目 + +## 项目概述 + +自动监控丰享商家端(fs.szfx.com)的新订单,同时推送到企业微信、飞书群和本地TTS语音播报**。 + +## 文件说明 + +| 文件 | 用途 | +|------|------| +| `order_monitor.py` | 主程序:监控订单 + 企业微信 + 飞书 + 本地TTS播报 | +| `push_latest_order.py` | 单次获取最新订单并推送企业微信 | +| `fetch_orders.py` | 复用 Cookie 获取订单数据(命令行) | +| `fengxiang_scraper.py` | 首次登录获取 Cookie(Playwright 半自动) | +| `setup_xiaomi.py` | 小米账号登录获取 serviceToken(Playwright) | +| `cookies.json` | 丰享平台的登录 Cookie | +| `xiaomi_config.json` | 小米账号和音箱配置 | +| `order_data.json` | 订单数据样例 | + +## 已逆向的 API + +### 丰享平台 + +- **登录**: `POST https://fspass.szfx.com/api/login` + - 参数: `uname`(手机号), `upass`(密码Base64反转), `ticket`, `randstr`(腾讯验证码) + - 登录后通过 `https://fs.szfx.com/saasmerchant/setstoken` 设置 Cookie + - Cookie 保存在浏览器中,主要: `USS`, `PTOKEN`, `CPTOKEN`, `STOKEN` + +- **订单列表**: `POST https://fs.szfx.com/saasmerchant/pcweb/order/quickpayorder/list` + - Content-Type: application/json + - Body: `{"shopId": "20434543575189", "page": 1, "pageSize": 20}` + - 需要登录 Cookie,未登录返回 `errno: 110003` + - 返回: `{errno: 0, data: {total, list: [...], count}}` + +- **关键字段**: + - `shopId`: `20434543575189` (益选便利店) + - `orderId`: 订单号 + - `amountPayable`: 实付金额(单位:分) + - `allPrice`: 订单金额(单位:分) + - `shopName`, `city`, `companyName`, `workplaceName`: 业务信息 + - `createTime`, `payTime`, `finishTime`: 时间戳(秒) + +### 本地 TTS(IndexTTS2) + +- 使用本地部署的 IndexTTS2 模型,无需联网 +- 模型路径: `E:\2025Code\python\IndexTT\index-tts\` +- 语音样本: `examples/voice_01.wav` +- 封装模块: `local_tts.py` +- 调用方式: `_local_speak(text)` → 合成 WAV → 本地播放 + +### 企业微信 Webhook +- URL: `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=644ab6d9-3b66-4166-88e9-5a8a89e3731d` +- 只能发消息,不能收消息 + +## order_monitor.py 核心逻辑 + +1. Playwright 打开浏览器,用户手动登录丰享平台 +2. 登录成功后,通过浏览器页面的 `fetch()` 调用订单 API(复用浏览器会话,最自然) +3. 每次拉取最近 N 条订单,和上次最新订单号对比,找出所有新订单 +4. 新订单同时推送企业微信 + 飞书 + 本地TTS播报 +5. 高峰期(11-13点、17-19点)10-20秒轮询,闲时 30-60 秒 +6. 每 5 分钟刷新页面防 Session 过期 +7. Session 过期自动检测 → 通知企业微信 → 等待用户重新登录 + +## 消息格式 + +- 企业微信: `【丰享丰食】订单收款成功,收款24.00元` +- 小爱音箱: `收到新订单,收款24元`(口语化,整数不带小数点) + +## 已安装的依赖 + +``` +pip install playwright requests +playwright install chromium +``` + +## 注意事项 + +- 丰享登录有**腾讯验证码 TCaptcha**,无法纯接口绕过,必须用 Playwright 浏览器登录 +- 小米 passToken 会过期,过期后需重新运行 `setup_xiaomi.py` 登录 +- Windows 终端输出中文会乱码,但实际发送的内容是正确的 UTF-8 +- MiService 的 `miaccount.py` 第 68 行已修改:`self.password.encode()` 加了 None 检查(passToken 登录时 password 为 None) diff --git a/README.md b/README.md new file mode 100644 index 0000000..c8bf873 --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# 丰享订单监控 + +自动监控丰享商家端(fs.szfx.com)新订单,同时推送到**企业微信**、**飞书群**并进行**本地TTS语音播报**。 + +## 快速开始 + +双击 `run.bat` 启动。首次运行会自动打开浏览器登录丰享平台,后续复用 Cookie 无需反复登录。 + +## 文件说明 + +| 文件 | 用途 | +|------|------| +| `order_monitor.py` | 主程序:监控订单 + 企业微信/飞书推送 + 本地TTS播报 | +| `local_tts.py` | 本地 TTS 封装模块(基于 IndexTTS2) | +| `run.bat` | 一键启动脚本 | +| `push_latest_order.py` | 单次获取最新订单并推送 | +| `fetch_orders.py` | 复用 Cookie 获取订单数据(命令行) | +| `fengxiang_scraper.py` | 首次登录获取 Cookie(Playwright 半自动) | +| `setup_xiaomi.py` | 小米账号登录(已弃用,TTS 改为本地) | +| `cookies.json` | 丰享平台登录 Cookie(自动生成) | + +## 运行环境 + +- Python 虚拟环境: `E:\2025Code\python\IndexTT\index-tts\.venv` +- 依赖: `playwright`、`requests` +- 浏览器: Chromium(Playwright 自动管理) +- TTS 模型: IndexTTS2(本地部署,首次加载需 1-2 分钟) + +## 通知渠道 + +| 渠道 | 说明 | +|------|------| +| 企业微信 | Webhook 推送消息 | +| 飞书群 | Webhook 推送消息 | +| 本地 TTS | IndexTTS2 合成语音 + 扬声器播放 | + +## 轮询策略 + +| 时段 | 间隔 | 拉取数量 | +|------|------|---------| +| 高峰期 11:00-13:00, 17:00-19:00 | 5-10秒 | 20条 | +| 闲时 | 5-10秒 | 5条 | +| 夜间 21:00-07:40 | 自动暂停 | — | + +## 消息格式 + +- 推送: `【丰享丰食】订单收款成功,收款24.00元` +- TTS: 同上文案语音播报 + +## 已逆向 API + +### 丰享平台 + +- **登录**: `POST https://fspass.szfx.com/api/login` + - 腾讯验证码 TCaptcha,必须浏览器手动登录 +- **订单列表**: `POST https://fs.szfx.com/saasmerchant/pcweb/order/quickpayorder/list` + - Body: `{"shopId": "20434543575189", "page": 1, "pageSize": 20}` + - 返回: `{errno: 0, data: {total, list: [...], count}}` + +## 注意事项 + +- Cookie 过期时自动弹浏览器提示重新登录,并推送通知 +- 每 5 分钟自动保活防 Session 过期 +- 无需小米账号或小爱音箱,TTS 完全本地运行 \ No newline at end of file diff --git a/fengxiang_scraper.py b/fengxiang_scraper.py new file mode 100644 index 0000000..ab2a671 --- /dev/null +++ b/fengxiang_scraper.py @@ -0,0 +1,114 @@ +""" +丰享商家端数据抓取脚本 +使用 Playwright 半自动登录 + requests 调用数据接口 +""" + +import json +import time +import requests +from playwright.sync_api import sync_playwright + +# ============ 配置 ============ +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" +MMS_BASE = "https://fs.szfx.com/MMS" +SHOP_ID = "20434543575189" +# ============================== + + +def login_and_get_cookies(): + """用 Playwright 打开登录页,等待用户手动登录,然后提取 Cookie""" + print("[1/3] 启动浏览器,请手动完成登录(包括验证码)...") + + with sync_playwright() as p: + browser = p.chromium.launch(headless=False, args=["--start-maximized"]) + context = browser.new_context(no_viewport=True) + page = context.new_page() + + page.goto(LOGIN_URL) + + # 等待用户登录成功(检测页面跳转到 MMS 首页) + print("[2/3] 等待登录完成...(请在浏览器中输入账号密码并完成验证码)") + try: + page.wait_for_url("**/MMS**", timeout=300_000) + print(" 登录成功!正在提取 Cookie...") + except Exception: + print(" 等待超时(5分钟),请重新运行脚本。") + browser.close() + return None + + # 确保访问过目标页面(触发必要的 Cookie 设置) + page.goto(MMS_BASE) + page.wait_for_load_state("networkidle") + time.sleep(2) + + # 提取所有 Cookie + playwright_cookies = context.cookies() + cookies = {c["name"]: c["value"] for c in playwright_cookies} + + print(f" 获取到 {len(cookies)} 个 Cookie") + browser.close() + return cookies + + +def fetch_order_list(cookies, page=1, page_size=20): + """调用快捷买单订单列表接口""" + 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": page, + "pageSize": page_size, + } + + print(f"[3/3] 正在请求订单数据(第{page}页)...") + resp = requests.post(DATA_API, json=payload, headers=headers, cookies=cookies, timeout=30) + data = resp.json() + + if data.get("errno") == 0: + print(" 请求成功!") + return data.get("data") + else: + print(f" 请求失败: errno={data.get('errno')}, errmsg={data.get('errmsg')}") + return None + + +def save_data(data, filename="order_data.json"): + """保存数据到文件""" + with open(filename, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + print(f" 数据已保存到 {filename}") + + +def main(): + # 第一步:登录获取 Cookie + cookies = login_and_get_cookies() + if not cookies: + return + + # 保存 Cookie 以便下次复用 + with open("cookies.json", "w", encoding="utf-8") as f: + json.dump(cookies, f, ensure_ascii=False, indent=2) + print(" Cookie 已保存到 cookies.json(下次可直接使用)\n") + + # 第二步:获取订单数据 + result = fetch_order_list(cookies) + if result: + save_data(result) + # 打印摘要 + if isinstance(result, dict): + print(f"\n 数据摘要:") + for k, v in result.items(): + if not isinstance(v, (list, dict)): + print(f" {k}: {v}") + elif isinstance(v, list): + print(f" {k}: 共 {len(v)} 条记录") + + +if __name__ == "__main__": + main() diff --git a/fetch_orders.py b/fetch_orders.py new file mode 100644 index 0000000..fbb7d62 --- /dev/null +++ b/fetch_orders.py @@ -0,0 +1,52 @@ +""" +丰享商家端数据抓取 - Cookie 复用模式 +已有 cookies.json 时直接调用接口,Cookie 过期再运行主脚本重新登录 +""" + +import json +import sys +import requests + +DATA_API = "https://fs.szfx.com/saasmerchant/pcweb/order/quickpayorder/list" +SHOP_ID = "20434543575189" + + +def load_cookies(): + try: + with open("cookies.json", "r", encoding="utf-8") as f: + return json.load(f) + except FileNotFoundError: + print("未找到 cookies.json,请先运行 fengxiang_scraper.py 完成登录") + sys.exit(1) + + +def fetch_order_list(cookies, page=1, page_size=20): + 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": page, "pageSize": page_size} + resp = requests.post(DATA_API, json=payload, headers=headers, cookies=cookies, timeout=30) + return resp.json() + + +def main(): + cookies = load_cookies() + print(f"已加载 {len(cookies)} 个 Cookie") + + page = int(sys.argv[1]) if len(sys.argv) > 1 else 1 + result = fetch_order_list(cookies, page=page) + + if result.get("errno") == 0: + data = result.get("data") + print(json.dumps(data, ensure_ascii=False, indent=2)) + elif result.get("errno") == 110003: + print("Cookie 已过期,请重新运行 fengxiang_scraper.py 完成登录") + else: + print(f"请求失败: {result}") + + +if __name__ == "__main__": + main() diff --git a/local_tts.py b/local_tts.py new file mode 100644 index 0000000..d4174b0 --- /dev/null +++ b/local_tts.py @@ -0,0 +1,67 @@ +""" +本地TTS语音播报模块 - 基于IndexTTS2 +""" +import os +import sys +import time +import tempfile +import winsound + +# IndexTTS2 路径配置 +_INDEXTTS_HOME = r"E:\2025Code\python\IndexTT\index-tts" +_INDEXTTS_MODEL_DIR = os.path.join(_INDEXTTS_HOME, "checkpoints") +_INDEXTTS_CFG = os.path.join(_INDEXTTS_MODEL_DIR, "config.yaml") +_INDEXTTS_VENV_PYTHON = os.path.join(_INDEXTTS_HOME, ".venv", "Scripts", "python.exe") + +# 默认语音样本 +_DEFAULT_VOICE_PROMPT = os.path.join(_INDEXTTS_HOME, "examples", "voice_01.wav") + +# 全局 TTS 实例(延迟初始化) +_tts_instance = None + + +def _get_tts(): + """延迟初始化 IndexTTS2 实例""" + global _tts_instance + if _tts_instance is None: + sys.path.insert(0, _INDEXTTS_HOME) + from indextts.infer_v2 import IndexTTS2 + print("[TTS] 正在加载 IndexTTS2 模型(首次加载较慢,请耐心等待)...") + _tts_instance = IndexTTS2( + cfg_path=_INDEXTTS_CFG, + model_dir=_INDEXTTS_MODEL_DIR, + use_fp16=True, + use_cuda_kernel=False, + use_deepspeed=False + ) + print("[TTS] IndexTTS2 模型加载完成") + return _tts_instance + + +def speak(text, voice_prompt=None): + """使用本地 IndexTTS2 合成语音并播放 + + Args: + text: 要播报的文本 + voice_prompt: 语音样本路径,默认使用 voice_01.wav + """ + if voice_prompt is None: + voice_prompt = _DEFAULT_VOICE_PROMPT + + tts = _get_tts() + output_path = os.path.join(tempfile.gettempdir(), f"fx_tts_{int(time.time() * 1000)}.wav") + + tts.infer( + spk_audio_prompt=voice_prompt, + text=text, + output_path=output_path, + verbose=False + ) + + # 播放生成的音频 + winsound.PlaySound(output_path, winsound.SND_FILENAME) + return output_path + + +if __name__ == "__main__": + speak("本地语音播报测试成功") diff --git a/order_monitor.py b/order_monitor.py new file mode 100644 index 0000000..8963605 --- /dev/null +++ b/order_monitor.py @@ -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() diff --git a/push_latest_order.py b/push_latest_order.py new file mode 100644 index 0000000..4d1bfda --- /dev/null +++ b/push_latest_order.py @@ -0,0 +1,91 @@ +"""获取最新一条订单并推送到企业微信群""" + +import json +import datetime +import requests + +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" + +STATUS_MAP = {1: "已完成", 3: "已退款", 4: "退款中", 5: "已关闭"} + + +def fmt_ts(ts): + return datetime.datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") if ts else "-" + + +def fmt_money(cents): + return f"{cents / 100:.2f}元" if cents else "0.00元" + + +def load_cookies(): + with open("cookies.json", "r", encoding="utf-8") as f: + return json.load(f) + + +def fetch_latest_order(): + cookies = load_cookies() + 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": 1} + resp = requests.post(DATA_API, json=payload, headers=headers, cookies=cookies, timeout=30) + data = resp.json() + if data.get("errno") != 0: + print(f"请求失败: {data}") + return None + return data["data"]["list"][0] + + +def format_order(order): + status = STATUS_MAP.get(order["status"], str(order["status"])) + return ( + f"【新订单通知】\n" + f"订单号:{order['orderId']}\n" + f"店铺:{order['shopName']}\n" + f"城市:{order['city']}\n" + f"大区:{order['largeAreaName']}\n" + f"企业:{order['companyName']}\n" + f"用餐地点:{order['workplaceName']}\n" + f"下单时间:{fmt_ts(order['createTime'])}\n" + f"支付时间:{fmt_ts(order['payTime'])}\n" + f"完成时间:{fmt_ts(order['finishTime'])}\n" + f"订单金额:{fmt_money(order['allPrice'])}\n" + f"实付金额:{fmt_money(order['amountPayable'])}\n" + f"支付方式:{order['payTypeName']}\n" + f"订单来源:{order['orderSourceName']}\n" + f"状态:{status}" + ) + + +def send_to_wecom(text): + payload = { + "msgtype": "text", + "text": {"content": text} + } + resp = requests.post(WEBHOOK_URL, json=payload, timeout=10) + result = resp.json() + if result.get("errcode") == 0: + print("推送成功!") + else: + print(f"推送失败: {result}") + return result + + +def main(): + order = fetch_latest_order() + if not order: + return + + msg = format_order(order) + print(msg) + print("\n--- 正在推送到企业微信群 ---\n") + send_to_wecom(msg) + + +if __name__ == "__main__": + main() diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..1e41576 --- /dev/null +++ b/run.bat @@ -0,0 +1,40 @@ +@echo off +chcp 65001 >nul +title 丰享订单监控 + +echo ============================================ +echo 丰享订单监控系统 v4 +echo 推送: 企业微信 + 本地TTS语音播报 +echo ============================================ +echo. + +cd /d "%~dp0" + +:: Python路径(IndexTTS虚拟环境) +set PYTHON=E:\2025Code\python\IndexTT\index-tts\.venv\Scripts\python.exe + +:: 检查Python是否存在 +if not exist "%PYTHON%" ( + echo [错误] 未找到Python: %PYTHON% + echo 请确认IndexTTS已正确安装 + pause + exit /b 1 +) + +:: 检查cookie文件 +if not exist "cookies.json" ( + echo [提示] 未检测到登录Cookie,首次运行将自动打开浏览器 + echo 请在浏览器中手动完成登录(含验证码) + echo. +) + +echo [启动] 正在启动订单监控... +echo. + +"%PYTHON%" order_monitor.py + +if %errorlevel% neq 0 ( + echo. + echo [异常] 监控程序异常退出,错误码: %errorlevel% + pause +) \ No newline at end of file diff --git a/setup_xiaomi.py b/setup_xiaomi.py new file mode 100644 index 0000000..2d38a6f --- /dev/null +++ b/setup_xiaomi.py @@ -0,0 +1,83 @@ +""" +小米账号登录 - 通过浏览器获取 serviceToken +浏览器打开后请手动登录,脚本会自动检测登录成功 +""" + +import json +import time +from playwright.sync_api import sync_playwright + +XIAOI_CONFIG = "xiaomi_config.json" + + +def login_and_get_tokens(): + print("启动浏览器,请登录小米账号...") + with sync_playwright() as p: + browser = p.chromium.launch(headless=False, args=["--start-maximized"]) + context = browser.new_context(no_viewport=True) + page = context.new_page() + + page.goto("https://account.xiaomi.com/pass/serviceLogin?sid=micoapi&_json=true&callback=https%3A%2F%2Fapi2.mina.mi.com%2Fsts") + page.wait_for_load_state("networkidle") + + print("请在浏览器中完成登录(输入手机号、密码、验证码)") + print("等待登录完成(最长5分钟)...") + + # 等待页面离开登录页(说明登录成功) + try: + page.wait_for_url(lambda url: "account.xiaomi.com/pass" not in url, timeout=300_000) + print("检测到登录成功!") + except Exception: + print("登录超时或失败") + browser.close() + return None + + time.sleep(3) + + # 获取 cookies + cookies = context.cookies() + cookie_dict = {c["name"]: c["value"] for c in cookies} + + service_token = cookie_dict.get("serviceToken", "") + user_id = cookie_dict.get("userId", "") + + print(f"获取到 {len(cookies)} 个 cookies") + print(f" userId: {user_id}") + print(f" serviceToken: {'有' if service_token else '无'}") + + # 通过浏览器调用 device_list API + print("\n获取设备列表...") + result = page.evaluate(""" + async () => { + const resp = await fetch('https://api2.mina.mi.com/admin/v2/device_list?master=0&requestId=app_ios_' + Math.random().toString(36).substr(2, 30)); + return await resp.json(); + } + """) + + devices = [] + if result.get("code") == 0: + devices = result.get("data", []) + print(f"找到 {len(devices)} 个设备:") + for d in devices: + print(f" 名称: {d.get('name')} | 型号: {d.get('hardware')} | DID: {d.get('deviceID')}") + else: + print(f"获取设备失败: {result}") + + # 保存配置 + config = { + "userId": user_id, + "serviceToken": service_token, + "devices": devices, + "allCookies": cookie_dict, + } + + with open(XIAOI_CONFIG, "w", encoding="utf-8") as f: + json.dump(config, f, ensure_ascii=False, indent=2) + + print(f"\n配置已保存到 {XIAOI_CONFIG}") + browser.close() + return config + + +if __name__ == "__main__": + login_and_get_tokens()