Files
inventory_analytics/REFACTOR_PLAN.md
T
2026-04-22 17:12:37 +08:00

10 KiB
Raw Blame History

飞书库存同步项目重构方案

1. 重构目标

当前项目的核心能力是从 PostgreSQL 查询库存、挂单量、销售和商品信息,按品牌筛选数据,并同步写入对应的飞书多维表格。

本次重构的目标是将原先集中在单个脚本中的逻辑拆分为更清晰、可维护、可扩展的工程结构:

  • 将配置读取、数据库查询、飞书 API、业务同步、命令行入口拆分到独立模块。
  • 将飞书字段名称、SQL 字段来源、字段类型、数字格式统一维护在映射表中。
  • 保留原有脚本入口,降低使用成本,避免改变已有执行习惯。
  • 提高后续新增品牌、新增字段、新增同步动作时的可读性和安全性。

2. 当前目录结构

inventory_analytics/
├── .env
├── .gitignore
├── list_feishu_base_tables.py
├── REFACTOR_PLAN.md
└── inventory_sync/
    ├── __init__.py
    ├── cli.py
    ├── feishu.py
    ├── postgres.py
    ├── service.py
    ├── settings.py
    ├── specs.py
    └── sql.py

3. 模块职责

3.1 入口脚本

文件:list_feishu_base_tables.py

职责:

  • 作为项目的兼容入口。
  • 将执行逻辑转交给 inventory_sync.cli.main()。
  • 保持原命令可继续使用。

示例:

python3 list_feishu_base_tables.py --action sync-brand

3.2 命令行模块

文件:inventory_sync/cli.py

职责:

  • 解析 --action 参数。
  • 初始化配置和飞书客户端。
  • 根据 action 调用对应服务。
  • 统一输出 JSON 格式结果。

当前支持的 action:

  • feishu:读取飞书 Base 下的表信息。
  • postgres:执行 PostgreSQL 查询并输出结果。
  • sync-brand:按照 BRAND_SYNC_TARGETS 配置筛选品牌数据并同步到对应飞书表。
  • all:依次执行飞书读取、数据库查询和品牌同步。

3.3 配置模块

文件:inventory_sync/settings.py

职责:

  • 自动查找并加载 .env 文件。
  • 从 .env 或当前 shell 环境变量中读取配置。
  • 校验必填配置。
  • 构建统一的 AppConfig 配置对象。

当前必填配置:

FEISHU_APP_ID
FEISHU_APP_SECRET 或 FEISHU_APP_SECRE
FEISHU_BASE_URL
ANALYTICS_DB_HOST
ANALYTICS_DB_PORT
ANALYTICS_DB_USER
ANALYTICS_DB_PASSWORD
ANALYTICS_DB_NAME

可选配置:

FEISHU_APP_TOKEN

3.4 字段映射模块

文件:inventory_sync/specs.py

职责:

  • 定义品牌到目标飞书表的同步配置。
  • 定义主字段名称和来源字段。
  • 定义 SQL 查询结果字段到飞书表字段的映射关系。
  • 定义飞书字段类型和数字格式。
  • 定义历史字段名到新字段名的重命名关系。

当前同步目标:

BRAND_SYNC_TARGETS = [
    BrandSyncTarget(brand_name="羽心堂", table_name="羽心堂"),
]
PRIMARY_FIELD_NAME = "货品编码"
PRIMARY_FIELD_SOURCE = "统一规格编码"

当前字段映射和展示顺序:

飞书字段名 SQL 来源字段 飞书字段类型 显示格式
货品编码 统一规格编码 文本主字段 文本
货品名称 商品名称 文本 文本
分类 一级分类 文本 文本
条形码 条形码 文本 文本
零售价 零售价 数字 保留 2 位小数
店铺名称 店铺名称 文本 文本
店铺可用库存 店铺挂单量 数字 不保留小数
总库存 总库存 数字 不保留小数
当前月份销售数量,例如 202604销售数量 本月销售数量 数字 不保留小数
上一月份销售数量,例如 202603销售数量 上月销售数量 数字 不保留小数
上上月份销售数量,例如 202602销售数量 上上月销售数量 数字 不保留小数
品牌 品牌名称 文本 文本
产品类别 货品简称 文本 文本

3.5 SQL 模块

文件:inventory_sync/sql.py

职责:

  • 集中维护 PostgreSQL 查询 SQL。
  • 避免业务代码中混入大段 SQL。
  • 后续如需调整库存、销量、商品维度逻辑,只需要修改该文件。

当前 SQL 逻辑包括:

  • 取每个统一规格编码、仓库的最新库存快照。
  • 汇总统一规格编码维度的总库存。
  • 取每个统一规格编码、店铺的最新挂单量快照。
  • 按店铺和统一规格编码汇总本月、上月、上上月销售数量。
  • 关联商品维表和店铺维表。
  • 输出用于飞书同步的明细结果。

3.6 PostgreSQL 模块

文件:inventory_sync/postgres.py

职责:

  • 使用 .env 中的数据库配置连接 PostgreSQL。
  • 执行 inventory_sync/sql.py 中定义的 SQL。
  • 将查询结果解析为结构化字典列表。
  • 对外返回统一结构,供 CLI 或同步服务使用。

3.7 飞书 API 模块

文件:inventory_sync/feishu.py

职责:

  • 获取飞书 tenant_access_token。
  • 解析飞书 Base 地址和 app_token。
  • 查询飞书 Base 下的表列表。
  • 查询、创建、更新飞书字段。
  • 根据字段映射重建受管字段,保证字段类型和展示顺序一致。
  • 分页读取记录。
  • 批量删除记录。
  • 批量创建记录。

