forked from erp-dev/erp
feat: tasks for sync products and customers
This commit is contained in:
@@ -53,6 +53,13 @@ CSRF_TRUSTED_ORIGINS = env.list('CSRF_TRUSTED_ORIGINS', default=[
|
||||
'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_ALLOW_ALL_ORIGINS = DEBUG # 开发环境允许所有源,生产环境需要配置白名单
|
||||
@@ -303,4 +310,22 @@ CELERY_BEAT_SCHEDULE = {
|
||||
'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
|
||||
# from typing import Optional, Dict, Any, List
|
||||
# from pydantic import BaseModel, Field, computed_field
|
||||
# from enum import Enum
|
||||
# import json
|
||||
import aiohttp
|
||||
from typing import Optional, Dict, Any, List
|
||||
from pydantic import BaseModel, Field, computed_field
|
||||
from enum import Enum
|
||||
import json
|
||||
|
||||
|
||||
# class Product(BaseModel):
|
||||
# """产品模型 - 请在此填入你的字段"""
|
||||
# uid: str
|
||||
# name: str
|
||||
# unit: str | None
|
||||
# color: str | None
|
||||
# detail_str: str | None = Field(exclude=True) # 在序列化时排除此字段
|
||||
# created_at: str
|
||||
class Product(BaseModel):
|
||||
"""产品模型 - 请在此填入你的字段"""
|
||||
uid: str
|
||||
rowid: str
|
||||
name: str
|
||||
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 {}
|
||||
@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 ProductListResponse(BaseModel):
|
||||
"""产品列表响应"""
|
||||
products: List[Product]
|
||||
total: int
|
||||
|
||||
|
||||
# mdy_table_map = {
|
||||
# 'product': 'spmx',
|
||||
# 'customer': 'quanbu',
|
||||
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',
|
||||
'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):
|
||||
# """HTTP 请求方法枚举"""
|
||||
# GET = "GET"
|
||||
# POST = "POST"
|
||||
def pick_product(fields: dict) -> Product:
|
||||
"""
|
||||
从字段字典中提取产品信息
|
||||
"""
|
||||
data = {k: fields.get(v, '') for k, v in product_type_map.items()}
|
||||
return Product(**data)
|
||||
|
||||
|
||||
# class MingDaoYunClient:
|
||||
# """明道云 API 客户端"""
|
||||
async def fetch_products_from_mingdaoyun(page: int = 1, page_size: int = 100) -> tuple[list[Product], int]:
|
||||
"""
|
||||
从明道云获取产品列表
|
||||
|
||||
# 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:
|
||||
# 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,
|
||||
# )
|
||||
products = [pick_product(item) for item in data.get('rows', [])]
|
||||
total_count = data.get('total', 0)
|
||||
return products, total_count
|
||||
|
||||
|
||||
# product_type_map = {
|
||||
# 'name': '668caa5eb80969563ecaee7d',
|
||||
# 'color': '66ed3b9ae01d5599bdb45f6d',
|
||||
# 'uid': '668caa5eb80969563ecaee7c',
|
||||
# 'unit': '668e2ee2ca758c8cc5d0dc1d',
|
||||
# 'detail_str': '668caa5eb80969563ecaee7e',
|
||||
# 'created_at': 'ctime',
|
||||
# }
|
||||
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': False,
|
||||
}
|
||||
)
|
||||
data = response.get('data')
|
||||
if not data:
|
||||
return [], 0
|
||||
|
||||
|
||||
# # 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()}
|
||||
# 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
|
||||
customers = [pick_customer(item) for item in data.get('rows', [])]
|
||||
total_count = data.get('total', 0)
|
||||
return customers, total_count
|
||||
|
||||
Reference in New Issue
Block a user