From 10dc45198c5731d2bb1247ff448bc56b9ba603d5 Mon Sep 17 00:00:00 2001 From: dekun Date: Tue, 26 May 2026 10:35:52 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BC=81=E4=B8=9A=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E6=8E=A8=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/main.py | 9 ++- backend/app/scheduler.py | 4 +- backend/app/wecom.py | 123 ++++++++++++++++++++++++++++----------- web/app.js | 4 +- web/index.html | 2 +- 5 files changed, 100 insertions(+), 42 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index 236c272..4900533 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -16,7 +16,7 @@ from .periods import get_daybefore_period, get_today_period, get_yesterday_perio from .scheduler import job_finalize_yesterday, job_push_wecom, job_refresh_today, start_scheduler, startup_tasks, stop_scheduler from .stats import compute_three_day_stats from .aggregator import aggregate_period -from .wecom import build_markdown, build_push_payload, send_wecom_markdown +from .wecom import build_markdown, build_push_payload, send_push_payload from .state import get_today_cache logging.basicConfig( @@ -111,14 +111,17 @@ async def api_push_test(): snap = get_latest_snapshot("yesterday") if not snap: raise HTTPException(500, "无法生成昨日数据") - ok, msg = await send_wecom_markdown(payload["markdown"]) + ok, msg = await send_push_payload(payload) log_push(snap["period_start"], snap["period_end"], ok, msg) if not ok: raise HTTPException(500, f"推送失败: {msg}") + parts = payload.get("parts", 1) return { "success": True, - "message": f"已推送 {payload.get('count', 0)} 个三日交集币种", + "message": f"已推送 {payload.get('count', 0)} 个币种" + + (f"(分 {parts} 条消息)" if parts > 1 else ""), "count": payload.get("count", 0), + "parts": parts, } diff --git a/backend/app/scheduler.py b/backend/app/scheduler.py index e3ff6d8..45bbcf7 100644 --- a/backend/app/scheduler.py +++ b/backend/app/scheduler.py @@ -13,7 +13,7 @@ from .periods import get_daybefore_period, get_today_period, get_yesterday_perio from .state import get_today_cache, set_today_cache from .funding_store import prefetch_funding from .kline_store import prefetch_symbols -from .wecom import build_push_payload, send_wecom_markdown +from .wecom import build_push_payload, send_push_payload logger = logging.getLogger(__name__) @@ -112,7 +112,7 @@ async def job_push_wecom() -> None: if not payload.get("ok"): logger.warning("WeCom push skipped: %s", payload.get("message")) return - ok, msg = await send_wecom_markdown(payload["markdown"]) + ok, msg = await send_push_payload(payload) log_push(ps, pe, ok, msg) if ok: logger.info("WeCom push succeeded") diff --git a/backend/app/wecom.py b/backend/app/wecom.py index 1ac1946..83ab2c0 100644 --- a/backend/app/wecom.py +++ b/backend/app/wecom.py @@ -9,6 +9,9 @@ from .stats import compute_three_day_stats logger = logging.getLogger(__name__) +# 企业微信 markdown 单条上限 4096,预留余量 +WECOM_MD_MAX = 3800 + def _format_period_label(period_start: str, period_end: str) -> str: start = period_start[:16].replace("T", " ") @@ -16,17 +19,64 @@ def _format_period_label(period_start: str, period_end: str) -> str: return f"{start} ~ {end}" -def _day_line(label: str, row: dict | None) -> str: +def _day_seg(label: str, row: dict | None) -> str: if not row or row.get("rank") is None: - return f"> {label}:—" - pct = row.get("price_change_pct_fmt") or f"{row.get('price_change_pct', 0):+.2f}%" - vol = row.get("quote_volume_fmt") or str(row.get("quote_volume", "")) - fr = row.get("funding_rate_fmt") or "—" - return f"> {label}:#{row['rank']} · 额 {vol} · 涨跌 {pct} · 费率 {fr}" + return f"{label}—" + pct = row.get("price_change_pct_fmt") or f"{row.get('price_change_pct', 0):+.1f}%" + return f"{label}#{row['rank']}{pct}" + + +def _coin_line(rank: int, row: dict) -> str: + sym = row["symbol"] + y, t, b = row.get("yesterday"), row.get("today"), row.get("daybefore") + return ( + f"**{rank}. {sym}** " + f"{_day_seg('昨', y)} {_day_seg('今', t)} {_day_seg('前', b)}" + ) + + +def _build_header(period_label: str, count: int, part: int | None = None) -> list[str]: + title = "## 币安 U本位 · 三日Top30交集" + if part and part > 1: + title += f"({part})" + lines = [ + title, + "", + f"> 昨日周期 {period_label}", + f"> 共 **{count}** 个 · 昨/今/前 = 排名+涨跌幅", + "", + ] + return lines + + +def _split_messages( + period_label: str, count: int, items: list[dict] +) -> list[str]: + """按企微长度限制拆成多条 markdown。""" + if not items: + body = "\n".join(_build_header(period_label, 0) + ["**暂无交集币种**"]) + return [body] + + messages: list[str] = [] + part = 1 + lines = _build_header(period_label, count, part) + for i, row in enumerate(items, 1): + coin = _coin_line(i, row) + extra = len(coin) + 1 + if len("\n".join(lines)) + extra > WECOM_MD_MAX and len(lines) > 5: + messages.append("\n".join(lines).rstrip()) + part += 1 + lines = _build_header(period_label, count, part) + lines.append(coin) + if part == 1: + lines.append("") + lines.append("> 仅三日均为 Top30 交集,涨跌不限") + messages.append("\n".join(lines).rstrip()) + return messages def build_push_payload() -> dict[str, Any]: - """构建企微推送内容:仅三日 Top30 交集,列表排版(非表格)。""" + """构建企微推送:仅三日 Top30 交集,紧凑单行/币,超长自动分批。""" stats = compute_three_day_stats() periods = stats.get("periods") or {} y_meta = periods.get("yesterday") or {} @@ -53,65 +103,51 @@ def build_push_payload() -> dict[str, Any]: "count": 0, "period_label": period_label, "markdown": md, + "messages": [md], + "parts": 1, "items": [], } items = stats.get("items") or [] - lines = [ - "## 币安 U本位 · 三日Top30交集", - "", - f"> **昨日周期**(北京时间 8:00 切日)", - f"> {period_label}", - f"> 连续三日成交额均为 Top{settings.top_n},共 **{len(items)}** 个", - "", - ] + messages = _split_messages(period_label, len(items), items) preview_items: list[dict] = [] for i, row in enumerate(items, 1): - sym = row["symbol"] - t, y, b = row.get("today"), row.get("yesterday"), row.get("daybefore") - lines.append(f"### {i}. {sym}") - lines.append(_day_line("昨日", y)) - lines.append(_day_line("今日", t)) - lines.append(_day_line("前日", b)) - lines.append("") preview_items.append( { "rank": i, - "symbol": sym, - "today": t, - "yesterday": y, - "daybefore": b, + "symbol": row["symbol"], + "today": row.get("today"), + "yesterday": row.get("yesterday"), + "daybefore": row.get("daybefore"), "total_quote_volume": row.get("total_quote_volume"), } ) - if not items: - lines.append("**暂无交集币种**(请确认今日/昨日/前日快照均已生成)") - - lines.append("---") - lines.append("> 说明:仅推送三日均为成交额 Top30 的合约;涨跌不限") - return { "ok": True, "message": stats.get("criteria", ""), "count": len(items), "period_label": period_label, - "markdown": "\n".join(lines), + "markdown": messages[0] if messages else "", + "messages": messages, + "parts": len(messages), "items": preview_items, } def build_markdown(snapshot: dict | None = None) -> str: - """兼容旧调用:返回企微 Markdown 文本(忽略 snapshot,以三日交集为准)。""" _ = snapshot - return build_push_payload()["markdown"] + payload = build_push_payload() + return payload.get("markdown") or "" async def send_wecom_markdown(content: str) -> tuple[bool, str]: url = settings.wecom_webhook_url.strip() if not url: return False, "WECOM_WEBHOOK_URL 未配置" + if len(content) > 4096: + return False, f"内容过长({len(content)}字),请使用分批推送" payload = {"msgtype": "markdown", "markdown": {"content": content}} last_err = "" for attempt in range(3): @@ -128,3 +164,20 @@ async def send_wecom_markdown(content: str) -> tuple[bool, str]: last_err = str(e) logger.warning("WeCom push attempt %d failed: %s", attempt + 1, e) return False, last_err + + +async def send_push_payload(payload: dict) -> tuple[bool, str]: + """发送推送,超长时按 messages 列表逐条发送。""" + parts = payload.get("messages") or [payload.get("markdown", "")] + if not parts or not parts[0]: + return False, "无推送内容" + for i, content in enumerate(parts, 1): + ok, msg = await send_wecom_markdown(content) + if not ok: + suffix = f"(第 {i}/{len(parts)} 条)" if len(parts) > 1 else "" + return False, f"{msg}{suffix}" + if i < len(parts): + logger.info("WeCom push part %d/%d sent", i, len(parts)) + if len(parts) > 1: + return True, f"已分 {len(parts)} 条发送" + return True, "ok" diff --git a/web/app.js b/web/app.js index 41eb1aa..9314837 100644 --- a/web/app.js +++ b/web/app.js @@ -356,7 +356,9 @@ function renderWecomPreview(payload) { return; } if (meta) { - meta.textContent = `${payload.period_label || "—"} · ${payload.count} 个币种`; + const partsHint = + payload.parts > 1 ? ` · 企微分 ${payload.parts} 条发送` : ""; + meta.textContent = `${payload.period_label || "—"} · ${payload.count} 个币种${partsHint}`; } if (!payload.items?.length) { cards.innerHTML = '

暂无三日交集币种

'; diff --git a/web/index.html b/web/index.html index 2103e00..0849226 100644 --- a/web/index.html +++ b/web/index.html @@ -80,7 +80,7 @@