该模块只关心飞书接口本身,不关心具体业务品牌和 SQL 字段。

3.8 业务服务模块

文件:inventory_sync/service.py

职责:

  • 编排完整业务流程。
  • 根据配置查询 PostgreSQL。
  • 按 BRAND_SYNC_TARGETS 逐个筛选品牌数据。
  • 根据 specs.py 中的映射表构造飞书记录。
  • 确认目标飞书表字段存在且类型正确。
  • 删除目标表已有记录。
  • 批量写入最新记录。

当前同步策略是全量覆盖:

查询 PostgreSQL -> 遍历品牌同步配置 -> 筛选目标品牌 -> 确认飞书字段 -> 删除目标表已有记录 -> 批量插入新记录

4. 执行方式

4.1 查看飞书表

python3 list_feishu_base_tables.py --action feishu

4.2 查询 PostgreSQL 数据

python3 list_feishu_base_tables.py --action postgres

4.3 同步“羽心堂”数据到飞书表

python3 list_feishu_base_tables.py --action sync-brand

4.4 执行完整流程

python3 list_feishu_base_tables.py --action all

5. 字段调整方式

如果只需要调整飞书表头名称、字段来源或字段类型,优先修改 inventory_sync/specs.py。

例如,将飞书表中的“店铺可用库存”改名为“可售库存”,只需要调整:

FieldSpec("店铺可用库存", "店铺挂单量", 2, "0", 0)

改为:

FieldSpec("可售库存", "店铺挂单量", 2, "0", 0)

如果 SQL 输出字段名发生变化,需要同步调整 source_name。

例如 SQL 输出从 店铺挂单量 改为 店铺库存,则需要调整:

FieldSpec("店铺可用库存", "店铺库存", 2, "0", 0)

6. 新增字段流程

新增字段建议按以下顺序处理:

  1. 在 inventory_sync/sql.py 中确认 SQL 已输出该字段。
  2. 在 inventory_sync/specs.py 的 FIELD_SPECS 中增加映射。
  3. 如果是数字字段,设置 field_type=2,并配置 formatter 和 scale。
  4. 执行 python3 list_feishu_base_tables.py --action sync-brand。
  5. 检查飞书表头和数据是否符合预期。

示例:

FieldSpec("新字段飞书名称", "SQL查询结果字段名", 1)
FieldSpec("新数字字段", "SQL查询结果数字字段名", 2, "0", 0)
FieldSpec("新金额字段", "SQL查询结果金额字段名", 2, "0.00", 2)

7. 新增品牌同步方式

当前版本已经使用 BRAND_SYNC_TARGETS 管理品牌和目标飞书表的对应关系,配置位置在 inventory_sync/specs.py:

BRAND_SYNC_TARGETS = [
    BrandSyncTarget(brand_name="羽心堂", table_name="羽心堂"),
]

后续新增品牌时,只需要在列表中增加一行,并确保飞书 Base 中存在同名或指定名称的目标表:

BRAND_SYNC_TARGETS = [
    BrandSyncTarget(brand_name="羽心堂", table_name="羽心堂"),
    BrandSyncTarget(brand_name="其他品牌", table_name="其他品牌"),
]

执行 python3 list_feishu_base_tables.py --action sync-brand 时,程序会先查询一次 PostgreSQL,再按该列表逐个品牌同步。

8. 当前设计取舍

8.1 保留原脚本入口

保留 list_feishu_base_tables.py 是为了不改变已有使用方式。后续如果项目继续扩大,可以新增更标准的入口,例如:

python3 -m inventory_sync.cli --action sync-brand

8.2 使用全量覆盖同步

当前采用删除旧记录后重新插入的方式,优点是逻辑简单、结果确定,不需要处理复杂的增量比对。为了稳定控制飞书表头顺序,同步前也会重建受管字段。

需要注意的是,如果目标飞书表中存在人工维护的额外行,全量覆盖会删除这些记录。后续如需保留人工数据,应改为基于主键“货品编码 + 店铺名称”做 upsert。

8.3 字段映射集中维护

所有“SQL 字段名 -> 飞书表头名”的关系集中放在 specs.py,这样可以避免同步逻辑中到处散落字段名,降低后续调整表头时漏改的风险。

9. 后续优化建议

  • 增加日志模块,将当前 print 输出改为结构化日志。
  • 增加 dry-run 模式,只预览将要删除和写入的数据,不真正修改飞书。
  • 增加 upsert 模式,避免每次全量删除重建。
  • 增加单元测试,重点覆盖字段映射、数字转换、配置校验。
  • 增加同步前数据质量检查,例如主字段为空、重复主键、数字无法转换。
  • 支持为不同品牌配置不同字段映射,适配不同品牌表结构差异。
  • 将 SQL 拆分为独立 .sql 文件,便于数据同学维护。

10. 验证命令

重构后建议至少执行以下命令验证:

PYTHONPYCACHEPREFIX=/tmp/pycache python3 -m py_compile list_feishu_base_tables.py inventory_sync/*.py
python3 list_feishu_base_tables.py --action feishu
python3 list_feishu_base_tables.py --action postgres
python3 list_feishu_base_tables.py --action sync-brand

如果 sync-brand 返回类似以下字段,说明同步流程执行成功:

{
  "target_count": 1,
  "source_row_count": 305,
  "targets": [
    {
      "brand_name": "羽心堂",
      "target_table_name": "羽心堂",
      "filtered_row_count": 200,
      "deleted_record_count": 198,
      "created_record_count": 200
    }
  ]
}