Initial commit: Fengxiang order monitor with WeChat & Xiaomi speaker push

This commit is contained in:
houhuan
2026-04-30 14:25:34 +08:00
commit 1bb7bba970
10 changed files with 970 additions and 0 deletions
+23
View File
@@ -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/
+106
View File
@@ -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
+114
View File
@@ -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()
+52
View File
@@ -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()
+330
View File
@@ -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()
+91
View File
@@ -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()
+12
View File
@@ -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
+31
View File
@@ -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'"
+83
View File
@@ -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()
+128
View File
@@ -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()