forked from erp-dev/erp
feat: refactor flower.utils to sub package, added some sync api in package, added plate_order sync result model but relation of plate_order was not query (do it next step)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.admin import action
|
from django.contrib.admin import action
|
||||||
from api_v1.models import UploadedFile, DataSync
|
from api_v1.models import UploadedFile, DataSync, MDYPlateOrderStaging
|
||||||
from .tasks import backup_database
|
from .tasks import backup_database
|
||||||
|
|
||||||
|
|
||||||
@@ -31,3 +31,13 @@ class DataSyncAdmin(admin.ModelAdmin):
|
|||||||
readonly_fields = ['created_at', 'updated_at']
|
readonly_fields = ['created_at', 'updated_at']
|
||||||
date_hierarchy = 'created_at'
|
date_hierarchy = 'created_at'
|
||||||
ordering = ['-created_at']
|
ordering = ['-created_at']
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(MDYPlateOrderStaging)
|
||||||
|
class MDYPlateOrderStagingAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['id', 'mdy_rowid', 'created_at', 'updated_at']
|
||||||
|
list_filter = ['created_at']
|
||||||
|
search_fields = ['mdy_rowid']
|
||||||
|
readonly_fields = ['created_at', 'updated_at']
|
||||||
|
date_hierarchy = 'created_at'
|
||||||
|
ordering = ['-created_at']
|
||||||
|
|||||||
27
api_v1/migrations/0004_mdy_plate_order_staging.py
Normal file
27
api_v1/migrations/0004_mdy_plate_order_staging.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('api_v1', '0003_datasync'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='MDYPlateOrderStaging',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
('mdy_rowid', models.CharField(db_index=True, help_text='明道云行记录的 rowid(全局唯一)', max_length=64, unique=True, verbose_name='明道云 RowID')),
|
||||||
|
('raw', models.JSONField(blank=True, default=dict, help_text='getFilterRows 返回的整行原始数据(包含各 controlId 字段)', verbose_name='原始行数据')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '明道云开版暂存',
|
||||||
|
'verbose_name_plural': '明道云开版暂存',
|
||||||
|
'db_table': 'api_mdy_plate_order_staging',
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -108,3 +108,36 @@ class DataSync(ModelBase):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.table_name} @ {self.created_at:%Y-%m-%d %H:%M:%S}'
|
return f'{self.table_name} @ {self.created_at:%Y-%m-%d %H:%M:%S}'
|
||||||
|
|
||||||
|
|
||||||
|
class MDYPlateOrderStaging(ModelBase):
|
||||||
|
"""明道云“开版数据表”同步暂存表(不影响 printing.PlateOrder)。
|
||||||
|
|
||||||
|
设计目标:
|
||||||
|
- 与现有业务模型完全隔离,避免影响业务数据与流程
|
||||||
|
- 仅保留 rowid 便于检索排查,其余信息全部落在 raw(JSON) 中
|
||||||
|
- 随着同步深入再逐步“属性化”字段(从 raw 实时提取/映射)
|
||||||
|
"""
|
||||||
|
|
||||||
|
mdy_rowid = models.CharField(
|
||||||
|
max_length=64,
|
||||||
|
unique=True,
|
||||||
|
db_index=True,
|
||||||
|
verbose_name='明道云 RowID',
|
||||||
|
help_text='明道云行记录的 rowid(全局唯一)',
|
||||||
|
)
|
||||||
|
raw = models.JSONField(
|
||||||
|
default=dict,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='原始行数据',
|
||||||
|
help_text='getFilterRows 返回的整行原始数据(包含各 controlId 字段)',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'api_mdy_plate_order_staging'
|
||||||
|
verbose_name = '明道云开版暂存'
|
||||||
|
verbose_name_plural = '明道云开版暂存'
|
||||||
|
ordering = ['-created_at']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.mdy_rowid}'
|
||||||
|
|||||||
@@ -220,7 +220,11 @@ def _upsert_product(product_data, merchant, category):
|
|||||||
@shared_task(bind=True)
|
@shared_task(bind=True)
|
||||||
def sync_mdy_products(self, page_size: int = 300, max_pages: int | None = None, max_records: int | None = None):
|
def sync_mdy_products(self, page_size: int = 300, max_pages: int | None = None, max_records: int | None = None):
|
||||||
"""
|
"""
|
||||||
从明道云同步产品数据(按 ctime 升序遍历,依赖 last_ctime/rowid 游标)
|
从明道云同步产品数据(按 ctime 升序分页扫描)。
|
||||||
|
|
||||||
|
- 默认从上次同步记录的 page_index 继续翻页
|
||||||
|
- last_ctime/last_rowid 用于页内游标(避免重复处理)
|
||||||
|
- max_pages 表示“单次任务最多处理多少页”(不是最大页码)
|
||||||
"""
|
"""
|
||||||
merchant = _get_mdy_merchant()
|
merchant = _get_mdy_merchant()
|
||||||
category = _get_mdy_category(merchant)
|
category = _get_mdy_category(merchant)
|
||||||
@@ -231,15 +235,18 @@ def sync_mdy_products(self, page_size: int = 300, max_pages: int | None = None,
|
|||||||
).order_by('-created_at').first()
|
).order_by('-created_at').first()
|
||||||
last_ctime = last_sync.last_ctime if last_sync else None
|
last_ctime = last_sync.last_ctime if last_sync else None
|
||||||
last_rowid = last_sync.last_rowid if last_sync else ''
|
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
|
synced_rows = 0
|
||||||
page_index = 1
|
page_index = max(1, start_page_index)
|
||||||
|
pages_processed = 0
|
||||||
total_count = 0
|
total_count = 0
|
||||||
latest_ctime = last_ctime
|
latest_ctime = last_ctime
|
||||||
latest_rowid = last_rowid
|
latest_rowid = last_rowid
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
if max_pages is not None and page_index > max_pages:
|
# max_pages: 单次任务最多处理多少页(不是“最大页码”)
|
||||||
|
if max_pages is not None and pages_processed >= max_pages:
|
||||||
break
|
break
|
||||||
if max_records and synced_rows >= max_records:
|
if max_records and synced_rows >= max_records:
|
||||||
break
|
break
|
||||||
@@ -249,8 +256,10 @@ def sync_mdy_products(self, page_size: int = 300, max_pages: int | None = None,
|
|||||||
if not products:
|
if not products:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
hit_max_records = False
|
||||||
for item in products:
|
for item in products:
|
||||||
if max_records and synced_rows >= max_records:
|
if max_records and synced_rows >= max_records:
|
||||||
|
hit_max_records = True
|
||||||
break
|
break
|
||||||
product_ctime = _parse_mdy_datetime(item.created_at)
|
product_ctime = _parse_mdy_datetime(item.created_at)
|
||||||
|
|
||||||
@@ -264,9 +273,18 @@ def sync_mdy_products(self, page_size: int = 300, max_pages: int | None = None,
|
|||||||
changed = _upsert_product(item, merchant, category)
|
changed = _upsert_product(item, merchant, category)
|
||||||
if changed:
|
if changed:
|
||||||
synced_rows += 1
|
synced_rows += 1
|
||||||
if product_ctime and (latest_ctime is None or product_ctime > latest_ctime):
|
if product_ctime:
|
||||||
latest_ctime = product_ctime
|
if latest_ctime is None or product_ctime > latest_ctime:
|
||||||
latest_rowid = item.rowid
|
latest_ctime = product_ctime
|
||||||
|
latest_rowid = item.rowid
|
||||||
|
elif product_ctime == latest_ctime:
|
||||||
|
# 同一秒内可能有多条记录,尽量把 rowid 推进到最后处理的那条
|
||||||
|
latest_rowid = item.rowid
|
||||||
|
|
||||||
|
pages_processed += 1
|
||||||
|
if hit_max_records:
|
||||||
|
# 达到单次任务的记录上限:下次从同一页继续(依赖 last_ctime/last_rowid 跳过已处理部分)
|
||||||
|
break
|
||||||
|
|
||||||
# 若返回不足一页,说明到尾部,可结束
|
# 若返回不足一页,说明到尾部,可结束
|
||||||
if len(products) < page_size:
|
if len(products) < page_size:
|
||||||
@@ -334,7 +352,11 @@ def _upsert_customer(customer_data, merchant):
|
|||||||
@shared_task(bind=True)
|
@shared_task(bind=True)
|
||||||
def sync_mdy_customers(self, page_size: int = 300, max_pages: int | None = None, max_records: int | None = None):
|
def sync_mdy_customers(self, page_size: int = 300, max_pages: int | None = None, max_records: int | None = None):
|
||||||
"""
|
"""
|
||||||
从明道云同步客户数据(按 ctime 升序遍历,依赖 last_ctime/rowid 游标)
|
从明道云同步客户数据(按 ctime 升序分页扫描)。
|
||||||
|
|
||||||
|
- 默认从上次同步记录的 page_index 继续翻页
|
||||||
|
- last_ctime/last_rowid 用于页内游标(避免重复处理)
|
||||||
|
- max_pages 表示“单次任务最多处理多少页”(不是最大页码)
|
||||||
"""
|
"""
|
||||||
merchant = _get_mdy_merchant()
|
merchant = _get_mdy_merchant()
|
||||||
max_records = max_records or 0 # 0 表示不限制
|
max_records = max_records or 0 # 0 表示不限制
|
||||||
@@ -344,15 +366,18 @@ def sync_mdy_customers(self, page_size: int = 300, max_pages: int | None = None,
|
|||||||
).order_by('-created_at').first()
|
).order_by('-created_at').first()
|
||||||
last_ctime = last_sync.last_ctime if last_sync else None
|
last_ctime = last_sync.last_ctime if last_sync else None
|
||||||
last_rowid = last_sync.last_rowid if last_sync else ''
|
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
|
synced_rows = 0
|
||||||
page_index = 1
|
page_index = max(1, start_page_index)
|
||||||
|
pages_processed = 0
|
||||||
total_count = 0
|
total_count = 0
|
||||||
latest_ctime = last_ctime
|
latest_ctime = last_ctime
|
||||||
latest_rowid = last_rowid
|
latest_rowid = last_rowid
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
if max_pages is not None and page_index > max_pages:
|
# max_pages: 单次任务最多处理多少页(不是“最大页码”)
|
||||||
|
if max_pages is not None and pages_processed >= max_pages:
|
||||||
break
|
break
|
||||||
if max_records and synced_rows >= max_records:
|
if max_records and synced_rows >= max_records:
|
||||||
break
|
break
|
||||||
@@ -362,8 +387,10 @@ def sync_mdy_customers(self, page_size: int = 300, max_pages: int | None = None,
|
|||||||
if not customers:
|
if not customers:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
hit_max_records = False
|
||||||
for item in customers:
|
for item in customers:
|
||||||
if max_records and synced_rows >= max_records:
|
if max_records and synced_rows >= max_records:
|
||||||
|
hit_max_records = True
|
||||||
break
|
break
|
||||||
record_ctime = _parse_mdy_datetime(item.created_at)
|
record_ctime = _parse_mdy_datetime(item.created_at)
|
||||||
|
|
||||||
@@ -376,9 +403,16 @@ def sync_mdy_customers(self, page_size: int = 300, max_pages: int | None = None,
|
|||||||
changed = _upsert_customer(item, merchant)
|
changed = _upsert_customer(item, merchant)
|
||||||
if changed:
|
if changed:
|
||||||
synced_rows += 1
|
synced_rows += 1
|
||||||
if record_ctime and (latest_ctime is None or record_ctime > latest_ctime):
|
if record_ctime:
|
||||||
latest_ctime = record_ctime
|
if latest_ctime is None or record_ctime > latest_ctime:
|
||||||
latest_rowid = item.rowid
|
latest_ctime = record_ctime
|
||||||
|
latest_rowid = item.rowid
|
||||||
|
elif record_ctime == latest_ctime:
|
||||||
|
latest_rowid = item.rowid
|
||||||
|
|
||||||
|
pages_processed += 1
|
||||||
|
if hit_max_records:
|
||||||
|
break
|
||||||
|
|
||||||
if len(customers) < page_size:
|
if len(customers) < page_size:
|
||||||
break
|
break
|
||||||
|
|||||||
388
api_v1/test_mingdaoyun_utils.py
Normal file
388
api_v1/test_mingdaoyun_utils.py
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
import asyncio
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.test import SimpleTestCase
|
||||||
|
|
||||||
|
from flower.utils.mingdaoyun import mappings
|
||||||
|
from flower.utils.mingdaoyun.client import MingDaoYunClient
|
||||||
|
from flower.utils.mingdaoyun.fetch import (
|
||||||
|
fetch_customers_from_mingdaoyun,
|
||||||
|
fetch_plate_orders_from_mingdaoyun,
|
||||||
|
fetch_products_from_mingdaoyun,
|
||||||
|
fetch_row_by_rowid_from_mingdaoyun,
|
||||||
|
sync_fabric_from_mingdaoyun,
|
||||||
|
)
|
||||||
|
from flower.utils.mingdaoyun.models import Customer, Fabric, Product
|
||||||
|
from flower.utils.mingdaoyun.parsers import pick_customer, pick_fabric, pick_product
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeMDYClient:
|
||||||
|
def __init__(self, response):
|
||||||
|
self.response = response
|
||||||
|
self.calls = []
|
||||||
|
|
||||||
|
async def post(self, endpoint, data=None, params=None, headers=None):
|
||||||
|
self.calls.append(
|
||||||
|
{
|
||||||
|
"endpoint": endpoint,
|
||||||
|
"data": data,
|
||||||
|
"params": params,
|
||||||
|
"headers": headers,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return self.response
|
||||||
|
|
||||||
|
|
||||||
|
class _AsyncCM:
|
||||||
|
def __init__(self, obj):
|
||||||
|
self.obj = obj
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self.obj
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc, tb):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeAiohttpResponse:
|
||||||
|
def __init__(self, json_body):
|
||||||
|
self._json_body = json_body
|
||||||
|
self.raise_for_status_called = False
|
||||||
|
|
||||||
|
def raise_for_status(self):
|
||||||
|
self.raise_for_status_called = True
|
||||||
|
|
||||||
|
async def json(self):
|
||||||
|
return self._json_body
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeAiohttpSession:
|
||||||
|
def __init__(self):
|
||||||
|
self.post_calls = []
|
||||||
|
|
||||||
|
def post(self, url, json=None):
|
||||||
|
resp = _FakeAiohttpResponse({"ok": True})
|
||||||
|
self.post_calls.append({"url": url, "json": json, "response": resp})
|
||||||
|
return _AsyncCM(resp)
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeAiohttpClientSession:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.headers = kwargs.get("headers")
|
||||||
|
self.session = _FakeAiohttpSession()
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self.session
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc, tb):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class MingDaoYunParsersTestCase(SimpleTestCase):
|
||||||
|
def test_pick_product_maps_fields_and_converts_numbers(self):
|
||||||
|
row = {
|
||||||
|
mappings.product_type_map["uid"]: "P-001",
|
||||||
|
mappings.product_type_map["rowid"]: "row-1",
|
||||||
|
mappings.product_type_map["created_at"]: "2025-12-01 00:00:00",
|
||||||
|
mappings.product_type_map["name"]: "产品A",
|
||||||
|
mappings.product_type_map["pieces"]: "12",
|
||||||
|
mappings.product_type_map["segment_size"]: "3",
|
||||||
|
mappings.product_type_map["unit"]: "米",
|
||||||
|
mappings.product_type_map["color"]: "红",
|
||||||
|
mappings.product_type_map["detail_str"]: '{"k": 1}',
|
||||||
|
}
|
||||||
|
|
||||||
|
product = pick_product(row)
|
||||||
|
self.assertIsInstance(product, Product)
|
||||||
|
self.assertEqual(product.uid, "P-001")
|
||||||
|
self.assertEqual(product.rowid, "row-1")
|
||||||
|
self.assertEqual(product.created_at, "2025-12-01 00:00:00")
|
||||||
|
self.assertEqual(product.name, "产品A")
|
||||||
|
self.assertEqual(product.pieces, 12)
|
||||||
|
self.assertEqual(product.segment_size, 3)
|
||||||
|
# width 与 segment_size 当前共用同一个 controlId;保持同步代码现状
|
||||||
|
self.assertEqual(product.width, "3")
|
||||||
|
self.assertEqual(product.unit, "米")
|
||||||
|
self.assertEqual(product.color, "红")
|
||||||
|
self.assertEqual(product.detail, {"k": 1})
|
||||||
|
|
||||||
|
def test_pick_product_invalid_numbers_become_none(self):
|
||||||
|
row = {
|
||||||
|
mappings.product_type_map["uid"]: "P-002",
|
||||||
|
mappings.product_type_map["rowid"]: "row-2",
|
||||||
|
mappings.product_type_map["created_at"]: "2025-12-01 00:00:00",
|
||||||
|
mappings.product_type_map["name"]: "产品B",
|
||||||
|
mappings.product_type_map["pieces"]: "invalid",
|
||||||
|
mappings.product_type_map["segment_size"]: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
product = pick_product(row)
|
||||||
|
self.assertIsNone(product.pieces)
|
||||||
|
self.assertIsNone(product.segment_size)
|
||||||
|
|
||||||
|
def test_pick_customer_maps_fields(self):
|
||||||
|
row = {
|
||||||
|
mappings.customer_type_map["uid"]: "C-001",
|
||||||
|
mappings.customer_type_map["rowid"]: "crow-1",
|
||||||
|
mappings.customer_type_map["created_at"]: "2025-12-01 00:00:00",
|
||||||
|
mappings.customer_type_map["name"]: "客户甲",
|
||||||
|
mappings.customer_type_map["area"]: "杭州",
|
||||||
|
}
|
||||||
|
|
||||||
|
customer = pick_customer(row)
|
||||||
|
self.assertIsInstance(customer, Customer)
|
||||||
|
self.assertEqual(customer.uid, "C-001")
|
||||||
|
self.assertEqual(customer.rowid, "crow-1")
|
||||||
|
self.assertEqual(customer.created_at, "2025-12-01 00:00:00")
|
||||||
|
self.assertEqual(customer.name, "客户甲")
|
||||||
|
self.assertEqual(customer.area, "杭州")
|
||||||
|
|
||||||
|
def test_pick_fabric_maps_fields(self):
|
||||||
|
row = {
|
||||||
|
mappings.fabric_type_map["name"]: "120克本白四面弹",
|
||||||
|
}
|
||||||
|
|
||||||
|
fabric = pick_fabric(row)
|
||||||
|
self.assertIsInstance(fabric, Fabric)
|
||||||
|
self.assertEqual(fabric.name, "120克本白四面弹")
|
||||||
|
|
||||||
|
|
||||||
|
class MingDaoYunFetchTestCase(SimpleTestCase):
|
||||||
|
def test_fetch_products_calls_client_with_expected_payload_and_parses(self):
|
||||||
|
response = {
|
||||||
|
"data": {
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
mappings.product_type_map["uid"]: "P-001",
|
||||||
|
mappings.product_type_map["rowid"]: "row-1",
|
||||||
|
mappings.product_type_map["created_at"]: "2025-12-01 00:00:00",
|
||||||
|
mappings.product_type_map["name"]: "产品A",
|
||||||
|
mappings.product_type_map["pieces"]: "1",
|
||||||
|
mappings.product_type_map["segment_size"]: "2",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 123,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fake_client = _FakeMDYClient(response)
|
||||||
|
|
||||||
|
with patch("flower.utils.mingdaoyun.fetch.get_default_mingdaoyun_client", return_value=fake_client):
|
||||||
|
products, total = asyncio.run(fetch_products_from_mingdaoyun(page=2, page_size=5))
|
||||||
|
|
||||||
|
self.assertEqual(total, 123)
|
||||||
|
self.assertEqual(len(products), 1)
|
||||||
|
self.assertIsInstance(products[0], Product)
|
||||||
|
self.assertEqual(products[0].uid, "P-001")
|
||||||
|
|
||||||
|
self.assertEqual(len(fake_client.calls), 1)
|
||||||
|
call = fake_client.calls[0]
|
||||||
|
self.assertEqual(call["endpoint"], "/v2/open/worksheet/getFilterRows")
|
||||||
|
self.assertEqual(call["data"]["worksheetId"], mappings.MDY_WORKSHEET_ID_PRODUCT)
|
||||||
|
self.assertEqual(call["data"]["pageIndex"], 2)
|
||||||
|
self.assertEqual(call["data"]["pageSize"], 5)
|
||||||
|
self.assertEqual(call["data"]["sortId"], "ctime")
|
||||||
|
self.assertIs(call["data"]["isAsc"], True)
|
||||||
|
|
||||||
|
def test_fetch_customers_calls_client_with_expected_payload_and_parses(self):
|
||||||
|
response = {
|
||||||
|
"data": {
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
mappings.customer_type_map["uid"]: "C-001",
|
||||||
|
mappings.customer_type_map["rowid"]: "crow-1",
|
||||||
|
mappings.customer_type_map["created_at"]: "2025-12-01 00:00:00",
|
||||||
|
mappings.customer_type_map["name"]: "客户甲",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 9,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fake_client = _FakeMDYClient(response)
|
||||||
|
|
||||||
|
with patch("flower.utils.mingdaoyun.fetch.get_default_mingdaoyun_client", return_value=fake_client):
|
||||||
|
customers, total = asyncio.run(fetch_customers_from_mingdaoyun(page=3, page_size=7))
|
||||||
|
|
||||||
|
self.assertEqual(total, 9)
|
||||||
|
self.assertEqual(len(customers), 1)
|
||||||
|
self.assertIsInstance(customers[0], Customer)
|
||||||
|
self.assertEqual(customers[0].uid, "C-001")
|
||||||
|
|
||||||
|
call = fake_client.calls[0]
|
||||||
|
self.assertEqual(call["endpoint"], "/v2/open/worksheet/getFilterRows")
|
||||||
|
self.assertEqual(call["data"]["worksheetId"], mappings.MDY_WORKSHEET_ID_CUSTOMER)
|
||||||
|
self.assertEqual(call["data"]["pageIndex"], 3)
|
||||||
|
self.assertEqual(call["data"]["pageSize"], 7)
|
||||||
|
|
||||||
|
def test_fetch_plate_orders_returns_raw_rows_and_total(self):
|
||||||
|
rows = [{"rowid": "r1", "ctime": "2025-12-17 10:07:00"}]
|
||||||
|
response = {"data": {"rows": rows, "total": 100}}
|
||||||
|
fake_client = _FakeMDYClient(response)
|
||||||
|
|
||||||
|
with patch("flower.utils.mingdaoyun.fetch.get_default_mingdaoyun_client", return_value=fake_client):
|
||||||
|
got_rows, total = asyncio.run(
|
||||||
|
fetch_plate_orders_from_mingdaoyun(page=4, page_size=11, sort_id="ctime", is_asc=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(total, 100)
|
||||||
|
self.assertEqual(got_rows, rows)
|
||||||
|
|
||||||
|
call = fake_client.calls[0]
|
||||||
|
self.assertEqual(call["data"]["worksheetId"], mappings.MDY_WORKSHEET_ID_PLATE_ORDER)
|
||||||
|
self.assertEqual(call["data"]["pageIndex"], 4)
|
||||||
|
self.assertEqual(call["data"]["pageSize"], 11)
|
||||||
|
self.assertEqual(call["data"]["sortId"], "ctime")
|
||||||
|
self.assertIs(call["data"]["isAsc"], False)
|
||||||
|
|
||||||
|
def test_fetch_products_returns_empty_when_no_data(self):
|
||||||
|
fake_client = _FakeMDYClient({})
|
||||||
|
with patch("flower.utils.mingdaoyun.fetch.get_default_mingdaoyun_client", return_value=fake_client):
|
||||||
|
products, total = asyncio.run(fetch_products_from_mingdaoyun(page=1, page_size=1))
|
||||||
|
|
||||||
|
self.assertEqual(products, [])
|
||||||
|
self.assertEqual(total, 0)
|
||||||
|
|
||||||
|
def test_fetch_row_by_rowid_builds_filters_payload_and_returns_first_row(self):
|
||||||
|
rowid = "row-xyz"
|
||||||
|
row = {
|
||||||
|
"rowid": rowid,
|
||||||
|
mappings.product_type_map["uid"]: "P-ROW",
|
||||||
|
}
|
||||||
|
response = {"data": {"rows": [row]}}
|
||||||
|
fake_client = _FakeMDYClient(response)
|
||||||
|
|
||||||
|
with patch("flower.utils.mingdaoyun.fetch.get_default_mingdaoyun_client", return_value=fake_client):
|
||||||
|
got = asyncio.run(
|
||||||
|
fetch_row_by_rowid_from_mingdaoyun(
|
||||||
|
worksheet_id=mappings.MDY_WORKSHEET_ID_PRODUCT,
|
||||||
|
rowid=rowid,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(got, row)
|
||||||
|
self.assertEqual(len(fake_client.calls), 1)
|
||||||
|
call = fake_client.calls[0]
|
||||||
|
self.assertEqual(call["endpoint"], "/v2/open/worksheet/getFilterRows")
|
||||||
|
self.assertEqual(call["data"]["worksheetId"], mappings.MDY_WORKSHEET_ID_PRODUCT)
|
||||||
|
self.assertEqual(call["data"]["listType"], 1)
|
||||||
|
self.assertEqual(call["data"]["sortId"], "ctime")
|
||||||
|
self.assertIs(call["data"]["isAsc"], False)
|
||||||
|
self.assertIs(call["data"]["notGetTotal"], True)
|
||||||
|
self.assertEqual(
|
||||||
|
call["data"]["filters"],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"controlId": "rowId",
|
||||||
|
"dataType": 2,
|
||||||
|
"filterType": 3,
|
||||||
|
"value": rowid,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_fetch_row_by_rowid_returns_none_when_no_data(self):
|
||||||
|
fake_client = _FakeMDYClient({})
|
||||||
|
with patch("flower.utils.mingdaoyun.fetch.get_default_mingdaoyun_client", return_value=fake_client):
|
||||||
|
got = asyncio.run(
|
||||||
|
fetch_row_by_rowid_from_mingdaoyun(
|
||||||
|
worksheet_id=mappings.MDY_WORKSHEET_ID_PRODUCT,
|
||||||
|
rowid="missing",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertIsNone(got)
|
||||||
|
|
||||||
|
|
||||||
|
class MingDaoYunFabricSyncTestCase(SimpleTestCase):
|
||||||
|
def test_sync_fabric_calls_create_hook_and_returns_total(self):
|
||||||
|
response = {
|
||||||
|
"data": {
|
||||||
|
"rows": [
|
||||||
|
{mappings.fabric_type_map["name"]: "布料A"},
|
||||||
|
{mappings.fabric_type_map["name"]: "布料B"},
|
||||||
|
],
|
||||||
|
"total": 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fake_client = _FakeMDYClient(response)
|
||||||
|
called = {}
|
||||||
|
|
||||||
|
def fake_create(page, page_size, fabrics):
|
||||||
|
called["page"] = page
|
||||||
|
called["page_size"] = page_size
|
||||||
|
called["fabrics"] = fabrics
|
||||||
|
|
||||||
|
def fake_sync_to_async(func):
|
||||||
|
async def _wrapper(*args, **kwargs):
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return _wrapper
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("flower.utils.mingdaoyun.fetch.get_default_mingdaoyun_client", return_value=fake_client),
|
||||||
|
patch("flower.utils.mingdaoyun.fetch._create_fabric_quick_inputs", side_effect=fake_create),
|
||||||
|
patch("flower.utils.mingdaoyun.fetch.sync_to_async", side_effect=fake_sync_to_async),
|
||||||
|
):
|
||||||
|
total = asyncio.run(sync_fabric_from_mingdaoyun(page=9, page_size=10))
|
||||||
|
|
||||||
|
self.assertEqual(total, 2)
|
||||||
|
self.assertEqual(called["page"], 9)
|
||||||
|
self.assertEqual(called["page_size"], 10)
|
||||||
|
self.assertEqual([f.name for f in called["fabrics"]], ["布料A", "布料B"])
|
||||||
|
|
||||||
|
def test_sync_fabric_returns_zero_when_no_data(self):
|
||||||
|
fake_client = _FakeMDYClient({})
|
||||||
|
called = {"hit": False}
|
||||||
|
|
||||||
|
def fake_create(*_args, **_kwargs):
|
||||||
|
called["hit"] = True
|
||||||
|
|
||||||
|
def fake_sync_to_async(func):
|
||||||
|
async def _wrapper(*args, **kwargs):
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return _wrapper
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("flower.utils.mingdaoyun.fetch.get_default_mingdaoyun_client", return_value=fake_client),
|
||||||
|
patch("flower.utils.mingdaoyun.fetch._create_fabric_quick_inputs", side_effect=fake_create),
|
||||||
|
patch("flower.utils.mingdaoyun.fetch.sync_to_async", side_effect=fake_sync_to_async),
|
||||||
|
):
|
||||||
|
total = asyncio.run(sync_fabric_from_mingdaoyun(page=1, page_size=1))
|
||||||
|
|
||||||
|
self.assertEqual(total, 0)
|
||||||
|
self.assertIs(called["hit"], False)
|
||||||
|
|
||||||
|
|
||||||
|
class MingDaoYunClientTestCase(SimpleTestCase):
|
||||||
|
def test_client_post_merges_auth_params_and_passes_headers(self):
|
||||||
|
created = {}
|
||||||
|
|
||||||
|
def fake_client_session(*args, **kwargs):
|
||||||
|
cs = _FakeAiohttpClientSession(*args, **kwargs)
|
||||||
|
created["cs"] = cs
|
||||||
|
return cs
|
||||||
|
|
||||||
|
client = MingDaoYunClient(app_key="ak", sign="sg", base_url="https://api.mingdao.com")
|
||||||
|
with patch("flower.utils.mingdaoyun.client.aiohttp.ClientSession", side_effect=fake_client_session):
|
||||||
|
body = asyncio.run(
|
||||||
|
client.post(
|
||||||
|
endpoint="/v2/open/worksheet/getFilterRows",
|
||||||
|
data={"worksheetId": "dummy", "pageIndex": 1},
|
||||||
|
headers={"X-Test": "1"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(body, {"ok": True})
|
||||||
|
|
||||||
|
cs = created["cs"]
|
||||||
|
self.assertIsInstance(cs.headers, dict)
|
||||||
|
self.assertEqual(cs.headers.get("Content-Type"), "application/json")
|
||||||
|
self.assertEqual(cs.headers.get("X-Test"), "1")
|
||||||
|
|
||||||
|
self.assertEqual(len(cs.session.post_calls), 1)
|
||||||
|
call = cs.session.post_calls[0]
|
||||||
|
self.assertEqual(call["url"], "https://api.mingdao.com/v2/open/worksheet/getFilterRows")
|
||||||
|
self.assertEqual(call["json"]["worksheetId"], "dummy")
|
||||||
|
self.assertEqual(call["json"]["pageIndex"], 1)
|
||||||
|
self.assertEqual(call["json"]["appKey"], "ak")
|
||||||
|
self.assertEqual(call["json"]["sign"], "sg")
|
||||||
|
self.assertTrue(call["response"].raise_for_status_called)
|
||||||
@@ -1723,8 +1723,10 @@ def _to_decimal(value, field_name: str) -> Decimal:
|
|||||||
|
|
||||||
def _ensure_non_zero_amount(value, field_name: str) -> Decimal:
|
def _ensure_non_zero_amount(value, field_name: str) -> Decimal:
|
||||||
amount = _to_decimal(value, field_name)
|
amount = _to_decimal(value, field_name)
|
||||||
if amount == 0:
|
# 金额类字段统一要求 > 0(amount ≤ 0 视为非法)
|
||||||
raise ValueError(f'{field_name} 不能为 0')
|
if amount <= 0:
|
||||||
|
# 与 API 文档/测试约定保持一致
|
||||||
|
raise ValueError(f'{field_name} 必须大于 0')
|
||||||
return amount
|
return amount
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -321,7 +321,7 @@ CELERY_BEAT_SCHEDULE = {
|
|||||||
},
|
},
|
||||||
'mdy_customer_sync': {
|
'mdy_customer_sync': {
|
||||||
'task': 'api_v1.tasks.sync_mdy_customers',
|
'task': 'api_v1.tasks.sync_mdy_customers',
|
||||||
'schedule': crontab(hour='*/5', minute=0),
|
'schedule': crontab(minute='*/5'),
|
||||||
'kwargs': {
|
'kwargs': {
|
||||||
'page_size': MDY_SYNC_PAGE_SIZE,
|
'page_size': MDY_SYNC_PAGE_SIZE,
|
||||||
'max_pages': MDY_SYNC_MAX_PAGES,
|
'max_pages': MDY_SYNC_MAX_PAGES,
|
||||||
|
|||||||
386
flower/utils.py
386
flower/utils.py
@@ -1,386 +0,0 @@
|
|||||||
import aiohttp
|
|
||||||
from typing import Optional, Dict, Any, List
|
|
||||||
from pydantic import BaseModel, Field, computed_field
|
|
||||||
from enum import Enum
|
|
||||||
import json
|
|
||||||
from asgiref.sync import sync_to_async
|
|
||||||
|
|
||||||
|
|
||||||
class Product(BaseModel):
|
|
||||||
"""产品模型 - 请在此填入你的字段"""
|
|
||||||
uid: str
|
|
||||||
rowid: str
|
|
||||||
name: str
|
|
||||||
pieces: int | None
|
|
||||||
segment_size: int | None
|
|
||||||
unit: str | None
|
|
||||||
color: str | None
|
|
||||||
width: str | None = None
|
|
||||||
detail_str: str | None = Field(exclude=True) # 在序列化时排除此字段
|
|
||||||
created_at: str
|
|
||||||
|
|
||||||
@computed_field
|
|
||||||
@property
|
|
||||||
def detail(self) -> dict[str, Any]:
|
|
||||||
"""将 detail_str 转换为字典"""
|
|
||||||
try:
|
|
||||||
return json.loads(self.detail_str) if self.detail_str else {}
|
|
||||||
except (json.JSONDecodeError, TypeError):
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
class ProductListResponse(BaseModel):
|
|
||||||
"""产品列表响应"""
|
|
||||||
products: List[Product]
|
|
||||||
total: int
|
|
||||||
|
|
||||||
|
|
||||||
mdy_table_map = {
|
|
||||||
'product': 'spmx',
|
|
||||||
'customer': 'quanbu',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPMethod(Enum):
|
|
||||||
"""HTTP 请求方法枚举"""
|
|
||||||
GET = "GET"
|
|
||||||
POST = "POST"
|
|
||||||
|
|
||||||
|
|
||||||
class MingDaoYunClient:
|
|
||||||
"""明道云 API 客户端"""
|
|
||||||
|
|
||||||
def __init__(self, app_key: str, sign: str, base_url: str = ""):
|
|
||||||
"""
|
|
||||||
初始化客户端
|
|
||||||
|
|
||||||
Args:
|
|
||||||
app_key: 应用密钥
|
|
||||||
sign: 签名
|
|
||||||
base_url: API 基础地址
|
|
||||||
"""
|
|
||||||
self.app_key = app_key
|
|
||||||
self.sign = sign
|
|
||||||
self.base_url = base_url
|
|
||||||
|
|
||||||
async def request(
|
|
||||||
self,
|
|
||||||
method: HTTPMethod,
|
|
||||||
endpoint: str,
|
|
||||||
data: Optional[Dict[str, Any]] = None,
|
|
||||||
params: Optional[Dict[str, Any]] = None,
|
|
||||||
headers: Optional[Dict[str, str]] = None,
|
|
||||||
) -> Any:
|
|
||||||
"""
|
|
||||||
通用异步请求函数
|
|
||||||
|
|
||||||
Args:
|
|
||||||
method: 请求方法 (GET/POST)
|
|
||||||
endpoint: API 端点路径
|
|
||||||
data: POST 请求体数据(JSON)
|
|
||||||
params: URL 查询参数
|
|
||||||
headers: 自定义请求头
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
响应 JSON 数据
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
aiohttp.ClientError: 请求失败时抛出
|
|
||||||
"""
|
|
||||||
url = f"{self.base_url}{endpoint}"
|
|
||||||
|
|
||||||
# 构建默认请求头
|
|
||||||
default_headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
if headers:
|
|
||||||
default_headers.update(headers)
|
|
||||||
|
|
||||||
# 添加认证参数
|
|
||||||
auth_params = {
|
|
||||||
"appKey": self.app_key,
|
|
||||||
"sign": self.sign
|
|
||||||
}
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
if method == HTTPMethod.GET:
|
|
||||||
async with session.get(url, params=params, json=auth_params) as response:
|
|
||||||
response.raise_for_status()
|
|
||||||
return await response.json()
|
|
||||||
|
|
||||||
elif method == HTTPMethod.POST:
|
|
||||||
if data is None:
|
|
||||||
data = {}
|
|
||||||
|
|
||||||
data.update(auth_params)
|
|
||||||
async with session.post(url, json=data) as response:
|
|
||||||
response.raise_for_status()
|
|
||||||
return await response.json()
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unsupported HTTP method: {method}")
|
|
||||||
|
|
||||||
async def get(
|
|
||||||
self,
|
|
||||||
endpoint: str,
|
|
||||||
params: Optional[Dict[str, Any]] = None,
|
|
||||||
headers: Optional[Dict[str, str]] = None,
|
|
||||||
) -> Any:
|
|
||||||
"""
|
|
||||||
发送 GET 请求
|
|
||||||
|
|
||||||
Args:
|
|
||||||
endpoint: API 端点路径
|
|
||||||
params: URL 查询参数
|
|
||||||
headers: 自定义请求头
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
响应 JSON 数据
|
|
||||||
"""
|
|
||||||
return await self.request(
|
|
||||||
method=HTTPMethod.GET,
|
|
||||||
endpoint=endpoint,
|
|
||||||
params=params,
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def post(
|
|
||||||
self,
|
|
||||||
endpoint: str,
|
|
||||||
data: Optional[Dict[str, Any]] = None,
|
|
||||||
params: Optional[Dict[str, Any]] = None,
|
|
||||||
headers: Optional[Dict[str, str]] = None,
|
|
||||||
) -> Any:
|
|
||||||
"""
|
|
||||||
发送 POST 请求
|
|
||||||
|
|
||||||
Args:
|
|
||||||
endpoint: API 端点路径
|
|
||||||
data: POST 请求体数据(JSON)
|
|
||||||
params: URL 查询参数
|
|
||||||
headers: 自定义请求头
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
响应 JSON 数据
|
|
||||||
"""
|
|
||||||
return await self.request(
|
|
||||||
method=HTTPMethod.POST,
|
|
||||||
endpoint=endpoint,
|
|
||||||
data=data,
|
|
||||||
params=params,
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
product_type_map = {
|
|
||||||
'name': '668caa5eb80969563ecaee7d',
|
|
||||||
'pieces': '689c5aa37c175367b29fcb87',
|
|
||||||
'color': '66ed3b9ae01d5599bdb45f6d',
|
|
||||||
'uid': '668caa5eb80969563ecaee7c',
|
|
||||||
'unit': '668e2ee2ca758c8cc5d0dc1d',
|
|
||||||
'segment_size': '668e2194790c0e04058b4f3c',
|
|
||||||
'width': '668e2194790c0e04058b4f3c',
|
|
||||||
'detail_str': '668caa5eb80969563ecaee7e',
|
|
||||||
'created_at': 'ctime',
|
|
||||||
'rowid': 'rowid',
|
|
||||||
}
|
|
||||||
|
|
||||||
customer_type_map = {
|
|
||||||
'name': '62d52f4b8d2972284492dcf9',
|
|
||||||
'area': '62d52f4b8d2972284492dd09',
|
|
||||||
'created_at': 'ctime',
|
|
||||||
'rowid': 'rowid',
|
|
||||||
'uid': '668bb9370207cf7520fe551e',
|
|
||||||
}
|
|
||||||
|
|
||||||
class Customer(BaseModel):
|
|
||||||
"""客户模型 - 请在此填入你的字段"""
|
|
||||||
uid: str
|
|
||||||
rowid: str
|
|
||||||
name: str
|
|
||||||
area: str | None
|
|
||||||
created_at: str
|
|
||||||
|
|
||||||
|
|
||||||
def pick_customer(fields: dict) -> Customer:
|
|
||||||
"""从字段字典中提取客户信息"""
|
|
||||||
data = {k: fields.get(v, '') for k, v in customer_type_map.items()}
|
|
||||||
return Customer(**data)
|
|
||||||
|
|
||||||
|
|
||||||
# json param example:
|
|
||||||
# {
|
|
||||||
# "appKey": "208e55fea5cea59f",
|
|
||||||
# "sign": "MWU0YmViYjkwZmM1ZDIzYzRiN2U3ZGQ4MmE4ZGNkMjc0MWM1ZmQ2ZjkwMjljODE4YmNkZTBhMzA0OTU2YzE2NA==",
|
|
||||||
# "worksheetId": "quanbu",
|
|
||||||
# "listType": 1,
|
|
||||||
# "sortId": "ctime",
|
|
||||||
# "isAsc": false,
|
|
||||||
# "notGetTotal": true
|
|
||||||
# }
|
|
||||||
|
|
||||||
|
|
||||||
def pick_product(fields: dict) -> Product:
|
|
||||||
"""
|
|
||||||
从字段字典中提取产品信息
|
|
||||||
"""
|
|
||||||
data = {k: fields.get(v, '') for k, v in product_type_map.items()}
|
|
||||||
|
|
||||||
def _to_int(value):
|
|
||||||
if value in ('', None):
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
return int(value)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
data['pieces'] = _to_int(data.get('pieces'))
|
|
||||||
data['segment_size'] = _to_int(data.get('segment_size'))
|
|
||||||
|
|
||||||
return Product(**data)
|
|
||||||
|
|
||||||
|
|
||||||
async def fetch_products_from_mingdaoyun(page: int = 1, page_size: int = 100) -> tuple[list[Product], int]:
|
|
||||||
"""
|
|
||||||
从明道云获取产品列表
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple: (产品列表, 总数量)
|
|
||||||
"""
|
|
||||||
client = MingDaoYunClient(
|
|
||||||
app_key='208e55fea5cea59f',
|
|
||||||
sign='MWU0YmViYjkwZmM1ZDIzYzRiN2U3ZGQ4MmE4ZGNkMjc0MWM1ZmQ2ZjkwMjljODE4YmNkZTBhMzA0OTU2YzE2NA==',
|
|
||||||
base_url="https://api.mingdao.com"
|
|
||||||
)
|
|
||||||
response = await client.post(
|
|
||||||
endpoint='/v2/open/worksheet/getFilterRows',
|
|
||||||
data={
|
|
||||||
'worksheetId': 'spmx',
|
|
||||||
'pageIndex': page,
|
|
||||||
'pageSize': page_size,
|
|
||||||
'sortId': 'ctime',
|
|
||||||
'isAsc': True,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
data = response.get('data')
|
|
||||||
if data:
|
|
||||||
print(f'[product-sync] page={page} size={page_size} rows={len(data.get("rows", []))} total={data.get("total")}')
|
|
||||||
else:
|
|
||||||
print(f'[product-sync] page={page} size={page_size} received empty data')
|
|
||||||
if not data:
|
|
||||||
return [], 0
|
|
||||||
|
|
||||||
products = [pick_product(item) for item in data.get('rows', [])]
|
|
||||||
total_count = data.get('total', 0)
|
|
||||||
return products, total_count
|
|
||||||
|
|
||||||
|
|
||||||
async def fetch_customers_from_mingdaoyun(page: int = 1, page_size: int = 100) -> tuple[list[Customer], int]:
|
|
||||||
"""
|
|
||||||
从明道云获取客户列表
|
|
||||||
"""
|
|
||||||
client = MingDaoYunClient(
|
|
||||||
app_key='208e55fea5cea59f',
|
|
||||||
sign='MWU0YmViYjkwZmM1ZDIzYzRiN2U3ZGQ4MmE4ZGNkMjc0MWM1ZmQ2ZjkwMjljODE4YmNkZTBhMzA0OTU2YzE2NA==',
|
|
||||||
base_url="https://api.mingdao.com"
|
|
||||||
)
|
|
||||||
response = await client.post(
|
|
||||||
endpoint='/v2/open/worksheet/getFilterRows',
|
|
||||||
data={
|
|
||||||
'worksheetId': 'quanbu',
|
|
||||||
'pageIndex': page,
|
|
||||||
'pageSize': page_size,
|
|
||||||
'sortId': 'ctime',
|
|
||||||
'isAsc': True,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
data = response.get('data')
|
|
||||||
if data:
|
|
||||||
print(f'[customer-sync] page={page} size={page_size} rows={len(data.get("rows", []))} total={data.get("total")}')
|
|
||||||
else:
|
|
||||||
print(f'[customer-sync] page={page} size={page_size} received empty data')
|
|
||||||
if not data:
|
|
||||||
return [], 0
|
|
||||||
|
|
||||||
customers = [pick_customer(item) for item in data.get('rows', [])]
|
|
||||||
total_count = data.get('total', 0)
|
|
||||||
return customers, total_count
|
|
||||||
|
|
||||||
|
|
||||||
fabric_type_map = {
|
|
||||||
'name': '62d52f4b8d2972284492de61',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Fabric(BaseModel):
|
|
||||||
"""面料模型 - 请在此填入你的字段"""
|
|
||||||
name: str
|
|
||||||
|
|
||||||
|
|
||||||
def pick_fabric(fields: dict) -> Fabric:
|
|
||||||
"""
|
|
||||||
从字段字典中提取面料信息
|
|
||||||
"""
|
|
||||||
data = {k: fields.get(v, '') for k, v in fabric_type_map.items()}
|
|
||||||
return Fabric(**data)
|
|
||||||
|
|
||||||
|
|
||||||
async def sync_fabric_from_mingdaoyun(page: int = 1, page_size: int = 100) -> int:
|
|
||||||
"""
|
|
||||||
从明道云同步面料数据
|
|
||||||
"""
|
|
||||||
client = MingDaoYunClient(
|
|
||||||
app_key='208e55fea5cea59f',
|
|
||||||
sign='MWU0YmViYjkwZmM1ZDIzYzRiN2U3ZGQ4MmE4ZGNkMjc0MWM1ZmQ2ZjkwMjljODE4YmNkZTBhMzA0OTU2YzE2NA==',
|
|
||||||
base_url="https://api.mingdao.com"
|
|
||||||
)
|
|
||||||
response = await client.post(
|
|
||||||
endpoint='/v2/open/worksheet/getFilterRows',
|
|
||||||
data={
|
|
||||||
'worksheetId': '668ba100fb551c850214067d',
|
|
||||||
'pageIndex': page,
|
|
||||||
'pageSize': page_size,
|
|
||||||
'sortId': 'ctime',
|
|
||||||
'isAsc': False,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
data = response.get('data')
|
|
||||||
if data:
|
|
||||||
rows = data.get("rows", [])
|
|
||||||
print(f'[fabric-sync] fetch rows={len(rows)} total={data.get("total")}')
|
|
||||||
if rows:
|
|
||||||
print(f'[fabric-sync] first row keys: {list(rows[0].keys())[:10]}')
|
|
||||||
print(f'[fabric-sync] first row sample: {rows[0]}')
|
|
||||||
else:
|
|
||||||
print(f'[fabric-sync] fetch empty data (page={page}, size={page_size})')
|
|
||||||
if not data:
|
|
||||||
return [], 0
|
|
||||||
fabrics = [pick_fabric(item) for item in data.get('rows', [])]
|
|
||||||
total_count = data.get('total', 0)
|
|
||||||
await sync_to_async(_create_fabric_quick_inputs)(
|
|
||||||
page,
|
|
||||||
page_size,
|
|
||||||
fabrics
|
|
||||||
)
|
|
||||||
return total_count
|
|
||||||
|
|
||||||
|
|
||||||
def _create_fabric_quick_inputs(page: int, page_size: int, fabrics: list[Fabric]) -> None:
|
|
||||||
from basic_info import models as basic_models
|
|
||||||
total = len(fabrics)
|
|
||||||
created_count = 0
|
|
||||||
sample_names = [fabric.name for fabric in fabrics[:5]]
|
|
||||||
print(f'[fabric-sync] page={page} size={page_size} fetched={total}')
|
|
||||||
print(f'[fabric-sync] sample names: {sample_names}')
|
|
||||||
for fabric in fabrics:
|
|
||||||
if not fabric.name:
|
|
||||||
continue
|
|
||||||
obj, created = basic_models.QuickInput.objects.update_or_create(
|
|
||||||
name=fabric.name,
|
|
||||||
group='布料名',
|
|
||||||
defaults={'value': fabric.name},
|
|
||||||
)
|
|
||||||
if created:
|
|
||||||
created_count += 1
|
|
||||||
print(f'[fabric-sync] page={page} new_records={created_count} updated_or_existing={total - created_count}')
|
|
||||||
100
flower/utils/__init__.py
Normal file
100
flower/utils/__init__.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
"""工具包。
|
||||||
|
|
||||||
|
历史上 `flower.utils` 是一个单文件模块(`utils.py`)。
|
||||||
|
为了降低耦合并便于扩展,现已拆分为目录包,并在此保留向后兼容的导出。
|
||||||
|
|
||||||
|
- 明道云相关:`flower.utils.mingdaoyun.*`
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .mingdaoyun import (
|
||||||
|
HTTPMethod,
|
||||||
|
MDY_APP_KEY,
|
||||||
|
MDY_BASE_URL,
|
||||||
|
MDY_ENDPOINT_GET_FILTER_ROWS,
|
||||||
|
MDY_SIGN,
|
||||||
|
MDY_WORKSHEET_ID_CUSTOMER,
|
||||||
|
MDY_WORKSHEET_ID_FABRIC,
|
||||||
|
MDY_WORKSHEET_ID_PLATE_ORDER,
|
||||||
|
MDY_WORKSHEET_ID_PLATE_ORDER_COLORING,
|
||||||
|
MDY_WORKSHEET_ID_PLATE_ORDER_COLOR_SCHEME,
|
||||||
|
MDY_WORKSHEET_ID_PLATE_ORDER_DRAWING,
|
||||||
|
MDY_WORKSHEET_ID_PLATE_ORDER_IMAGE_DEVELOPMENT,
|
||||||
|
MDY_WORKSHEET_ID_PLATE_ORDER_MODIFY_DRAWING,
|
||||||
|
MDY_WORKSHEET_ID_PLATE_ORDER_PATTERN_SET,
|
||||||
|
MDY_WORKSHEET_ID_PRODUCT,
|
||||||
|
Customer,
|
||||||
|
Fabric,
|
||||||
|
MDYAttachmentItem,
|
||||||
|
MDYCollaboratorItem,
|
||||||
|
MDYPlateOrder,
|
||||||
|
MDYRelationItem,
|
||||||
|
MingDaoYunClient,
|
||||||
|
Product,
|
||||||
|
ProductListResponse,
|
||||||
|
customer_type_map,
|
||||||
|
fabric_type_map,
|
||||||
|
fetch_customers_from_mingdaoyun,
|
||||||
|
fetch_plate_orders_from_mingdaoyun,
|
||||||
|
fetch_products_from_mingdaoyun,
|
||||||
|
fetch_row_by_rowid_from_mingdaoyun,
|
||||||
|
get_default_mingdaoyun_client,
|
||||||
|
mdy_table_map,
|
||||||
|
pick_customer,
|
||||||
|
pick_fabric,
|
||||||
|
pick_product,
|
||||||
|
plate_order_field_definitions,
|
||||||
|
plate_order_related_worksheet_map,
|
||||||
|
plate_order_related_worksheet_map_cn,
|
||||||
|
plate_order_type_map,
|
||||||
|
product_type_map,
|
||||||
|
sync_fabric_from_mingdaoyun,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# client
|
||||||
|
"HTTPMethod",
|
||||||
|
"MingDaoYunClient",
|
||||||
|
"get_default_mingdaoyun_client",
|
||||||
|
"MDY_BASE_URL",
|
||||||
|
"MDY_ENDPOINT_GET_FILTER_ROWS",
|
||||||
|
"MDY_APP_KEY",
|
||||||
|
"MDY_SIGN",
|
||||||
|
# worksheet ids / maps
|
||||||
|
"mdy_table_map",
|
||||||
|
"MDY_WORKSHEET_ID_PRODUCT",
|
||||||
|
"MDY_WORKSHEET_ID_CUSTOMER",
|
||||||
|
"MDY_WORKSHEET_ID_FABRIC",
|
||||||
|
"MDY_WORKSHEET_ID_PLATE_ORDER",
|
||||||
|
"MDY_WORKSHEET_ID_PLATE_ORDER_DRAWING",
|
||||||
|
"MDY_WORKSHEET_ID_PLATE_ORDER_COLORING",
|
||||||
|
"MDY_WORKSHEET_ID_PLATE_ORDER_PATTERN_SET",
|
||||||
|
"MDY_WORKSHEET_ID_PLATE_ORDER_MODIFY_DRAWING",
|
||||||
|
"MDY_WORKSHEET_ID_PLATE_ORDER_COLOR_SCHEME",
|
||||||
|
"MDY_WORKSHEET_ID_PLATE_ORDER_IMAGE_DEVELOPMENT",
|
||||||
|
"product_type_map",
|
||||||
|
"customer_type_map",
|
||||||
|
"fabric_type_map",
|
||||||
|
"plate_order_field_definitions",
|
||||||
|
"plate_order_related_worksheet_map",
|
||||||
|
"plate_order_related_worksheet_map_cn",
|
||||||
|
"plate_order_type_map",
|
||||||
|
# models
|
||||||
|
"Product",
|
||||||
|
"ProductListResponse",
|
||||||
|
"Customer",
|
||||||
|
"Fabric",
|
||||||
|
"MDYRelationItem",
|
||||||
|
"MDYAttachmentItem",
|
||||||
|
"MDYCollaboratorItem",
|
||||||
|
"MDYPlateOrder",
|
||||||
|
# parsers
|
||||||
|
"pick_product",
|
||||||
|
"pick_customer",
|
||||||
|
"pick_fabric",
|
||||||
|
# fetch
|
||||||
|
"fetch_products_from_mingdaoyun",
|
||||||
|
"fetch_customers_from_mingdaoyun",
|
||||||
|
"fetch_row_by_rowid_from_mingdaoyun",
|
||||||
|
"fetch_plate_orders_from_mingdaoyun",
|
||||||
|
"sync_fabric_from_mingdaoyun",
|
||||||
|
]
|
||||||
96
flower/utils/mingdaoyun/__init__.py
Normal file
96
flower/utils/mingdaoyun/__init__.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
from .client import (
|
||||||
|
HTTPMethod,
|
||||||
|
MDY_APP_KEY,
|
||||||
|
MDY_BASE_URL,
|
||||||
|
MDY_ENDPOINT_GET_FILTER_ROWS,
|
||||||
|
MDY_SIGN,
|
||||||
|
MingDaoYunClient,
|
||||||
|
get_default_mingdaoyun_client,
|
||||||
|
)
|
||||||
|
from .fetch import (
|
||||||
|
fetch_customers_from_mingdaoyun,
|
||||||
|
fetch_plate_orders_from_mingdaoyun,
|
||||||
|
fetch_products_from_mingdaoyun,
|
||||||
|
fetch_row_by_rowid_from_mingdaoyun,
|
||||||
|
sync_fabric_from_mingdaoyun,
|
||||||
|
)
|
||||||
|
from .mappings import (
|
||||||
|
MDY_WORKSHEET_ID_CUSTOMER,
|
||||||
|
MDY_WORKSHEET_ID_FABRIC,
|
||||||
|
MDY_WORKSHEET_ID_PLATE_ORDER,
|
||||||
|
MDY_WORKSHEET_ID_PLATE_ORDER_COLORING,
|
||||||
|
MDY_WORKSHEET_ID_PLATE_ORDER_COLOR_SCHEME,
|
||||||
|
MDY_WORKSHEET_ID_PLATE_ORDER_DRAWING,
|
||||||
|
MDY_WORKSHEET_ID_PLATE_ORDER_IMAGE_DEVELOPMENT,
|
||||||
|
MDY_WORKSHEET_ID_PLATE_ORDER_MODIFY_DRAWING,
|
||||||
|
MDY_WORKSHEET_ID_PLATE_ORDER_PATTERN_SET,
|
||||||
|
MDY_WORKSHEET_ID_PRODUCT,
|
||||||
|
customer_type_map,
|
||||||
|
fabric_type_map,
|
||||||
|
mdy_table_map,
|
||||||
|
plate_order_field_definitions,
|
||||||
|
plate_order_related_worksheet_map,
|
||||||
|
plate_order_related_worksheet_map_cn,
|
||||||
|
plate_order_type_map,
|
||||||
|
product_type_map,
|
||||||
|
)
|
||||||
|
from .models import (
|
||||||
|
Customer,
|
||||||
|
Fabric,
|
||||||
|
MDYAttachmentItem,
|
||||||
|
MDYCollaboratorItem,
|
||||||
|
MDYPlateOrder,
|
||||||
|
MDYRelationItem,
|
||||||
|
Product,
|
||||||
|
ProductListResponse,
|
||||||
|
)
|
||||||
|
from .parsers import pick_customer, pick_fabric, pick_product
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# client
|
||||||
|
"HTTPMethod",
|
||||||
|
"MingDaoYunClient",
|
||||||
|
"get_default_mingdaoyun_client",
|
||||||
|
"MDY_BASE_URL",
|
||||||
|
"MDY_ENDPOINT_GET_FILTER_ROWS",
|
||||||
|
"MDY_APP_KEY",
|
||||||
|
"MDY_SIGN",
|
||||||
|
# worksheet ids / maps
|
||||||
|
"mdy_table_map",
|
||||||
|
"MDY_WORKSHEET_ID_PRODUCT",
|
||||||
|
"MDY_WORKSHEET_ID_CUSTOMER",
|
||||||
|
"MDY_WORKSHEET_ID_FABRIC",
|
||||||
|
"MDY_WORKSHEET_ID_PLATE_ORDER",
|
||||||
|
"MDY_WORKSHEET_ID_PLATE_ORDER_DRAWING",
|
||||||
|
"MDY_WORKSHEET_ID_PLATE_ORDER_COLORING",
|
||||||
|
"MDY_WORKSHEET_ID_PLATE_ORDER_PATTERN_SET",
|
||||||
|
"MDY_WORKSHEET_ID_PLATE_ORDER_MODIFY_DRAWING",
|
||||||
|
"MDY_WORKSHEET_ID_PLATE_ORDER_COLOR_SCHEME",
|
||||||
|
"MDY_WORKSHEET_ID_PLATE_ORDER_IMAGE_DEVELOPMENT",
|
||||||
|
"product_type_map",
|
||||||
|
"customer_type_map",
|
||||||
|
"fabric_type_map",
|
||||||
|
"plate_order_field_definitions",
|
||||||
|
"plate_order_related_worksheet_map",
|
||||||
|
"plate_order_related_worksheet_map_cn",
|
||||||
|
"plate_order_type_map",
|
||||||
|
# models
|
||||||
|
"Product",
|
||||||
|
"ProductListResponse",
|
||||||
|
"Customer",
|
||||||
|
"Fabric",
|
||||||
|
"MDYRelationItem",
|
||||||
|
"MDYAttachmentItem",
|
||||||
|
"MDYCollaboratorItem",
|
||||||
|
"MDYPlateOrder",
|
||||||
|
# parsers
|
||||||
|
"pick_product",
|
||||||
|
"pick_customer",
|
||||||
|
"pick_fabric",
|
||||||
|
# fetch
|
||||||
|
"fetch_products_from_mingdaoyun",
|
||||||
|
"fetch_customers_from_mingdaoyun",
|
||||||
|
"fetch_row_by_rowid_from_mingdaoyun",
|
||||||
|
"fetch_plate_orders_from_mingdaoyun",
|
||||||
|
"sync_fabric_from_mingdaoyun",
|
||||||
|
]
|
||||||
109
flower/utils/mingdaoyun/client.py
Normal file
109
flower/utils/mingdaoyun/client.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
# 明道云开放接口:基础配置
|
||||||
|
MDY_BASE_URL = "https://api.mingdao.com"
|
||||||
|
MDY_ENDPOINT_GET_FILTER_ROWS = "/v2/open/worksheet/getFilterRows"
|
||||||
|
|
||||||
|
# NOTE:
|
||||||
|
# - 目前项目里 appKey/sign 仍是硬编码(与现有同步代码保持一致)
|
||||||
|
# - 后续如果要做多环境/更安全配置,建议迁移到环境变量或 Django settings
|
||||||
|
MDY_APP_KEY = "208e55fea5cea59f"
|
||||||
|
MDY_SIGN = "MWU0YmViYjkwZmM1ZDIzYzRiN2U3ZGQ4MmE4ZGNkMjc0MWM1ZmQ2ZjkwMjljODE4YmNkZTBhMzA0OTU2YzE2NA=="
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPMethod(Enum):
|
||||||
|
"""HTTP 请求方法枚举"""
|
||||||
|
|
||||||
|
GET = "GET"
|
||||||
|
POST = "POST"
|
||||||
|
|
||||||
|
|
||||||
|
class MingDaoYunClient:
|
||||||
|
"""明道云 API 客户端(异步)"""
|
||||||
|
|
||||||
|
def __init__(self, app_key: str, sign: str, base_url: str = MDY_BASE_URL):
|
||||||
|
self.app_key = app_key
|
||||||
|
self.sign = sign
|
||||||
|
self.base_url = base_url
|
||||||
|
|
||||||
|
async def request(
|
||||||
|
self,
|
||||||
|
method: HTTPMethod,
|
||||||
|
endpoint: str,
|
||||||
|
data: Optional[Dict[str, Any]] = None,
|
||||||
|
params: Optional[Dict[str, Any]] = None,
|
||||||
|
headers: Optional[Dict[str, str]] = None,
|
||||||
|
) -> Any:
|
||||||
|
"""通用异步请求函数"""
|
||||||
|
|
||||||
|
url = f"{self.base_url}{endpoint}"
|
||||||
|
|
||||||
|
default_headers: Dict[str, str] = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
if headers:
|
||||||
|
default_headers.update(headers)
|
||||||
|
|
||||||
|
# 添加认证参数
|
||||||
|
auth_params = {
|
||||||
|
"appKey": self.app_key,
|
||||||
|
"sign": self.sign,
|
||||||
|
}
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession(headers=default_headers) as session:
|
||||||
|
if method == HTTPMethod.GET:
|
||||||
|
async with session.get(url, params=params, json=auth_params) as response:
|
||||||
|
response.raise_for_status()
|
||||||
|
return await response.json()
|
||||||
|
|
||||||
|
if method == HTTPMethod.POST:
|
||||||
|
payload: Dict[str, Any] = dict(data or {})
|
||||||
|
payload.update(auth_params)
|
||||||
|
async with session.post(url, json=payload) as response:
|
||||||
|
response.raise_for_status()
|
||||||
|
return await response.json()
|
||||||
|
|
||||||
|
raise ValueError(f"Unsupported HTTP method: {method}")
|
||||||
|
|
||||||
|
async def get(
|
||||||
|
self,
|
||||||
|
endpoint: str,
|
||||||
|
params: Optional[Dict[str, Any]] = None,
|
||||||
|
headers: Optional[Dict[str, str]] = None,
|
||||||
|
) -> Any:
|
||||||
|
"""发送 GET 请求"""
|
||||||
|
|
||||||
|
return await self.request(
|
||||||
|
method=HTTPMethod.GET,
|
||||||
|
endpoint=endpoint,
|
||||||
|
params=params,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def post(
|
||||||
|
self,
|
||||||
|
endpoint: str,
|
||||||
|
data: Optional[Dict[str, Any]] = None,
|
||||||
|
params: Optional[Dict[str, Any]] = None,
|
||||||
|
headers: Optional[Dict[str, str]] = None,
|
||||||
|
) -> Any:
|
||||||
|
"""发送 POST 请求"""
|
||||||
|
|
||||||
|
return await self.request(
|
||||||
|
method=HTTPMethod.POST,
|
||||||
|
endpoint=endpoint,
|
||||||
|
data=data,
|
||||||
|
params=params,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_mingdaoyun_client(base_url: str = MDY_BASE_URL) -> MingDaoYunClient:
|
||||||
|
"""获取默认配置的明道云客户端(复用项目现有 appKey/sign)"""
|
||||||
|
|
||||||
|
return MingDaoYunClient(app_key=MDY_APP_KEY, sign=MDY_SIGN, base_url=base_url)
|
||||||
225
flower/utils/mingdaoyun/fetch.py
Normal file
225
flower/utils/mingdaoyun/fetch.py
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from asgiref.sync import sync_to_async
|
||||||
|
|
||||||
|
from .client import MDY_ENDPOINT_GET_FILTER_ROWS, get_default_mingdaoyun_client
|
||||||
|
from .mappings import (
|
||||||
|
MDY_WORKSHEET_ID_CUSTOMER,
|
||||||
|
MDY_WORKSHEET_ID_FABRIC,
|
||||||
|
MDY_WORKSHEET_ID_PLATE_ORDER,
|
||||||
|
MDY_WORKSHEET_ID_PRODUCT,
|
||||||
|
)
|
||||||
|
from .models import Customer, Fabric, Product
|
||||||
|
from .parsers import pick_customer, pick_fabric, pick_product
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_products_from_mingdaoyun(page: int = 1, page_size: int = 100) -> tuple[list[Product], int]:
|
||||||
|
"""从明道云获取产品列表"""
|
||||||
|
|
||||||
|
client = get_default_mingdaoyun_client()
|
||||||
|
response = await client.post(
|
||||||
|
endpoint=MDY_ENDPOINT_GET_FILTER_ROWS,
|
||||||
|
data={
|
||||||
|
"worksheetId": MDY_WORKSHEET_ID_PRODUCT,
|
||||||
|
"pageIndex": page,
|
||||||
|
"pageSize": page_size,
|
||||||
|
"sortId": "ctime",
|
||||||
|
"isAsc": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
data = response.get("data") if isinstance(response, dict) else None
|
||||||
|
if data:
|
||||||
|
print(
|
||||||
|
f'[product-sync] page={page} size={page_size} rows={len(data.get("rows", []))} total={data.get("total")}'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(f"[product-sync] page={page} size={page_size} received empty data")
|
||||||
|
if not data:
|
||||||
|
return [], 0
|
||||||
|
|
||||||
|
products = [pick_product(item) for item in data.get("rows", [])]
|
||||||
|
total_count = data.get("total", 0)
|
||||||
|
return products, total_count
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_customers_from_mingdaoyun(page: int = 1, page_size: int = 100) -> tuple[list[Customer], int]:
|
||||||
|
"""从明道云获取客户列表"""
|
||||||
|
|
||||||
|
client = get_default_mingdaoyun_client()
|
||||||
|
response = await client.post(
|
||||||
|
endpoint=MDY_ENDPOINT_GET_FILTER_ROWS,
|
||||||
|
data={
|
||||||
|
"worksheetId": MDY_WORKSHEET_ID_CUSTOMER,
|
||||||
|
"pageIndex": page,
|
||||||
|
"pageSize": page_size,
|
||||||
|
"sortId": "ctime",
|
||||||
|
"isAsc": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
data = response.get("data") if isinstance(response, dict) else None
|
||||||
|
if data:
|
||||||
|
print(
|
||||||
|
f'[customer-sync] page={page} size={page_size} rows={len(data.get("rows", []))} total={data.get("total")}'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(f"[customer-sync] page={page} size={page_size} received empty data")
|
||||||
|
if not data:
|
||||||
|
return [], 0
|
||||||
|
|
||||||
|
customers = [pick_customer(item) for item in data.get("rows", [])]
|
||||||
|
total_count = data.get("total", 0)
|
||||||
|
return customers, total_count
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_row_by_rowid_from_mingdaoyun(
|
||||||
|
worksheet_id: str,
|
||||||
|
rowid: str,
|
||||||
|
*,
|
||||||
|
sort_id: str = "ctime",
|
||||||
|
is_asc: bool = False,
|
||||||
|
list_type: int = 1,
|
||||||
|
not_get_total: bool = True,
|
||||||
|
data_type: int = 2,
|
||||||
|
filter_type: int = 3,
|
||||||
|
control_id: str = "rowId",
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
"""按 rowId 查询工作表的单条记录(仅封装 rowId 等值查询)。
|
||||||
|
|
||||||
|
按照明道云 getFilterRows 的 filters 结构构造请求:
|
||||||
|
- dataType=2 表示字符串
|
||||||
|
- filterType=3 表示“相等”
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- 匹配到记录:返回第一条 row dict
|
||||||
|
- 未匹配或响应无 data:返回 None
|
||||||
|
"""
|
||||||
|
|
||||||
|
client = get_default_mingdaoyun_client()
|
||||||
|
response = await client.post(
|
||||||
|
endpoint=MDY_ENDPOINT_GET_FILTER_ROWS,
|
||||||
|
data={
|
||||||
|
"worksheetId": worksheet_id,
|
||||||
|
"listType": list_type,
|
||||||
|
"sortId": sort_id,
|
||||||
|
"isAsc": is_asc,
|
||||||
|
"notGetTotal": not_get_total,
|
||||||
|
"filters": [
|
||||||
|
{
|
||||||
|
"controlId": control_id,
|
||||||
|
"dataType": data_type,
|
||||||
|
"filterType": filter_type,
|
||||||
|
"value": rowid,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
data = response.get("data") if isinstance(response, dict) else None
|
||||||
|
if not data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
rows = data.get("rows") or []
|
||||||
|
if not rows:
|
||||||
|
return None
|
||||||
|
if isinstance(rows[0], dict):
|
||||||
|
return rows[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_plate_orders_from_mingdaoyun(
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 100,
|
||||||
|
*,
|
||||||
|
sort_id: str = "ctime",
|
||||||
|
is_asc: bool = True,
|
||||||
|
) -> tuple[list[dict[str, Any]], int]:
|
||||||
|
"""从明道云获取“开版数据表”记录列表(原始行字典,不做字段映射解析)"""
|
||||||
|
|
||||||
|
client = get_default_mingdaoyun_client()
|
||||||
|
response = await client.post(
|
||||||
|
endpoint=MDY_ENDPOINT_GET_FILTER_ROWS,
|
||||||
|
data={
|
||||||
|
"worksheetId": MDY_WORKSHEET_ID_PLATE_ORDER,
|
||||||
|
"pageIndex": page,
|
||||||
|
"pageSize": page_size,
|
||||||
|
"sortId": sort_id,
|
||||||
|
"isAsc": is_asc,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
data = response.get("data") if isinstance(response, dict) else None
|
||||||
|
if data:
|
||||||
|
print(
|
||||||
|
f'[plate-order-fetch] page={page} size={page_size} rows={len(data.get("rows", []))} total={data.get("total")}'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(f"[plate-order-fetch] page={page} size={page_size} received empty data")
|
||||||
|
if not data:
|
||||||
|
return [], 0
|
||||||
|
|
||||||
|
rows = data.get("rows", [])
|
||||||
|
total_count = data.get("total", 0)
|
||||||
|
return rows, total_count
|
||||||
|
|
||||||
|
|
||||||
|
async def sync_fabric_from_mingdaoyun(page: int = 1, page_size: int = 100) -> int:
|
||||||
|
"""从明道云同步面料数据(用于创建/更新 QuickInput: 布料名)"""
|
||||||
|
|
||||||
|
client = get_default_mingdaoyun_client()
|
||||||
|
response = await client.post(
|
||||||
|
endpoint=MDY_ENDPOINT_GET_FILTER_ROWS,
|
||||||
|
data={
|
||||||
|
"worksheetId": MDY_WORKSHEET_ID_FABRIC,
|
||||||
|
"pageIndex": page,
|
||||||
|
"pageSize": page_size,
|
||||||
|
"sortId": "ctime",
|
||||||
|
"isAsc": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
data = response.get("data") if isinstance(response, dict) else None
|
||||||
|
if data:
|
||||||
|
rows = data.get("rows", [])
|
||||||
|
print(f"[fabric-sync] fetch rows={len(rows)} total={data.get('total')}")
|
||||||
|
if rows:
|
||||||
|
print(f"[fabric-sync] first row keys: {list(rows[0].keys())[:10]}")
|
||||||
|
print(f"[fabric-sync] first row sample: {rows[0]}")
|
||||||
|
else:
|
||||||
|
print(f"[fabric-sync] fetch empty data (page={page}, size={page_size})")
|
||||||
|
if not data:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
fabrics = [pick_fabric(item) for item in data.get("rows", [])]
|
||||||
|
total_count = data.get("total", 0)
|
||||||
|
|
||||||
|
await sync_to_async(_create_fabric_quick_inputs)(
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
fabrics,
|
||||||
|
)
|
||||||
|
return total_count
|
||||||
|
|
||||||
|
|
||||||
|
def _create_fabric_quick_inputs(page: int, page_size: int, fabrics: list[Fabric]) -> None:
|
||||||
|
from basic_info import models as basic_models
|
||||||
|
|
||||||
|
total = len(fabrics)
|
||||||
|
created_count = 0
|
||||||
|
sample_names = [fabric.name for fabric in fabrics[:5]]
|
||||||
|
print(f"[fabric-sync] page={page} size={page_size} fetched={total}")
|
||||||
|
print(f"[fabric-sync] sample names: {sample_names}")
|
||||||
|
for fabric in fabrics:
|
||||||
|
if not fabric.name:
|
||||||
|
continue
|
||||||
|
obj, created = basic_models.QuickInput.objects.update_or_create(
|
||||||
|
name=fabric.name,
|
||||||
|
group="布料名",
|
||||||
|
defaults={"value": fabric.name},
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
created_count += 1
|
||||||
|
print(f"[fabric-sync] page={page} new_records={created_count} updated_or_existing={total - created_count}")
|
||||||
167
flower/utils/mingdaoyun/mappings.py
Normal file
167
flower/utils/mingdaoyun/mappings.py
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# 明道云:worksheetId 常量
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
MDY_WORKSHEET_ID_PRODUCT = "spmx"
|
||||||
|
MDY_WORKSHEET_ID_CUSTOMER = "quanbu"
|
||||||
|
MDY_WORKSHEET_ID_FABRIC = "668ba100fb551c850214067d"
|
||||||
|
MDY_WORKSHEET_ID_PLATE_ORDER = "668ba100fb551c8502140660" # 开版数据表
|
||||||
|
|
||||||
|
# 开版表关联数据 worksheetId(用于 Relation 关联查询)
|
||||||
|
MDY_WORKSHEET_ID_PLATE_ORDER_DRAWING = "668ba100fb551c850214066b" # 画图
|
||||||
|
MDY_WORKSHEET_ID_PLATE_ORDER_COLORING = "668ba100fb551c850214066c" # 调色
|
||||||
|
MDY_WORKSHEET_ID_PLATE_ORDER_PATTERN_SET = "668ba100fb551c8502140681" # 套纸样
|
||||||
|
MDY_WORKSHEET_ID_PLATE_ORDER_MODIFY_DRAWING = "66e2eb5a3c1a53053f6d234d" # 改图
|
||||||
|
MDY_WORKSHEET_ID_PLATE_ORDER_COLOR_SCHEME = "672adc61e72ce9924f763a0b" # 配色
|
||||||
|
MDY_WORKSHEET_ID_PLATE_ORDER_IMAGE_DEVELOPMENT = "66e2eee45d7e45f9c5ae41b4" # 照图开发
|
||||||
|
|
||||||
|
|
||||||
|
mdy_table_map = {
|
||||||
|
"product": MDY_WORKSHEET_ID_PRODUCT,
|
||||||
|
"customer": MDY_WORKSHEET_ID_CUSTOMER,
|
||||||
|
"plate_order": MDY_WORKSHEET_ID_PLATE_ORDER,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 开版表关联数据表映射(后续做跨表查询时使用)
|
||||||
|
plate_order_related_worksheet_map = {
|
||||||
|
"drawing": MDY_WORKSHEET_ID_PLATE_ORDER_DRAWING,
|
||||||
|
"coloring": MDY_WORKSHEET_ID_PLATE_ORDER_COLORING,
|
||||||
|
"pattern_set": MDY_WORKSHEET_ID_PLATE_ORDER_PATTERN_SET,
|
||||||
|
"modify_drawing": MDY_WORKSHEET_ID_PLATE_ORDER_MODIFY_DRAWING,
|
||||||
|
"color_scheme": MDY_WORKSHEET_ID_PLATE_ORDER_COLOR_SCHEME,
|
||||||
|
"image_development": MDY_WORKSHEET_ID_PLATE_ORDER_IMAGE_DEVELOPMENT,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 同上(中文 key,便于排查/对照)
|
||||||
|
plate_order_related_worksheet_map_cn = {
|
||||||
|
"画图": MDY_WORKSHEET_ID_PLATE_ORDER_DRAWING,
|
||||||
|
"调色": MDY_WORKSHEET_ID_PLATE_ORDER_COLORING,
|
||||||
|
"套纸样": MDY_WORKSHEET_ID_PLATE_ORDER_PATTERN_SET,
|
||||||
|
"改图": MDY_WORKSHEET_ID_PLATE_ORDER_MODIFY_DRAWING,
|
||||||
|
"配色": MDY_WORKSHEET_ID_PLATE_ORDER_COLOR_SCHEME,
|
||||||
|
"照图开发": MDY_WORKSHEET_ID_PLATE_ORDER_IMAGE_DEVELOPMENT,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# 明道云:字段映射(内部字段名 -> controlId)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
product_type_map = {
|
||||||
|
"name": "668caa5eb80969563ecaee7d",
|
||||||
|
"pieces": "689c5aa37c175367b29fcb87",
|
||||||
|
"color": "66ed3b9ae01d5599bdb45f6d",
|
||||||
|
"uid": "668caa5eb80969563ecaee7c",
|
||||||
|
"unit": "668e2ee2ca758c8cc5d0dc1d",
|
||||||
|
"segment_size": "668e2194790c0e04058b4f3c",
|
||||||
|
"width": "668e2194790c0e04058b4f3c",
|
||||||
|
"detail_str": "668caa5eb80969563ecaee7e",
|
||||||
|
"created_at": "ctime",
|
||||||
|
"rowid": "rowid",
|
||||||
|
}
|
||||||
|
|
||||||
|
customer_type_map = {
|
||||||
|
"name": "62d52f4b8d2972284492dcf9",
|
||||||
|
"area": "62d52f4b8d2972284492dd09",
|
||||||
|
"created_at": "ctime",
|
||||||
|
"rowid": "rowid",
|
||||||
|
"uid": "668bb9370207cf7520fe551e",
|
||||||
|
}
|
||||||
|
|
||||||
|
fabric_type_map = {
|
||||||
|
"name": "62d52f4b8d2972284492de61",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# 明道云:开版数据表(worksheetId: 668ba100fb551c8502140660)
|
||||||
|
# 字段说明(controlId -> 元信息)。按需求保留为 dict,暂不用于拉取请求。
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
plate_order_field_definitions: Dict[str, Dict[str, Any]] = {
|
||||||
|
# 成员 / 人员
|
||||||
|
"62d8fffb625ac34fa91299c3": {"name": "调色", "type": "Collaborator"},
|
||||||
|
# 关联记录(Relation)—— 展现 rowid + name + link
|
||||||
|
"62d52f4b8d2972284492dd28": {"name": "打版面料", "type": "Relation"},
|
||||||
|
"62d52f4b8d2972284492dd14": {"name": "客户", "type": "Relation"},
|
||||||
|
# 下拉/文本/数值/日期
|
||||||
|
"62d52f4b8d2972284492dd15": {"name": "做货方式", "type": "Dropdown"},
|
||||||
|
"62d52f4b8d2972284492dd0e": {"name": "设计编号", "type": "AutoNumber"},
|
||||||
|
"62d91d0e130624d31368bd16": {"name": "开版方式", "type": "Dropdown"},
|
||||||
|
"64a64bc4402bb5226ee34b3c": {"name": "等级", "type": "Rating"},
|
||||||
|
"660b79da4125de62cd904b39": {"name": "布料", "type": "Dropdown"},
|
||||||
|
"62db8e78510fb7962439b19c": {"name": "调色设计师", "type": "Text"},
|
||||||
|
"62d52f4b8d2972284492dd10": {"name": "开发进程", "type": "Dropdown"},
|
||||||
|
"62d52f4b8d2972284492dd1e": {"name": "是否套唛架", "type": "Dropdown"},
|
||||||
|
"62d52f4b8d2972284492dd2c": {"name": "款号名称", "type": "Text"},
|
||||||
|
"62d52f4b8d2972284492dd1d": {"name": "审批结果", "type": "Dropdown"},
|
||||||
|
"62d52f4b8d2972284492dd12": {"name": "下版时间", "type": "DateTime"},
|
||||||
|
"62d52f4b8d2972284492dd18": {"name": "复版原因", "type": "Dropdown"},
|
||||||
|
"62d52f4b8d2972284492dd0f": {"name": "起版情况", "type": "Dropdown"},
|
||||||
|
"62d52f4b8d2972284492dd19": {"name": "客户要求米样米数", "type": "Number"},
|
||||||
|
"62d52f4b8d2972284492dd11": {"name": "紧急程度", "type": "Dropdown"},
|
||||||
|
"660b75c3422f028085086b3c": {"name": "幅宽", "type": "Dropdown"},
|
||||||
|
"62d52f4b8d2972284492dd13": {"name": "要求完成时间", "type": "Date"},
|
||||||
|
"62f72a26d73f8581fbe7a488": {"name": "难度评级", "type": "Dropdown"},
|
||||||
|
# 附件(Attachment)—— 展现文件名 + 下载链接
|
||||||
|
"62d52f4b8d2972284492dd27": {"name": "开版图", "type": "Attachment"},
|
||||||
|
# 评级/开关/组合字段
|
||||||
|
"6451f4ad25766313eb8cb057": {"name": "日期", "type": "DateTime"},
|
||||||
|
"66c2ebc8666ad6264b709fa1": {"name": "画图评级", "type": "Dropdown"},
|
||||||
|
"66c2ebc8666ad6264b709fa2": {"name": "调色评级", "type": "Dropdown"},
|
||||||
|
"66c2ebc8666ad6264b709fa3": {"name": "套样评级", "type": "Dropdown"},
|
||||||
|
"6718ac791f08f8ec31257ebc": {"name": "记录id(文本组合)", "type": "Concatenate"},
|
||||||
|
"67f4d7d61c5f650e952fcee9": {"name": "客户名称", "type": "Lookup"},
|
||||||
|
# “有这个字段,但本条记录可能为空”的 Relation(占位:[])
|
||||||
|
"62d52f4b8d2972284492dd2d": {"name": "套纸样", "type": "Relation"},
|
||||||
|
"62d52f4b8d2972284492dd21": {"name": "画图", "type": "Relation"},
|
||||||
|
"62d52f4b8d2972284492dd22": {"name": "调色(关联)", "type": "Relation"},
|
||||||
|
"62d52f4b8d2972284492dd2a": {"name": "套纸样(关联)", "type": "Relation"},
|
||||||
|
"66e2ebb7da66655f355bf708": {"name": "改图(关联)", "type": "Relation"},
|
||||||
|
"66e2efb0da66655f355bf964": {"name": "找图开发", "type": "Relation"},
|
||||||
|
"672adc9b156abb9a08ab2a60": {"name": "配色", "type": "Relation"},
|
||||||
|
}
|
||||||
|
|
||||||
|
# 同步代码风格:内部字段名 -> controlId(后续 pick / parse 时会用到)
|
||||||
|
plate_order_type_map = {
|
||||||
|
"colorist": "62d8fffb625ac34fa91299c3",
|
||||||
|
"fabric_relation": "62d52f4b8d2972284492dd28",
|
||||||
|
"customer_relation": "62d52f4b8d2972284492dd14",
|
||||||
|
"production_method": "62d52f4b8d2972284492dd15",
|
||||||
|
"design_no": "62d52f4b8d2972284492dd0e",
|
||||||
|
"plate_method": "62d91d0e130624d31368bd16",
|
||||||
|
"level_rating": "64a64bc4402bb5226ee34b3c",
|
||||||
|
"fabric_source": "660b79da4125de62cd904b39",
|
||||||
|
"color_designer": "62db8e78510fb7962439b19c",
|
||||||
|
"dev_progress": "62d52f4b8d2972284492dd10",
|
||||||
|
"need_marker_frame": "62d52f4b8d2972284492dd1e",
|
||||||
|
"style_name": "62d52f4b8d2972284492dd2c",
|
||||||
|
"approval_result": "62d52f4b8d2972284492dd1d",
|
||||||
|
"plate_time": "62d52f4b8d2972284492dd12",
|
||||||
|
"rework_reason": "62d52f4b8d2972284492dd18",
|
||||||
|
"start_plate_status": "62d52f4b8d2972284492dd0f",
|
||||||
|
"sample_meters": "62d52f4b8d2972284492dd19", # 对应样品米数(宇问云)
|
||||||
|
"urgency": "62d52f4b8d2972284492dd11",
|
||||||
|
"width": "660b75c3422f028085086b3c",
|
||||||
|
"required_finish_date": "62d52f4b8d2972284492dd13",
|
||||||
|
"difficulty_level": "62f72a26d73f8581fbe7a488",
|
||||||
|
"plate_images": "62d52f4b8d2972284492dd27",
|
||||||
|
"record_datetime": "6451f4ad25766313eb8cb057",
|
||||||
|
"drawing_rating": "66c2ebc8666ad6264b709fa1",
|
||||||
|
"color_rating": "66c2ebc8666ad6264b709fa2",
|
||||||
|
"pattern_fit_rating": "66c2ebc8666ad6264b709fa3",
|
||||||
|
"record_id": "6718ac791f08f8ec31257ebc",
|
||||||
|
"customer_name": "67f4d7d61c5f650e952fcee9",
|
||||||
|
# relations(可能为空)
|
||||||
|
"pattern_set": "62d52f4b8d2972284492dd2d",
|
||||||
|
"drawing_relation": "62d52f4b8d2972284492dd21",
|
||||||
|
"color_relation": "62d52f4b8d2972284492dd22",
|
||||||
|
"pattern_set_relation": "62d52f4b8d2972284492dd2a",
|
||||||
|
"modify_drawing_relation": "66e2ebb7da66655f355bf708",
|
||||||
|
"image_development_relation": "66e2efb0da66655f355bf964",
|
||||||
|
"color_scheme_relation": "672adc9b156abb9a08ab2a60",
|
||||||
|
# 系统字段
|
||||||
|
"created_at": "ctime",
|
||||||
|
"rowid": "rowid",
|
||||||
|
}
|
||||||
142
flower/utils/mingdaoyun/models.py
Normal file
142
flower/utils/mingdaoyun/models.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Any, List
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field, computed_field
|
||||||
|
|
||||||
|
|
||||||
|
class Product(BaseModel):
|
||||||
|
"""产品模型"""
|
||||||
|
|
||||||
|
uid: str
|
||||||
|
rowid: str
|
||||||
|
name: str
|
||||||
|
pieces: int | None
|
||||||
|
segment_size: int | None
|
||||||
|
unit: str | None
|
||||||
|
color: str | None
|
||||||
|
width: str | None = None
|
||||||
|
detail_str: str | None = Field(exclude=True) # 在序列化时排除此字段
|
||||||
|
created_at: str
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def detail(self) -> dict[str, Any]:
|
||||||
|
"""将 detail_str 转换为字典"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json.loads(self.detail_str) if self.detail_str else {}
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class ProductListResponse(BaseModel):
|
||||||
|
"""产品列表响应"""
|
||||||
|
|
||||||
|
products: List[Product]
|
||||||
|
total: int
|
||||||
|
|
||||||
|
|
||||||
|
class Customer(BaseModel):
|
||||||
|
"""客户模型"""
|
||||||
|
|
||||||
|
uid: str
|
||||||
|
rowid: str
|
||||||
|
name: str
|
||||||
|
area: str | None
|
||||||
|
created_at: str
|
||||||
|
|
||||||
|
|
||||||
|
class Fabric(BaseModel):
|
||||||
|
"""面料模型"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# 明道云:通用值类型(Relation / Attachment / Collaborator 等)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
class MDYRelationItem(BaseModel):
|
||||||
|
"""Relation 字段的单项(跨表关联)"""
|
||||||
|
|
||||||
|
rowid: str
|
||||||
|
name: str
|
||||||
|
link: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class MDYAttachmentItem(BaseModel):
|
||||||
|
"""Attachment 字段的单项(附件)"""
|
||||||
|
|
||||||
|
original_file_name: str
|
||||||
|
file_id: str
|
||||||
|
download_url: str
|
||||||
|
preview_url: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class MDYCollaboratorItem(BaseModel):
|
||||||
|
"""Collaborator 字段的单项(成员/人员)
|
||||||
|
|
||||||
|
说明:明道云在不同场景下可能返回对象或列表;这里先按“对象字段”建模,
|
||||||
|
后续解析时可以按实际返回结构再做兼容适配。
|
||||||
|
"""
|
||||||
|
|
||||||
|
accountId: str | None = None
|
||||||
|
fullname: str | None = None
|
||||||
|
avatar: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class MDYPlateOrder(BaseModel):
|
||||||
|
"""开版数据表(明道云)的一行记录:仅做类型定义"""
|
||||||
|
|
||||||
|
rowid: str
|
||||||
|
created_at: str
|
||||||
|
|
||||||
|
# 成员 / 人员
|
||||||
|
colorist: MDYCollaboratorItem | List[MDYCollaboratorItem] | str | None = None
|
||||||
|
|
||||||
|
# Relation(跨表关联)
|
||||||
|
fabric_relation: List[MDYRelationItem] = Field(default_factory=list)
|
||||||
|
customer_relation: List[MDYRelationItem] = Field(default_factory=list)
|
||||||
|
|
||||||
|
# 基础字段
|
||||||
|
production_method: str | None = None
|
||||||
|
design_no: str | None = None
|
||||||
|
plate_method: str | None = None
|
||||||
|
level_rating: int | None = None
|
||||||
|
fabric_source: str | None = None
|
||||||
|
color_designer: str | None = None
|
||||||
|
dev_progress: str | None = None
|
||||||
|
need_marker_frame: str | None = None
|
||||||
|
style_name: str | None = None
|
||||||
|
approval_result: str | None = None
|
||||||
|
|
||||||
|
plate_time: str | None = None # DateTime: 'YYYY-MM-DD HH:MM:SS'
|
||||||
|
required_finish_date: str | None = None # Date: 'YYYY-MM-DD'
|
||||||
|
record_datetime: str | None = None # DateTime: 'YYYY-MM-DD HH:MM:SS'
|
||||||
|
|
||||||
|
rework_reason: str | None = None
|
||||||
|
start_plate_status: str | None = None
|
||||||
|
sample_meters: int | float | None = None
|
||||||
|
urgency: str | None = None
|
||||||
|
width: str | None = None
|
||||||
|
difficulty_level: str | None = None
|
||||||
|
|
||||||
|
# 附件
|
||||||
|
plate_images: List[MDYAttachmentItem] = Field(default_factory=list)
|
||||||
|
|
||||||
|
# 评级/组合字段
|
||||||
|
drawing_rating: str | None = None
|
||||||
|
color_rating: str | None = None
|
||||||
|
pattern_fit_rating: str | None = None
|
||||||
|
record_id: str | None = None
|
||||||
|
customer_name: str | None = None
|
||||||
|
|
||||||
|
# 其它 Relation(可能为空)
|
||||||
|
pattern_set: List[MDYRelationItem] = Field(default_factory=list)
|
||||||
|
drawing_relation: List[MDYRelationItem] = Field(default_factory=list)
|
||||||
|
color_relation: List[MDYRelationItem] = Field(default_factory=list)
|
||||||
|
pattern_set_relation: List[MDYRelationItem] = Field(default_factory=list)
|
||||||
|
modify_drawing_relation: List[MDYRelationItem] = Field(default_factory=list)
|
||||||
|
image_development_relation: List[MDYRelationItem] = Field(default_factory=list)
|
||||||
|
color_scheme_relation: List[MDYRelationItem] = Field(default_factory=list)
|
||||||
39
flower/utils/mingdaoyun/parsers.py
Normal file
39
flower/utils/mingdaoyun/parsers.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .mappings import customer_type_map, fabric_type_map, product_type_map
|
||||||
|
from .models import Customer, Fabric, Product
|
||||||
|
|
||||||
|
|
||||||
|
def pick_customer(fields: dict[str, Any]) -> Customer:
|
||||||
|
"""从字段字典中提取客户信息"""
|
||||||
|
|
||||||
|
data = {k: fields.get(v, "") for k, v in customer_type_map.items()}
|
||||||
|
return Customer(**data)
|
||||||
|
|
||||||
|
|
||||||
|
def pick_product(fields: dict[str, Any]) -> Product:
|
||||||
|
"""从字段字典中提取产品信息"""
|
||||||
|
|
||||||
|
data = {k: fields.get(v, "") for k, v in product_type_map.items()}
|
||||||
|
|
||||||
|
def _to_int(value):
|
||||||
|
if value in ("", None):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
data["pieces"] = _to_int(data.get("pieces"))
|
||||||
|
data["segment_size"] = _to_int(data.get("segment_size"))
|
||||||
|
|
||||||
|
return Product(**data)
|
||||||
|
|
||||||
|
|
||||||
|
def pick_fabric(fields: dict[str, Any]) -> Fabric:
|
||||||
|
"""从字段字典中提取面料信息"""
|
||||||
|
|
||||||
|
data = {k: fields.get(v, "") for k, v in fabric_type_map.items()}
|
||||||
|
return Fabric(**data)
|
||||||
@@ -175,7 +175,40 @@ def advance_to_next_state(business_object: 'models.BusinessObject', user, **para
|
|||||||
- state_log: 创建的状态日志(成功时)
|
- state_log: 创建的状态日志(成功时)
|
||||||
"""
|
"""
|
||||||
# 业务约束:BusinessObject 必须能追溯到真实业务对象,否则状态流转记录没有业务意义。
|
# 业务约束:BusinessObject 必须能追溯到真实业务对象,否则状态流转记录没有业务意义。
|
||||||
# DB 允许为空仅为历史兼容;但新增数据一律禁止。
|
# DB 允许为空仅为历史兼容;但实际运行中存在“先创建 BO、后绑定到业务模型”的非标准路径。
|
||||||
|
# 为避免这类数据导致推进失败,这里做一次自愈:
|
||||||
|
# - 若 BO 未绑定 content_type/object_id(或 content_object 解析不到实例)
|
||||||
|
# - 则尝试通过常见的一对一反向关系(如 printing_job/plate_order)推断真实业务对象并补齐绑定
|
||||||
|
if business_object.content_type_id is None or business_object.object_id is None or business_object.content_object is None:
|
||||||
|
inferred_instance = None
|
||||||
|
inferred_default_name = None
|
||||||
|
|
||||||
|
# PrintingJob.business_object -> related_name='printing_job'
|
||||||
|
try:
|
||||||
|
inferred_instance = getattr(business_object, 'printing_job', None)
|
||||||
|
if inferred_instance is not None:
|
||||||
|
inferred_default_name = f"PrintingJob-{getattr(inferred_instance, 'pk', '')}"
|
||||||
|
except (ObjectDoesNotExist, AttributeError):
|
||||||
|
inferred_instance = None
|
||||||
|
|
||||||
|
# PlateOrder.business_object -> related_name='plate_order'
|
||||||
|
if inferred_instance is None:
|
||||||
|
try:
|
||||||
|
inferred_instance = getattr(business_object, 'plate_order', None)
|
||||||
|
if inferred_instance is not None:
|
||||||
|
inferred_default_name = f"PlateOrder-{getattr(inferred_instance, 'pk', '')}"
|
||||||
|
except (ObjectDoesNotExist, AttributeError):
|
||||||
|
inferred_instance = None
|
||||||
|
|
||||||
|
if inferred_instance is not None:
|
||||||
|
ensure_business_object_bound_to_instance(
|
||||||
|
business_object,
|
||||||
|
inferred_instance,
|
||||||
|
default_name=inferred_default_name,
|
||||||
|
)
|
||||||
|
# GenericForeignKey 可能缓存了旧值,刷新以确保后续 content_object 判断正确
|
||||||
|
business_object.refresh_from_db(fields=['content_type', 'object_id', 'name'])
|
||||||
|
|
||||||
if business_object.content_type_id is None or business_object.object_id is None:
|
if business_object.content_type_id is None or business_object.object_id is None:
|
||||||
return False, "业务对象未绑定关联对象(content_type/object_id),禁止推进", None
|
return False, "业务对象未绑定关联对象(content_type/object_id),禁止推进", None
|
||||||
if business_object.content_object is None:
|
if business_object.content_object is None:
|
||||||
|
|||||||
Reference in New Issue
Block a user