# -*- coding: utf-8 -*-
from __future__ import annotations
import html
import os
import shutil
import zipfile
from datetime import datetime, timezone
from pathlib import Path
BASE = Path(__file__).resolve().parent
OUT = BASE / "订单与供应监测模块架构汇报.pptx"
BUILD = BASE / "pptx-build-py"
EMU = 9525
def emu(px: float) -> int:
return int(px * EMU)
shape_id = 1
def next_id() -> int:
global shape_id
shape_id += 1
return shape_id
def rgb(value: str) -> str:
return value.strip("#").upper()
def write(path: Path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
def text_shape(text: str, x: int, y: int, w: int, h: int, size: int, color: str, bold: bool = False) -> str:
sid = next_id()
bold_attr = ' b="1"' if bold else ""
paragraphs = []
for line in str(text).splitlines() or [""]:
paragraphs.append(
f''
f''
f''
f'{html.escape(line)}'
f''
)
return f"""
{''.join(paragraphs)}
"""
def rect_shape(x: int, y: int, w: int, h: int, fill: str, line: str = "FFFFFF", rounded: bool = False) -> str:
sid = next_id()
geom = "roundRect" if rounded else "rect"
return f"""
"""
def card(title: str, body: str, x: int, y: int, w: int, h: int, accent: str) -> str:
return (
rect_shape(x, y, w, h, "FFFFFF", "DDDDDD", True)
+ rect_shape(x, y, 8, h, accent, accent, False)
+ text_shape(title, x + 22, y + 16, w - 35, 34, 17, "17304A", True)
+ text_shape(body, x + 22, y + 58, w - 35, h - 68, 13, "555555", False)
)
def node(text: str, x: int, y: int, w: int, h: int, fill: str, line: str) -> str:
return rect_shape(x, y, w, h, fill, line, True) + text_shape(text, x + 10, y + 15, w - 20, h - 20, 14, "17304A", True)
def title(text: str, subtitle: str = "") -> str:
body = text_shape(text, 70, 42, 980, 52, 30, "17304A", True)
if subtitle:
body += text_shape(subtitle, 72, 92, 980, 30, 14, "666666")
body += rect_shape(70, 128, 1140, 3, "2B74B1", "2B74B1")
return body
slides = [
{
"bg": "FBFAF7",
"body": text_shape("订单监测与供应监测", 90, 170, 900, 58, 38, "17304A", True)
+ text_shape("模块架构、功能逻辑与系统关联汇报", 92, 238, 880, 38, 20, "666666")
+ text_shape("面向管理层的简版说明", 94, 300, 420, 28, 15, "2B74B1", True)
+ card("汇报重点", "1. 两个模块分别解决什么问题\n2. 数据如何进入系统并形成分析结果\n3. 与库存、产品、生命周期等模块如何关联", 760, 170, 390, 250, "2B74B1"),
},
{
"bg": "FFFFFF",
"body": title("一页总览", "两个模块都采用:数据接入 -> 数据入库 -> 后端聚合 -> 前端看板展示")
+ card("订单监测", "关注销售侧:订单金额、商品贡献、渠道/平台表现、支付效率、退款漏损、连带购买。\n\n当前数据库已有核心表,具备较完整的数据分析基础。", 80, 170, 500, 320, "2B74B1")
+ card("供应监测", "关注采购侧:供应商履约、采购订单、入库金额、账期、准交率、付款计划。\n\n当前数据库只有部分基础数据,需补齐采购订单、账期和交付匹配数据。", 700, 170, 500, 320, "449B7A"),
},
{
"bg": "FFFFFF",
"body": title("整体模块架构", "前端页面保持稳定,后端逐步将文件依赖切换为数据库驱动")
+ node("数据文件上传\nCSV / Excel", 70, 220, 180, 78, "DAE7F4", "2B74B1")
+ text_shape("→", 268, 234, 50, 50, 28, "666666", True)
+ node("导入服务\n解析 / 清洗 / 校验", 330, 220, 210, 78, "EAF5EC", "449B7A")
+ text_shape("→", 558, 234, 50, 50, 28, "666666", True)
+ node("业务数据库\n订单 / 供应 / 库存", 620, 220, 210, 78, "F7F1E8", "E88B3A")
+ text_shape("→", 848, 234, 50, 50, 28, "666666", True)
+ node("分析服务\nMapper / 聚合逻辑", 910, 220, 210, 78, "F5F5F5", "666666")
+ text_shape("↓", 995, 305, 50, 50, 26, "666666", True)
+ node("管理看板\n图表 / 报表 / 对比", 910, 370, 210, 78, "FFFFFF", "2B74B1")
+ card("架构原则", "接口尽量保持不变,减少前端改造;数据统一入库,避免依赖本地路径;分析逻辑集中在 Service 和 Mapper 层。", 120, 500, 1030, 105, "2B74B1"),
},
{
"bg": "FFFFFF",
"body": title("订单监测:功能逻辑", "订单监测已经具备数据库化基础,重点是持续完善导入和统计口径")
+ card("核心功能", "订单价值分析:GMV、成交金额、退款金额\n商品分析:Top 商品、连带购买\n渠道分析:平台、渠道、业务单元贡献\n支付分析:平均支付耗时、支付漏斗\n经营报表:按日/月/年聚合", 70, 165, 360, 370, "2B74B1")
+ card("实现逻辑", "1. 订单和店铺价值数据进入数据库\n2. Mapper 查询订单主表和店铺价值明细表\n3. Service 聚合指标并返回统一结构\n4. 前端看板按日期范围展示结果", 460, 165, 360, 370, "449B7A")
+ card("当前基础", "dtm_order_main:订单主表\ndtm_shop_value_main:店铺价值明细表\n\n当前数据库中两张核心表已有较多数据,可支撑订单监测主要分析。", 850, 165, 360, 370, "E88B3A"),
},
{
"bg": "FFFFFF",
"body": title("供应监测:功能逻辑", "供应监测目标是从 Excel 文件读取升级为数据库驱动")
+ card("核心功能", "供应商统计:订单金额、入库金额、完成率、准交率\n供应商对比:多个供应商横向比较\n付款计划:账期 + 验收/入库日期计算预计付款\n数据导入:账期、订单、入库、匹配结果", 70, 165, 360, 370, "449B7A")
+ card("实现逻辑", "1. 上传供应监测相关 Excel\n2. 解析供应商、订单、入库、账期字段\n3. 写入供应监测数据表\n4. Service 从数据库聚合供应商指标\n5. 前端继续使用现有查询接口", 460, 165, 360, 370, "2B74B1")
+ card("当前缺口", "数据库已有:供应商基础表、采购入库基础表。\n\n仍缺:采购订单、供应商账期、计划交货日期、实际验收日期、订单入库匹配结果。", 850, 165, 360, 370, "E88B3A"),
},
{
"bg": "FFFFFF",
"body": title("模块关联关系", "两个监测模块不是孤立看板,而是与基础数据和分析模块共享数据资产")
+ card("订单监测关联", "产品模块:通过 SKU 关联商品、SPU、产品信息。\n生命周期模块:复用订单销售数据做热销、趋势、生命周期分析。\n店铺模块:复用平台、渠道、业务单元维度。", 90, 170, 500, 280, "2B74B1")
+ card("供应监测关联", "库存模块:复用采购入库和库存流转数据。\n产品模块:通过 SKU 关联采购产品。\n供应商资料:复用供应商编号和名称,扩展账期和履约指标。", 690, 170, 500, 280, "449B7A")
+ card("通用关联", "系统权限和操作日志可统一管理导入权限、导入记录和异常追踪;导入批次设计可在订单和供应两个模块复用。", 190, 495, 900, 95, "E88B3A"),
},
{
"bg": "FFFFFF",
"body": title("数据入库建设重点", "管理层需要关注的是数据完整性、稳定性和可追踪性")
+ card("订单监测", "建设重点:完善导入批次、重复数据处理、异常行反馈。\n\n收益:订单看板稳定、历史数据可追溯、跨模块复用能力强。", 90, 170, 500, 300, "2B74B1")
+ card("供应监测", "建设重点:补齐采购订单、账期、计划交货日期、实际验收日期、匹配结果。\n\n收益:供应商履约分析和付款计划可以稳定落地。", 690, 170, 500, 300, "449B7A"),
},
{
"bg": "FFFFFF",
"body": title("推进路径", "建议分阶段推进,先稳定数据,再扩展分析能力")
+ node("阶段一\n确认数据源和字段口径", 80, 220, 210, 85, "DAE7F4", "2B74B1")
+ text_shape("→", 308, 240, 50, 50, 28, "666666", True)
+ node("阶段二\n建设导入和入库表", 370, 220, 210, 85, "EAF5EC", "449B7A")
+ text_shape("→", 598, 240, 50, 50, 28, "666666", True)
+ node("阶段三\n切换 Service 查询数据库", 660, 220, 230, 85, "F7F1E8", "E88B3A")
+ text_shape("→", 908, 240, 50, 50, 28, "666666", True)
+ node("阶段四\n完善看板和管理报表", 970, 220, 210, 85, "FFFFFF", "2B74B1")
+ card("结论", "订单监测已有较好的数据库基础,重点是规范化导入和追踪。供应监测需要先补齐核心数据表,再将现有 Excel 分析迁移到数据库查询。", 120, 430, 1040, 130, "2B74B1"),
},
]
def slide_xml(slide: dict[str, str]) -> str:
return f'''
{slide["body"]}
'''
def build() -> None:
if OUT.exists():
OUT.unlink()
if BUILD.exists():
shutil.rmtree(BUILD)
for folder in [
"_rels",
"docProps",
"ppt/_rels",
"ppt/slides/_rels",
"ppt/slideMasters/_rels",
"ppt/slideLayouts/_rels",
"ppt/theme",
]:
(BUILD / folder).mkdir(parents=True, exist_ok=True)
overrides = [
'',
'',
'',
'',
'',
'',
]
for index, slide in enumerate(slides, start=1):
overrides.append(f'')
write(BUILD / f"ppt/slides/slide{index}.xml", slide_xml(slide))
write(
BUILD / f"ppt/slides/_rels/slide{index}.xml.rels",
'''
''',
)
write(
BUILD / "[Content_Types].xml",
f'''
{' '.join(overrides)}
''',
)
write(
BUILD / "_rels/.rels",
'''
''',
)
slide_ids = []
rels = ['']
for index in range(1, len(slides) + 1):
rid = index + 1
slide_ids.append(f'')
rels.append(f'')
write(
BUILD / "ppt/presentation.xml",
f'''
{''.join(slide_ids)}
''',
)
write(BUILD / "ppt/_rels/presentation.xml.rels", f'''
{' '.join(rels)}
''')
write(BUILD / "ppt/slideMasters/slideMaster1.xml", '''
''')
write(BUILD / "ppt/slideMasters/_rels/slideMaster1.xml.rels", '''
''')
write(BUILD / "ppt/slideLayouts/slideLayout1.xml", '''
''')
write(BUILD / "ppt/slideLayouts/_rels/slideLayout1.xml.rels", '''
''')
write(BUILD / "ppt/theme/theme1.xml", '''
''')
stamp = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
write(BUILD / "docProps/core.xml", f'''
订单与供应监测模块架构汇报CodexCodex{stamp}{stamp}
''')
write(BUILD / "docProps/app.xml", f'''
Codex OpenXML GeneratorWidescreen{len(slides)}DTM
''')
with zipfile.ZipFile(OUT, "w", zipfile.ZIP_DEFLATED) as zf:
for root, _, files in os.walk(BUILD):
for file in files:
path = Path(root) / file
zf.write(path, path.relative_to(BUILD).as_posix())
shutil.rmtree(BUILD)
if __name__ == "__main__":
build()
print(OUT)