Final version: Fixed date selection logic and verified full flow
This commit is contained in:
@@ -110,38 +110,62 @@ class ReportAutomation:
|
|||||||
logger.info(f"访问统计页面: {self.secsion_stats_url}")
|
logger.info(f"访问统计页面: {self.secsion_stats_url}")
|
||||||
await page.goto(self.secsion_stats_url)
|
await page.goto(self.secsion_stats_url)
|
||||||
|
|
||||||
# 等待页面加载完成,以“导出报表”按钮为准
|
# 等待页面加载完成
|
||||||
logger.info("等待统计页面加载...")
|
logger.info("等待统计页面加载...")
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
export_btn = page.get_by_role("button", name="导出报表")
|
export_btn = page.get_by_role("button", name="导出报表")
|
||||||
await export_btn.wait_for(state="visible", timeout=20000)
|
await export_btn.wait_for(state="visible", timeout=20000)
|
||||||
|
|
||||||
logger.info(f"设置查询日期范围: {self.start_date} 至 {self.end_date}")
|
logger.info(f"设置查询日期范围: {self.start_date} 至 {self.end_date}")
|
||||||
|
|
||||||
|
# 严格按照参考文件 secsion_export.py 的逻辑
|
||||||
try:
|
try:
|
||||||
# 找到所有的日期输入框
|
# 两个日期输入框都叫“请选择日期”,用 nth 区分
|
||||||
inputs = page.locator("input")
|
start_input = page.get_by_role("textbox", name="请选择日期").nth(0)
|
||||||
count = await inputs.count()
|
end_input = page.get_by_role("textbox", name="请选择日期").nth(1)
|
||||||
|
|
||||||
date_input_indices = []
|
async def set_date(input_box, date_str: str):
|
||||||
for i in range(count):
|
logger.info(f"尝试设置日期: {date_str}")
|
||||||
val = await inputs.nth(i).input_value()
|
# 1. 点击输入框
|
||||||
if "-" in val and len(val) >= 10:
|
await input_box.click()
|
||||||
date_input_indices.append(i)
|
|
||||||
|
|
||||||
if len(date_input_indices) >= 2:
|
# 2. 先尝试直接填值 + Enter (参考文件逻辑)
|
||||||
# 填入开始日期
|
try:
|
||||||
await inputs.nth(date_input_indices[0]).click()
|
# 由于是 readonly,fill 可能会超时,这里设置较短超时
|
||||||
await inputs.nth(date_input_indices[0]).fill(self.start_date)
|
await input_box.fill(date_str, timeout=2000)
|
||||||
await inputs.nth(date_input_indices[0]).press("Enter")
|
await input_box.press("Enter")
|
||||||
|
await page.wait_for_timeout(200)
|
||||||
|
except Exception:
|
||||||
|
logger.info("直接填值失败或超时,将尝试点击日历单元格")
|
||||||
|
|
||||||
|
# 3. 若没有生效,则打开日历点击“日”单元格 (参考文件逻辑)
|
||||||
|
val = await input_box.input_value()
|
||||||
|
if val != date_str:
|
||||||
|
logger.info(f"值未同步({val} != {date_str}),执行日历单元格点击")
|
||||||
|
# 确保日历已弹出
|
||||||
|
await input_box.click()
|
||||||
|
day = str(int(date_str.split("-")[2]))
|
||||||
|
# 参考文件使用 cell 角色
|
||||||
|
# page.get_by_role("cell", name=day).click()
|
||||||
|
# 考虑到可能有多个月份显示,取最后一个弹出的
|
||||||
|
await page.get_by_role("cell", name=day).last.click()
|
||||||
|
await page.wait_for_timeout(500)
|
||||||
|
|
||||||
|
await set_date(start_input, self.start_date)
|
||||||
|
await set_date(end_input, self.end_date)
|
||||||
|
|
||||||
|
# 等待数据请求完成
|
||||||
|
logger.info("等待数据请求完成...")
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
# 截图确认日期设置后的状态
|
||||||
|
logger.info("保存日期设置确认截图: date_setting_check.png")
|
||||||
|
await page.screenshot(path="date_setting_check.png")
|
||||||
|
|
||||||
# 填入结束日期
|
|
||||||
await inputs.nth(date_input_indices[1]).click()
|
|
||||||
await inputs.nth(date_input_indices[1]).fill(self.end_date)
|
|
||||||
await inputs.nth(date_input_indices[1]).press("Enter")
|
|
||||||
else:
|
|
||||||
logger.warning("未找到足够的日期输入框,尝试使用默认日期导出")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"填充日期时遇到问题: {str(e)}")
|
logger.error(f"日期选择逻辑执行失败: {str(e)}")
|
||||||
|
await page.screenshot(path="date_error.png")
|
||||||
|
raise e
|
||||||
|
|
||||||
# 点击导出报表并捕获下载
|
# 点击导出报表并捕获下载
|
||||||
logger.info("点击导出报表...")
|
logger.info("点击导出报表...")
|
||||||
@@ -149,7 +173,8 @@ class ReportAutomation:
|
|||||||
await export_btn.click()
|
await export_btn.click()
|
||||||
|
|
||||||
download = await download_info.value
|
download = await download_info.value
|
||||||
filename = f"commodity_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
# 使用服务器建议的文件名
|
||||||
|
filename = download.suggested_filename
|
||||||
save_path = os.path.join(self.download_dir, filename)
|
save_path = os.path.join(self.download_dir, filename)
|
||||||
|
|
||||||
await download.save_as(save_path)
|
await download.save_as(save_path)
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
secsion.com 报表导出脚本(给 OpenClaw/自动化调用)
|
||||||
|
|
||||||
|
功能:
|
||||||
|
1) 登录 https://secsion.com:8000/
|
||||||
|
2) 进入 /commodityStatistics
|
||||||
|
3) 选择开始/结束日期
|
||||||
|
4) 点击“导出报表”,保存 xlsx 到本地目录
|
||||||
|
|
||||||
|
推荐使用 Playwright(更稳定地处理下载与证书忽略)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from playwright.sync_api import TimeoutError as PWTimeoutError
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Config:
|
||||||
|
base_url: str = "https://secsion.com:8000"
|
||||||
|
login_url: str = "https://secsion.com:8000/login?redirect=%252Fhomepage"
|
||||||
|
report_url: str = "https://secsion.com:8000/commodityStatistics"
|
||||||
|
role: str = "店铺"
|
||||||
|
username: str = ""
|
||||||
|
password: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_date(s: str) -> str:
|
||||||
|
# 统一校验并格式化为 YYYY-MM-DD
|
||||||
|
dt = datetime.strptime(s, "%Y-%m-%d")
|
||||||
|
return dt.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_dir(p: Path) -> None:
|
||||||
|
p.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _download_via_requests(url: str, out_dir: Path, filename: Optional[str] = None) -> Path:
|
||||||
|
# 导出链接常见为 https://secsion.com:8082/xxx.xlsx(可能证书不受信任)
|
||||||
|
# verify=False 用于忽略证书(等价于浏览器“继续前往”)
|
||||||
|
resp = requests.get(url, stream=True, timeout=120, verify=False)
|
||||||
|
resp.raise_for_status()
|
||||||
|
|
||||||
|
if not filename:
|
||||||
|
path = urlparse(url).path
|
||||||
|
filename = Path(path).name or f"commodity_export_{int(time.time())}.xlsx"
|
||||||
|
if not filename.lower().endswith(".xlsx"):
|
||||||
|
filename += ".xlsx"
|
||||||
|
|
||||||
|
save_path = out_dir / filename
|
||||||
|
with open(save_path, "wb") as f:
|
||||||
|
for chunk in resp.iter_content(chunk_size=1024 * 128):
|
||||||
|
if chunk:
|
||||||
|
f.write(chunk)
|
||||||
|
return save_path
|
||||||
|
|
||||||
|
|
||||||
|
def export_report(
|
||||||
|
*,
|
||||||
|
cfg: Config,
|
||||||
|
start_date: str,
|
||||||
|
end_date: str,
|
||||||
|
out_dir: Path,
|
||||||
|
headless: bool = True,
|
||||||
|
timeout_ms: int = 30_000,
|
||||||
|
) -> Path:
|
||||||
|
_ensure_dir(out_dir)
|
||||||
|
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = p.chromium.launch(headless=headless)
|
||||||
|
context = browser.new_context(ignore_https_errors=True, accept_downloads=True)
|
||||||
|
page = context.new_page()
|
||||||
|
|
||||||
|
# 1) 打开登录页
|
||||||
|
page.goto(cfg.login_url, timeout=timeout_ms, wait_until="domcontentloaded")
|
||||||
|
|
||||||
|
# 2) 选择角色(默认“店铺”)
|
||||||
|
# 页面是 radio,按可见文本定位
|
||||||
|
page.get_by_role("radio", name=cfg.role).check()
|
||||||
|
|
||||||
|
# 3) 输入账号密码
|
||||||
|
page.get_by_role("textbox", name="请输入用户名").fill(cfg.username)
|
||||||
|
page.get_by_role("textbox", name="请输入密码").fill(cfg.password)
|
||||||
|
|
||||||
|
# 4) 登录
|
||||||
|
page.get_by_role("button", name="登 录").click()
|
||||||
|
# 等待跳转(不强依赖具体 URL,避免页面改版)
|
||||||
|
page.wait_for_load_state("networkidle", timeout=timeout_ms)
|
||||||
|
|
||||||
|
# 5) 进入报表页
|
||||||
|
page.goto(cfg.report_url, timeout=timeout_ms, wait_until="domcontentloaded")
|
||||||
|
page.wait_for_load_state("networkidle", timeout=timeout_ms)
|
||||||
|
|
||||||
|
# 6) 设置日期范围
|
||||||
|
# 两个日期输入框都叫“请选择日期”,用 nth 区分
|
||||||
|
start_input = page.get_by_role("textbox", name="请选择日期").nth(0)
|
||||||
|
end_input = page.get_by_role("textbox", name="请选择日期").nth(1)
|
||||||
|
|
||||||
|
def set_date(input_box, date_str: str) -> None:
|
||||||
|
# 先尝试直接填值 + Enter(部分组件会生效)
|
||||||
|
input_box.click()
|
||||||
|
input_box.fill(date_str)
|
||||||
|
input_box.press("Enter")
|
||||||
|
page.wait_for_timeout(200)
|
||||||
|
|
||||||
|
# 若没有生效,则打开日历点击“日”单元格(更稳)
|
||||||
|
val = input_box.input_value()
|
||||||
|
if val != date_str:
|
||||||
|
input_box.click()
|
||||||
|
day = str(int(date_str.split("-")[2]))
|
||||||
|
# 月份不一致时这里可能需要先切换月份;多数情况下导出同月数据足够。
|
||||||
|
page.get_by_role("cell", name=day).click()
|
||||||
|
page.wait_for_timeout(200)
|
||||||
|
|
||||||
|
set_date(start_input, start_date)
|
||||||
|
set_date(end_input, end_date)
|
||||||
|
|
||||||
|
# 7) 导出
|
||||||
|
export_btn = page.get_by_role("button", name="导出报表")
|
||||||
|
|
||||||
|
# 尝试走 Playwright 的下载通道(最优)
|
||||||
|
try:
|
||||||
|
with page.expect_download(timeout=20_000) as d:
|
||||||
|
export_btn.click()
|
||||||
|
download = d.value
|
||||||
|
suggested = download.suggested_filename or f"commodity_export_{start_date}_{end_date}.xlsx"
|
||||||
|
if not suggested.lower().endswith(".xlsx"):
|
||||||
|
suggested += ".xlsx"
|
||||||
|
save_path = out_dir / suggested
|
||||||
|
download.save_as(str(save_path))
|
||||||
|
browser.close()
|
||||||
|
return save_path
|
||||||
|
except PWTimeoutError:
|
||||||
|
# 回退:有些情况下会打开一个新地址(.xlsx 链接)但不触发下载事件
|
||||||
|
export_btn.click()
|
||||||
|
page.wait_for_timeout(1500)
|
||||||
|
|
||||||
|
xlsx_url = None
|
||||||
|
for pg in context.pages:
|
||||||
|
if pg.url.lower().endswith(".xlsx"):
|
||||||
|
xlsx_url = pg.url
|
||||||
|
break
|
||||||
|
if not xlsx_url and page.url.lower().endswith(".xlsx"):
|
||||||
|
xlsx_url = page.url
|
||||||
|
|
||||||
|
if not xlsx_url:
|
||||||
|
browser.close()
|
||||||
|
raise RuntimeError("未捕获到导出的 xlsx 链接;请把页面导出后的实际跳转 URL 发我,我再适配。")
|
||||||
|
|
||||||
|
# 直链下载(忽略证书)
|
||||||
|
filename = f"commodity_export_{start_date}_{end_date}.xlsx"
|
||||||
|
save_path = _download_via_requests(xlsx_url, out_dir=out_dir, filename=filename)
|
||||||
|
browser.close()
|
||||||
|
return save_path
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="secsion.com 报表导出(支持某一天/时间段)。",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||||
|
)
|
||||||
|
parser.add_argument("--start", required=False, help="开始日期 YYYY-MM-DD(若仅导出某一天,可只填 start)")
|
||||||
|
parser.add_argument("--end", required=False, help="结束日期 YYYY-MM-DD(缺省时与 start 相同)")
|
||||||
|
parser.add_argument("--out", default="./downloads", help="导出文件保存目录")
|
||||||
|
parser.add_argument("--headless", action="store_true", help="无头模式运行(CI/机器人推荐)")
|
||||||
|
parser.add_argument("--show", action="store_true", help="显示浏览器窗口(调试用,优先级高于 --headless)")
|
||||||
|
|
||||||
|
parser.add_argument("--role", default="店铺", help="登录角色:店铺编码/管理员/店铺/业务员")
|
||||||
|
parser.add_argument("--username", default=os.getenv("SECSION_USERNAME", ""), help="账号(也可用环境变量 SECSION_USERNAME)")
|
||||||
|
parser.add_argument("--password", default=os.getenv("SECSION_PASSWORD", ""), help="密码(也可用环境变量 SECSION_PASSWORD)")
|
||||||
|
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
# 交互式兜底(给 OpenClaw 也可以直接传参,不走交互)
|
||||||
|
start = args.start or input("请输入开始日期(YYYY-MM-DD): ").strip()
|
||||||
|
end = args.end or input("请输入结束日期(YYYY-MM-DD,回车=同开始日期): ").strip() or start
|
||||||
|
|
||||||
|
try:
|
||||||
|
start = _parse_date(start)
|
||||||
|
end = _parse_date(end)
|
||||||
|
except ValueError:
|
||||||
|
print("日期格式错误,请使用 YYYY-MM-DD,例如 2026-04-15", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
if not args.username or not args.password:
|
||||||
|
print("缺少账号或密码:请传 --username/--password 或设置环境变量 SECSION_USERNAME/SECSION_PASSWORD", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
cfg = Config(role=args.role, username=args.username, password=args.password)
|
||||||
|
out_dir = Path(args.out).expanduser().resolve()
|
||||||
|
headless = False if args.show else bool(args.headless)
|
||||||
|
|
||||||
|
try:
|
||||||
|
saved = export_report(cfg=cfg, start_date=start, end_date=end, out_dir=out_dir, headless=headless)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"导出失败:{e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(str(saved))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
|
|
||||||
Reference in New Issue
Block a user