# -*- 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)