调整 字段顺序、名称
This commit is contained in:
@@ -1,2 +1,4 @@
|
|||||||
.env
|
.env
|
||||||
.feishu_oauth_tokens.json
|
.feishu_oauth_tokens.json
|
||||||
|
inventory_sync/__pycache__
|
||||||
|
.DS_Store
|
||||||
|
|||||||
+10
-10
@@ -118,23 +118,23 @@ PRIMARY_FIELD_NAME = "货品编码"
|
|||||||
PRIMARY_FIELD_SOURCE = "统一规格编码"
|
PRIMARY_FIELD_SOURCE = "统一规格编码"
|
||||||
```
|
```
|
||||||
|
|
||||||
当前字段映射:
|
当前字段映射和展示顺序:
|
||||||
|
|
||||||
| 飞书字段名 | SQL 来源字段 | 飞书字段类型 | 显示格式 |
|
| 飞书字段名 | SQL 来源字段 | 飞书字段类型 | 显示格式 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 货品编码 | 统一规格编码 | 文本主字段 | 文本 |
|
| 货品编码 | 统一规格编码 | 文本主字段 | 文本 |
|
||||||
| 品牌 | 品牌名称 | 文本 | 文本 |
|
|
||||||
| 分类 | 一级分类 | 文本 | 文本 |
|
|
||||||
| 产品类别 | 货品简称 | 文本 | 文本 |
|
|
||||||
| 货品名称 | 商品名称 | 文本 | 文本 |
|
| 货品名称 | 商品名称 | 文本 | 文本 |
|
||||||
|
| 分类 | 一级分类 | 文本 | 文本 |
|
||||||
| 条形码 | 条形码 | 文本 | 文本 |
|
| 条形码 | 条形码 | 文本 | 文本 |
|
||||||
| 零售价 | 零售价 | 数字 | 保留 2 位小数 |
|
| 零售价 | 零售价 | 数字 | 保留 2 位小数 |
|
||||||
| 店铺名称 | 店铺名称 | 文本 | 文本 |
|
| 店铺名称 | 店铺名称 | 文本 | 文本 |
|
||||||
| 店铺可用库存 | 店铺挂单量 | 数字 | 不保留小数 |
|
| 店铺可用库存 | 店铺挂单量 | 数字 | 不保留小数 |
|
||||||
| 总库存 | 总库存 | 数字 | 不保留小数 |
|
| 总库存 | 总库存 | 数字 | 不保留小数 |
|
||||||
| 本月销售数量 | 本月销售数量 | 数字 | 不保留小数 |
|
| 当前月份销售数量,例如 202604销售数量 | 本月销售数量 | 数字 | 不保留小数 |
|
||||||
| 上月销售数量 | 上月销售数量 | 数字 | 不保留小数 |
|
| 上一月份销售数量,例如 202603销售数量 | 上月销售数量 | 数字 | 不保留小数 |
|
||||||
| 上上月销售数量 | 上上月销售数量 | 数字 | 不保留小数 |
|
| 上上月份销售数量,例如 202602销售数量 | 上上月销售数量 | 数字 | 不保留小数 |
|
||||||
|
| 品牌 | 品牌名称 | 文本 | 文本 |
|
||||||
|
| 产品类别 | 货品简称 | 文本 | 文本 |
|
||||||
|
|
||||||
### 3.5 SQL 模块
|
### 3.5 SQL 模块
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ PRIMARY_FIELD_SOURCE = "统一规格编码"
|
|||||||
- 取每个统一规格编码、仓库的最新库存快照。
|
- 取每个统一规格编码、仓库的最新库存快照。
|
||||||
- 汇总统一规格编码维度的总库存。
|
- 汇总统一规格编码维度的总库存。
|
||||||
- 取每个统一规格编码、店铺的最新挂单量快照。
|
- 取每个统一规格编码、店铺的最新挂单量快照。
|
||||||
- 汇总本月、上月、上上月销售数量。
|
- 按店铺和统一规格编码汇总本月、上月、上上月销售数量。
|
||||||
- 关联商品维表和店铺维表。
|
- 关联商品维表和店铺维表。
|
||||||
- 输出用于飞书同步的明细结果。
|
- 输出用于飞书同步的明细结果。
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ PRIMARY_FIELD_SOURCE = "统一规格编码"
|
|||||||
- 解析飞书 Base 地址和 `app_token`。
|
- 解析飞书 Base 地址和 `app_token`。
|
||||||
- 查询飞书 Base 下的表列表。
|
- 查询飞书 Base 下的表列表。
|
||||||
- 查询、创建、更新飞书字段。
|
- 查询、创建、更新飞书字段。
|
||||||
- 根据字段映射创建或修正目标表字段。
|
- 根据字段映射重建受管字段,保证字段类型和展示顺序一致。
|
||||||
- 分页读取记录。
|
- 分页读取记录。
|
||||||
- 批量删除记录。
|
- 批量删除记录。
|
||||||
- 批量创建记录。
|
- 批量创建记录。
|
||||||
@@ -304,7 +304,7 @@ python3 -m inventory_sync.cli --action sync-brand
|
|||||||
|
|
||||||
### 8.2 使用全量覆盖同步
|
### 8.2 使用全量覆盖同步
|
||||||
|
|
||||||
当前采用删除旧记录后重新插入的方式,优点是逻辑简单、结果确定,不需要处理复杂的增量比对。
|
当前采用删除旧记录后重新插入的方式,优点是逻辑简单、结果确定,不需要处理复杂的增量比对。为了稳定控制飞书表头顺序,同步前也会重建受管字段。
|
||||||
|
|
||||||
需要注意的是,如果目标飞书表中存在人工维护的额外行,全量覆盖会删除这些记录。后续如需保留人工数据,应改为基于主键“货品编码 + 店铺名称”做 upsert。
|
需要注意的是,如果目标飞书表中存在人工维护的额外行,全量覆盖会删除这些记录。后续如需保留人工数据,应改为基于主键“货品编码 + 店铺名称”做 upsert。
|
||||||
|
|
||||||
|
|||||||
+16
-26
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -160,6 +161,14 @@ class FeishuClient:
|
|||||||
if data.get("code") != 0:
|
if data.get("code") != 0:
|
||||||
raise FeishuApiError(f"新增字段失败({payload.get('field_name')}): {data}")
|
raise FeishuApiError(f"新增字段失败({payload.get('field_name')}): {data}")
|
||||||
|
|
||||||
|
def _is_managed_field(self, field_name: str) -> bool:
|
||||||
|
managed_names = {spec.feishu_name for spec in FIELD_SPECS}
|
||||||
|
managed_names.update(LEGACY_NAME_MAP.keys())
|
||||||
|
managed_names.update(LEGACY_NAME_MAP.values())
|
||||||
|
if field_name in managed_names:
|
||||||
|
return True
|
||||||
|
return bool(re.fullmatch(r"\d{1,2}月销售数量", field_name) or re.fullmatch(r"\d{6}销售数量", field_name))
|
||||||
|
|
||||||
def batch_delete_records(self, access_token: str, app_token: str, table_id: str, record_ids: list[str]) -> int:
|
def batch_delete_records(self, access_token: str, app_token: str, table_id: str, record_ids: list[str]) -> int:
|
||||||
deleted = 0
|
deleted = 0
|
||||||
for start in range(0, len(record_ids), 500):
|
for start in range(0, len(record_ids), 500):
|
||||||
@@ -217,40 +226,21 @@ class FeishuClient:
|
|||||||
self.update_field(access_token, app_token, table_id, primary_field["field_id"], payload)
|
self.update_field(access_token, app_token, table_id, primary_field["field_id"], payload)
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
|
|
||||||
for old_name, new_name in LEGACY_NAME_MAP.items():
|
|
||||||
fields = self.list_fields(access_token, app_token, table_id)
|
fields = self.list_fields(access_token, app_token, table_id)
|
||||||
field = next((item for item in fields if item.get("field_name") == old_name), None)
|
for field in fields:
|
||||||
if field and old_name != new_name:
|
field_name = field.get("field_name")
|
||||||
payload = {"field_name": new_name, "type": field.get("type")}
|
field_id = field.get("field_id")
|
||||||
if field.get("property") is not None:
|
if field.get("is_primary") or not field_name or not field_id:
|
||||||
payload["property"] = field.get("property")
|
continue
|
||||||
self.update_field(access_token, app_token, table_id, field["field_id"], payload)
|
if self._is_managed_field(field_name):
|
||||||
|
self.delete_field(access_token, app_token, table_id, field_id)
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
|
|
||||||
fields = self.list_fields(access_token, app_token, table_id)
|
|
||||||
fields_by_name = {field.get("field_name"): field for field in fields if field.get("field_name")}
|
|
||||||
for spec in FIELD_SPECS:
|
for spec in FIELD_SPECS:
|
||||||
field = fields_by_name.get(spec.feishu_name)
|
|
||||||
if not field:
|
|
||||||
payload = {"field_name": spec.feishu_name, "type": spec.field_type}
|
payload = {"field_name": spec.feishu_name, "type": spec.field_type}
|
||||||
if spec.formatter is not None:
|
if spec.formatter is not None:
|
||||||
payload["property"] = {"formatter": spec.formatter}
|
payload["property"] = {"formatter": spec.formatter}
|
||||||
self.create_field(access_token, app_token, table_id, payload)
|
self.create_field(access_token, app_token, table_id, payload)
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
fields = self.list_fields(access_token, app_token, table_id)
|
|
||||||
fields_by_name = {item.get("field_name"): item for item in fields if item.get("field_name")}
|
|
||||||
field = fields_by_name.get(spec.feishu_name)
|
|
||||||
if not field:
|
|
||||||
continue
|
|
||||||
current_formatter = (field.get("property") or {}).get("formatter")
|
|
||||||
if field.get("type") != spec.field_type or current_formatter != spec.formatter:
|
|
||||||
payload = {"field_name": spec.feishu_name, "type": spec.field_type}
|
|
||||||
if spec.formatter is not None:
|
|
||||||
payload["property"] = {"formatter": spec.formatter}
|
|
||||||
elif field.get("property") is not None:
|
|
||||||
payload["property"] = field.get("property")
|
|
||||||
self.update_field(access_token, app_token, table_id, field["field_id"], payload)
|
|
||||||
time.sleep(0.2)
|
|
||||||
|
|
||||||
return [field.get("field_name") for field in self.list_fields(access_token, app_token, table_id) if field.get("field_name")]
|
return [field.get("field_name") for field in self.list_fields(access_token, app_token, table_id) if field.get("field_name")]
|
||||||
|
|
||||||
|
|||||||
+19
-6
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
|
||||||
PRIMARY_FIELD_NAME = "货品编码"
|
PRIMARY_FIELD_NAME = "货品编码"
|
||||||
@@ -26,19 +27,28 @@ BRAND_SYNC_TARGETS = [
|
|||||||
BrandSyncTarget(brand_name="羽心堂", table_name="羽心堂"),
|
BrandSyncTarget(brand_name="羽心堂", table_name="羽心堂"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def month_sales_field_name(month_offset: int, today: date | None = None) -> str:
|
||||||
|
current = today or date.today()
|
||||||
|
month_index = current.year * 12 + current.month - 1 - month_offset
|
||||||
|
year = month_index // 12
|
||||||
|
month = month_index % 12 + 1
|
||||||
|
return f"{year}{month:02d}销售数量"
|
||||||
|
|
||||||
|
|
||||||
FIELD_SPECS = [
|
FIELD_SPECS = [
|
||||||
FieldSpec("品牌", "品牌名称", 1),
|
|
||||||
FieldSpec("分类", "一级分类", 1),
|
|
||||||
FieldSpec("产品类别", "货品简称", 1),
|
|
||||||
FieldSpec("货品名称", "商品名称", 1),
|
FieldSpec("货品名称", "商品名称", 1),
|
||||||
|
FieldSpec("分类", "一级分类", 1),
|
||||||
FieldSpec("条形码", "条形码", 1),
|
FieldSpec("条形码", "条形码", 1),
|
||||||
FieldSpec("零售价", "零售价", 2, "0.00", 2),
|
FieldSpec("零售价", "零售价", 2, "0.00", 2),
|
||||||
FieldSpec("店铺名称", "店铺名称", 1),
|
FieldSpec("店铺名称", "店铺名称", 1),
|
||||||
FieldSpec("店铺可用库存", "店铺挂单量", 2, "0", 0),
|
FieldSpec("店铺可用库存", "店铺挂单量", 2, "0", 0),
|
||||||
FieldSpec("总库存", "总库存", 2, "0", 0),
|
FieldSpec("总库存", "总库存", 2, "0", 0),
|
||||||
FieldSpec("本月销售数量", "本月销售数量", 2, "0", 0),
|
FieldSpec(month_sales_field_name(0), "本月销售数量", 2, "0", 0),
|
||||||
FieldSpec("上月销售数量", "上月销售数量", 2, "0", 0),
|
FieldSpec(month_sales_field_name(1), "上月销售数量", 2, "0", 0),
|
||||||
FieldSpec("上上月销售数量", "上上月销售数量", 2, "0", 0),
|
FieldSpec(month_sales_field_name(2), "上上月销售数量", 2, "0", 0),
|
||||||
|
FieldSpec("品牌", "品牌名称", 1),
|
||||||
|
FieldSpec("产品类别", "货品简称", 1),
|
||||||
]
|
]
|
||||||
|
|
||||||
LEGACY_NAME_MAP = {
|
LEGACY_NAME_MAP = {
|
||||||
@@ -47,5 +57,8 @@ LEGACY_NAME_MAP = {
|
|||||||
"货品简称": "产品类别",
|
"货品简称": "产品类别",
|
||||||
"商品名称": "货品名称",
|
"商品名称": "货品名称",
|
||||||
"店铺挂单量": "店铺可用库存",
|
"店铺挂单量": "店铺可用库存",
|
||||||
|
"本月销售数量": month_sales_field_name(0),
|
||||||
|
"上月销售数量": month_sales_field_name(1),
|
||||||
|
"上上月销售数量": month_sales_field_name(2),
|
||||||
"统一规格编码": PRIMARY_FIELD_NAME,
|
"统一规格编码": PRIMARY_FIELD_NAME,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ shop_listing_latest AS (
|
|||||||
sales_summary AS (
|
sales_summary AS (
|
||||||
SELECT
|
SELECT
|
||||||
fs."统一规格编码",
|
fs."统一规格编码",
|
||||||
|
fs."店铺主键",
|
||||||
SUM(
|
SUM(
|
||||||
CASE
|
CASE
|
||||||
WHEN fs."发货时间" >= date_trunc('month', now())
|
WHEN fs."发货时间" >= date_trunc('month', now())
|
||||||
@@ -82,7 +83,7 @@ sales_summary AS (
|
|||||||
WHERE NULLIF(TRIM(fs."统一规格编码"), '') IS NOT NULL
|
WHERE NULLIF(TRIM(fs."统一规格编码"), '') IS NOT NULL
|
||||||
AND fs."发货时间" >= date_trunc('month', now()) - INTERVAL '2 month'
|
AND fs."发货时间" >= date_trunc('month', now()) - INTERVAL '2 month'
|
||||||
AND fs."发货时间" < date_trunc('month', now()) + INTERVAL '1 month'
|
AND fs."发货时间" < date_trunc('month', now()) + INTERVAL '1 month'
|
||||||
GROUP BY fs."统一规格编码"
|
GROUP BY fs."统一规格编码", fs."店铺主键"
|
||||||
),
|
),
|
||||||
|
|
||||||
product_dim AS (
|
product_dim AS (
|
||||||
@@ -120,8 +121,8 @@ LEFT JOIN analytics_layer.dim_shop ds
|
|||||||
ON sl."店铺主键" = ds."店铺主键"
|
ON sl."店铺主键" = ds."店铺主键"
|
||||||
LEFT JOIN sales_summary ss
|
LEFT JOIN sales_summary ss
|
||||||
ON sl."统一规格编码" = ss."统一规格编码"
|
ON sl."统一规格编码" = ss."统一规格编码"
|
||||||
|
AND sl."店铺主键" = ss."店铺主键"
|
||||||
LEFT JOIN product_dim pd
|
LEFT JOIN product_dim pd
|
||||||
ON sl."统一规格编码" = pd."统一规格编码"
|
ON sl."统一规格编码" = pd."统一规格编码"
|
||||||
ORDER BY sl."统一规格编码", sl."店铺主键"
|
ORDER BY sl."统一规格编码", sl."店铺主键"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user