From 1bb7bba97023ca628809e531082bee90fbde2379 Mon Sep 17 00:00:00 2001 From: houhuan Date: Thu, 30 Apr 2026 14:25:34 +0800 Subject: [PATCH] Initial commit: Fengxiang order monitor with WeChat & Xiaomi speaker push --- .gitignore | 23 +++ README.md | 106 ++++++++++++++ fengxiang_scraper.py | 114 +++++++++++++++ fetch_orders.py | 52 +++++++ order_monitor.py | 330 +++++++++++++++++++++++++++++++++++++++++++ push_latest_order.py | 91 ++++++++++++ run_monitor.bat | 12 ++ setup_autostart.ps1 | 31 ++++ setup_xiaomi.py | 83 +++++++++++ test_push.py | 128 +++++++++++++++++ 10 files changed, 970 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 fengxiang_scraper.py create mode 100644 fetch_orders.py create mode 100644 order_monitor.py create mode 100644 push_latest_order.py create mode 100644 run_monitor.bat create mode 100644 setup_autostart.ps1 create mode 100644 setup_xiaomi.py create mode 100644 test_push.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..326bfa2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Sensitive data - NEVER commit +CLAUDE.md +cookies.json +xiaomi_raw_cookies.json +xiaomi_config.json +order_data.json +*.mi.token + +# Python +__pycache__/ +*.pyc +*.pyo + +# IDE +.vscode/ +.idea/ + +# OS +Thumbs.db +Desktop.ini + +# Claude +.claude/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..5357042 --- /dev/null +++ b/README.md @@ -0,0 +1,106 @@ +# 丰享订单监控 + +自动监控丰享商家端(fs.szfx.com)的新订单,同时推送到**企业微信群**和**小爱音箱语音播报**。 + +## 功能特性 + +- **自动轮询**:高峰期(11-13点、17-19点)10-20秒/次,闲时30-60秒/次 +- **双通道推送**:新订单同时推送企业微信消息 + 小爱音箱 TTS 语音播报 +- **Cookie 复用**:首次登录后保存 Cookie,后续启动无需重复登录 +- **夜间暂停**:21:00 ~ 07:40 自动暂停轮询,节省资源 +- **Session 保活**:每5分钟自动发请求维持 Cookie 活性 +- **过期自动恢复**:Cookie 过期时弹浏览器提示重新登录,并推送企业微信通知 +- **开机自启**:支持 Windows 计划任务,登录系统后自动运行 + +## 快速开始 + +### 1. 安装依赖 + +```bash +pip install playwright requests miservice_fork aiohttp +playwright install chromium +``` + +### 2. 配置小米音箱(可选) + +首次使用需要获取小米 passToken: + +```bash +python setup_xiaomi.py +``` + +浏览器打开后手动登录小米账号,Token 自动保存到 `~/.mi.token`。 + +### 3. 运行监控 + +```bash +python order_monitor.py +``` + +首次运行会打开浏览器,手动完成丰享平台登录(有腾讯验证码,需人工操作)。登录成功后 Cookie 自动保存,之后重启无需再登录。 + +### 4. 测试推送 + +```bash +python test_push.py +``` + +拉取最新一条订单,测试企业微信和小爱音箱是否正常。 + +## 文件说明 + +| 文件 | 用途 | +|------|------| +| `order_monitor.py` | 主监控程序,轮询订单 + 推送 + 播报 | +| `test_push.py` | 测试脚本,验证企业微信和音箱通道 | +| `push_latest_order.py` | 单次获取最新订单并推送企业微信 | +| `fetch_orders.py` | 命令行获取订单数据(JSON 输出) | +| `fengxiang_scraper.py` | Playwright 半自动登录获取 Cookie | +| `setup_xiaomi.py` | 小米账号登录获取 serviceToken | +| `run_monitor.bat` | Windows 启动批处理 | +| `setup_autostart.ps1` | Windows 开机自启配置脚本 | + +## 已逆向的 API + +### 丰享平台 + +- **登录**:`POST https://fspass.szfx.com/api/login` + - 参数:`uname`(手机号)、`upass`(密码 Base64 反转)、`ticket`、`randstr`(腾讯验证码) + - Cookie:`USS`、`PTOKEN`、`CPTOKEN`、`STOKEN` + +- **订单列表**:`POST https://fs.szfx.com/saasmerchant/pcweb/order/quickpayorder/list` + - Content-Type: application/json + - Body: `{"shopId": "...", "page": 1, "pageSize": 20}` + - 返回:`{errno: 0, data: {total, list: [...], count}}` + +### 小米/小爱音箱 + +- 使用 [miservice_fork](https://github.com/nicepkg/miservice_fork) Python 库 +- TTS 播报:`MiNAService.text_to_speech(deviceId, text)` +- Token 保存在 `~/.mi.token` + +## 消息格式 + +企业微信和小爱音箱使用统一格式: + +> 【丰享丰食】订单收款成功,收款24.00元 + +## 开机自启(Windows) + +已配置计划任务 `FXOrderMonitor`,用户登录时自动运行。 + +```powershell +# 手动配置 +.\setup_autostart.ps1 +``` + +## 注意事项 + +- 丰享登录有腾讯验证码 TCaptcha,无法纯接口绕过,首次必须用浏览器登录 +- Cookie 有效期较长,持续使用可维持;过期会自动弹窗提示重新登录 +- 小米 passToken 会过期,过期后需重新运行 `setup_xiaomi.py` +- 配置文件(cookies.json、xiaomi_config.json)含敏感信息,已加入 .gitignore + +## License + +MIT 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/order_monitor.py b/order_monitor.py new file mode 100644 index 0000000..8b6a2b8 --- /dev/null +++ b/order_monitor.py @@ -0,0 +1,330 @@ +""" +丰享订单监控 v4 +- 优先使用 Cookie 文件请求 API,无需每次都打开浏览器 +- Cookie 过期时自动打开浏览器提示重新登录 +- 晚上 21:00 到早上 7:40 自动暂停 +- 高峰期缩短间隔,闲时拉长间隔 +""" + +import json +import random +import time +import datetime +import asyncio +import threading +import requests +from pathlib import Path +from aiohttp import ClientSession +from miservice import MiAccount, MiNAService +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" +SHOP_ID = "20434543575189" +WEBHOOK_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=644ab6d9-3b66-4166-88e9-5a8a89e3731d" +COOKIES_FILE = "cookies.json" + +# 小爱音箱配置 +XIAOMI_USER_ID = "1136458602" +XIAOMI_TOKEN_PATH = str(Path.home() / ".mi.token") +XIAOMI_SPEAKER_DID = "3ba2c1e8-d8cb-45c5-b88a-15624e7a02f3" + +# 高峰时段配置 +PEAK_HOURS = [(11, 13), (17, 19)] +PEAK_INTERVAL_MIN = 10 +PEAK_INTERVAL_MAX = 20 +PEAK_PAGE_SIZE = 20 + +IDLE_INTERVAL_MIN = 30 +IDLE_INTERVAL_MAX = 60 +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 PEAK_INTERVAL_MIN, PEAK_INTERVAL_MAX, PEAK_PAGE_SIZE + return IDLE_INTERVAL_MIN, IDLE_INTERVAL_MAX, IDLE_PAGE_SIZE + + +def format_msg(order): + return f"【丰享丰食】订单收款成功,收款{fmt_money(order['amountPayable'])}" + + +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 _run_async_in_thread(coro): + """在独立线程中运行协程,避免与 Playwright 事件循环冲突""" + result = None + error = None + + def _target(): + nonlocal result, error + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + result = loop.run_until_complete(coro) + except Exception as e: + error = e + finally: + loop.close() + + t = threading.Thread(target=_target) + t.start() + t.join(timeout=15) + if error: + raise error + return result + + +def speak(text): + """调用小爱音箱语音播报""" + try: + async def _tts(): + async with ClientSession() as session: + account = MiAccount(session, XIAOMI_USER_ID, None, XIAOMI_TOKEN_PATH) + mina = MiNAService(account) + return await mina.text_to_speech(XIAOMI_SPEAKER_DID, text) + + result = _run_async_in_thread(_tts()) + if result and result.get("code") == 0: + log("语音播报成功") + else: + log(f"语音播报失败: {result}") + 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: + send_to_wecom("【监控通知】需要登录丰享平台,请查看电脑浏览器完成登录") + 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 在夜间过期,需要重新登录") + send_to_wecom("【监控通知】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 已过期!") + send_to_wecom("【监控异常】Cookie 已过期,请查看电脑浏览器完成登录") + cookies = browser_login() + if not cookies: + send_to_wecom("登录超时,监控已退出。请手动重新运行。") + 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) + send_to_wecom(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_monitor.bat b/run_monitor.bat new file mode 100644 index 0000000..c856fc0 --- /dev/null +++ b/run_monitor.bat @@ -0,0 +1,12 @@ +@echo off +title Fengxiang Order Monitor +cd /d d:\coding\fengxiang +echo ========================================= +echo Fengxiang Order Monitor v3 +echo %date% %time% +echo ========================================= +echo. +python order_monitor.py +echo. +echo Monitor stopped. Press any key to close... +pause >nul diff --git a/setup_autostart.ps1 b/setup_autostart.ps1 new file mode 100644 index 0000000..ed43d19 --- /dev/null +++ b/setup_autostart.ps1 @@ -0,0 +1,31 @@ +# 设置丰享订单监控开机自启 +# 右键此文件 → "使用 PowerShell 运行",或在管理员 PowerShell 中执行 + +$taskName = "丰享订单监控" +$scriptPath = "d:\coding\fengxiang\run_monitor.bat" +$taskExists = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue + +if ($taskExists) { + Write-Host "检测到已有任务,正在更新..." + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false +} + +# 创建任务:用户登录时自动运行 +$action = New-ScheduledTaskAction -Execute "cmd.exe" -Argument "/c `"$scriptPath`"" +$trigger = New-ScheduledTaskTrigger -AtLogOn +$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -LogonType Interactive -RunLevel Highest +$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew + +Register-ScheduledTask -TaskName $taskName ` + -Action $action ` + -Trigger $trigger ` + -Principal $principal ` + -Settings $settings ` + -Description "丰享订单监控 - 开机自动启动,监控新订单并推送到企业微信和小爱音箱" + +Write-Host "✓ 开机自启设置成功!" +Write-Host " 任务名称: $taskName" +Write-Host " 下次登录 Windows 时将自动运行" +Write-Host "" +Write-Host "立即测试运行?运行以下命令:" +Write-Host " Start-ScheduledTask -TaskName '$taskName'" 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() diff --git a/test_push.py b/test_push.py new file mode 100644 index 0000000..eed318a --- /dev/null +++ b/test_push.py @@ -0,0 +1,128 @@ +""" +测试脚本:验证企业微信推送 + 小爱音箱播报 +- 拉取最新订单,推送到企业微信和小爱音箱 +- 显示 API 返回结果,方便排查问题 +""" + +import json +import time +import requests +import asyncio +from pathlib import Path +from aiohttp import ClientSession +from miservice import MiAccount, MiNAService + +WEBHOOK_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=644ab6d9-3b66-4166-88e9-5a8a89e3731d" +DATA_API = "https://fs.szfx.com/saasmerchant/pcweb/order/quickpayorder/list" +SHOP_ID = "20434543575189" + +XIAOMI_USER_ID = "1136458602" +XIAOMI_TOKEN_PATH = str(Path.home() / ".mi.token") +XIAOMI_SPEAKER_DID = "3ba2c1e8-d8cb-45c5-b88a-15624e7a02f3" + + +def fmt_money(cents): + return f"{cents / 100:.2f}元" if cents else "0.00元" + + +def fmt_ts(ts): + import datetime + return datetime.datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") if ts else "-" + + +def load_cookies(): + with open("cookies.json", "r", encoding="utf-8") as f: + return json.load(f) + + +def fetch_latest_order(): + """使用 cookies.json 拉取最新订单""" + 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"[FAIL] API error: errno={data.get('errno')}, errmsg={data.get('errmsg')}") + return None + return data["data"]["list"][0] + + +def test_wecom(msg): + """发送到企业微信""" + print("\n--- Test: WeChat Work ---") + payload = {"msgtype": "text", "text": {"content": msg}} + try: + resp = requests.post(WEBHOOK_URL, json=payload, timeout=10) + result = resp.json() + print(f" Response: {json.dumps(result, ensure_ascii=False)}") + if result.get("errcode") == 0: + print(" [OK] WeChat push success") + else: + print(" [FAIL] WeChat push failed") + except Exception as e: + print(f" [FAIL] Exception: {e}") + + +async def _tts(text): + async with ClientSession() as session: + account = MiAccount(session, XIAOMI_USER_ID, None, XIAOMI_TOKEN_PATH) + mina = MiNAService(account) + return await mina.text_to_speech(XIAOMI_SPEAKER_DID, text) + + +def test_speaker(msg): + """发送到小爱音箱""" + print("\n--- Test: Xiaomi Speaker ---") + print(f" Text: {msg}") + try: + result = asyncio.run(_tts(msg)) + print(f" Response: {json.dumps(result, ensure_ascii=False)}") + if result and result.get("code") == 0: + print(" [OK] Speaker TTS success") + else: + print(" [FAIL] Speaker TTS failed - token may be expired, run setup_xiaomi.py") + except Exception as e: + print(f" [FAIL] Exception: {e}") + + +def main(): + print("=" * 50) + print(" Fengxiang Order Monitor - Test Script") + print("=" * 50) + + # 1. Fetch latest order + print("\n1. Fetch latest order...") + order = fetch_latest_order() + if not order: + print("[FAIL] Cannot fetch order, check cookies.json") + return + + print(f" orderId: {order['orderId']}") + print(f" shopName: {order['shopName']}") + print(f" allPrice: {fmt_money(order['allPrice'])}") + print(f" amountPayable: {fmt_money(order['amountPayable'])}") + print(f" payTime: {fmt_ts(order['payTime'])}") + + # 2. Format message + msg = f"【丰享丰食】订单收款成功,收款{fmt_money(order['amountPayable'])}" + print(f"\n2. Message to send:\n {msg}") + + # 3. Test WeChat and Speaker + test_wecom(msg) + test_speaker(msg) + + print("\n" + "=" * 50) + print(" Test complete. Check:") + print(" - WeChat: open the enterprise WeChat group") + print(" - Speaker: the L15A speaker in the shop") + print("=" * 50) + + +if __name__ == "__main__": + main()