1
0
forked from erp-dev/erp

feat: tasks for sync products and customers

This commit is contained in:
2025-12-08 22:22:18 +08:00
parent fc18f71615
commit a36a7e5265
14 changed files with 717 additions and 248 deletions

View File

@@ -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']

View 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'],
},
),
]

View File

@@ -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}'

View File

@@ -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

View File

@@ -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: 作废开版订单

View File

@@ -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')

View 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='是否来自明道云同步',
),
),
]

View 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='是否来自明道云同步'),
),
]

View File

@@ -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.

View File

@@ -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,
},
},
} }

View File

@@ -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