forked from erp-dev/erp
feat: tasks for sync products and customers
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
|
from api_v1.models import UploadedFile, DataSync
|
||||||
from .tasks import backup_database
|
from .tasks import backup_database
|
||||||
|
|
||||||
|
|
||||||
@@ -21,3 +21,13 @@ class UploadedFileAdmin(admin.ModelAdmin):
|
|||||||
@action(description='备份数据库')
|
@action(description='备份数据库')
|
||||||
def backup_database(self, request, queryset):
|
def backup_database(self, request, queryset):
|
||||||
backup_database.delay()
|
backup_database.delay()
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(DataSync)
|
||||||
|
class DataSyncAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['id', 'table_name', 'page_index', 'page_size', 'total_count', 'synced_rows', 'last_ctime', 'last_rowid', 'note']
|
||||||
|
list_filter = ['table_name', 'created_at']
|
||||||
|
search_fields = ['table_name', 'last_rowid', 'note']
|
||||||
|
readonly_fields = ['created_at', 'updated_at']
|
||||||
|
date_hierarchy = 'created_at'
|
||||||
|
ordering = ['-created_at']
|
||||||
|
|||||||
33
api_v1/migrations/0003_datasync.py
Normal file
33
api_v1/migrations/0003_datasync.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('api_v1', '0002_alter_uploadedfile_path'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DataSync',
|
||||||
|
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='更新时间')),
|
||||||
|
('table_name', models.CharField(choices=[('product', '产品'), ('customer', '客户')], max_length=50, verbose_name='同步目标')),
|
||||||
|
('page_index', models.PositiveIntegerField(default=1, verbose_name='页码')),
|
||||||
|
('page_size', models.PositiveIntegerField(default=100, verbose_name='每页数量')),
|
||||||
|
('total_count', models.PositiveIntegerField(default=0, verbose_name='远端总数')),
|
||||||
|
('synced_rows', models.PositiveIntegerField(default=0, verbose_name='本次同步数量')),
|
||||||
|
('last_ctime', models.DateTimeField(blank=True, null=True, verbose_name='最新数据时间')),
|
||||||
|
('last_rowid', models.CharField(blank=True, max_length=64, verbose_name='最新 RowID')),
|
||||||
|
('note', models.CharField(blank=True, max_length=255, verbose_name='备注')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '数据同步记录',
|
||||||
|
'verbose_name_plural': '数据同步记录',
|
||||||
|
'db_table': 'api_data_sync',
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -82,3 +82,29 @@ class UploadedFile(ModelBase):
|
|||||||
if self.path:
|
if self.path:
|
||||||
return self.path.url
|
return self.path.url
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class DataSync(ModelBase):
|
||||||
|
"""记录外部数据同步的执行情况"""
|
||||||
|
|
||||||
|
class TableName(models.TextChoices):
|
||||||
|
PRODUCT = 'product', '产品'
|
||||||
|
CUSTOMER = 'customer', '客户'
|
||||||
|
|
||||||
|
table_name = models.CharField(max_length=50, choices=TableName.choices, verbose_name='同步目标')
|
||||||
|
page_index = models.PositiveIntegerField(default=1, verbose_name='页码')
|
||||||
|
page_size = models.PositiveIntegerField(default=100, verbose_name='每页数量')
|
||||||
|
total_count = models.PositiveIntegerField(default=0, verbose_name='远端总数')
|
||||||
|
synced_rows = models.PositiveIntegerField(default=0, verbose_name='本次同步数量')
|
||||||
|
last_ctime = models.DateTimeField(null=True, blank=True, verbose_name='最新数据时间')
|
||||||
|
last_rowid = models.CharField(max_length=64, blank=True, verbose_name='最新 RowID')
|
||||||
|
note = models.CharField(max_length=255, blank=True, verbose_name='备注')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'api_data_sync'
|
||||||
|
verbose_name = '数据同步记录'
|
||||||
|
verbose_name_plural = '数据同步记录'
|
||||||
|
ordering = ['-created_at']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.table_name} @ {self.created_at:%Y-%m-%d %H:%M:%S}'
|
||||||
|
|||||||
330
api_v1/tasks.py
330
api_v1/tasks.py
@@ -1,7 +1,11 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from datetime import datetime
|
||||||
|
from decimal import Decimal, InvalidOperation
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
@@ -9,47 +13,16 @@ from django.conf import settings
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from basic_info import models as basic_models
|
from basic_info import models as basic_models
|
||||||
|
from api_v1 import models as api_models
|
||||||
|
from flower.utils import (
|
||||||
|
fetch_products_from_mingdaoyun,
|
||||||
|
fetch_customers_from_mingdaoyun,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@shared_task(bind=True)
|
|
||||||
def ping_task(self, message: str = 'ping'):
|
|
||||||
"""
|
|
||||||
最简单的心跳任务,用于验证 Celery worker 是否能够
|
|
||||||
正确消费队列并返回结果。
|
|
||||||
"""
|
|
||||||
payload = {
|
|
||||||
'task_id': self.request.id,
|
|
||||||
'message': message,
|
|
||||||
'timestamp': timezone.now().isoformat(),
|
|
||||||
}
|
|
||||||
logger.info('Celery ping_task 执行成功: %s', payload)
|
|
||||||
return payload
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task(bind=True)
|
|
||||||
def merchant_product_count(self, merchant_id: int):
|
|
||||||
"""
|
|
||||||
计算指定商户下的产品数量,用于演示如何在任务中访问数据库。
|
|
||||||
"""
|
|
||||||
count = basic_models.Product.objects.filter(merchant_id=merchant_id).count()
|
|
||||||
payload = {
|
|
||||||
'task_id': self.request.id,
|
|
||||||
'merchant_id': merchant_id,
|
|
||||||
'product_count': count,
|
|
||||||
'calculated_at': timezone.now().isoformat(),
|
|
||||||
}
|
|
||||||
logger.info(
|
|
||||||
'Celery merchant_product_count 统计完成: merchant=%s count=%s task=%s',
|
|
||||||
merchant_id,
|
|
||||||
count,
|
|
||||||
self.request.id,
|
|
||||||
)
|
|
||||||
return payload
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_backup_dir(output_dir: str | None) -> Path:
|
def _ensure_backup_dir(output_dir: str | None) -> Path:
|
||||||
base_dir = Path(settings.BASE_DIR)
|
base_dir = Path(settings.BASE_DIR)
|
||||||
backup_dir = Path(output_dir) if output_dir else (base_dir / 'data-bak')
|
backup_dir = Path(output_dir) if output_dir else (base_dir / 'data-bak')
|
||||||
@@ -124,3 +97,288 @@ def backup_database(self, output_dir: str | None = None, filename_prefix: str =
|
|||||||
logger.info('数据库备份完成: %s', payload)
|
logger.info('数据库备份完成: %s', payload)
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def _get_mdy_merchant():
|
||||||
|
merchant_id = getattr(settings, 'MDY_MERCHANT_ID', None)
|
||||||
|
qs = basic_models.Merchant.objects.all()
|
||||||
|
if merchant_id:
|
||||||
|
qs = qs.filter(id=merchant_id)
|
||||||
|
merchant = qs.order_by('id').first()
|
||||||
|
if not merchant:
|
||||||
|
raise RuntimeError('未找到用于明道云同步的商户,请先创建商户或配置 MDY_MERCHANT_ID')
|
||||||
|
return merchant
|
||||||
|
|
||||||
|
|
||||||
|
def _get_mdy_category(merchant):
|
||||||
|
category_id = getattr(settings, 'MDY_PRODUCT_CATEGORY_ID', None)
|
||||||
|
qs = basic_models.ProductCategory.objects.filter(merchant=merchant)
|
||||||
|
if category_id:
|
||||||
|
qs = qs.filter(id=category_id)
|
||||||
|
category = qs.order_by('id').first()
|
||||||
|
if not category:
|
||||||
|
raise RuntimeError('未找到用于明道云同步的产品类别,请先创建类别或配置 MDY_PRODUCT_CATEGORY_ID')
|
||||||
|
return category
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def _map_unit(unit_label: str | None) -> int:
|
||||||
|
if not unit_label:
|
||||||
|
return basic_models.ProductUnitEnum.METER
|
||||||
|
mapping = {
|
||||||
|
'米': basic_models.ProductUnitEnum.METER,
|
||||||
|
'码': basic_models.ProductUnitEnum.YARD,
|
||||||
|
'公斤': basic_models.ProductUnitEnum.KG,
|
||||||
|
}
|
||||||
|
return mapping.get(unit_label, basic_models.ProductUnitEnum.METER)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_decimal(value: str | None):
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return Decimal(str(value))
|
||||||
|
except (InvalidOperation, TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _run_fetch(page: int, page_size: int):
|
||||||
|
return asyncio.run(fetch_products_from_mingdaoyun(page=page, page_size=page_size))
|
||||||
|
|
||||||
|
|
||||||
|
def _run_fetch_customers(page: int, page_size: int):
|
||||||
|
return asyncio.run(fetch_customers_from_mingdaoyun(page=page, page_size=page_size))
|
||||||
|
|
||||||
|
|
||||||
|
def _upsert_product(product_data, merchant, category):
|
||||||
|
description = ''
|
||||||
|
if product_data.detail:
|
||||||
|
description = json.dumps(product_data.detail, ensure_ascii=False)
|
||||||
|
defaults = {
|
||||||
|
'category': category,
|
||||||
|
'name': product_data.name or product_data.uid,
|
||||||
|
'color': product_data.color or '',
|
||||||
|
'description': description,
|
||||||
|
'unit': _map_unit(product_data.unit),
|
||||||
|
'from_mdy': True,
|
||||||
|
}
|
||||||
|
width_decimal = _ensure_decimal(product_data.width)
|
||||||
|
if width_decimal is not None:
|
||||||
|
defaults['width_size'] = width_decimal
|
||||||
|
|
||||||
|
product_obj, created = basic_models.Product.objects.get_or_create(
|
||||||
|
merchant=merchant,
|
||||||
|
human_id=product_data.uid,
|
||||||
|
defaults=defaults,
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not product_obj.from_mdy:
|
||||||
|
return False
|
||||||
|
|
||||||
|
updated = False
|
||||||
|
for field, value in defaults.items():
|
||||||
|
if getattr(product_obj, field) != value:
|
||||||
|
setattr(product_obj, field, value)
|
||||||
|
updated = True
|
||||||
|
if updated:
|
||||||
|
product_obj.save(update_fields=list(defaults.keys()))
|
||||||
|
return updated
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(bind=True)
|
||||||
|
def sync_mdy_products(self, page_size: int = 300, max_pages: int = 5, max_records: int | None = None):
|
||||||
|
"""
|
||||||
|
从明道云同步产品数据
|
||||||
|
"""
|
||||||
|
merchant = _get_mdy_merchant()
|
||||||
|
category = _get_mdy_category(merchant)
|
||||||
|
max_records = max_records or page_size
|
||||||
|
|
||||||
|
last_sync = api_models.DataSync.objects.filter(
|
||||||
|
table_name=api_models.DataSync.TableName.PRODUCT
|
||||||
|
).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 ''
|
||||||
|
|
||||||
|
synced_rows = 0
|
||||||
|
page_index = 1
|
||||||
|
total_count = 0
|
||||||
|
latest_ctime = last_ctime
|
||||||
|
latest_rowid = last_rowid
|
||||||
|
reached_existing = False
|
||||||
|
|
||||||
|
while page_index <= max_pages and synced_rows < max_records:
|
||||||
|
products, total = _run_fetch(page_index, page_size)
|
||||||
|
total_count = total
|
||||||
|
if not products:
|
||||||
|
break
|
||||||
|
|
||||||
|
for item in products:
|
||||||
|
product_ctime = _parse_mdy_datetime(item.created_at)
|
||||||
|
if last_ctime and product_ctime:
|
||||||
|
if product_ctime < last_ctime:
|
||||||
|
reached_existing = True
|
||||||
|
break
|
||||||
|
if product_ctime == last_ctime and last_rowid and item.rowid == last_rowid:
|
||||||
|
reached_existing = True
|
||||||
|
break
|
||||||
|
|
||||||
|
changed = _upsert_product(item, merchant, category)
|
||||||
|
if changed:
|
||||||
|
synced_rows += 1
|
||||||
|
if product_ctime and (latest_ctime is None or product_ctime > latest_ctime):
|
||||||
|
latest_ctime = product_ctime
|
||||||
|
latest_rowid = item.rowid
|
||||||
|
|
||||||
|
if synced_rows >= max_records:
|
||||||
|
break
|
||||||
|
|
||||||
|
if reached_existing or synced_rows >= max_records or len(products) < 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.PRODUCT,
|
||||||
|
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='reached existing data' if reached_existing else '',
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'task_id': self.request.id,
|
||||||
|
'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,
|
||||||
|
}
|
||||||
|
logger.info('明道云产品同步完成: %s', payload)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def _upsert_customer(customer_data, merchant):
|
||||||
|
mdy_uid = customer_data.uid or customer_data.rowid
|
||||||
|
if not mdy_uid:
|
||||||
|
return False
|
||||||
|
defaults = {
|
||||||
|
'merchant': merchant,
|
||||||
|
'name': customer_data.name or customer_data.uid,
|
||||||
|
'area': customer_data.area or '',
|
||||||
|
'from_mdy': True,
|
||||||
|
}
|
||||||
|
customer_obj, created = basic_models.Customer.objects.get_or_create(
|
||||||
|
mdy_uid=mdy_uid,
|
||||||
|
defaults=defaults,
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not customer_obj.from_mdy:
|
||||||
|
return False
|
||||||
|
|
||||||
|
updated_fields = {}
|
||||||
|
for field, value in defaults.items():
|
||||||
|
if getattr(customer_obj, field) != value:
|
||||||
|
updated_fields[field] = value
|
||||||
|
|
||||||
|
if updated_fields:
|
||||||
|
for field, value in updated_fields.items():
|
||||||
|
setattr(customer_obj, field, value)
|
||||||
|
customer_obj.save(update_fields=list(updated_fields.keys()))
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(bind=True)
|
||||||
|
def sync_mdy_customers(self, page_size: int = 300, max_pages: int = 5, max_records: int | None = None):
|
||||||
|
"""
|
||||||
|
从明道云同步客户数据
|
||||||
|
"""
|
||||||
|
merchant = _get_mdy_merchant()
|
||||||
|
max_records = max_records or page_size
|
||||||
|
|
||||||
|
last_sync = api_models.DataSync.objects.filter(
|
||||||
|
table_name=api_models.DataSync.TableName.CUSTOMER
|
||||||
|
).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 ''
|
||||||
|
|
||||||
|
synced_rows = 0
|
||||||
|
page_index = 1
|
||||||
|
total_count = 0
|
||||||
|
latest_ctime = last_ctime
|
||||||
|
latest_rowid = last_rowid
|
||||||
|
reached_existing = False
|
||||||
|
|
||||||
|
while page_index <= max_pages and synced_rows < max_records:
|
||||||
|
customers, total = _run_fetch_customers(page_index, page_size)
|
||||||
|
total_count = total
|
||||||
|
if not customers:
|
||||||
|
break
|
||||||
|
|
||||||
|
for item in customers:
|
||||||
|
record_ctime = _parse_mdy_datetime(item.created_at)
|
||||||
|
if last_ctime and record_ctime:
|
||||||
|
if record_ctime < last_ctime:
|
||||||
|
reached_existing = True
|
||||||
|
break
|
||||||
|
if record_ctime == last_ctime and last_rowid and item.rowid == last_rowid:
|
||||||
|
reached_existing = True
|
||||||
|
break
|
||||||
|
|
||||||
|
changed = _upsert_customer(item, merchant)
|
||||||
|
if changed:
|
||||||
|
synced_rows += 1
|
||||||
|
if record_ctime and (latest_ctime is None or record_ctime > latest_ctime):
|
||||||
|
latest_ctime = record_ctime
|
||||||
|
latest_rowid = item.rowid
|
||||||
|
|
||||||
|
if synced_rows >= max_records:
|
||||||
|
break
|
||||||
|
|
||||||
|
if reached_existing or synced_rows >= max_records or len(customers) < 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.CUSTOMER,
|
||||||
|
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='reached existing data' if reached_existing else '',
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'task_id': self.request.id,
|
||||||
|
'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,
|
||||||
|
}
|
||||||
|
logger.info('明道云客户同步完成: %s', payload)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|||||||
@@ -516,6 +516,7 @@ class PlateOrderFilterSet(django_filters.FilterSet):
|
|||||||
"""开版订单过滤器"""
|
"""开版订单过滤器"""
|
||||||
customer_name = django_filters.CharFilter(field_name='customer__name', lookup_expr='icontains')
|
customer_name = django_filters.CharFilter(field_name='customer__name', lookup_expr='icontains')
|
||||||
customer_phone = django_filters.CharFilter(field_name='customer__mobile', lookup_expr='icontains')
|
customer_phone = django_filters.CharFilter(field_name='customer__mobile', lookup_expr='icontains')
|
||||||
|
image_name = django_filters.CharFilter(lookup_expr='icontains')
|
||||||
salesperson = django_filters.NumberFilter()
|
salesperson = django_filters.NumberFilter()
|
||||||
merchandiser = django_filters.NumberFilter()
|
merchandiser = django_filters.NumberFilter()
|
||||||
plate_type = django_filters.CharFilter(lookup_expr='icontains')
|
plate_type = django_filters.CharFilter(lookup_expr='icontains')
|
||||||
@@ -546,6 +547,7 @@ class PlateOrderViewSet(viewsets.ModelViewSet):
|
|||||||
list: 获取开版订单列表
|
list: 获取开版订单列表
|
||||||
retrieve: 获取开版订单详情
|
retrieve: 获取开版订单详情
|
||||||
create: 创建开版订单
|
create: 创建开版订单
|
||||||
|
image_name: 图片名称(模糊查询)
|
||||||
update: 更新开版订单
|
update: 更新开版订单
|
||||||
partial_update: 部分更新开版订单
|
partial_update: 部分更新开版订单
|
||||||
invalidate: 作废开版订单
|
invalidate: 作废开版订单
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class UserProfileAdmin(AdminBase):
|
|||||||
|
|
||||||
@admin.register(models.Merchant)
|
@admin.register(models.Merchant)
|
||||||
class MerchantAdmin(admin.ModelAdmin):
|
class MerchantAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'type', 'email', 'mobile', 'auto_complete_stock_change')
|
list_display = ('id', 'name', 'type', 'email', 'mobile', 'auto_complete_stock_change')
|
||||||
search_fields = ('name', 'mobile')
|
search_fields = ('name', 'mobile')
|
||||||
list_filter = ('type', 'auto_complete_stock_change')
|
list_filter = ('type', 'auto_complete_stock_change')
|
||||||
|
|
||||||
|
|||||||
20
basic_info/migrations/0018_product_from_mdy.py
Normal file
20
basic_info/migrations/0018_product_from_mdy.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('basic_info', '0017_alter_customer_mobile'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='from_mdy',
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text='标记该产品是否通过明道云接口同步',
|
||||||
|
verbose_name='是否来自明道云同步',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
21
basic_info/migrations/0019_customer_mdy_fields.py
Normal file
21
basic_info/migrations/0019_customer_mdy_fields.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('basic_info', '0018_product_from_mdy'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customer',
|
||||||
|
name='mdy_uid',
|
||||||
|
field=models.CharField(blank=True, help_text='对应明道云记录的唯一标识', max_length=100, null=True, unique=True, verbose_name='明道云UID'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customer',
|
||||||
|
name='from_mdy',
|
||||||
|
field=models.BooleanField(default=False, help_text='标记该客户是否通过明道云接口同步', verbose_name='是否来自明道云同步'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -261,6 +261,11 @@ class Product(ModelBase):
|
|||||||
null=True,
|
null=True,
|
||||||
verbose_name='公斤转换率',
|
verbose_name='公斤转换率',
|
||||||
)
|
)
|
||||||
|
from_mdy = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name='是否来自明道云同步',
|
||||||
|
help_text='标记该产品是否通过明道云接口同步',
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -318,6 +323,19 @@ class Customer(ModelBase):
|
|||||||
email = models.EmailField(blank=True, null=True, verbose_name='电子邮箱')
|
email = models.EmailField(blank=True, null=True, verbose_name='电子邮箱')
|
||||||
contact = models.CharField(blank=True, null=True, verbose_name='联系人')
|
contact = models.CharField(blank=True, null=True, verbose_name='联系人')
|
||||||
description = models.TextField(blank=True, null=True, verbose_name='备注描述')
|
description = models.TextField(blank=True, null=True, verbose_name='备注描述')
|
||||||
|
mdy_uid = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
unique=True,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='明道云UID',
|
||||||
|
help_text='对应明道云记录的唯一标识',
|
||||||
|
)
|
||||||
|
from_mdy = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name='是否来自明道云同步',
|
||||||
|
help_text='标记该客户是否通过明道云接口同步',
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -53,6 +53,13 @@ CSRF_TRUSTED_ORIGINS = env.list('CSRF_TRUSTED_ORIGINS', default=[
|
|||||||
'https://yuwenerp.yuwen.cloud',
|
'https://yuwenerp.yuwen.cloud',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
# 明道云同步配置
|
||||||
|
MDY_MERCHANT_ID = env.int('MDY_MERCHANT_ID', default=1)
|
||||||
|
MDY_PRODUCT_CATEGORY_ID = env.int('MDY_PRODUCT_CATEGORY_ID', default=1)
|
||||||
|
MDY_SYNC_PAGE_SIZE = env.int('MDY_SYNC_PAGE_SIZE', default=100)
|
||||||
|
MDY_SYNC_MAX_PAGES = env.int('MDY_SYNC_MAX_PAGES', default=2)
|
||||||
|
MDY_SYNC_MAX_RECORDS = env.int('MDY_SYNC_MAX_RECORDS', default=200)
|
||||||
|
|
||||||
|
|
||||||
# CORS 配置
|
# CORS 配置
|
||||||
CORS_ALLOW_ALL_ORIGINS = DEBUG # 开发环境允许所有源,生产环境需要配置白名单
|
CORS_ALLOW_ALL_ORIGINS = DEBUG # 开发环境允许所有源,生产环境需要配置白名单
|
||||||
@@ -303,4 +310,22 @@ CELERY_BEAT_SCHEDULE = {
|
|||||||
'filename_prefix': 'db-backup',
|
'filename_prefix': 'db-backup',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'mdy_product_sync': {
|
||||||
|
'task': 'api_v1.tasks.sync_mdy_products',
|
||||||
|
'schedule': crontab(hour='*/1', minute=0),
|
||||||
|
'kwargs': {
|
||||||
|
'page_size': MDY_SYNC_PAGE_SIZE,
|
||||||
|
'max_pages': MDY_SYNC_MAX_PAGES,
|
||||||
|
'max_records': MDY_SYNC_MAX_RECORDS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'mdy_customer_sync': {
|
||||||
|
'task': 'api_v1.tasks.sync_mdy_customers',
|
||||||
|
'schedule': crontab(hour='*/1', minute=0),
|
||||||
|
'kwargs': {
|
||||||
|
'page_size': MDY_SYNC_PAGE_SIZE,
|
||||||
|
'max_pages': MDY_SYNC_MAX_PAGES,
|
||||||
|
'max_records': MDY_SYNC_MAX_RECORDS,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
476
flower/utils.py
476
flower/utils.py
@@ -1,227 +1,283 @@
|
|||||||
# import aiohttp
|
import aiohttp
|
||||||
# from typing import Optional, Dict, Any, List
|
from typing import Optional, Dict, Any, List
|
||||||
# from pydantic import BaseModel, Field, computed_field
|
from pydantic import BaseModel, Field, computed_field
|
||||||
# from enum import Enum
|
from enum import Enum
|
||||||
# import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
# class Product(BaseModel):
|
class Product(BaseModel):
|
||||||
# """产品模型 - 请在此填入你的字段"""
|
"""产品模型 - 请在此填入你的字段"""
|
||||||
# uid: str
|
uid: str
|
||||||
# name: str
|
rowid: str
|
||||||
# unit: str | None
|
name: str
|
||||||
# color: str | None
|
unit: str | None
|
||||||
# detail_str: str | None = Field(exclude=True) # 在序列化时排除此字段
|
color: str | None
|
||||||
# created_at: str
|
width: str | None = None
|
||||||
|
detail_str: str | None = Field(exclude=True) # 在序列化时排除此字段
|
||||||
|
created_at: str
|
||||||
|
|
||||||
# @computed_field
|
@computed_field
|
||||||
# @property
|
@property
|
||||||
# def detail(self) -> dict[str, Any]:
|
def detail(self) -> dict[str, Any]:
|
||||||
# """将 detail_str 转换为字典"""
|
"""将 detail_str 转换为字典"""
|
||||||
# try:
|
try:
|
||||||
# return json.loads(self.detail_str) if self.detail_str else {}
|
return json.loads(self.detail_str) if self.detail_str else {}
|
||||||
# except (json.JSONDecodeError, TypeError):
|
except (json.JSONDecodeError, TypeError):
|
||||||
# return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
# class ProductListResponse(BaseModel):
|
class ProductListResponse(BaseModel):
|
||||||
# """产品列表响应"""
|
"""产品列表响应"""
|
||||||
# products: List[Product]
|
products: List[Product]
|
||||||
# total: int
|
total: int
|
||||||
|
|
||||||
|
|
||||||
# mdy_table_map = {
|
mdy_table_map = {
|
||||||
# 'product': 'spmx',
|
'product': 'spmx',
|
||||||
# 'customer': 'quanbu',
|
'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',
|
||||||
|
'color': '66ed3b9ae01d5599bdb45f6d',
|
||||||
|
'uid': '668caa5eb80969563ecaee7c',
|
||||||
|
'unit': '668e2ee2ca758c8cc5d0dc1d',
|
||||||
|
'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
|
||||||
# }
|
# }
|
||||||
|
|
||||||
|
|
||||||
# class HTTPMethod(Enum):
|
def pick_product(fields: dict) -> Product:
|
||||||
# """HTTP 请求方法枚举"""
|
"""
|
||||||
# GET = "GET"
|
从字段字典中提取产品信息
|
||||||
# POST = "POST"
|
"""
|
||||||
|
data = {k: fields.get(v, '') for k, v in product_type_map.items()}
|
||||||
|
return Product(**data)
|
||||||
|
|
||||||
|
|
||||||
# class MingDaoYunClient:
|
async def fetch_products_from_mingdaoyun(page: int = 1, page_size: int = 100) -> tuple[list[Product], int]:
|
||||||
# """明道云 API 客户端"""
|
"""
|
||||||
|
从明道云获取产品列表
|
||||||
|
|
||||||
# def __init__(self, app_key: str, sign: str, base_url: str = ""):
|
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': False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
data = response.get('data')
|
||||||
|
if not data:
|
||||||
|
return [], 0
|
||||||
|
|
||||||
# Args:
|
products = [pick_product(item) for item in data.get('rows', [])]
|
||||||
# app_key: 应用密钥
|
total_count = data.get('total', 0)
|
||||||
# sign: 签名
|
return products, total_count
|
||||||
# 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 = {
|
async def fetch_customers_from_mingdaoyun(page: int = 1, page_size: int = 100) -> tuple[list[Customer], int]:
|
||||||
# 'name': '668caa5eb80969563ecaee7d',
|
"""
|
||||||
# 'color': '66ed3b9ae01d5599bdb45f6d',
|
从明道云获取客户列表
|
||||||
# 'uid': '668caa5eb80969563ecaee7c',
|
"""
|
||||||
# 'unit': '668e2ee2ca758c8cc5d0dc1d',
|
client = MingDaoYunClient(
|
||||||
# 'detail_str': '668caa5eb80969563ecaee7e',
|
app_key='208e55fea5cea59f',
|
||||||
# 'created_at': 'ctime',
|
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': False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
data = response.get('data')
|
||||||
|
if not data:
|
||||||
|
return [], 0
|
||||||
|
|
||||||
|
customers = [pick_customer(item) for item in data.get('rows', [])]
|
||||||
# # json param example:
|
total_count = data.get('total', 0)
|
||||||
# # {
|
return customers, total_count
|
||||||
# # "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()}
|
|
||||||
# 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,
|
|
||||||
# }
|
|
||||||
# )
|
|
||||||
# data = response.get('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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user