1
0
forked from erp-dev/erp

feat: batch update for plate order

This commit is contained in:
2025-12-18 17:36:43 +08:00
parent 98d797221a
commit 1a6ce32441
33 changed files with 32087 additions and 8 deletions

View File

@@ -0,0 +1,232 @@
import asyncio
import logging
from datetime import datetime
import time
from django.utils import timezone
from api_v1 import models as api_models
from flower.utils.mingdaoyun.fetch import (
fetch_plate_orders_from_mingdaoyun,
fetch_row_by_rowid_from_mingdaoyun,
)
from flower.utils.mingdaoyun.mappings import plate_order_related_worksheet_map
from flower.utils.mingdaoyun.relations import (
extract_plate_order_related_rowids,
flatten_plate_order_related_row,
)
logger = logging.getLogger(__name__)
def _parse_mdy_datetime(value: str | None):
if not value:
return None
try:
dt = datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
except ValueError:
return None
if timezone.is_naive(dt):
dt = timezone.make_aware(dt, timezone.get_current_timezone())
return dt
async def _fetch_related_flattened(
plate_order_row: dict,
*,
max_related_per_type: int = 5,
request_interval_seconds: float = 0.0,
) -> list[dict]:
"""根据开版主表 row 的 Relation 字段,拉取并碾平关联表数据。"""
related_rowids_map = extract_plate_order_related_rowids(plate_order_row)
if not related_rowids_map:
return []
flattened: list[dict] = []
for related_key, rowids in related_rowids_map.items():
wsid = plate_order_related_worksheet_map.get(related_key)
if not wsid:
continue
for rid in rowids[: max_related_per_type or 0]:
related_row = await fetch_row_by_rowid_from_mingdaoyun(
worksheet_id=wsid,
rowid=rid,
)
if not related_row:
continue
flattened.append(flatten_plate_order_related_row(related_key, related_row))
if request_interval_seconds:
await asyncio.sleep(request_interval_seconds)
return flattened
async def _fetch_related_for_rows(
rows: list[dict],
*,
max_related_per_type: int,
request_interval_seconds: float = 0.0,
) -> dict[str, list[dict]]:
"""批量拉取并碾平关联表数据(仅网络请求,不涉及 ORM"""
result: dict[str, list[dict]] = {}
for row in rows:
rowid = row.get("rowid")
if not rowid:
continue
result[rowid] = await _fetch_related_flattened(
row,
max_related_per_type=max_related_per_type,
request_interval_seconds=request_interval_seconds,
)
return result
def sync_mdy_plate_orders_to_staging(
*,
page_size: int = 100,
max_pages: int | None = None,
max_records: int | None = None,
with_related: bool = True,
max_related_per_type: int = 5,
request_interval_seconds: float = 0.02,
) -> dict:
"""同步明道云开版数据表到暂存表(含可选跨表关联数据)。
- 写入 `api_v1.MDYPlateOrderStaging`
- `raw`:整行原始数据
- `related`:跨表数据(碾平 list[dict]
- 进度记录到 `api_v1.DataSync`table_name=plate_order
说明:
- max_pages 表示“单次任务最多处理多少页”(不是最大页码)
- max_records 表示“单次任务最多处理多少条记录”0/None 表示不限制)
"""
max_records = max_records or 0
last_sync = api_models.DataSync.objects.filter(
table_name=api_models.DataSync.TableName.PLATE_ORDER
).order_by("-created_at").first()
last_ctime = last_sync.last_ctime if last_sync else None
last_rowid = last_sync.last_rowid if last_sync else ""
start_page_index = last_sync.page_index if last_sync else 1
synced_rows = 0
page_index = max(1, start_page_index)
pages_processed = 0
total_count = 0
latest_ctime = last_ctime
latest_rowid = last_rowid
while True:
if max_pages is not None and pages_processed >= max_pages:
break
if max_records and synced_rows >= max_records:
break
rows, total = asyncio.run(
fetch_plate_orders_from_mingdaoyun(
page=page_index,
page_size=page_size,
sort_id="ctime",
is_asc=True,
)
)
if request_interval_seconds:
time.sleep(request_interval_seconds)
total_count = total
if not rows:
break
hit_max_records = False
to_process: list[dict] = []
for row in rows:
if max_records and synced_rows + len(to_process) >= max_records:
hit_max_records = True
break
rowid = row.get("rowid")
if not rowid:
continue
record_ctime = _parse_mdy_datetime(row.get("ctime"))
# 跳过已同步到的游标
if last_ctime and record_ctime:
if record_ctime < last_ctime:
continue
if record_ctime == last_ctime and last_rowid and rowid == last_rowid:
continue
to_process.append(row)
related_map: dict[str, list[dict]] = {}
if with_related and to_process:
related_map = asyncio.run(
_fetch_related_for_rows(
to_process,
max_related_per_type=max_related_per_type,
request_interval_seconds=request_interval_seconds,
)
)
for row in to_process:
rowid = row.get("rowid")
if not rowid:
continue
api_models.MDYPlateOrderStaging.objects.update_or_create(
mdy_rowid=rowid,
defaults={
"ctime": _parse_mdy_datetime(row.get("ctime")),
"utime": _parse_mdy_datetime(row.get("utime")),
"raw": row,
"related": related_map.get(rowid, []),
},
)
synced_rows += 1
record_ctime = _parse_mdy_datetime(row.get("ctime"))
if record_ctime:
if latest_ctime is None or record_ctime > latest_ctime:
latest_ctime = record_ctime
latest_rowid = rowid
elif record_ctime == latest_ctime:
latest_rowid = rowid
pages_processed += 1
if hit_max_records:
break
if len(rows) < page_size:
break
page_index += 1
record_last_ctime = latest_ctime or last_ctime
record_last_rowid = latest_rowid or last_rowid
api_models.DataSync.objects.create(
table_name=api_models.DataSync.TableName.PLATE_ORDER,
page_index=page_index,
page_size=page_size,
synced_rows=synced_rows,
total_count=total_count,
last_ctime=record_last_ctime,
last_rowid=record_last_rowid,
note=f"asc scan; with_related={with_related}",
)
payload = {
"synced_rows": synced_rows,
"page_index": page_index,
"page_size": page_size,
"total_count": total_count,
"last_ctime": record_last_ctime.isoformat() if record_last_ctime else None,
"with_related": with_related,
}
logger.info("明道云开版暂存同步完成: %s", payload)
return payload