From 682985ffc0bb4854d9ebab54a62c5d6d9466e744 Mon Sep 17 00:00:00 2001 From: BXY0414 <1079823339@qq.com> Date: Mon, 16 Mar 2026 14:21:12 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AD=97=E6=AE=B5=E7=BC=96?= =?UTF-8?q?=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 6148 -> 8196 bytes .gitignore | 5 +- .../project_bitable_common.cpython-312.pyc | Bin 48616 -> 67978 bytes project_bitable_common.py | 223 ++++++++++++++++++ run_project_bitable_pipeline.py | 9 + 5 files changed, 236 insertions(+), 1 deletion(-) diff --git a/.DS_Store b/.DS_Store index cc5d92e5bb180a1158c2134a636247d60c0b4384..45bae9d4666962b9749505901bcd0e5a149fc99d 100644 GIT binary patch delta 559 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 diff --git a/project_bitable_common.py b/project_bitable_common.py index 9f12c57..9a5c503 100644 --- a/project_bitable_common.py +++ b/project_bitable_common.py @@ -614,6 +614,39 @@ class FeishuBitableClient: self._fields_cache.pop(table_id, None) log_step(f"字段创建完成: table_id={table_id}, field={field_name}") + def try_list_field_groups(self, table_id: str) -> list[dict[str, Any]] | None: + path = f"/open-apis/bitable/v1/apps/{self.app_token}/tables/{table_id}/field_groups" + url = f"https://open.feishu.cn{path}" + log_step(f"尝试读取字段编组: table_id={table_id}") + resp = self.session.request("GET", url, timeout=60) + try: + payload = resp.json() + except ValueError: + log_step(f"字段编组接口返回非 JSON,跳过读取: table_id={table_id}, status={resp.status_code}") + return None + if resp.status_code == 404: + log_step(f"字段编组读取接口不可用: table_id={table_id}, payload={payload}") + return None + if resp.status_code >= 400 or payload.get("code") != 0: + log_step(f"字段编组读取失败,跳过比对: table_id={table_id}, payload={payload}") + return None + data = payload.get("data") or {} + items = data.get("items") or data.get("field_groups") or [] + log_step(f"字段编组读取成功: table_id={table_id}, 编组数={len(items)}") + return items + + def create_field_groups(self, table_id: str, field_groups: list[dict[str, Any]]) -> None: + log_step( + f"开始创建字段编组: table_id={table_id}, " + f"groups={[(group.get('name') or '', len(group.get('children') or [])) for group in field_groups]}" + ) + self.request( + "POST", + f"/open-apis/bitable/v1/apps/{self.app_token}/tables/{table_id}/field_groups", + json={"field_groups": field_groups}, + ) + log_step(f"字段编组创建请求完成: table_id={table_id}") + def list_records(self, table_id: str) -> list[dict[str, Any]]: log_step(f"开始获取记录列表: table_id={table_id}") items: list[dict[str, Any]] = [] @@ -881,6 +914,196 @@ def iter_dates(start_date: date, end_date: date) -> list[date]: return dates +def build_group_dates_by_month(start_date: date, end_date: date) -> dict[tuple[int, int], list[date]]: + grouped: dict[tuple[int, int], list[date]] = defaultdict(list) + for stat_date in iter_dates(start_date, end_date): + grouped[(stat_date.year, stat_date.month)].append(stat_date) + return grouped + + +def build_field_group_payload( + fields: list[dict[str, Any]], + store_field_name: str, + allowed_field_names: set[str] | None = None, +) -> list[dict[str, Any]]: + gmv_fields: list[dict[str, str]] = [] + other_fields: list[dict[str, str]] = [] + for field in fields: + field_id = str(field.get("field_id") or "").strip() + field_name = str(field.get("field_name") or "").strip() + if not field_id: + continue + if field_name == store_field_name: + continue + if allowed_field_names is not None and field_name not in allowed_field_names: + continue + child = {"type": "field", "id": field_id} + if "gmv" in field_name.lower(): + gmv_fields.append(child) + else: + other_fields.append(child) + groups: list[dict[str, Any]] = [] + if gmv_fields: + groups.append({"name": "GMV", "children": gmv_fields}) + if other_fields: + groups.append({"name": "销售额/退款额", "children": other_fields}) + return groups + + +def normalize_field_groups(field_groups: list[dict[str, Any]]) -> dict[str, set[str]]: + normalized: dict[str, set[str]] = {} + for group in field_groups: + name = str(group.get("name") or "").strip() + if not name: + continue + normalized[name] = { + str(child.get("id") or "").strip() + for child in (group.get("children") or []) + if str(child.get("id") or "").strip() + } + return normalized + + +def get_field_group_order(field_groups: list[dict[str, Any]]) -> list[str]: + return [str(group.get("name") or "").strip() for group in field_groups if str(group.get("name") or "").strip()] + + +def ensure_field_groups_for_table( + client: FeishuBitableClient, + table: TableInfo, + store_field_name: str, + allowed_field_names: set[str] | None, + dry_run: bool, +) -> None: + log_step(f"开始检查字段编组: 表={table.name}") + fields = client.list_fields(table.table_id) + desired_groups = build_field_group_payload(fields, store_field_name, allowed_field_names) + if not desired_groups: + log_step(f"跳过字段编组: 表={table.name}, 原因=没有可分组字段") + return + desired_summary = {group["name"]: len(group.get("children") or []) for group in desired_groups} + log_step(f"目标字段编组: 表={table.name}, summary={desired_summary}") + existing_groups = client.try_list_field_groups(table.table_id) + if existing_groups is not None: + existing_normalized = normalize_field_groups(existing_groups) + desired_normalized = normalize_field_groups(desired_groups) + existing_order = get_field_group_order(existing_groups) + desired_order = get_field_group_order(desired_groups) + if existing_normalized == desired_normalized and existing_order == desired_order: + log_step(f"字段编组已存在且一致,跳过创建: 表={table.name}") + return + log_step( + f"字段编组存在差异: 表={table.name}, " + f"existing={ {name: len(ids) for name, ids in existing_normalized.items()} }, " + f"desired={ {name: len(ids) for name, ids in desired_normalized.items()} }, " + f"existing_order={existing_order}, desired_order={desired_order}" + ) + if dry_run: + print(f"[DRY-RUN] 表={table.name} 将创建/更新字段编组: {desired_summary}") + return + try: + client.create_field_groups(table.table_id, desired_groups) + log_step(f"字段编组处理完成: 表={table.name}") + except Exception as exc: + error_text = str(exc) + log_step(f"字段编组创建失败: 表={table.name}, error={error_text}") + if "99991672" in error_text or "base:field_group:create" in error_text: + log_step( + f"跳过字段编组创建: 表={table.name}, " + "原因=应用缺少 base:field_group:create 权限,请在飞书开放平台开通后重试" + ) + return + if "1254122" in error_text or "Child belongs to multiple field groups" in error_text: + log_step(f"检测到字段已被其他编组占用,尝试逐个创建编组: 表={table.name}") + for group in desired_groups: + try: + client.create_field_groups(table.table_id, [group]) + log_step( + f"单个字段编组创建成功: 表={table.name}, group={group.get('name')}, " + f"字段数={len(group.get('children') or [])}" + ) + except Exception as single_exc: + log_step( + f"单个字段编组创建失败: 表={table.name}, group={group.get('name')}, error={single_exc}" + ) + return + raise + + +def sync_field_groups_for_range( + start_date: date, + end_date: date, + base_url: str, + store_field_name: str, + table_map_file: str, + dry_run: bool, + skip_missing_tables: bool, +) -> None: + log_step(f"开始同步字段编组: {start_date.isoformat()} -> {end_date.isoformat()}") + table_overrides = load_table_name_overrides(table_map_file) + if table_overrides: + log_step(f"加载表名映射完成: {len(table_overrides)} 条") + client = FeishuBitableClient(base_url) + tables = client.list_tables() + for (year, month), _ in sorted(build_group_dates_by_month(start_date, end_date).items()): + for project in SUPPORTED_PROJECTS: + try: + table = find_target_table(tables, project, year, month, table_overrides) + except ValueError as exc: + if skip_missing_tables: + log_step(f"跳过字段编组同步: {exc}") + continue + raise + ensure_field_groups_for_table( + client=client, + table=table, + store_field_name=store_field_name, + dry_run=dry_run, + ) + log_step("字段编组同步完成") + + +def sync_field_groups_for_day( + stat_date: date, + base_url: str, + store_field_name: str, + table_map_file: str, + dry_run: bool, + skip_missing_tables: bool, +) -> None: + day_mode = "完整编组" if stat_date.day == 1 else "增量编组" + log_step(f"开始同步字段编组: 日期={stat_date.isoformat()}, 模式={day_mode}") + table_overrides = load_table_name_overrides(table_map_file) + if table_overrides: + log_step(f"加载表名映射完成: {len(table_overrides)} 条") + client = FeishuBitableClient(base_url) + tables = client.list_tables() + allowed_field_names: set[str] | None = None + if stat_date.day != 1: + allowed_field_names = { + get_gmv_field_name(stat_date), + get_sales_field_name(stat_date), + get_refund_field_name(stat_date), + } + log_step(f"增量编组目标字段: 日期={stat_date.isoformat()}, fields={sorted(allowed_field_names)}") + for project in SUPPORTED_PROJECTS: + try: + table = find_target_table(tables, project, stat_date.year, stat_date.month, table_overrides) + except ValueError as exc: + if skip_missing_tables: + log_step(f"跳过字段编组同步: {exc}") + continue + raise + ensure_field_groups_for_table( + client=client, + table=table, + store_field_name=store_field_name, + allowed_field_names=allowed_field_names, + dry_run=dry_run, + ) + log_step(f"字段编组同步完成: 日期={stat_date.isoformat()}, 模式={day_mode}") + + def sync_metric_table_for_month( client: FeishuBitableClient, table: TableInfo, diff --git a/run_project_bitable_pipeline.py b/run_project_bitable_pipeline.py index e486900..c85b3d1 100644 --- a/run_project_bitable_pipeline.py +++ b/run_project_bitable_pipeline.py @@ -17,6 +17,7 @@ from project_bitable_common import ( run_metric_sync, send_feishu_app_card, send_feishu_sync_card, + sync_field_groups_for_day, validate_date, ) @@ -78,6 +79,14 @@ def main() -> None: dry_run=args.dry_run, skip_missing_tables=args.skip_missing_tables, ) + sync_field_groups_for_day( + stat_date=stat_date, + base_url=args.base_url, + store_field_name=args.store_field_name, + table_map_file=args.table_map_file, + dry_run=args.dry_run, + skip_missing_tables=args.skip_missing_tables, + ) log_step(f"日期处理完成: {stat_date.isoformat()}") if not args.dry_run: if args.chat_id: