Files

398 lines
25 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""预存「商业态势总览」演示数据(task-data 数据交付 · 第一块对接)。
对齐前端页面(code-1 仓库)``#/situation-overview/action/business-overview?positionId=planning``
的 mock 逻辑(``features/business-overview/mock/businessTimelineSnapshots.js``):
- 默认任务 ``BUSINESS-OVERVIEW-DEMO``(对方系统任务号,前端对接用它);
- 13 期时间轴刻度 = 页面小时刻度:2026-06-17 22:00 → 2026-06-18 10:00(每小时一期,
Beijing;最后一期为「当前期」);
- 每期 ``block_1`` payload = 页面 ``createBusinessTimelineSnapshot`` 同构 JSON
(整页 dashboard 对象,前端拿到即可直接渲染),按同一 slot 公式选相位;
- ``block_1`` 标题置为「商业态势总览」,其余块保持默认。
幂等:重复运行会先级联删除演示任务再重建(不影响其它任务)。
用法(在 backend 目录)
======================
::
PYTHONPATH=. uv run python scripts/seed_business_overview_demo.py # 预存
PYTHONPATH=. uv run python scripts/seed_business_overview_demo.py --dry-run # 只打印不写库
"""
from __future__ import annotations
import argparse
import asyncio
import logging
from datetime import datetime, timedelta, timezone
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger("seed_business_overview_demo")
SOURCE_TASK_ID = "BUSINESS-OVERVIEW-DEMO"
TZ_BEIJING = timezone(timedelta(hours=8))
# 前端 BUSINESS_TIMELINE_ANCHOR = new Date(2026, 5, 18, 10, 30)(本地时区,页面按北京时区使用)。
ANCHOR = datetime(2026, 6, 18, 10, 30, tzinfo=TZ_BEIJING)
# 页面小时刻度:13 个刻度、终点 = 锚点整点,逐小时回退(与截图范围一致)。
TICK_COUNT = 13
def timeline_ticks() -> list[datetime]:
end = ANCHOR.replace(minute=0, second=0, microsecond=0)
return [end - timedelta(hours=TICK_COUNT - 1 - index) for index in range(TICK_COUNT)]
# ── 前端 dashboardViews.overview(BusinessOverviewDashboard.vue,逐字对齐) ────
BASE_DASHBOARD: dict = {
"leftTitle": "经营概览",
"signalTitle": "经营信号",
"actionTitle": "本月重点动作",
"watchTitle": "经营关注",
"pageTitle": "商业态势总览",
"pageDescription": "围绕收入增长、回款效率与商机质量,实时掌握经营节奏。",
"briefs": [
{"title": "当前判断", "label": "经营主线", "description": "收入增长保持稳定,当前工作重点从获取增量转向提升重点项目的签约确定性。", "tone": "blue"},
{"title": "协同重点", "label": "本周推进", "description": "销售、交付与客户成功团队围绕到期客户建立共跟机制,确保信息与行动同步。", "tone": "green"},
{"title": "需持续关注", "label": "节奏风险", "description": "部分项目的审批周期仍有不确定性,应提前准备替代推进路径。", "tone": "amber"},
],
"objectives": [
{"name": "年度收入目标", "label": "核心目标", "category": "财务增长", "description": "保持收入目标达成率在 82% 以上,确保二季度末进入集中签约窗口。", "level": "core"},
{"name": "重点客户续约", "label": "关键目标", "category": "客户经营", "description": "提前锁定 12 个到期项目的续约与增购机会,提升存量收入质量。", "level": "key"},
{"name": "回款效率提升", "label": "潜力目标", "category": "现金流", "description": "分层推进逾期账款治理,保障季度现金流目标稳定达成。", "level": "potential"},
],
"events": [],
"operatingMetrics": [
{"label": "年度确认收入", "value": "¥128.6M", "change": "+18.4%", "tone": "positive"},
{"label": "回款达成率", "value": "94.7%", "change": "+3.1pct", "tone": "positive"},
{"label": "活跃客户数", "value": "286", "change": "+22", "tone": "positive"},
{"label": "重点商机金额", "value": "¥348.2M", "change": "环比 +9.6%", "tone": "neutral"},
],
"headlineMetrics": [
{"label": "年度收入目标", "value": "¥156.0M", "change": "达成 82.4%", "tone": "positive"},
{"label": "综合毛利率", "value": "31.8%", "change": "同比 +1.7pct", "tone": "positive"},
{"label": "销售费用率", "value": "12.6%", "change": "预算内", "tone": "neutral"},
],
"businessNarratives": [],
"insights": [],
"priorities": [
{"index": "01", "title": "华东渠道拓展", "description": "完成 3 家核心伙伴的联合方案签约,扩大区域覆盖。", "status": "推进中", "tone": "blue"},
{"index": "02", "title": "大客户续约提升", "description": "聚焦 12 个到期项目,提前锁定续约与增购机会。", "status": "重点", "tone": "amber"},
{"index": "03", "title": "回款专项治理", "description": "分层跟进逾期账款,确保季度现金流目标达成。", "status": "进行中", "tone": "blue"},
],
"signals": [
{"title": "商机储备充足", "description": "中高概率商机覆盖未来两个季度收入目标的 2.2 倍。", "tone": "positive"},
{"title": "产品组合持续优化", "description": "高毛利订阅业务占比提升至 46%,盈利结构更加稳健。", "tone": "positive"},
{"title": "客户活跃度提升", "description": "核心客户月活与增购意向同步上行,续费基础良好。", "tone": "blue"},
],
"watchItems": [
{"title": "两项重点商机审批周期延长", "description": "预计影响 ¥16.8M 的当季确认节奏,已进入管理层协同跟进。", "level": "中风险", "tone": "medium"},
{"title": "区域回款进度存在差异", "description": "西南区域 30 天以上应收占比高于整体均值 4.2 个百分点。", "level": "关注", "tone": "low"},
],
"visualization": {"type": "trend", "title": "收入与经营目标趋势"},
"chart": {
"title": "收入与经营目标趋势",
"actualName": "确认收入",
"targetName": "经营目标",
"barName": "毛利率",
"ariaLabel": "近十二个月确认收入、经营目标与毛利率趋势图",
"leftAxisName": "收入(百万元)",
"rightAxisName": "毛利率",
"months": ["7月", "8月", "9月", "10月", "11月", "12月", "1月", "2月", "3月", "4月", "5月", "6月"],
"actual": [7.2, 8.1, 8.8, 9.6, 10.4, 11.8, 8.7, 9.3, 10.5, 11.2, 12.1, 13.4],
"target": [7.5, 8.3, 9.1, 9.8, 10.7, 12.2, 9.0, 9.6, 10.9, 11.5, 12.6, 13.0],
"bar": [28.1, 28.7, 29.2, 30.1, 29.8, 30.6, 30.2, 30.9, 31.3, 31.1, 31.6, 31.8],
"leftAxisMax": 16,
"leftAxisInterval": 4,
"rightAxisMin": 20,
"rightAxisMax": 40,
"rightAxisInterval": 5,
"rightAxisSuffix": "%",
},
}
# ── 前端 timelinePhases(businessTimelineSnapshots.js,逐字对齐) ─────────────
TIMELINE_PHASES: list[dict] = [
{
"name": "实时校准",
"summary": "重点项目签约口径完成最新校准,经营节奏保持在目标区间。",
"chartDelta": 0,
"marginDelta": 0,
"briefs": [
{"label": "实时判断", "description": "重点项目签约口径完成最新校准,确认收入与经营目标之间的差距继续收窄。"},
{"label": "联合跟进", "description": "销售、交付与客户成功团队已同步最新客户反馈,关键行动进入同频推进。"},
{"label": "审批观察", "description": "两项重点商机仍等待客户内部审批,需要保留替代推进路径与资源窗口。"},
],
"objectives": [
{"description": "根据最新签约反馈,将年度收入目标达成率稳定在 82% 以上,并锁定季度末确认窗口。"},
{"description": "完成 12 个重点续约项目的责任人与下一步动作校准,优先保障高价值客户。"},
{"description": "按账龄与区域更新回款清单,保障本月现金流目标可追踪、可纠偏。"},
],
"events": [
{"title": "重点商机口径完成实时校准", "content": "华东 3 个渠道项目更新商务状态,预计新增确认收入 ¥8.6M。", "topic": "签约推进"},
{"title": "续约客户反馈同步至共跟清单", "content": "客户成功团队补齐关键使用场景,销售侧已更新下一次沟通动作。", "topic": "客户经营"},
{"title": "区域回款风险完成重新分层", "content": "西南区域两笔应收进入专项协同,责任人与预计到账节点已明确。", "topic": "现金流"},
],
"narratives": [
{"title": "收入节奏", "value": "健康", "description": "最新确认口径显示,重点项目仍按季度目标窗口稳定推进。"},
{"title": "客户结构", "value": "优化", "description": "订阅与增购机会占比继续提升,收入质量保持改善趋势。"},
{"title": "资金回笼", "value": "可控", "description": "高风险应收已进入专项协同,当前未形成新增重大敞口。"},
],
"insights": [
{"label": "增长动能", "highlight": "华东渠道与重点客户续约", "description": "仍是当前最确定的增量来源。"},
{"label": "即时建议", "highlight": "维持重点项目逐单校准,", "description": "避免审批变化影响季度确认节奏。"},
],
},
{
"name": "签约提速",
"summary": "渠道项目释放积极信号,当前窗口应优先推动商务条款确认。",
"chartDelta": 0.24,
"marginDelta": 0.18,
"briefs": [
{"label": "机会增强", "description": "两项高概率商机完成决策人确认,预计签约节奏较上一时点提前。"},
{"label": "商务协同", "description": "法务与交付资源已提前介入,联合处理付款条件和实施边界。"},
{"label": "资源提醒", "description": "售前资源短时集中,需要优先保障本周具备成交条件的重点项目。"},
],
"objectives": [
{"description": "抓住高概率项目的商务窗口,优先完成收入确认条件与合同边界核验。"},
{"description": "围绕续约增购项目补齐决策链信息,推动 4 个项目进入报价确认。"},
{"description": "将回款条款前置到合同协商环节,降低后续账期拉长风险。"},
],
"events": [
{"title": "华东渠道项目进入商务确认", "content": "核心伙伴已接受联合报价框架,合同条款进入最终核验。", "topic": "渠道拓展"},
{"title": "战略客户增购范围得到确认", "content": "客户新增两个业务场景,预计提升本次续约合同价值。", "topic": "客户增购"},
{"title": "售前资源启动优先级调度", "content": "两名解决方案专家转入高概率项目,保障本周方案澄清效率。", "topic": "资源协同"},
],
"narratives": [
{"title": "签约窗口", "value": "提速", "description": "高概率项目的商务条件趋于清晰,适合集中推动确认。"},
{"title": "增购空间", "value": "扩大", "description": "客户新增使用场景带动服务包与订阅组合机会。"},
{"title": "资源负荷", "value": "偏高", "description": "售前能力短时集中,需控制低概率项目的并行投入。"},
],
"insights": [
{"label": "最佳动作", "highlight": "优先完成付款条件和交付边界确认,", "description": "可进一步缩短合同流转周期。"},
{"label": "转化预测", "highlight": "高概率项目若按计划签约,", "description": "本月确认收入有望上调约 ¥2.1M。"},
],
},
{
"name": "续约推进",
"summary": "重点客户续约反馈集中回流,客户经营成为当前主要推进线。",
"chartDelta": 0.12,
"marginDelta": 0.1,
"briefs": [
{"label": "客户反馈", "description": "续约客户对服务稳定性评价积极,但希望进一步明确增购后的交付节奏。"},
{"label": "场景共创", "description": "客户成功团队正在补齐业务收益案例,为增购报价提供量化依据。"},
{"label": "决策风险", "description": "部分客户尚未确认最终预算归属,需要加强业务与采购双线沟通。"},
],
"objectives": [
{"description": "保持年度目标节奏,同时将存量客户贡献作为本周收入确定性的主要支撑。"},
{"description": "围绕 12 个重点客户逐一确认预算、决策人和续约范围,提升续约确定性。"},
{"description": "把续约付款节点纳入共跟清单,避免合同确认与实际到账节奏脱节。"},
],
"events": [
{"title": "重点续约客户完成价值复盘", "content": "客户确认现有服务达到预期,并提出新增数据分析场景。", "topic": "客户经营"},
{"title": "增购方案进入联合测算", "content": "销售与交付共同评估资源投入,准备输出分阶段报价方案。", "topic": "方案共创"},
{"title": "客户预算路径完成补充核验", "content": "4 个续约项目已明确预算归属,后续转入采购沟通。", "topic": "预算确认"},
],
"narratives": [
{"title": "客户意愿", "value": "积极", "description": "重点客户续约意愿稳定,增购讨论逐步进入量化阶段。"},
{"title": "价值证明", "value": "补强", "description": "业务收益案例正在完善,有利于缩短客户内部决策时间。"},
{"title": "预算路径", "value": "待确认", "description": "少量项目仍需同步业务负责人和采购侧预算口径。"},
],
"insights": [
{"label": "客户策略", "highlight": "用业务收益案例支撑增购报价,", "description": "比单纯价格优惠更有利于稳定续约价值。"},
{"label": "协同建议", "highlight": "销售与客户成功共同参加下一轮沟通,", "description": "同步解决价值与交付问题。"},
],
},
{
"name": "回款预警",
"summary": "区域到账节奏出现短时偏差,当前应优先压实高账龄应收责任。",
"chartDelta": -0.32,
"marginDelta": -0.26,
"briefs": [
{"label": "现金流提醒", "description": "西南区域两笔重点应收预计到账时间后移,月度回款预测需要下修。"},
{"label": "专项处置", "description": "财务与客户负责人已建立逐笔跟进机制,优先处理高账龄款项。"},
{"label": "目标影响", "description": "若本周未形成明确付款承诺,可能影响月末现金流安全边际。"},
],
"objectives": [
{"description": "收入目标保持不变,但新增合同需同步核验付款条件和客户信用情况。"},
{"description": "续约项目继续推进,并将历史回款表现纳入商务条款决策。"},
{"description": "集中处置 30 天以上应收,明确管理层协同事项与最晚到账节点。"},
],
"events": [
{"title": "区域到账预测出现偏差", "content": "两笔应收未按原计划进入付款流程,财务已发起风险提示。", "topic": "回款预警"},
{"title": "高账龄款项启动管理层协同", "content": "客户负责人补充付款障碍与决策链信息,形成逐笔处置方案。", "topic": "专项治理"},
{"title": "新增合同付款条件复核", "content": "商务团队统一检查首付款比例与账期,防止新增现金流风险。", "topic": "合同风控"},
],
"narratives": [
{"title": "到账节奏", "value": "承压", "description": "区域回款偏差扩大,需要以逐笔承诺替代笼统预测。"},
{"title": "风险敞口", "value": "中等", "description": "当前问题集中在少数高价值款项,仍具备专项化解条件。"},
{"title": "处置进度", "value": "已启动", "description": "财务、销售与管理层已进入联合跟进状态。"},
],
"insights": [
{"label": "风险动作", "highlight": "优先取得客户书面付款承诺,", "description": "并同步准备分期或节点化替代方案。"},
{"label": "经营影响", "highlight": "收入确认暂不受影响,", "description": "但现金流预测需保留约 ¥3.4M 风险缓冲。"},
],
},
{
"name": "资源协同",
"summary": "跨团队依赖集中显现,经营重点转向资源排期与关键节点保障。",
"chartDelta": 0.08,
"marginDelta": -0.08,
"briefs": [
{"label": "执行判断", "description": "商机质量保持稳定,交付与售前资源能否按期到位成为主要约束。"},
{"label": "排期协同", "description": "重点项目已按收入贡献与成交概率重新排序,资源将向前两级项目倾斜。"},
{"label": "交付提醒", "description": "两个项目存在并行上线需求,需要提前确认实施负责人和验收条件。"},
],
"objectives": [
{"description": "通过资源优先级调度保障高价值项目按期签约与确认,稳定年度目标节奏。"},
{"description": "对重点续约项目同步售前、交付和客户成功排期,减少内部等待时间。"},
{"description": "将资源投入与回款节点联动管理,优先保障付款条件清晰的项目。"},
],
"events": [
{"title": "重点项目资源优先级完成调整", "content": "售前与交付排期按项目价值重新排序,优先保障高概率机会。", "topic": "资源调度"},
{"title": "并行上线项目启动依赖核验", "content": "实施负责人、环境准备与验收标准已进入联合确认。", "topic": "交付协同"},
{"title": "跨部门问题转入日清机制", "content": "未闭环依赖将在每日经营例会上更新责任人与处理时限。", "topic": "机制优化"},
],
"narratives": [
{"title": "资源供给", "value": "趋紧", "description": "重点项目进入集中阶段,售前与交付资源需要动态调度。"},
{"title": "项目排序", "value": "已校准", "description": "当前优先级已综合收入贡献、成交概率、付款质量。"},
{"title": "协同效率", "value": "改善", "description": "跨部门问题进入日清机制,等待时间开始缩短。"},
],
"insights": [
{"label": "资源原则", "highlight": "优先保障高概率、高价值、付款清晰项目,", "description": "暂停低确定性定制投入。"},
{"label": "节奏建议", "highlight": "每日校准一次关键依赖,", "description": "确保资源变化及时反映到经营预测。"},
],
},
]
# ── 前端 createBusinessTimelineSnapshot 的 Python 复刻 ───────────────────────
def _positive_modulo(value: int, divisor: int) -> int:
return value % divisor
def _two_digits(value: int) -> str:
return f"{value:02d}"
def _format_snapshot_time(moment: datetime) -> str:
return f"{_two_digits(moment.month)}-{_two_digits(moment.day)} {_two_digits(moment.hour)}:{_two_digits(moment.minute)}"
def _merge_rows(source: list[dict], patches: list[dict]) -> list[dict]:
return [{**item, **(patches[index] if index < len(patches) else {})} for index, item in enumerate(source)]
def _adjust_series(values: list[float], delta: float) -> list[float]:
length = max(len(values), 1)
return [round(float(value) + delta * ((index + 1) / length), 1) for index, value in enumerate(values)]
def build_snapshot(moment: datetime) -> dict:
"""复刻 ``createBusinessTimelineSnapshot(baseDashboard, timestamp)``。"""
minute_slot = round((moment - ANCHOR).total_seconds() / 60)
slot = minute_slot + minute_slot // 5 + minute_slot // 60
phase = TIMELINE_PHASES[_positive_modulo(slot, len(TIMELINE_PHASES))]
time_label = _format_snapshot_time(moment)
events = [
{**event, "date": (moment - timedelta(minutes=17 * index)).strftime("%H:%M")}
for index, event in enumerate(phase["events"])
]
snapshot = dict(BASE_DASHBOARD)
snapshot.update(
{
"pageDescription": f"{phase['summary']} 当前展示 {time_label} 经营快照。",
"snapshotName": phase["name"],
"snapshotTime": moment.isoformat(),
"briefs": _merge_rows(BASE_DASHBOARD["briefs"], phase["briefs"]),
"objectives": _merge_rows(BASE_DASHBOARD["objectives"], phase["objectives"]),
"events": events,
"businessNarratives": phase["narratives"],
"insights": phase["insights"],
"chart": {
**BASE_DASHBOARD["chart"],
"ariaLabel": f"{time_label} {BASE_DASHBOARD['chart']['ariaLabel']}",
"actual": _adjust_series(BASE_DASHBOARD["chart"]["actual"], phase["chartDelta"]),
"bar": _adjust_series(BASE_DASHBOARD["chart"]["bar"], phase["marginDelta"]),
},
}
)
return snapshot
# ── 预存主流程 ───────────────────────────────────────────────────────────────
async def _run(dry_run: bool) -> int:
from deerflow.config import get_app_config
from deerflow.persistence.engine import close_engine, get_session_factory, init_engine_from_config
from deerflow.persistence.task_analysis import make_task_analysis_store
from deerflow.runtime.task_analysis.data_delivery import submit_block_data
config = get_app_config()
await init_engine_from_config(config.database)
sf = get_session_factory()
store = make_task_analysis_store(sf)
ticks = timeline_ticks()
snapshots = {tick: build_snapshot(tick) for tick in ticks}
if dry_run:
for tick in ticks:
snap = snapshots[tick]
logger.info("期 %s → %s · %s · %s", ticks.index(tick) + 1, tick.isoformat(), snap["snapshotName"], snap["pageDescription"][:40])
await close_engine()
return 0
# 幂等:演示任务已存在则级联删除后重建。
existing = await store.get_task_by_source(SOURCE_TASK_ID)
if existing is not None:
logger.info("演示任务已存在(id=%s),删除后重建", existing["id"])
await store.delete_task_cascade(existing["id"])
task = await store.create_task(
{
"source_task_id": SOURCE_TASK_ID,
"name": "商业态势总览(演示)",
"description": "定时数据交付演示任务:13 期小时刻度(2026-06-17 22:00 → 2026-06-18 10:00),block_1 = 商业态势总览页面数据。",
}
)
await store.update_block_defs(task["id"], [{"block_key": "block_1", "title": "商业态势总览"}])
for index, tick in enumerate(ticks, start=1):
period = await store.open_period(task["id"], source="cron", opened_at=tick)
result = await submit_block_data(store, task_id=task["id"], block_key="block_1", data=snapshots[tick])
logger.info(
"期 %s/%s opened_at=%s 相位=%s keys=%s",
period["period_index"], TICK_COUNT, tick.isoformat(), snapshots[tick]["snapshotName"], result["keys"],
)
logger.info("")
logger.info("预存完成。前端对接:")
logger.info(" 任务号 source_task_id = %s", SOURCE_TASK_ID)
logger.info(" 块定义 GET /api/task-analysis/external/tasks/%s/blocks", SOURCE_TASK_ID)
logger.info(" 时间轴 GET /api/task-analysis/external/tasks/%s/periods", SOURCE_TASK_ID)
logger.info(" 单期数据 GET /api/task-analysis/external/tasks/%s/periods/{n|latest}", SOURCE_TASK_ID)
await close_engine()
return 0
def main() -> int:
parser = argparse.ArgumentParser(description="预存商业态势总览演示数据(13 期小时刻度 + block_1 快照)")
parser.add_argument("--dry-run", action="store_true", help="只打印将生成的期与相位,不写库")
args = parser.parse_args()
try:
return asyncio.run(_run(args.dry_run))
except Exception:
logger.exception("预存失败")
return 1
if __name__ == "__main__":
raise SystemExit(main())