forked from erp-dev/erp
fix: added name filter for api_man apis, fixed plate_orders search exclude id (only search via design_code)
This commit is contained in:
@@ -2,11 +2,80 @@ from rest_framework import viewsets
|
|||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework.pagination import LimitOffsetPagination
|
from rest_framework.pagination import LimitOffsetPagination
|
||||||
from rest_framework.permissions import IsAuthenticated, DjangoModelPermissions
|
from rest_framework.permissions import IsAuthenticated, DjangoModelPermissions
|
||||||
from django.contrib.auth.models import Permission
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
from django_filters import rest_framework as dj_filters
|
||||||
|
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
class BaseViewSet(viewsets.ModelViewSet):
|
class BasicInfoFilterMixin:
|
||||||
|
"""
|
||||||
|
提供基于 name/title 的基础过滤能力,自动判定字段,避免重复配置。
|
||||||
|
"""
|
||||||
|
filter_backends = [DjangoFilterBackend]
|
||||||
|
_auto_filterset_cache = {}
|
||||||
|
|
||||||
|
def _detect_filter_field(self):
|
||||||
|
"""
|
||||||
|
返回 field_name 或 None。
|
||||||
|
"""
|
||||||
|
model = getattr(getattr(self, 'queryset', None), 'model', None)
|
||||||
|
if model is None:
|
||||||
|
try:
|
||||||
|
qs = super().get_queryset()
|
||||||
|
model = getattr(qs, 'model', None)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
field_names = {f.name for f in model._meta.get_fields() if hasattr(f, 'name')}
|
||||||
|
# 优先级:name > title > driver_name
|
||||||
|
if 'name' in field_names:
|
||||||
|
return 'name'
|
||||||
|
if 'title' in field_names:
|
||||||
|
return 'title'
|
||||||
|
if 'driver_name' in field_names:
|
||||||
|
return 'driver_name'
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _build_filterset_class(self, model, field):
|
||||||
|
"""
|
||||||
|
动态构建 FilterSet,参数名直接使用字段名,lookup 使用 icontains。
|
||||||
|
"""
|
||||||
|
cache_key = (model, field)
|
||||||
|
if cache_key in self._auto_filterset_cache:
|
||||||
|
return self._auto_filterset_cache[cache_key]
|
||||||
|
|
||||||
|
meta_cls = type(
|
||||||
|
'Meta',
|
||||||
|
(),
|
||||||
|
{
|
||||||
|
'model': model,
|
||||||
|
'fields': [field],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
auto_filter_cls = type(
|
||||||
|
f'{model.__name__}AutoFilter',
|
||||||
|
(dj_filters.FilterSet,),
|
||||||
|
{
|
||||||
|
field: dj_filters.CharFilter(field_name=field, lookup_expr='icontains'),
|
||||||
|
'Meta': meta_cls,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self._auto_filterset_cache[cache_key] = auto_filter_cls
|
||||||
|
return auto_filter_cls
|
||||||
|
|
||||||
|
def filter_queryset(self, queryset):
|
||||||
|
field = self._detect_filter_field()
|
||||||
|
model = getattr(getattr(self, 'queryset', None), 'model', None)
|
||||||
|
if not field or not model:
|
||||||
|
return super().filter_queryset(queryset)
|
||||||
|
|
||||||
|
self.filterset_class = self._build_filterset_class(model, field)
|
||||||
|
return super().filter_queryset(queryset)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseViewSet(BasicInfoFilterMixin, viewsets.ModelViewSet):
|
||||||
pagination_class = LimitOffsetPagination
|
pagination_class = LimitOffsetPagination
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -25,9 +94,10 @@ class BaseViewSet(viewsets.ModelViewSet):
|
|||||||
raise PermissionDenied("无权限创建该对象")
|
raise PermissionDenied("无权限创建该对象")
|
||||||
|
|
||||||
|
|
||||||
class QuickInputViewSet(viewsets.ModelViewSet):
|
class QuickInputViewSet(BasicInfoFilterMixin, viewsets.ModelViewSet):
|
||||||
queryset = serializers.basic_models.QuickInput.objects
|
queryset = serializers.basic_models.QuickInput.objects.all()
|
||||||
serializer_class = serializers.QuickInputSerializer
|
serializer_class = serializers.QuickInputSerializer
|
||||||
|
pagination_class = LimitOffsetPagination
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
qs = super().filter_queryset(queryset)
|
qs = super().filter_queryset(queryset)
|
||||||
|
|||||||
@@ -198,13 +198,13 @@ 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 = 5, 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 游标)
|
||||||
"""
|
"""
|
||||||
merchant = _get_mdy_merchant()
|
merchant = _get_mdy_merchant()
|
||||||
category = _get_mdy_category(merchant)
|
category = _get_mdy_category(merchant)
|
||||||
max_records = max_records or page_size
|
max_records = max_records or 0 # 0 表示不限制
|
||||||
|
|
||||||
last_sync = api_models.DataSync.objects.filter(
|
last_sync = api_models.DataSync.objects.filter(
|
||||||
table_name=api_models.DataSync.TableName.PRODUCT
|
table_name=api_models.DataSync.TableName.PRODUCT
|
||||||
@@ -217,23 +217,29 @@ def sync_mdy_products(self, page_size: int = 300, max_pages: int = 5, max_record
|
|||||||
total_count = 0
|
total_count = 0
|
||||||
latest_ctime = last_ctime
|
latest_ctime = last_ctime
|
||||||
latest_rowid = last_rowid
|
latest_rowid = last_rowid
|
||||||
reached_existing = False
|
|
||||||
|
|
||||||
while page_index <= max_pages and synced_rows < max_records:
|
while True:
|
||||||
|
if max_pages is not None and page_index > max_pages:
|
||||||
|
break
|
||||||
|
if max_records and synced_rows >= max_records:
|
||||||
|
break
|
||||||
|
|
||||||
products, total = _run_fetch(page_index, page_size)
|
products, total = _run_fetch(page_index, page_size)
|
||||||
total_count = total
|
total_count = total
|
||||||
if not products:
|
if not products:
|
||||||
break
|
break
|
||||||
|
|
||||||
for item in products:
|
for item in products:
|
||||||
|
if max_records and synced_rows >= max_records:
|
||||||
|
break
|
||||||
product_ctime = _parse_mdy_datetime(item.created_at)
|
product_ctime = _parse_mdy_datetime(item.created_at)
|
||||||
|
|
||||||
|
# 跳过已同步到的游标
|
||||||
if last_ctime and product_ctime:
|
if last_ctime and product_ctime:
|
||||||
if product_ctime < last_ctime:
|
if product_ctime < last_ctime:
|
||||||
reached_existing = True
|
continue
|
||||||
break
|
|
||||||
if product_ctime == last_ctime and last_rowid and item.rowid == last_rowid:
|
if product_ctime == last_ctime and last_rowid and item.rowid == last_rowid:
|
||||||
reached_existing = True
|
continue
|
||||||
break
|
|
||||||
|
|
||||||
changed = _upsert_product(item, merchant, category)
|
changed = _upsert_product(item, merchant, category)
|
||||||
if changed:
|
if changed:
|
||||||
@@ -242,10 +248,8 @@ def sync_mdy_products(self, page_size: int = 300, max_pages: int = 5, max_record
|
|||||||
latest_ctime = product_ctime
|
latest_ctime = product_ctime
|
||||||
latest_rowid = item.rowid
|
latest_rowid = item.rowid
|
||||||
|
|
||||||
if synced_rows >= max_records:
|
# 若返回不足一页,说明到尾部,可结束
|
||||||
break
|
if len(products) < page_size:
|
||||||
|
|
||||||
if reached_existing or synced_rows >= max_records or len(products) < page_size:
|
|
||||||
break
|
break
|
||||||
page_index += 1
|
page_index += 1
|
||||||
|
|
||||||
@@ -259,7 +263,7 @@ def sync_mdy_products(self, page_size: int = 300, max_pages: int = 5, max_record
|
|||||||
total_count=total_count,
|
total_count=total_count,
|
||||||
last_ctime=record_last_ctime,
|
last_ctime=record_last_ctime,
|
||||||
last_rowid=record_last_rowid,
|
last_rowid=record_last_rowid,
|
||||||
note='reached existing data' if reached_existing else '',
|
note='asc scan',
|
||||||
)
|
)
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
@@ -308,12 +312,12 @@ 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 = 5, 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 游标)
|
||||||
"""
|
"""
|
||||||
merchant = _get_mdy_merchant()
|
merchant = _get_mdy_merchant()
|
||||||
max_records = max_records or page_size
|
max_records = max_records or 0 # 0 表示不限制
|
||||||
|
|
||||||
last_sync = api_models.DataSync.objects.filter(
|
last_sync = api_models.DataSync.objects.filter(
|
||||||
table_name=api_models.DataSync.TableName.CUSTOMER
|
table_name=api_models.DataSync.TableName.CUSTOMER
|
||||||
@@ -326,23 +330,28 @@ def sync_mdy_customers(self, page_size: int = 300, max_pages: int = 5, max_recor
|
|||||||
total_count = 0
|
total_count = 0
|
||||||
latest_ctime = last_ctime
|
latest_ctime = last_ctime
|
||||||
latest_rowid = last_rowid
|
latest_rowid = last_rowid
|
||||||
reached_existing = False
|
|
||||||
|
|
||||||
while page_index <= max_pages and synced_rows < max_records:
|
while True:
|
||||||
|
if max_pages is not None and page_index > max_pages:
|
||||||
|
break
|
||||||
|
if max_records and synced_rows >= max_records:
|
||||||
|
break
|
||||||
|
|
||||||
customers, total = _run_fetch_customers(page_index, page_size)
|
customers, total = _run_fetch_customers(page_index, page_size)
|
||||||
total_count = total
|
total_count = total
|
||||||
if not customers:
|
if not customers:
|
||||||
break
|
break
|
||||||
|
|
||||||
for item in customers:
|
for item in customers:
|
||||||
|
if max_records and synced_rows >= max_records:
|
||||||
|
break
|
||||||
record_ctime = _parse_mdy_datetime(item.created_at)
|
record_ctime = _parse_mdy_datetime(item.created_at)
|
||||||
|
|
||||||
if last_ctime and record_ctime:
|
if last_ctime and record_ctime:
|
||||||
if record_ctime < last_ctime:
|
if record_ctime < last_ctime:
|
||||||
reached_existing = True
|
continue
|
||||||
break
|
|
||||||
if record_ctime == last_ctime and last_rowid and item.rowid == last_rowid:
|
if record_ctime == last_ctime and last_rowid and item.rowid == last_rowid:
|
||||||
reached_existing = True
|
continue
|
||||||
break
|
|
||||||
|
|
||||||
changed = _upsert_customer(item, merchant)
|
changed = _upsert_customer(item, merchant)
|
||||||
if changed:
|
if changed:
|
||||||
@@ -351,10 +360,7 @@ def sync_mdy_customers(self, page_size: int = 300, max_pages: int = 5, max_recor
|
|||||||
latest_ctime = record_ctime
|
latest_ctime = record_ctime
|
||||||
latest_rowid = item.rowid
|
latest_rowid = item.rowid
|
||||||
|
|
||||||
if synced_rows >= max_records:
|
if len(customers) < page_size:
|
||||||
break
|
|
||||||
|
|
||||||
if reached_existing or synced_rows >= max_records or len(customers) < page_size:
|
|
||||||
break
|
break
|
||||||
page_index += 1
|
page_index += 1
|
||||||
|
|
||||||
@@ -368,7 +374,7 @@ def sync_mdy_customers(self, page_size: int = 300, max_pages: int = 5, max_recor
|
|||||||
total_count=total_count,
|
total_count=total_count,
|
||||||
last_ctime=record_last_ctime,
|
last_ctime=record_last_ctime,
|
||||||
last_rowid=record_last_rowid,
|
last_rowid=record_last_rowid,
|
||||||
note='reached existing data' if reached_existing else '',
|
note='asc scan',
|
||||||
)
|
)
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ from rest_framework.permissions import BasePermission
|
|||||||
from rest_framework.pagination import LimitOffsetPagination
|
from rest_framework.pagination import LimitOffsetPagination
|
||||||
from rest_framework.permissions import DjangoModelPermissions
|
from rest_framework.permissions import DjangoModelPermissions
|
||||||
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||||
|
from django.db.models import CharField
|
||||||
|
from django.db.models.functions import Cast, Coalesce
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from django_filters import rest_framework as django_filters
|
from django_filters import rest_framework as django_filters
|
||||||
from django_filters import IsoDateTimeFilter
|
from django_filters import IsoDateTimeFilter
|
||||||
@@ -590,7 +592,8 @@ class PlateOrderViewSet(viewsets.ModelViewSet):
|
|||||||
pagination_class = LimitOffsetPagination
|
pagination_class = LimitOffsetPagination
|
||||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||||
filterset_class = PlateOrderFilterSet
|
filterset_class = PlateOrderFilterSet
|
||||||
search_fields = ['design_code', 'style_name', 'customer__name', 'fabric']
|
# design_code_normalized: 当 design_code 为空时用 id 兜底,便于搜索数字编号
|
||||||
|
search_fields = ['design_code', 'design_code_normalized', 'style_name', 'customer__name', 'fabric']
|
||||||
ordering_fields = [
|
ordering_fields = [
|
||||||
'id', 'created_at', 'updated_at', 'plate_date',
|
'id', 'created_at', 'updated_at', 'plate_date',
|
||||||
'required_completion_date', 'completion_date',
|
'required_completion_date', 'completion_date',
|
||||||
@@ -618,6 +621,10 @@ class PlateOrderViewSet(viewsets.ModelViewSet):
|
|||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
if self.action in ['list', 'retrieve']:
|
if self.action in ['list', 'retrieve']:
|
||||||
queryset = queryset.select_related('customer', 'salesperson', 'merchandiser', 'business_object')
|
queryset = queryset.select_related('customer', 'salesperson', 'merchandiser', 'business_object')
|
||||||
|
# 为搜索提供 design_code 的兜底(为空时使用主键字符串)
|
||||||
|
queryset = queryset.annotate(
|
||||||
|
design_code_normalized=Coalesce('design_code', Cast('id', output_field=CharField()))
|
||||||
|
)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
8186
data-bak/db-backup-20251208-190000.sql
Normal file
8186
data-bak/db-backup-20251208-190000.sql
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ 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
|
||||||
|
from asgiref.sync import sync_to_async
|
||||||
|
|
||||||
|
|
||||||
class Product(BaseModel):
|
class Product(BaseModel):
|
||||||
@@ -243,10 +244,14 @@ async def fetch_products_from_mingdaoyun(page: int = 1, page_size: int = 100) ->
|
|||||||
'pageIndex': page,
|
'pageIndex': page,
|
||||||
'pageSize': page_size,
|
'pageSize': page_size,
|
||||||
'sortId': 'ctime',
|
'sortId': 'ctime',
|
||||||
'isAsc': False,
|
'isAsc': True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
data = response.get('data')
|
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:
|
if not data:
|
||||||
return [], 0
|
return [], 0
|
||||||
|
|
||||||
@@ -271,13 +276,95 @@ async def fetch_customers_from_mingdaoyun(page: int = 1, page_size: int = 100) -
|
|||||||
'pageIndex': page,
|
'pageIndex': page,
|
||||||
'pageSize': page_size,
|
'pageSize': page_size,
|
||||||
'sortId': 'ctime',
|
'sortId': 'ctime',
|
||||||
'isAsc': False,
|
'isAsc': True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
data = response.get('data')
|
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:
|
if not data:
|
||||||
return [], 0
|
return [], 0
|
||||||
|
|
||||||
customers = [pick_customer(item) for item in data.get('rows', [])]
|
customers = [pick_customer(item) for item in data.get('rows', [])]
|
||||||
total_count = data.get('total', 0)
|
total_count = data.get('total', 0)
|
||||||
return customers, total_count
|
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}')
|
||||||
|
|||||||
Reference in New Issue
Block a user