first commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
from .role import role_controller as role_controller
|
||||
from .user import user_controller as user_controller
|
||||
@@ -0,0 +1,109 @@
|
||||
from app.core.crud import CRUDBase
|
||||
from app.schemas.msg import (
|
||||
ActionCreate,
|
||||
ActionUpdate,
|
||||
)
|
||||
from app.models.automation import Action, ActionType
|
||||
from app.models.weixin import WeixinCustomer, CustomerGroup, WeixinGroupChat, WeixinUser
|
||||
from some_sdk.lintao_sdk.biz.by_order import get_order_relative_user
|
||||
from some_sdk.services.binder import lintao_client
|
||||
from app.controllers.weixin.customer import weixin_customer_controller
|
||||
from datetime import datetime
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ActionController(CRUDBase[Action, ActionCreate, ActionUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=Action)
|
||||
|
||||
async def new_action(self, type: ActionType, user, done=False, **kwargs):
|
||||
action = ActionCreate(userid=user.userid, username=f'{user.username}({user.english_name})', done=done, type=type, detail=kwargs)
|
||||
return await self.create(action)
|
||||
|
||||
async def check_create_group_action_is_done(self, group_info, shop_name: str = None):
|
||||
name = group_info.get('name')
|
||||
chat_id = group_info.get('chat_id')
|
||||
|
||||
# 完成建群操作
|
||||
create_group_action = await self.model.filter(type=ActionType.CREATE_GROUP, done=False, detail__contains={"group_name": name}).order_by('-id').first()
|
||||
if not create_group_action: return
|
||||
|
||||
create_group_action_detail = create_group_action.detail
|
||||
create_group_action_detail.update({
|
||||
"chat_id": chat_id,
|
||||
})
|
||||
|
||||
create_group_action.done = True
|
||||
create_group_action.done_at = datetime.now()
|
||||
create_group_action.result = group_info
|
||||
create_group_action.detail = create_group_action_detail
|
||||
await create_group_action.save()
|
||||
|
||||
# 完成建群操作后,将群成员添加到客户群中
|
||||
weixin_group = await WeixinGroupChat.filter(chat_id=chat_id).first()
|
||||
if not weixin_group: return
|
||||
|
||||
staff = await WeixinUser.filter(userid=create_group_action.userid).first()
|
||||
|
||||
external_user_ids = create_group_action.detail.get('external_user_ids') or []
|
||||
for userid in external_user_ids:
|
||||
customer = await WeixinCustomer.filter(weixin_id=userid).first()
|
||||
if not customer: continue
|
||||
|
||||
membership = await CustomerGroup.create(
|
||||
staff_userid=create_group_action.userid,
|
||||
customer_userid=customer.weixin_id,
|
||||
group_chatid=chat_id,
|
||||
staff=staff,
|
||||
customer=customer,
|
||||
group=weixin_group,
|
||||
shop_name=shop_name,
|
||||
order_id=create_group_action.detail.get('order_id'),
|
||||
join_time=create_group_action.created_at,
|
||||
)
|
||||
logger.info(f'【建群成功】保存客户的群聊 {name}({chat_id}) 信息')
|
||||
|
||||
async def check_remark_action_is_done(self):
|
||||
actions = await self.model.filter(done=False, type=ActionType.WRITE_REMARK)
|
||||
logger.info(f'检查备注操作是否完成,监控列表中:共有{len(actions)}条')
|
||||
|
||||
update_list = []
|
||||
for action in actions:
|
||||
action_info = action.detail
|
||||
order_id = action_info.get("order_id")
|
||||
remark = action_info.get("remark")
|
||||
if not order_id: continue
|
||||
|
||||
done = False
|
||||
action_result = []
|
||||
|
||||
if remark:
|
||||
async for order in get_order_relative_user(lintao_client, trade_no=order_id):
|
||||
|
||||
try:
|
||||
await weixin_customer_controller.remark_order(order, remark=remark)
|
||||
if order.get('title'):
|
||||
logger.info(f'【监控列表中,订单被领单】重新为订单 {order.get("trade_no")} 打上备注 {remark}')
|
||||
# 未领单的订单,需要添加到监控列表中进行监控,防止被刷掉
|
||||
done = True
|
||||
action_result.append({
|
||||
"trade_no": order.get("trade_no"),
|
||||
"title": order.get("title"),
|
||||
"remark": remark,
|
||||
})
|
||||
except Exception as e:
|
||||
logger.exception(f'【监控列表中,订单被领单】重新为订单为订单 {order.get("trade_no")} 打上备注 {remark} 失败,异常: {e}')
|
||||
|
||||
if done or not remark:
|
||||
action.done = True
|
||||
action.done_at = datetime.now()
|
||||
action.result = action_result
|
||||
update_list.append(action)
|
||||
|
||||
if update_list:
|
||||
await self.model.bulk_update(update_list, fields=['done', 'result', 'done_at'])
|
||||
|
||||
|
||||
action_controller = ActionController()
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
from fastapi.routing import APIRoute
|
||||
|
||||
from app.core.crud import CRUDBase
|
||||
import logging
|
||||
from app.models.admin import Api
|
||||
from app.schemas.apis import ApiCreate, ApiUpdate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ApiController(CRUDBase[Api, ApiCreate, ApiUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=Api)
|
||||
|
||||
async def refresh_api(self):
|
||||
from app import app
|
||||
|
||||
# 删除废弃API数据
|
||||
all_api_list = []
|
||||
for route in app.routes:
|
||||
# 只更新有鉴权的API
|
||||
if isinstance(route, APIRoute) and len(route.dependencies) > 0:
|
||||
all_api_list.append((list(route.methods)[0], route.path_format))
|
||||
delete_api = []
|
||||
for api in await Api.all():
|
||||
if (api.method, api.path) not in all_api_list:
|
||||
delete_api.append((api.method, api.path))
|
||||
for item in delete_api:
|
||||
method, path = item
|
||||
logger.debug(f"API Deleted {method} {path}")
|
||||
await Api.filter(method=method, path=path).delete()
|
||||
|
||||
for route in app.routes:
|
||||
if isinstance(route, APIRoute) and len(route.dependencies) > 0:
|
||||
method = list(route.methods)[0]
|
||||
path = route.path_format
|
||||
summary = route.summary
|
||||
tags = list(route.tags)[0]
|
||||
api_obj = await Api.filter(method=method, path=path).first()
|
||||
if api_obj:
|
||||
await api_obj.update_from_dict(dict(method=method, path=path, summary=summary, tags=tags)).save()
|
||||
else:
|
||||
logger.debug(f"API Created {method} {path}")
|
||||
await Api.create(**dict(method=method, path=path, summary=summary, tags=tags))
|
||||
|
||||
|
||||
api_controller = ApiController()
|
||||
@@ -0,0 +1,82 @@
|
||||
# app/controllers/automation.py (继续追加)
|
||||
from app.core.crud import CRUDBase
|
||||
from typing import List, Optional, Dict, Any
|
||||
from app.models.automation import Action
|
||||
from app.models.enums import ActionType
|
||||
from app.schemas.automation import ActionCreate, ActionUpdate
|
||||
from app.utils.common import transform_pydantic_to_list
|
||||
from app.schemas.automation import NotifyAction
|
||||
|
||||
class ActionController(CRUDBase[Action, ActionCreate, ActionUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=Action)
|
||||
|
||||
async def get_pending_actions_by_task(self, task_id: int) -> List[Action]:
|
||||
"""获取某任务中未完成的动作"""
|
||||
return await self.model.filter(task_id=task_id, done=False).all()
|
||||
|
||||
async def list_automation_actions(self) -> List[Dict[str, Any]]:
|
||||
"""获取动作类型映射"""
|
||||
|
||||
ActionTypeMap = [
|
||||
{"value": ActionType.CREATE_GROUP, "label": "创建群聊", "automation": True, "allowAutoExecute": False, "autoExecute": False, "enable": True},
|
||||
{"value": ActionType.VIEW_ERP_LOG, "label": "查看ERP日志", "automation": False, "allowAutoExecute": False, "autoExecute": False, "enable": True},
|
||||
{"value": ActionType.BIND_USER, "label": "绑定用户", "automation": False, "allowAutoExecute": False, "autoExecute": False, "enable": True},
|
||||
{"value": ActionType.BIND_ORDER, "label": "绑定订单", "automation": False, "allowAutoExecute": False, "autoExecute": False, "enable": True},
|
||||
{"value": ActionType.WRITE_REMARK, "label": "写ERP额外备注", "automation": True, "allowAutoExecute": True, "autoExecute": True, "enable": True},
|
||||
{"value": ActionType.SEND_WECHAT_NOTIFY, "label": "发送通知", "automation": True, "allowAutoExecute": True, "autoExecute": True, "enable": True, "schema": transform_pydantic_to_list(NotifyAction)},
|
||||
{"value": ActionType.CLEAN_GROUP, "label": "清理群聊", "automation": True, "allowAutoExecute": False, "autoExecute": True, "enable": True},
|
||||
{"value": ActionType.SET_GROUP_ADMIN, "label": "设置群管理员", "automation": True, "allowAutoExecute": False, "autoExecute": True, "enable": True},
|
||||
]
|
||||
|
||||
return [item for item in ActionTypeMap if item["automation"]]
|
||||
|
||||
async def mark_action_done(
|
||||
self,
|
||||
action_id: int,
|
||||
userid: str,
|
||||
username: str,
|
||||
result: Dict[str, Any],
|
||||
notes: Optional[str] = None
|
||||
) -> Action:
|
||||
"""标记动作为已完成"""
|
||||
update_data = ActionUpdate(
|
||||
done=True,
|
||||
done_at=self.model._meta.db_fields.get("done_at").to_db_value(None, None), # 实际用 datetime.now()
|
||||
userid=userid,
|
||||
username=username,
|
||||
result=result,
|
||||
notes=notes
|
||||
)
|
||||
# Note: 建议在 service 层处理 done_at = datetime.utcnow()
|
||||
return await self.update(action_id, update_data)
|
||||
|
||||
async def create_actions_for_task(self, task_id: int, actions_def: List[Dict]) -> List[Action]:
|
||||
"""为任务批量创建动作实例"""
|
||||
actions = []
|
||||
for act in actions_def:
|
||||
action_in = ActionCreate(
|
||||
task_id=task_id,
|
||||
type=act.get("type"),
|
||||
detail=act.get("detail", {}),
|
||||
done=False
|
||||
)
|
||||
action = await self.create(action_in)
|
||||
actions.append(action)
|
||||
return actions
|
||||
|
||||
async def trigger_scenarios(self, scenario_list: List[Action], event_data: Dict[str, Any]) -> None:
|
||||
"""触发场景执行"""
|
||||
for scenario in scenario_list:
|
||||
if not scenario.enabled:
|
||||
continue
|
||||
|
||||
for action in scenario.actions:
|
||||
if not action.enabled:
|
||||
continue
|
||||
|
||||
if action.type == ActionType.SEND_WECHAT_NOTIFY:
|
||||
await self.trigger_scenarios_by_action(action, event_data)
|
||||
|
||||
|
||||
action_controller = ActionController()
|
||||
@@ -0,0 +1,222 @@
|
||||
# app/controllers/automation.py
|
||||
|
||||
from typing import List, Optional, Tuple, Dict, Any
|
||||
from app.core.crud import CRUDBase
|
||||
from app.models.automation import Scenario, ScenarioTriggerIndex, ScenarioScope
|
||||
from app.schemas.automation import ScenarioCreate, ScenarioUpdate
|
||||
from app.utils.event_task import event_manager
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ScenarioController(CRUDBase[Scenario, ScenarioCreate, ScenarioUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=Scenario)
|
||||
|
||||
async def update_trigger_index(self, scenario: Scenario, user_id: str) -> Scenario:
|
||||
"""更新场景"""
|
||||
add_list = []
|
||||
event_set = set()
|
||||
for condition in scenario.trigger.get('conditions') or []:
|
||||
event_name = condition.get('event_name', '')
|
||||
if event_name and event_name in event_set:
|
||||
continue
|
||||
event_set.add(event_name)
|
||||
|
||||
orm = ScenarioTriggerIndex(
|
||||
owner_user_id=user_id,
|
||||
scenario_id=scenario.id,
|
||||
is_global=scenario.is_global,
|
||||
event_name=condition.get('event_name', ''),
|
||||
enabled=condition.get('enabled', True),
|
||||
scope=condition.get('scope', ScenarioScope.PERSONAL),
|
||||
)
|
||||
add_list.append(orm)
|
||||
|
||||
if add_list:
|
||||
await ScenarioTriggerIndex.bulk_create(add_list)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def new_scenario(self, scenario_in: ScenarioCreate, user_id: str) -> Scenario:
|
||||
"""创建新场景"""
|
||||
obj = await self.create(scenario_in)
|
||||
|
||||
# 更新触发条件
|
||||
if obj.trigger:
|
||||
await self.update_trigger_index(obj, user_id)
|
||||
|
||||
return obj
|
||||
|
||||
async def update_scenario(self, scenario_in: ScenarioUpdate, user_id: str) -> Scenario:
|
||||
"""更新场景"""
|
||||
obj = await self.update(id=scenario_in.id, obj_in=scenario_in)
|
||||
|
||||
# 更新触发条件
|
||||
if obj.trigger:
|
||||
await ScenarioTriggerIndex.filter(scenario_id=obj.id).delete()
|
||||
await self.update_trigger_index(obj, user_id)
|
||||
|
||||
return obj
|
||||
|
||||
async def remove_scenario(self, scenario_id: int) -> None:
|
||||
"""删除场景"""
|
||||
await self.model.filter(id=scenario_id).delete()
|
||||
await ScenarioTriggerIndex.filter(scenario_id=scenario_id).delete()
|
||||
return True
|
||||
|
||||
async def list_automation_events(self) -> List[Dict[str, Any]]:
|
||||
"""获取所有自动化事件"""
|
||||
return [a for a in event_manager.automation_event_handlers if a.get("automation_event")]
|
||||
|
||||
async def get_global_scenarios(self, enabled: bool = True) -> List[Scenario]:
|
||||
"""获取所有启用的全局场景"""
|
||||
return await self.model.filter(is_global=True, enabled=enabled).all()
|
||||
|
||||
async def get_user_scenarios(self, user_id: str, enabled: bool = True) -> List[Scenario]:
|
||||
"""获取某用户的启用个人场景"""
|
||||
return await self.model.filter(owner_user_id=user_id, is_global=False, enabled=enabled).all()
|
||||
|
||||
async def get_applicable_scenarios(self, user_id: str, enabled: bool = True) -> List[Scenario]:
|
||||
"""获取对某用户生效的所有场景(全局 + 个人)"""
|
||||
global_scenarios = await self.get_global_scenarios(enabled=enabled)
|
||||
personal_scenarios = await self.get_user_scenarios(user_id, enabled=enabled)
|
||||
return global_scenarios + personal_scenarios
|
||||
|
||||
|
||||
class AutomationScenarioController(ScenarioController):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
async def find_active_scenarios(self, event_name: str, event_data: Dict[str, Any]) -> List[Scenario]:
|
||||
"""查找所有实际激活的场景配置"""
|
||||
scenario_index_list = await ScenarioTriggerIndex.filter(enabled=True, event_name=event_name).all()
|
||||
|
||||
scenario_list = []
|
||||
if scenario_index_list:
|
||||
scenario_list = await self.model.filter(id__in=[s.scenario_id for s in scenario_index_list]).all()
|
||||
|
||||
logger.info(f'find_active_scenarios: {len(scenario_list)}')
|
||||
|
||||
active_scenarios = []
|
||||
for scenario in scenario_list:
|
||||
result, reason = await self.check_is_applicable(scenario, event_data)
|
||||
if result:
|
||||
active_scenarios.append((scenario, reason))
|
||||
|
||||
return active_scenarios
|
||||
|
||||
async def check_is_applicable(self, scenario: Scenario, event_data: Dict[str, Any]) -> tuple[bool, str]:
|
||||
"""检查场景是否适用于当前事件"""
|
||||
|
||||
trigger = scenario.trigger
|
||||
# 如果没有条件,默认返回True,即只要事件发生就会激活场景
|
||||
if not trigger or not trigger.get('conditions'):
|
||||
logger.info(f'scenario {scenario.id} trigger without conditions, always return True')
|
||||
return True, '没有设置具体的触发条件,事件发生就激活场景'
|
||||
|
||||
logic = trigger.get('logic', 'and')
|
||||
conditions = trigger.get('conditions', [])
|
||||
|
||||
if logic == 'or':
|
||||
for condition in conditions:
|
||||
condition = condition.get('condition', {})
|
||||
|
||||
result, reason = self.check_condition(condition, event_data)
|
||||
if result:
|
||||
logger.info(f'scenario {scenario.id} trigger condition {condition} return True, reason: {reason}')
|
||||
return True, reason
|
||||
return False, '所有触发条件都不满足'
|
||||
elif logic == 'and':
|
||||
|
||||
reason_list = []
|
||||
for condition in conditions:
|
||||
condition = condition.get('condition', {})
|
||||
|
||||
result, reason = self.check_condition(condition, event_data)
|
||||
if not result:
|
||||
logger.info(f'scenario {scenario.id} trigger condition {condition} return False, reason: {reason}')
|
||||
return False, f'触发条件 {condition} 不满足, 原因: {reason}'
|
||||
else:
|
||||
reason_list.append(reason)
|
||||
|
||||
return True, f'{", ".join(reason_list)}'
|
||||
|
||||
raise ValueError(f"Invalid logic operator: {logic}")
|
||||
|
||||
def check_condition(self, condition: Dict[str, Any], event_data: Dict[str, Any]) -> tuple[bool, str]:
|
||||
"""检查单个条件是否满足"""
|
||||
def find_field_val(field: str, event_data: Dict[str, Any]) -> Any:
|
||||
"""递归查找字段值"""
|
||||
if '.' in field:
|
||||
parts = field.split('.')
|
||||
current = event_data
|
||||
for part in parts:
|
||||
if isinstance(current, list):
|
||||
# current = current[int(part)]
|
||||
result = []
|
||||
for item in current:
|
||||
# find_field_val
|
||||
field_val = find_field_val(part, item)
|
||||
if field_val is not None:
|
||||
result.append(field_val)
|
||||
return result
|
||||
elif isinstance(current, dict):
|
||||
current = current.get(part, None)
|
||||
else:
|
||||
return None
|
||||
return current
|
||||
return event_data.get(field, None)
|
||||
|
||||
if not condition or not condition.get('field', ''):
|
||||
logger.error(f'scenario check_condition: field is empty, 当做无效条件处理')
|
||||
return True, '没有设置具体的触发条件,事件发生就激活场景'
|
||||
|
||||
field = condition.get('field', '')
|
||||
|
||||
value = condition.get('value', '')
|
||||
operator = condition.get('operator', '')
|
||||
field_val = find_field_val(field, event_data)
|
||||
if field_val is None:
|
||||
logger.error(f'scenario check_condition: field {field} value is None, event_data: {event_data}')
|
||||
return False, f'触发条件 {condition} 字段 {field} 不存在'
|
||||
|
||||
# 字符串比较
|
||||
if operator in ['contains', 'not_contains']:
|
||||
# if not isinstance(field_val, str):
|
||||
# return False, f'字段 {field} 不是字符串类型'
|
||||
if operator == 'contains' and value in field_val:
|
||||
return True, f'字段 {field} 包含 {value}'
|
||||
if operator == 'not_contains' and value not in field_val:
|
||||
return True, f'字段 {field} 不包含 {value}'
|
||||
return False, f'字段 {field} 不满足 {operator} {value}'
|
||||
|
||||
# 数字、时间比较
|
||||
if operator in ['gt', 'gte', 'lt', 'lte', 'eq', 'ne']:
|
||||
if not isinstance(field_val, (int, float, str)):
|
||||
return False, f'字段 {field} 不是数字或时间类型'
|
||||
try:
|
||||
field_val = float(field_val)
|
||||
value = float(value)
|
||||
except ValueError:
|
||||
return False, f'字段 {field} 不是数字或时间类型'
|
||||
|
||||
if operator == 'gt' and field_val > value:
|
||||
return True, f'字段 {field} 大于 {value}'
|
||||
if operator == 'gte' and field_val >= value:
|
||||
return True, f'字段 {field} 大于等于 {value}'
|
||||
if operator == 'lt' and field_val < value:
|
||||
return True, f'字段 {field} 小于 {value}'
|
||||
if operator == 'lte' and field_val <= value:
|
||||
return True, f'字段 {field} 小于等于 {value}'
|
||||
if operator == 'eq' and field_val == value:
|
||||
return True, f'字段 {field} 等于 {value}'
|
||||
if operator == 'ne' and field_val != value:
|
||||
return True, f'字段 {field} 不等于 {value}'
|
||||
|
||||
return False, f'字段 {field} 不满足 {operator} {value}'
|
||||
|
||||
|
||||
|
||||
automation_scenario_controller = AutomationScenarioController()
|
||||
@@ -0,0 +1,77 @@
|
||||
# app/controllers/automation.py (继续追加)
|
||||
from app.core.crud import CRUDBase
|
||||
from typing import List, Optional, Dict, Any, Tuple
|
||||
|
||||
from tortoise.expressions import Q
|
||||
from app.models.automation import Task, Scenario, ScenarioScope
|
||||
from app.schemas.automation import TaskCreate, TaskUpdate
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
class TaskController(CRUDBase[Task, TaskCreate, TaskUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=Task)
|
||||
|
||||
async def get_user_tasks(
|
||||
self,
|
||||
user_id: str,
|
||||
status: Optional[str] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20
|
||||
) -> Tuple[int, List[Task]]:
|
||||
"""分页获取某用户的待办(支持按状态过滤)"""
|
||||
query = Q(assignee_user_id=user_id)
|
||||
if status:
|
||||
query &= Q(status=status)
|
||||
return await self.list(page=page, page_size=page_size, search=query, order=["-created_at"])
|
||||
|
||||
async def get_tasks_by_customer(self, customer_id: str) -> List[Task]:
|
||||
"""获取与某客户相关的所有待办(用于侧边栏上下文)"""
|
||||
return await self.model.filter(related_customer_id=customer_id).order_by("-created_at").all()
|
||||
|
||||
async def create_from_scenario(
|
||||
self,
|
||||
scenario: Scenario,
|
||||
event_data: Optional[Dict[str, Any]] = None,
|
||||
assignee_user_id: Optional[str] = None,
|
||||
assignee_username: Optional[str] = None,
|
||||
reason: Optional[str] = None,
|
||||
) -> Task:
|
||||
"""根据场景模板创建待办任务"""
|
||||
# 计算截止时间(示例:场景中定义的 due_days 天后)
|
||||
due_at = scenario.due_days and (datetime.now() + timedelta(days=scenario.due_days)) or None
|
||||
|
||||
owner_user_id = None
|
||||
if scenario.scope == ScenarioScope.ALL:
|
||||
owner_user_id = ScenarioScope.ALL
|
||||
elif scenario.scope == ScenarioScope.PERSONAL:
|
||||
owner_user_id = scenario.owner_user_id
|
||||
|
||||
task_in = TaskCreate(
|
||||
title=scenario.title,
|
||||
reason=reason,
|
||||
notes=scenario.notes,
|
||||
event_data=event_data or {},
|
||||
source_scenario=scenario,
|
||||
source_scenario_id=scenario.id,
|
||||
owner_user_id=owner_user_id,
|
||||
assignee_user_id=assignee_user_id,
|
||||
assignee_username=assignee_username,
|
||||
due_at=due_at,
|
||||
status="pending",
|
||||
auto_closeable=True,
|
||||
)
|
||||
return await self.create(task_in)
|
||||
|
||||
async def auto_close_by_event(self, event_type: str, payload: Dict[str, Any]) -> List[Task]:
|
||||
"""
|
||||
根据事件自动关闭匹配的待办(用于自动消除)
|
||||
例如:当 group_id 匹配且动作含 create_group 时,关闭待办
|
||||
"""
|
||||
# 示例逻辑:后续可扩展为规则引擎
|
||||
closed_tasks = []
|
||||
# 这里可遍历 pending 任务,检查 content 中的动作是否已被事件满足
|
||||
# 为简化,此处留作扩展点
|
||||
return closed_tasks
|
||||
|
||||
|
||||
task_controller = TaskController()
|
||||
@@ -0,0 +1,235 @@
|
||||
import asyncio
|
||||
from typing import List, Dict, Any
|
||||
|
||||
import concurrent # 添加这行
|
||||
import concurrent.futures
|
||||
|
||||
from app.core.crud import CRUDBase
|
||||
from app.schemas.crm import (
|
||||
CrmCustomerCreate,
|
||||
CrmCustomerUpdate,
|
||||
CrmCustomerCreate,
|
||||
CrmBindInfoCreate,
|
||||
CrmBindInfoUpdate,
|
||||
)
|
||||
|
||||
from some_sdk.services import binder as binder_service
|
||||
from some_sdk.lintao_sdk.biz.by_order import get_order_relative_user
|
||||
from app.models.weixin import CrmCustomer, CrmBindInfo
|
||||
from tortoise.expressions import Subquery
|
||||
from tortoise.functions import Count # 导入 Count 函数
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def split_generator(iterable, chunk_size=50):
|
||||
"""
|
||||
将可迭代对象拆分为多个子列表
|
||||
"""
|
||||
chunk = []
|
||||
for item in iterable:
|
||||
chunk.append(item)
|
||||
if len(chunk) == chunk_size:
|
||||
yield chunk
|
||||
chunk = []
|
||||
if chunk:
|
||||
yield chunk
|
||||
|
||||
class CrmCustomerController(CRUDBase[CrmCustomer, CrmCustomerCreate, CrmCustomerUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=CrmCustomer)
|
||||
|
||||
async def get_unbound_customers_method_four_alt(self, limit: int = 50):
|
||||
# 创建子查询
|
||||
bound_ids_subquery = CrmBindInfo.all().values_list('customer_id', flat=True)
|
||||
|
||||
# 使用正确的子查询语法
|
||||
unbound_customers = await self.model.filter(
|
||||
cid__not_in=Subquery(bound_ids_subquery)
|
||||
).limit(limit)
|
||||
return unbound_customers
|
||||
|
||||
async def bind_crm_all_customer_by_db(self):
|
||||
|
||||
# 定义一个函数来执行单个同步的 bind_shop_info 调用
|
||||
def sync_fetch_bind_info(user_data: Dict[str, Any]):
|
||||
# 同步调用 binder_service
|
||||
all_users = []
|
||||
for crm_user, user_list_from_bind in binder_service.bind_shop_info([user_data]):
|
||||
all_users.extend(user_list_from_bind)
|
||||
return all_users
|
||||
|
||||
parsed_set = set()
|
||||
while True:
|
||||
# 获取一批未绑定的用户 ORM 对象
|
||||
unbound_orm_objects = await self.get_unbound_customers_method_four_alt(limit=50)
|
||||
if not unbound_orm_objects:
|
||||
break
|
||||
print(f'获取到 {len(unbound_orm_objects)} 个待绑定的 ORM 对象', flush=True)
|
||||
expected_cids = {obj.cid for obj in unbound_orm_objects if obj.cid is not None and obj.cid not in parsed_set}
|
||||
if not expected_cids: break
|
||||
user_data_list = [await obj.to_dict() for obj in unbound_orm_objects if obj.cid not in parsed_set]
|
||||
[parsed_set.add(cid) for cid in expected_cids]
|
||||
print(f'待绑定用户数(转换后):{len(user_data_list)}', flush=True)
|
||||
await self._bind_crm_all_customer_by_db(user_data_list, expected_cids, sync_fetch_bind_info)
|
||||
|
||||
async def _bind_crm_all_customer_by_db(self, not_in_db_list: List[Dict[str, Any]], expected_cids: set, sync_fetch_bind_info):
|
||||
print(f'待绑定用户数(传入列表):{len(not_in_db_list)}', flush=True)
|
||||
|
||||
# --- 并发执行所有同步的 bind_shop_info 调用 ---
|
||||
# 使用 asyncio.to_thread (Python 3.9+) 或 run_in_executor 将同步函数移到线程池执行
|
||||
# 限制并发线程数很重要,避免创建过多线程
|
||||
max_workers = 10 # 限制线程池大小,根据系统性能调整
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# 创建任务列表
|
||||
tasks = []
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
for user_data in not_in_db_list:
|
||||
# 将同步函数提交到线程池执行,并返回一个 Future
|
||||
# asyncio.run_in_executor 将 Future 包装成 awaitable 的协程
|
||||
task = loop.run_in_executor(executor, sync_fetch_bind_info, user_data)
|
||||
tasks.append(task)
|
||||
|
||||
# 等待所有线程池任务完成
|
||||
all_bind_lists_results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# 处理结果,将异常和正常结果分开
|
||||
all_users_to_process = []
|
||||
for result in all_bind_lists_results:
|
||||
if isinstance(result, Exception):
|
||||
logger.exception(f"Thread pool task failed: {result}")
|
||||
# 可以选择跳过或记录错误
|
||||
else:
|
||||
all_users_to_process.extend(result)
|
||||
|
||||
# --- 去重逻辑 ---
|
||||
unique_users_to_process = {}
|
||||
for user in all_users_to_process:
|
||||
customer_id = user.get("customer_id", 0)
|
||||
tenant_name = user.get("tenant_name", 0)
|
||||
platform = user.get("platform", 0)
|
||||
|
||||
key = f"{customer_id}_{tenant_name}_{platform}"
|
||||
if key not in unique_users_to_process:
|
||||
unique_users_to_process[key] = user
|
||||
|
||||
print(f"去重后待处理用户数: {len(unique_users_to_process)}", flush=True)
|
||||
|
||||
# --- 并发处理去重后的用户 ---
|
||||
# 这部分可以保持原有逻辑,因为它已经是并发的了
|
||||
# 但要注意,如果同时有多个任务尝试插入相同的 customer_id,可能会有并发问题
|
||||
# 可以考虑使用数据库的 INSERT IGNORE 或 ON DUPLICATE KEY UPDATE 等特性
|
||||
|
||||
async def process_user(user_obj: Dict[str, Any]):
|
||||
try:
|
||||
user_type = user_obj.get("type", 0)
|
||||
tenant_name = user_obj.get("tenant_name", 0)
|
||||
customer_id = user_obj.get("customer_id", 0)
|
||||
platform = user_obj.get("platform", 0)
|
||||
|
||||
if user_type == 'staff':
|
||||
print(f'绑定员工:{user_obj}', flush=True)
|
||||
else:
|
||||
print(f'绑定客户:{user_obj}, Customer ID: {customer_id}', flush=True)
|
||||
# --- 检查数据库中是否已存在绑定 ---
|
||||
existing_bind = await CrmBindInfo.filter(customer_id=customer_id, platform=platform, tenant_name=tenant_name).first()
|
||||
if not existing_bind:
|
||||
bind_instance = CrmBindInfo.create_bind(user_obj)
|
||||
await bind_instance.save()
|
||||
print(f"客户 {customer_id} 绑定成功", flush=True)
|
||||
else:
|
||||
print(f"客户 {customer_id} 已存在绑定", flush=True)
|
||||
logger.warning(f"Attempted to bind customer_id {customer_id} which already exists in CrmBindInfo.")
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
print(f"处理用户 {user_obj.get('customer_id', 'unknown')} 时出错: {e}", flush=True)
|
||||
|
||||
max_concurrent_db_tasks = 10 # 可以独立控制数据库操作的并发数
|
||||
semaphore = asyncio.Semaphore(max_concurrent_db_tasks)
|
||||
|
||||
async def process_user_with_semaphore(user_obj):
|
||||
async with semaphore:
|
||||
return await process_user(user_obj)
|
||||
|
||||
tasks = [process_user_with_semaphore(user_obj) for user_obj in unique_users_to_process.values()]
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
print("当前批次所有用户绑定任务完成", flush=True)
|
||||
|
||||
async def bind_crm_all_customer(self):
|
||||
"""
|
||||
绑定CRM客户和微信用户
|
||||
"""
|
||||
|
||||
await self.bind_crm_all_customer_by_db()
|
||||
|
||||
for shop in binder_service.iter_shop():
|
||||
shopId = shop.get("shopId", "")
|
||||
shopName = shop.get("shopName", "")
|
||||
print(f"开始处理店铺:{shopName}", flush=True)
|
||||
|
||||
not_in_db_list = []
|
||||
|
||||
shop_info = {
|
||||
'shopId': shopId,
|
||||
'shopName': shopName,
|
||||
}
|
||||
|
||||
is_done = False
|
||||
user_list_iter = binder_service.iter_list_trade_user(binder_service.xy_client, shopId=shopId)
|
||||
for user_list in split_generator(user_list_iter):
|
||||
if is_done: break
|
||||
# 提取所有待检查的 cid
|
||||
all_cids = [str(user.get("cid")) for user in user_list]
|
||||
# print(user_list[0])
|
||||
# break
|
||||
|
||||
# 只查询 user_list 中存在的 cid
|
||||
in_objs = await self.model.filter(cid__in=all_cids)
|
||||
|
||||
# 提取已存在的 cid
|
||||
in_db_cid_set = {user.cid for user in in_objs}
|
||||
|
||||
print(f'已处理用户数:{len(in_db_cid_set)}', flush=True)
|
||||
|
||||
# 筛选待处理用户
|
||||
not_in_db_list = [user for user in user_list if user.get("cid") not in in_db_cid_set]
|
||||
for user in not_in_db_list:
|
||||
id = user.pop("id", None)
|
||||
# print('user', user)
|
||||
|
||||
# if not not_in_db_list:
|
||||
# is_done = True
|
||||
# break
|
||||
|
||||
print(f'待处理用户数:{len(not_in_db_list)}', flush=True)
|
||||
|
||||
if not_in_db_list:
|
||||
model_list = [self.model(**CrmCustomerCreate(**user).model_dump(exclude_unset=True)) for user in not_in_db_list]
|
||||
await self.model.bulk_create(model_list)
|
||||
|
||||
if not not_in_db_list:
|
||||
print(f'店铺:{shopName},无待处理用户', flush=True)
|
||||
continue
|
||||
|
||||
await self.bind_crm_all_customer_by_db()
|
||||
|
||||
async def bind_user(self, bind_info: CrmBindInfoCreate):
|
||||
"""
|
||||
绑定用户
|
||||
"""
|
||||
bind_info_dict = bind_info.model_dump(exclude_unset=True)
|
||||
platform = bind_info_dict.get("platform", None)
|
||||
platform_id = bind_info_dict.get("platform_id", None)
|
||||
bind_info_dict.pop("id", None)
|
||||
|
||||
existing_bind = await CrmBindInfo.filter(platform=platform, platform_id=platform_id).first()
|
||||
if existing_bind:
|
||||
await CrmBindInfo.filter(id=existing_bind.id).update(**bind_info_dict)
|
||||
else:
|
||||
obj = CrmBindInfo(**bind_info_dict)
|
||||
await obj.save()
|
||||
return {"message": "用户绑定成功"}
|
||||
|
||||
crm_customer_controller = CrmCustomerController()
|
||||
@@ -0,0 +1,41 @@
|
||||
from typing import List
|
||||
|
||||
from app.core.crud import CRUDBase
|
||||
from app.models.admin import Api, Menu, Datasource
|
||||
from app.schemas.codegen import DatasourceCreate, DatasourceUpdate, DatasourceInfo
|
||||
from app.utils.db import DatabaseInfo
|
||||
|
||||
class DatasourceController(CRUDBase[Datasource, DatasourceCreate, DatasourceUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=Datasource)
|
||||
|
||||
async def load_tables(self, name: str) -> list[DatasourceInfo]:
|
||||
# datasource_obj = await self.model.filter(name=name).first()
|
||||
# if not datasource_obj:
|
||||
# raise HTTPException(status_code=400, detail="数据源不存在")
|
||||
|
||||
with DatabaseInfo(
|
||||
host="lt.330770.xyz",
|
||||
port=3307,
|
||||
user="root",
|
||||
password="rap_sky",
|
||||
database="rpa"
|
||||
) as db:
|
||||
tables = db.get_all_tables()
|
||||
|
||||
return tables
|
||||
|
||||
|
||||
async def update_datasources(self, datasource: Datasource, menu_ids: List[int], api_infos: List[dict]) -> None:
|
||||
await datasource.menus.clear()
|
||||
for menu_id in menu_ids:
|
||||
menu_obj = await Menu.filter(id=menu_id).first()
|
||||
await datasource.menus.add(menu_obj)
|
||||
|
||||
await datasource.apis.clear()
|
||||
for item in api_infos:
|
||||
api_obj = await Api.filter(path=item.get("path"), method=item.get("method")).first()
|
||||
await datasource.apis.add(api_obj)
|
||||
|
||||
|
||||
datasource_controller = DatasourceController()
|
||||
@@ -0,0 +1,86 @@
|
||||
from tortoise.expressions import Q
|
||||
from tortoise.transactions import atomic
|
||||
|
||||
from app.core.crud import CRUDBase
|
||||
from app.models.admin import Dept, DeptClosure
|
||||
from app.schemas.depts import DeptCreate, DeptUpdate
|
||||
|
||||
|
||||
class DeptController(CRUDBase[Dept, DeptCreate, DeptUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=Dept)
|
||||
|
||||
async def get_dept_tree(self, name):
|
||||
q = Q()
|
||||
# 获取所有未被软删除的部门
|
||||
q &= Q(is_deleted=False)
|
||||
if name:
|
||||
q &= Q(name__contains=name)
|
||||
all_depts = await self.model.filter(q).order_by("order")
|
||||
|
||||
# 辅助函数,用于递归构建部门树
|
||||
def build_tree(parent_id):
|
||||
return [
|
||||
{
|
||||
"id": dept.id,
|
||||
"name": dept.name,
|
||||
"desc": dept.desc,
|
||||
"order": dept.order,
|
||||
"parent_id": dept.parent_id,
|
||||
"children": build_tree(dept.id), # 递归构建子部门
|
||||
}
|
||||
for dept in all_depts
|
||||
if dept.parent_id == parent_id
|
||||
]
|
||||
|
||||
# 从顶级部门(parent_id=0)开始构建部门树
|
||||
dept_tree = build_tree(0)
|
||||
return dept_tree
|
||||
|
||||
async def get_dept_info(self):
|
||||
pass
|
||||
|
||||
async def update_dept_closure(self, obj: Dept):
|
||||
parent_depts = await DeptClosure.filter(descendant=obj.parent_id)
|
||||
for i in parent_depts:
|
||||
print(i.ancestor, i.descendant)
|
||||
dept_closure_objs: list[DeptClosure] = []
|
||||
# 插入父级关系
|
||||
for item in parent_depts:
|
||||
dept_closure_objs.append(DeptClosure(ancestor=item.ancestor, descendant=obj.id, level=item.level + 1))
|
||||
# 插入自身x
|
||||
dept_closure_objs.append(DeptClosure(ancestor=obj.id, descendant=obj.id, level=0))
|
||||
# 创建关系
|
||||
await DeptClosure.bulk_create(dept_closure_objs)
|
||||
|
||||
@atomic()
|
||||
async def create_dept(self, obj_in: DeptCreate):
|
||||
# 创建
|
||||
if obj_in.parent_id != 0:
|
||||
await self.get(id=obj_in.parent_id)
|
||||
new_obj = await self.create(obj_in=obj_in)
|
||||
await self.update_dept_closure(new_obj)
|
||||
|
||||
@atomic()
|
||||
async def update_dept(self, obj_in: DeptUpdate):
|
||||
dept_obj = await self.get(id=obj_in.id)
|
||||
# 更新部门关系
|
||||
if dept_obj.parent_id != obj_in.parent_id:
|
||||
await DeptClosure.filter(ancestor=dept_obj.id).delete()
|
||||
await DeptClosure.filter(descendant=dept_obj.id).delete()
|
||||
await self.update_dept_closure(dept_obj)
|
||||
# 更新部门信息
|
||||
dept_obj.update_from_dict(obj_in.model_dump(exclude_unset=True))
|
||||
await dept_obj.save()
|
||||
|
||||
@atomic()
|
||||
async def delete_dept(self, dept_id: int):
|
||||
# 删除部门
|
||||
obj = await self.get(id=dept_id)
|
||||
obj.is_deleted = True
|
||||
await obj.save()
|
||||
# 删除关系
|
||||
await DeptClosure.filter(descendant=dept_id).delete()
|
||||
|
||||
|
||||
dept_controller = DeptController()
|
||||
@@ -0,0 +1,43 @@
|
||||
from .finance_parse import parse_finance_data
|
||||
from tortoise.expressions import Q
|
||||
from tortoise.transactions import atomic
|
||||
|
||||
from app.models.automation import Task, TaskStatus
|
||||
from app.schemas.task import TaskModel, DecodeTaskParams, DecodeTaskResult
|
||||
|
||||
class TaskController(object):
|
||||
async def get_task(self, name, type):
|
||||
q = Q()
|
||||
# 获取所有未被软删除的任务
|
||||
q &= Q(is_deleted=False)
|
||||
if name: q &= Q(name__contains=name)
|
||||
if type: q &= Q(type=type)
|
||||
all_tasks = await self.model.filter(q).order_by("id")
|
||||
return all_tasks
|
||||
|
||||
async def list(self, name, type, page: int = 1, page_size: int = 10, order: list[str] = ["id"]):
|
||||
q = Q()
|
||||
if name: q &= Q(name__contains=name)
|
||||
if type: q &= Q(type=type)
|
||||
query = Task.filter(q)
|
||||
return await query.count(), await query.offset((page - 1) * page_size).limit(page_size).order_by(*order)
|
||||
|
||||
@atomic()
|
||||
async def create_task(self, name: str, obj_in: DecodeTaskParams):
|
||||
task_obj = TaskModel.create_decode_task(name, obj_in)
|
||||
task_obj.result = task_obj.result or {}
|
||||
obj = Task(**task_obj.model_dump())
|
||||
await obj.save()
|
||||
return obj, task_obj
|
||||
|
||||
@atomic()
|
||||
async def update_task(self, obj_in: TaskModel, task_id: int, status: TaskStatus, result: DecodeTaskResult):
|
||||
task_obj = await Task.get(id=task_id)
|
||||
obj_in.status = status
|
||||
obj_in.set_result(result)
|
||||
|
||||
# 更新任务信息
|
||||
task_obj.update_from_dict(obj_in.model_dump(exclude_unset=True))
|
||||
await task_obj.save()
|
||||
|
||||
task_controller = TaskController()
|
||||
@@ -0,0 +1,616 @@
|
||||
# spacy_training/scripts/predict.py
|
||||
import re
|
||||
import cn2an
|
||||
import os.path
|
||||
from tqdm import tqdm
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# 单位与连接符定义
|
||||
MAX_ITEM_OFFSET = 10
|
||||
UNIT_PATTERN = "个|张|片|块|条|幅|根"
|
||||
SIZE_UNITS_PATTERN = r'mm|cm|dm|m'
|
||||
SIZE_JOIN_PATTERN = "xX×"
|
||||
NUMS_PATTERN = r'\d+(?:\.\d+)' # 修复:原缺少 ? 导致整数不匹配
|
||||
CNT_PATTERN = rf'{NUMS_PATTERN}?[^克+\s\d-]*'
|
||||
EXCEPT_TEXT = "解析失败"
|
||||
INDEX_COL = '序号'
|
||||
|
||||
base_colume = ['解析备注', '尺寸备注', '描述', '异常信息']
|
||||
mapping_size = {
|
||||
"width_mm": "长",
|
||||
"height_mm": "宽",
|
||||
# "original_size": "原始尺寸",
|
||||
# "total_quantity": "总数量",
|
||||
"style_count": "款数",
|
||||
"quantity_per_style": "数量",
|
||||
"unit": "单位",
|
||||
"描述": "描述",
|
||||
"解析备注": "解析备注",
|
||||
"size_unit": "尺寸单位",
|
||||
"exception_msg": '异常信息'
|
||||
}
|
||||
|
||||
import spacy
|
||||
import pandas as pd
|
||||
|
||||
# 将 doc 转换为字典
|
||||
def doc_to_dict(doc):
|
||||
return {
|
||||
"text": doc.text,
|
||||
"entities": [
|
||||
{
|
||||
"text": ent.text,
|
||||
"label": ent.label_,
|
||||
"start": ent.start_char,
|
||||
"end": ent.end_char
|
||||
}
|
||||
for ent in doc.ents
|
||||
],
|
||||
}
|
||||
|
||||
def predict_file(df, model_path="./spacy_training/model", key='备注'):
|
||||
print(model_path)
|
||||
nlp = spacy.load(model_path)
|
||||
if "sentencizer" not in nlp.pipe_names:
|
||||
nlp.add_pipe("sentencizer")
|
||||
|
||||
# 统一表头空格
|
||||
df.columns = df.columns.str.strip()
|
||||
key = key.strip()
|
||||
|
||||
for i, row in tqdm(df.iterrows(), total=len(df)):
|
||||
order = str(row[key]).strip()
|
||||
base_row = {key: val for key, val in row.items()} # 复制原行数据
|
||||
yield order, base_row, doc_to_dict(nlp(order))
|
||||
# doc = nlp(text)
|
||||
# print(f"\n🔤 文本: {text}")
|
||||
# import json; print(json.dumps(doc_to_dict(doc), indent=4, ensure_ascii=False))
|
||||
|
||||
# break
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class StructuredOrder:
|
||||
key: str
|
||||
width_mm: float
|
||||
height_mm: float
|
||||
original_size: str
|
||||
style_count: int
|
||||
quantity_per_style: float
|
||||
unit: str
|
||||
total_quantity: float
|
||||
original_qty: str
|
||||
size_position: list
|
||||
qty_position: list
|
||||
exception_msg: str
|
||||
calc_type: str
|
||||
size_unit: str
|
||||
meta: dict
|
||||
|
||||
def expand_to_structured(raw_pairs, size_unit=''):
|
||||
unit_map = {
|
||||
'百': 100, '千': 1000, '万': 10000,
|
||||
'百万': 1000000, '千万': 10000000, '亿': 100000000,
|
||||
}
|
||||
|
||||
UNIT_REGEX = re.compile(rf'({UNIT_PATTERN})$')
|
||||
exception_msg = []
|
||||
|
||||
def extract_multiplier(text):
|
||||
"""
|
||||
从字符串中提取数值乘数,支持:
|
||||
- 阿拉伯数字 + 中文单位:3.5万 → 35000
|
||||
- 纯中文数字 + 单位:三万五千 → 35000
|
||||
- 纯中文:两千万 → 20000000
|
||||
"""
|
||||
# 去除末尾单位(保留前面的部分)
|
||||
clean = UNIT_REGEX.sub('', text.strip())
|
||||
|
||||
# 如果去单位后为空,原字符串可能是纯单位(如“万”),默认系数为1
|
||||
if not clean:
|
||||
matched_unit = UNIT_REGEX.search(text)
|
||||
if matched_unit:
|
||||
unit = matched_unit.group(1)
|
||||
return unit_map.get(unit, 1.0)
|
||||
return 1.0
|
||||
|
||||
# 尝试直接转阿拉伯数字
|
||||
try:
|
||||
return float(clean)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 尝试转中文数字(如“三万五千”虽然不合理,但“三万”可以)
|
||||
try:
|
||||
# 注意:cn2an 可以直接处理“三万五千”这种
|
||||
num = cn2an.cn2an(clean, "smart") # smart 模式支持混合写法
|
||||
if isinstance(num, (int, float)):
|
||||
# 检查原字符串是否有单位后缀(比如“三万”中的“万”已被去除,需补回)
|
||||
matched_unit = UNIT_REGEX.search(text)
|
||||
if matched_unit:
|
||||
unit = matched_unit.group(1)
|
||||
num *= unit_map[unit]
|
||||
return float(num)
|
||||
except Exception:
|
||||
exception_msg.append(f"尺寸解析异常:{text}")
|
||||
pass
|
||||
|
||||
return 1.0
|
||||
|
||||
def parse_size(size_str):
|
||||
join_str = ''.join(re.findall(f"[{SIZE_JOIN_PATTERN}]+", size_str))
|
||||
if len(join_str) == 0 or re.search('比例|等高|等比|等宽', size_str):
|
||||
result = f'异常值:{size_str}'
|
||||
return result, result, size_unit
|
||||
size_str = size_str.replace(join_str, "x")
|
||||
match = re.match(rf'({NUMS_PATTERN}?)([a-z]+)?[{SIZE_JOIN_PATTERN}]({NUMS_PATTERN}?)([a-z]+)?', size_str, re.IGNORECASE)
|
||||
if not match:
|
||||
return None, None, size_unit
|
||||
w_val, w_unit, h_val, h_unit = match.groups()
|
||||
unit = (w_unit or h_unit or size_unit).lower()
|
||||
conv = {'m': 1000, 'dm': 100, 'cm': 10, 'mm': 1}
|
||||
if unit not in conv:
|
||||
print(f'未知单位: {unit} size_str:{size_str} size_unit:{size_unit}')
|
||||
exception_msg.append(f"尺寸解析异常:{size_str}, 未知单位: {unit}")
|
||||
return None, None, unit
|
||||
w_mm = conv[unit] * float(w_val)
|
||||
h_mm = conv[unit] * float(h_val)
|
||||
return round(w_mm, 3), round(h_mm, 3), unit
|
||||
|
||||
def parse_quantity(qty_str):
|
||||
qty_str = "".join(qty_str.split())
|
||||
multi_match = re.match((
|
||||
rf'[共计]?(.+?)款\w*?([共计]?[\d.一二两三四五六七八九十百千万亿]+)({UNIT_PATTERN})'
|
||||
), qty_str, re.IGNORECASE)
|
||||
if multi_match:
|
||||
style_part, qty_part = multi_match.group(1), multi_match.group(2)
|
||||
if qty_part[0] in '共计':
|
||||
qty_part = qty_part[1:]
|
||||
calc_type = 'total'
|
||||
else:
|
||||
calc_type = 'single'
|
||||
style_count = int(extract_multiplier(style_part))
|
||||
quantity_per_style = int(extract_multiplier(qty_part))
|
||||
unit = multi_match.group(3)
|
||||
|
||||
return {"style_count": style_count, "quantity_per_style": quantity_per_style, "unit": unit, "calc_type": calc_type}
|
||||
|
||||
single_match = re.match(rf'(.+?)({UNIT_PATTERN})', qty_str, re.IGNORECASE)
|
||||
if single_match:
|
||||
qty_part = single_match.group(1)
|
||||
if qty_part[0] in '共计':
|
||||
qty_part = qty_part[1:]
|
||||
calc_type = 'total'
|
||||
else:
|
||||
calc_type = 'single'
|
||||
quantity_per_style = int(extract_multiplier(qty_part))
|
||||
unit = single_match.group(2)
|
||||
return {"style_count": 1, "quantity_per_style": quantity_per_style, "unit": unit, "calc_type": calc_type}
|
||||
|
||||
return {"style_count": 0, "quantity_per_style": 0, "unit": "个"}
|
||||
|
||||
structured = []
|
||||
|
||||
for size_info, qty_info in raw_pairs:
|
||||
s_start, s_end, s_text, *_ = size_info
|
||||
q_start, q_end, q_text, *_ = qty_info
|
||||
|
||||
if s_text == EXCEPT_TEXT:
|
||||
width_mm, height_mm = EXCEPT_TEXT, EXCEPT_TEXT
|
||||
else:
|
||||
size_parsed = parse_size(s_text)
|
||||
if size_parsed:
|
||||
width_mm, height_mm, size_unit = size_parsed
|
||||
else:
|
||||
width_mm, height_mm, size_unit = None, None, None
|
||||
|
||||
if q_text == EXCEPT_TEXT:
|
||||
style_count, quantity_per_style, unit = 0, EXCEPT_TEXT, EXCEPT_TEXT
|
||||
else:
|
||||
qty_parsed = parse_quantity(q_text)
|
||||
style_count, quantity_per_style, unit = qty_parsed["style_count"], qty_parsed["quantity_per_style"], qty_parsed["unit"]
|
||||
|
||||
structured.append(StructuredOrder(
|
||||
key=f"{s_start}{s_end}{s_text}",
|
||||
width_mm=width_mm,
|
||||
height_mm=height_mm,
|
||||
original_size=s_text,
|
||||
style_count=style_count,
|
||||
quantity_per_style=quantity_per_style,
|
||||
unit=unit,
|
||||
total_quantity=style_count * quantity_per_style if style_count and style_count else None,
|
||||
original_qty=q_text,
|
||||
size_position=[s_start, s_end],
|
||||
qty_position=[q_start, q_end],
|
||||
exception_msg="; ".join(exception_msg),
|
||||
calc_type=qty_parsed.get('calc_type', None),
|
||||
size_unit=size_unit,
|
||||
meta={
|
||||
"size_text_except": s_text if s_text == EXCEPT_TEXT or (not width_mm and not height_mm) else None,
|
||||
"qty_text_except": q_text if q_text == EXCEPT_TEXT else None,
|
||||
}
|
||||
))
|
||||
|
||||
return structured
|
||||
|
||||
|
||||
def decode(doc_dict_list, is_horizontal=False):
|
||||
ent_group = {
|
||||
# "单号": ["单号"],
|
||||
"刮刮膜尺寸": ["刮刮膜尺寸"],
|
||||
# "产品": ["产品"],
|
||||
# "材质": ["材质"],
|
||||
# "工艺": ["工艺"],
|
||||
# "用户": ["用户信息"],
|
||||
# "加急": ["加急"],
|
||||
"数量": ["尺寸", "数量"],
|
||||
}
|
||||
columns = list(ent_group.keys())
|
||||
columns.remove('数量')
|
||||
columns.extend(base_colume)
|
||||
group_reflect = {v: k for k, vs in ent_group.items() for v in vs}
|
||||
result_items = []
|
||||
exception_list = []
|
||||
|
||||
for doc_dict in doc_dict_list:
|
||||
sentence = doc_dict["text"]
|
||||
entities = doc_dict["entities"]
|
||||
|
||||
is_except = False
|
||||
|
||||
# 强关联实体需要进行组合处理
|
||||
# ======================================== 实体分组 ========================================
|
||||
ent_group_dict = {k: [] for k in ent_group.keys()}
|
||||
base_ent_group_dict = {}
|
||||
|
||||
for ent in entities:
|
||||
start, end, label, text = ent["start"], ent["end"], ent["label"], ent["text"]
|
||||
group_name = group_reflect[label]
|
||||
ent_group_dict[group_name].append((start, end, text, label))
|
||||
base_ent_group_dict[group_name] = text
|
||||
|
||||
result = []
|
||||
|
||||
# 解析出尺寸和数量的组合
|
||||
# ======================================== 解析尺寸和数量 ========================================
|
||||
group_list = []
|
||||
size_and_qty_list = [[], []]
|
||||
|
||||
# 数量需要和尺寸进行组合处理
|
||||
base_ent_group_dict.pop("数量", None)
|
||||
for cnt_item in ent_group_dict["数量"]:
|
||||
size_info, qty_info = size_and_qty_list
|
||||
label = cnt_item[-1]
|
||||
if label == '数量':
|
||||
qty_info.append(cnt_item)
|
||||
elif label == '尺寸':
|
||||
start, end, text, _label = cnt_item
|
||||
if f'-({text[:2]}' in sentence:
|
||||
continue
|
||||
if qty_info:
|
||||
group_list.append(size_and_qty_list)
|
||||
size_and_qty_list = [[], []]
|
||||
size_info, qty_info = size_and_qty_list
|
||||
size_info.append(cnt_item)
|
||||
|
||||
# 存在数量和尺寸
|
||||
if size_and_qty_list[0] or size_and_qty_list[1]:
|
||||
group_list.append(size_and_qty_list)
|
||||
|
||||
# 如果只有一对尺寸,有可能反过来描述
|
||||
if len(group_list) == 2:
|
||||
_1, qty_info = group_list[0]
|
||||
size_info, _2 = group_list[1]
|
||||
|
||||
if not _1 and not _2:
|
||||
group_list[:] = [[qty_info, size_info]]
|
||||
|
||||
# print(f'订单中尺寸和数量组合:{group_list}')
|
||||
|
||||
# 解析出尺寸和数量的组合
|
||||
# ======================================== 解析长、宽、款数、数量 ========================================
|
||||
size_unit = ''
|
||||
for size_info, qty_info in group_list:
|
||||
units = set(''.join(re.findall(SIZE_UNITS_PATTERN, ent[2])) for ent in size_info)
|
||||
units = [unit for unit in units if unit]
|
||||
if len(units) == 1:
|
||||
size_unit = units[0]
|
||||
|
||||
size_and_qty_parsed_result = []
|
||||
for size_info, qty_info in group_list:
|
||||
size_len, qty_len = len(size_info), len(qty_info)
|
||||
if size_len == 0:
|
||||
print(f'解析异常,尺寸为空,数量为{qty_info},原文:{sentence}', entities)
|
||||
is_except = True
|
||||
# 起始位置、结束位置、尺寸文本、尺寸标签
|
||||
qty_info = [(0, 0, EXCEPT_TEXT, '数量')]
|
||||
if qty_len == 0:
|
||||
print(f'解析异常,数量为空,尺寸为{size_info},原文:{sentence}', entities)
|
||||
is_except = True
|
||||
# 起始位置、结束位置、尺寸文本、尺寸标签
|
||||
size_info = [(0, 0, EXCEPT_TEXT, '尺寸')]
|
||||
# continue
|
||||
|
||||
# print('size_info, qty_info', size_info, qty_info)
|
||||
if size_len == qty_len:
|
||||
decode_desc = "单尺寸、单款式描述的订单" if size_len == 1 else "多尺寸、多款式描述一一匹配的订单"
|
||||
|
||||
for size, qty in zip(size_info, qty_info):
|
||||
expand = expand_to_structured([(size, qty)], size_unit) or []
|
||||
exceptions = [item.meta for item in expand if item.meta.get('size_text_except') or item.meta.get('qty_text_except')]
|
||||
if exceptions: print("解析异常", exceptions, '原文', sentence, entities)
|
||||
for item in expand:
|
||||
parsed_item = item.__dict__.copy()
|
||||
parsed_item['描述'] = f"{size[2]}|{qty[2]}"
|
||||
parsed_item['解析备注'] = decode_desc
|
||||
parsed_item['size_len'] = size_len
|
||||
parsed_item['qty_len'] = qty_len
|
||||
size_and_qty_parsed_result.append(parsed_item)
|
||||
elif size_len>1 and qty_len>1:
|
||||
|
||||
if size_len > qty_len:
|
||||
total_qty = []
|
||||
for size, qty in zip(size_info, qty_info):
|
||||
expand = expand_to_structured([(size, qty)], size_unit) or []
|
||||
for item in expand:
|
||||
total_qty.append(item.style_count)
|
||||
|
||||
if size_len == sum(total_qty):
|
||||
decode_desc = "多尺寸、多款式描述:多款式描述和尺寸相等的订单"
|
||||
item = []
|
||||
for idx, qty in enumerate(total_qty):
|
||||
item.extend(qty_info[idx] for _ in range(qty))
|
||||
print('解析:', size_info, qty_info, item)
|
||||
# breakpoint()
|
||||
qty_info = item
|
||||
qty_len = len(qty_info)
|
||||
else:
|
||||
# qty_info = []
|
||||
decode_desc = f"匹配异常:多尺寸、多款式描述的订单: {size_info} {qty_info}"
|
||||
else:
|
||||
# qty_info = []
|
||||
decode_desc = f"匹配异常:多尺寸、多款式描述的订单: {size_info} {qty_info}"
|
||||
|
||||
for size, qty in zip(size_info, qty_info):
|
||||
expand = expand_to_structured([(size, qty)], size_unit) or []
|
||||
exceptions = [item.meta for item in expand if item.meta.get('size_text_except') or item.meta.get('qty_text_except')]
|
||||
if exceptions: print("解析异常", exceptions, '原文', sentence, entities)
|
||||
for item in expand:
|
||||
parsed_item = item.__dict__.copy()
|
||||
parsed_item['style_count'] = 1
|
||||
parsed_item['描述'] = f"{size[2]}|{qty[2]}"
|
||||
parsed_item['解析备注'] = decode_desc
|
||||
parsed_item['size_len'] = size_len
|
||||
parsed_item['qty_len'] = qty_len
|
||||
size_and_qty_parsed_result.append(parsed_item)
|
||||
else:
|
||||
# 多尺寸-单数量、单尺寸-多数量 告警多尺寸-多数量
|
||||
iter_item = ((size, qty) for size in size_info for qty in qty_info)
|
||||
if size_len > 1 and qty_len == 1:
|
||||
decode_desc = "多尺寸、单款式描述的订单"
|
||||
elif size_len == 1 and qty_len > 1:
|
||||
decode_desc = "单尺寸、多款式描述的订单"
|
||||
else:
|
||||
decode_desc = f"异常:多尺寸、多款式描述的订单: {size_info} {qty_info}"
|
||||
|
||||
for item in iter_item:
|
||||
size, qty = item
|
||||
expand = expand_to_structured([(size, qty)], size_unit) or []
|
||||
exceptions = [item.meta for item in expand if item.meta.get('size_text_except') or item.meta.get('qty_text_except')]
|
||||
if exceptions: print("解析异常", exceptions, '原文', sentence, entities)
|
||||
for item in expand:
|
||||
parsed_item = item.__dict__.copy()
|
||||
parsed_item['描述'] = f"{size[2]}|{qty[2]}"
|
||||
parsed_item['解析备注'] = decode_desc
|
||||
parsed_item['size_len'] = size_len
|
||||
parsed_item['qty_len'] = qty_len
|
||||
size_and_qty_parsed_result.append(parsed_item)
|
||||
|
||||
result.extend(size_and_qty_parsed_result)
|
||||
|
||||
for item in result:
|
||||
# print('item', item)
|
||||
new_item = base_ent_group_dict.copy()
|
||||
for key in mapping_size:
|
||||
col_name = mapping_size[key]
|
||||
new_item[col_name] = item[key]
|
||||
columns.append(col_name)
|
||||
size_len = item.pop('size_len', None)
|
||||
qty_len = item.pop('qty_len', None)
|
||||
calc_type = item.pop('calc_type', None)
|
||||
|
||||
if size_len > 1 and calc_type == 'total':
|
||||
new_item[f'数量'] = f"警告:异常值(总计值:{new_item[f'数量']})"
|
||||
|
||||
if size_len == 1 or size_len == qty_len:
|
||||
pass
|
||||
# new_item[f'解析备注'] = '单尺寸的订单'
|
||||
else:
|
||||
if size_len == new_item['款数']:
|
||||
new_item[f'款数'] = 1
|
||||
# new_item[f'解析备注'] = '尺寸数量和款数相同的订单'
|
||||
elif new_item['款数'] == 1:
|
||||
new_item[f'款数'] = 1
|
||||
# new_item[f'解析备注'] = '单款的订单'
|
||||
else:
|
||||
new_item[f'款数'] = "异常值:款数和尺寸数不一致"
|
||||
new_item[f'数量'] = "异常值:款数和尺寸数不一致"
|
||||
# new_item[f'解析备注'] = '多尺寸并且和款数不同的订单'
|
||||
|
||||
result_items.append(new_item)
|
||||
# print(json.dumps(new_item, indent=4, ensure_ascii=False))
|
||||
|
||||
if not result:
|
||||
if base_ent_group_dict:
|
||||
base_ent_group_dict['是否异常'] = is_except
|
||||
base_ent_group_dict[f'解析备注'] = '未解析到实际尺寸的订单1'
|
||||
result_items.append(base_ent_group_dict.copy())
|
||||
else:
|
||||
result_items.append({
|
||||
"是否异常": is_except,
|
||||
"解析备注": "未解析到任何实体"
|
||||
})
|
||||
|
||||
return exception_list, columns, result_items
|
||||
|
||||
|
||||
def parse_finance_data(file_path, target_index, is_horizontal, sheet_name="Sheet1"):
|
||||
|
||||
df = pd.read_excel(file_path, dtype=str, sheet_name=sheet_name)
|
||||
total_amount = len(df)
|
||||
|
||||
items = predict_file(df, './app/resources/models/订单尺寸识别/best_model', target_index)
|
||||
# items = predict_file(file_path, './resources/models/lintao/best_model', target_index)
|
||||
exception_list, results, outputs_columns = [], [], []
|
||||
origin_item = None
|
||||
|
||||
parsed_dict = {}
|
||||
mapping_size_values = mapping_size.values()
|
||||
|
||||
for text, origin_item, item in items:
|
||||
|
||||
decode_result = []
|
||||
|
||||
base_info = {}
|
||||
result_index = parsed_dict.get(f"{text}_index")
|
||||
if result_index:
|
||||
exceptions, columns = [], []
|
||||
try:
|
||||
decode_item = parsed_dict[text][result_index]
|
||||
for key in origin_item:
|
||||
if key in mapping_size_values:
|
||||
continue
|
||||
decode_item[key] = origin_item[key]
|
||||
decode_item['尺寸备注'] = '多组尺寸解析'
|
||||
parsed_dict[f"{text}_index"] += 1
|
||||
continue
|
||||
except IndexError:
|
||||
print(f'解析到的尺寸不足:{text},{result_index}, {parsed_dict[text]}')
|
||||
base_item = parsed_dict[text][0]
|
||||
for col in base_colume:
|
||||
base_info[col] = base_item.get(col, '')
|
||||
base_info["长"] = '异常值:没有解析到那么多尺寸'
|
||||
|
||||
decode_items = []
|
||||
parsed_dict[f"{text}_index"] += 1
|
||||
else:
|
||||
exceptions, columns, decode_items = decode([item], is_horizontal) or []
|
||||
|
||||
# if '(250731202428388557)' in text:
|
||||
# print(result_index, text, exceptions, columns, decode_items)
|
||||
# breakpoint()
|
||||
|
||||
if len(columns) > len(outputs_columns):
|
||||
outputs_columns = columns
|
||||
|
||||
if len(decode_items) > 1:
|
||||
for item in decode_items:
|
||||
item['尺寸备注'] = '多组尺寸解析'
|
||||
|
||||
for idx, decode_item in enumerate(decode_items):
|
||||
base_row = origin_item.copy()
|
||||
if idx and INDEX_COL in base_row: base_row[INDEX_COL] = ''
|
||||
# base_row = origin_item.copy() if idx == 0 else {}
|
||||
base_row.update(decode_item)
|
||||
is_except = base_row.pop('是否异常', False)
|
||||
if is_except:
|
||||
for col in base_colume:
|
||||
origin_item[col] = base_row.get(col, '')
|
||||
decode_result.append(origin_item)
|
||||
exception_list.append(base_row)
|
||||
else:
|
||||
decode_result.append(base_row)
|
||||
if not decode_items:
|
||||
if base_info:
|
||||
origin_item.update(base_info)
|
||||
|
||||
decode_result.append(origin_item)
|
||||
else:
|
||||
if is_horizontal:
|
||||
new_decode_result = decode_result[0].copy()
|
||||
for idx, item in enumerate(decode_result[1:], 1):
|
||||
for key in mapping_size_values:
|
||||
new_key = f"{key}{idx}"
|
||||
new_decode_result[new_key] = item[key]
|
||||
if new_key not in outputs_columns:
|
||||
outputs_columns.append(new_key)
|
||||
decode_result = [new_decode_result]
|
||||
|
||||
if text not in parsed_dict:
|
||||
parsed_dict[text] = decode_result
|
||||
parsed_dict[f"{text}_index"] = 0
|
||||
parsed_dict[f"{text}_index"] += 1
|
||||
|
||||
# if decode_result:
|
||||
# results.append(decode_result[0])
|
||||
if len(decode_items) > 1:
|
||||
for item in decode_result[1:]:
|
||||
item['尺寸备注'] = '复制新增:多组尺寸解析'
|
||||
results.extend(decode_result)
|
||||
|
||||
# break
|
||||
|
||||
print('横向解析', outputs_columns)
|
||||
if origin_item:
|
||||
# outputs_columns.
|
||||
_outputs_columns = list(origin_item.keys()) + outputs_columns
|
||||
col_set = set()
|
||||
outputs_columns = []
|
||||
|
||||
for col in _outputs_columns:
|
||||
if col not in col_set:
|
||||
col_set.add(col)
|
||||
outputs_columns.append(col)
|
||||
|
||||
df = pd.DataFrame(results, columns=outputs_columns)
|
||||
|
||||
file_name = os.path.basename(file_path).replace(".xlsx", '')
|
||||
filename = file_name + '_解析' + ('_横向排列' if is_horizontal else '_纵向排列')
|
||||
|
||||
upload_dir = Path("outputs")
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
save_file_path = upload_dir / f'{filename}_1.xlsx'
|
||||
|
||||
# 使用 ExcelWriter 同时写入多个 sheet
|
||||
with pd.ExcelWriter(save_file_path, engine='openpyxl') as writer:
|
||||
df.to_excel(writer, sheet_name='正常解析', index=False)
|
||||
if exception_list:
|
||||
except_df = pd.DataFrame(exception_list, columns=outputs_columns)
|
||||
except_df.to_excel(writer, sheet_name='异常解析', index=False)
|
||||
|
||||
print(f'解析结果保存在:{save_file_path}')
|
||||
return save_file_path, total_amount
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
# print(expand_to_structured([[(0,1,'600x50cm','r'), (0,1,'3款各1张','e')]]))
|
||||
# exit()
|
||||
# predict("Apple is opening a new office in Tokyo.")
|
||||
# predict("Google hired Sarah Connor from Berlin last year.")
|
||||
file_info = ['d:/会计组/新领图8月.xlsx', '备注']
|
||||
# file_info = ['d:/会计组/即客2025年8月尺寸整理.xlsx', '系统文件名']
|
||||
file_info = ['d:/会计组/8月订单明细9.6.xlsx', '文件名']
|
||||
file_info = ['d:/会计组/ZHX-8月订单明细.xlsx', '文件名']
|
||||
file_info = ['d:/会计组/国税数据源/智韬2025年8月尺寸整理(1).xlsx', '系统文件名']
|
||||
file_info = ['d:/会计组/彩印通8月数码9.23.xlsx', 'ERP系统文件名']
|
||||
file_info = ['d:/会计组/CYT8月明细9.24.xlsx', 'ERP系统文件名']
|
||||
file_info = ['d:/会计组/9.1-9.26.xlsx', '备注']
|
||||
file_info = ['d:/会计组/JD.xlsx', '文件名']
|
||||
file_info = ['d:/会计组/泰州即客2025年9月尺寸整理.xlsx', '系统文件名']
|
||||
file_info = ['d:/会计组/ZHX需拆明细9月.xlsx', '文件名', False]
|
||||
file_info = ['d:/会计组/七彩2024年9月账单尺寸整理.xlsx', '系统文件名', True]
|
||||
file_info = ['d:/会计组/艾印图文2024年9月账单尺寸整理.xlsx', '系统文件名', True]
|
||||
file_info = ['d:/会计组/9月转印.xlsx', 'erp', True]
|
||||
file_info = ['d:/会计组/9月CYT.xlsx', '文件名', False]
|
||||
file_info = ['d:/会计组/智韬2025年9月尺寸整理.xlsx', '系统文件名', True]
|
||||
file_info = ['d:/会计组/9月UV转印贴.xlsx', '文件名', True]
|
||||
file_info = ['d:/会计组/彩印通2025年9月(数码).xlsx', '文件名', True]
|
||||
file_info = ['d:/会计组/9月名片.xlsx', '文件名', True]
|
||||
file_info = ['d:/会计组/9月不干胶.xlsx', '文件名', True]
|
||||
file_path, target_index, is_horizontal = file_info
|
||||
parse_finance_data(file_path, target_index, is_horizontal)
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
pandas==2.3.3
|
||||
spacy==3.8.7
|
||||
tqdm==4.67.1
|
||||
cn2an==0.5.23
|
||||
openpyxl==3.1.5
|
||||
aiomysql
|
||||
@@ -0,0 +1,16 @@
|
||||
from typing import Optional
|
||||
|
||||
from app.core.crud import CRUDBase
|
||||
from app.models.admin import Menu
|
||||
from app.schemas.menus import MenuCreate, MenuUpdate
|
||||
|
||||
|
||||
class MenuController(CRUDBase[Menu, MenuCreate, MenuUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=Menu)
|
||||
|
||||
async def get_by_menu_path(self, path: str) -> Optional["Menu"]:
|
||||
return await self.model.filter(path=path).first()
|
||||
|
||||
|
||||
menu_controller = MenuController()
|
||||
@@ -0,0 +1,461 @@
|
||||
from tortoise.exceptions import IntegrityError
|
||||
from app.core.crud import CRUDBase
|
||||
from app.schemas.msg import (
|
||||
MsgCreate,
|
||||
MsgUpdate,
|
||||
MsgNewOrder,
|
||||
FollowFormErp,
|
||||
MsgType
|
||||
)
|
||||
import httpx
|
||||
from app.models.msg import Msg, Follow
|
||||
from app.models.weixin import WeixinCustomer, WeixinUser
|
||||
from some_sdk.wk_weixin_sdk.apis.extern_user import get_external_user_chat_info
|
||||
from some_sdk.services.binder import lintao_client, feishu_client
|
||||
from some_sdk.lintao_sdk.biz.by_order import get_order_relative_user
|
||||
from some_sdk.feishu_sdk.apis.doc import batch_create as create_feishu_records
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from app.utils.common import async_split_generator, split_generator, gen_random_str
|
||||
from typing import List
|
||||
import re
|
||||
|
||||
import hashlib
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
import asyncio
|
||||
_sync_to_feishu_lock = asyncio.Lock()
|
||||
|
||||
class MsgController(CRUDBase[Msg, MsgCreate, MsgUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=Msg)
|
||||
|
||||
async def send_to_wexin(self, data:dict ):
|
||||
# pass
|
||||
url = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=57ba9cd5-2c62-43dd-bfea-78c7b4073128'
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(url,
|
||||
headers={"Content-Type": "application/json"},
|
||||
json=data,
|
||||
)
|
||||
data = resp.json()
|
||||
print(data)
|
||||
return data
|
||||
|
||||
async def send_order_to_weixin(self, msg: Msg):
|
||||
"""
|
||||
极简订单通知:仅展示店铺、旺旺ID、金额、归属客服
|
||||
"""
|
||||
url = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=57ba9cd5-2c62-43dd-bfea-78c7b4073128'
|
||||
|
||||
order_list = msg.detail or []
|
||||
if not order_list:
|
||||
logger.warning("订单详情为空,跳过发送")
|
||||
return
|
||||
|
||||
order = order_list[0]
|
||||
# users = order.get("users", [])
|
||||
# buyer = next((u for u in users if u.get("role") == "buyer"), None)
|
||||
|
||||
# 关键字段
|
||||
shop_name = order.get("shop_name", "未知店铺")
|
||||
buyer_name = msg.title
|
||||
# buyer_name = buyer.get("name") if buyer else ""
|
||||
# buyer_name = buyer_name or msg.content.split(")", 1)[0].split("(")[1]
|
||||
|
||||
is_refund = order.get("is_refund", 0) > 0
|
||||
msg_title = "💰 用户退款" if is_refund else "🛒 新订单"
|
||||
|
||||
real_payment = order.get("real_payment", "0.00")
|
||||
qiwei_name = msg.owner_name or "未知客服"
|
||||
|
||||
# 极简 Markdown
|
||||
markdown_content = f"""# {msg_title}\n
|
||||
|
||||
**店铺**:{shop_name}
|
||||
**旺旺ID**:`{buyer_name}`
|
||||
**金额**:¥{real_payment}
|
||||
**企微客服**:{qiwei_name}
|
||||
**订单编号**:{order.get("trade_no", "未知订单号")}
|
||||
"""
|
||||
|
||||
data = {
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"content": markdown_content
|
||||
}
|
||||
}
|
||||
result = await self.send_to_wexin(data)
|
||||
logger.info(f"企业微信极简订单通知发送结果: {result}")
|
||||
|
||||
|
||||
async def parse_order_msg(self, msg: Msg, send_set: set, msg_dict: dict):
|
||||
title = msg.title
|
||||
try:
|
||||
if title not in send_set:
|
||||
send_set.add(title)
|
||||
# try: await self.send_to_wexin(msg)
|
||||
owner_name = '、'.join([ (name or '').split('(')[0].split('vip客服-')[-1].split('-印刷定制')[0] for name in msg_dict[title] ])msg.owner_name = owner_name
|
||||
|
||||
try: await self.send_order_to_weixin(msg)
|
||||
except Exception as e:
|
||||
logger.error(f'发送订单消息到微信失败,{e}')
|
||||
return
|
||||
|
||||
# await self.send_to_wexin(msg, mentioned_list=msg_dict[title])
|
||||
logger.info(f'发送消息到微信成功,{msg.content}')
|
||||
|
||||
msg.is_send = True
|
||||
msg.send_at = datetime.now()
|
||||
await msg.save()
|
||||
except Exception as e:
|
||||
logger.error(f'发送消息到微信失败,{e}')
|
||||
|
||||
async def new_system_msg(self, title: str, content: str, owner_name: str = None):
|
||||
await self.create(
|
||||
dict(
|
||||
hash_id=hashlib.md5(f'{title}{content}{owner_name}'.encode()).hexdigest(),
|
||||
title=title,
|
||||
content=content,
|
||||
type=MsgType.SYSTEM,
|
||||
owner_name=owner_name,
|
||||
)
|
||||
)
|
||||
|
||||
async def send_system_msg(self, msg: Msg):
|
||||
msg.send_at = datetime.now()
|
||||
markdown_content = f"""# 📣 系统通知 \n
|
||||
## {msg.title} \n
|
||||
{msg.content} \n
|
||||
|
||||
{msg.send_at.strftime("%Y-%m-%d %H:%M:%S")}
|
||||
"""
|
||||
|
||||
data = {
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"content": markdown_content
|
||||
}
|
||||
}
|
||||
result = await self.send_to_wexin(data)
|
||||
logger.info(f"企业微信极简订单通知发送结果: {result}")
|
||||
|
||||
msg.is_send = True
|
||||
await msg.save()
|
||||
|
||||
async def sync_msg(self):
|
||||
msg_list = await self.model.filter(is_send=False)
|
||||
|
||||
msg_dict = {}
|
||||
for msg in msg_list:
|
||||
title = msg.title
|
||||
msg_dict[title] = msg_dict.get(title, []) + [msg.owner_name]
|
||||
|
||||
send_set = set()
|
||||
for msg in msg_list:
|
||||
title = msg.title
|
||||
if msg.type == MsgType.NEW_ORDER:
|
||||
await self.parse_order_msg(msg, send_set, msg_dict)
|
||||
elif msg.type == MsgType.SYSTEM:
|
||||
await self.send_system_msg(msg)
|
||||
|
||||
async def fill_customer_info(self):
|
||||
msg_orm_list = await self.model.filter(id__gt=306)
|
||||
|
||||
update_list = []
|
||||
for msg in msg_orm_list:
|
||||
detail = msg.detail or []
|
||||
taobao_name = msg.title
|
||||
|
||||
detail_list = []
|
||||
for order_item in detail:
|
||||
customer_orm = await WeixinCustomer.filter(taobao_name=taobao_name).first()
|
||||
if not customer_orm:
|
||||
logger.warning(f'未找到淘宝用户{taobao_name}的企微客户')
|
||||
continue
|
||||
|
||||
for user in order_item.get("users", []):
|
||||
if user.get("role") != "buyer": continue
|
||||
user.update(customer_orm.to_dict())
|
||||
break
|
||||
|
||||
detail_list.append(order_item)
|
||||
|
||||
if detail_list:
|
||||
msg.detail = detail_list
|
||||
update_list.append(msg)
|
||||
|
||||
if update_list:
|
||||
await self.model.bulk_update(update_list, fields=['detail'])
|
||||
|
||||
|
||||
async def new_order(self, order_id: str, customer_orm: WeixinCustomer, is_refund: bool = False):
|
||||
|
||||
msg_set = set()
|
||||
msg_list = []
|
||||
now = datetime.now()
|
||||
|
||||
order_list = get_order_relative_user(lintao_client, order_id)
|
||||
order_unique_list = {}
|
||||
async for order in order_list:
|
||||
order_state_string = order.get('order_state_string')
|
||||
trade_no = order.get('trade_no')
|
||||
if not is_refund and order_state_string not in ['待领单', '待抢单']:
|
||||
logger.warning(f'订单{trade_no} 状态为{order_state_string},跳过')
|
||||
continue
|
||||
|
||||
remark = order.get('remark')
|
||||
if not is_refund and remark and '企微联系' in remark:
|
||||
logger.warning(f'订单{trade_no} 状态为{order_state_string}, 备注:{remark}, 已备注企微联系,跳过')
|
||||
continue
|
||||
|
||||
create_time = order.get('create_time')
|
||||
if create_time and not is_refund:
|
||||
create_time = datetime.fromisoformat(create_time.replace("Z", "+00:00"))
|
||||
if (now - create_time).total_seconds() > 3600:
|
||||
logger.warning(f'订单{order_id} 创建时间({create_time})与当前时间({now})相差超过60秒,跳过')
|
||||
continue
|
||||
|
||||
if trade_no not in order_unique_list:
|
||||
order_unique_list[trade_no] = order
|
||||
logger.info(f'订单{order_id} 订单号{trade_no} 创建时间{create_time} 订单状态:{order_state_string} {order.get("title")}')
|
||||
|
||||
# 填充买家信息
|
||||
for user in order.get("users", []):
|
||||
if user.get("role") != "buyer": continue
|
||||
user.update(customer_orm.to_dict())
|
||||
break
|
||||
|
||||
|
||||
if not order_unique_list:
|
||||
logger.warning(f'订单{order_id} 没有符合条件的订单,跳过')
|
||||
return
|
||||
|
||||
chat_info = await get_external_user_chat_info(customer_orm.weixin_id)
|
||||
follow_user_list = chat_info.get('follow_user', [])
|
||||
|
||||
order_list = list(order_unique_list.values())
|
||||
order_info = [
|
||||
f"在({order.get('shop_name')})下单{order.get('price')}元,单号({order.get('trade_no')})"
|
||||
for order in order_list
|
||||
]
|
||||
for follow_user in follow_user_list:
|
||||
follow_userid = follow_user.get('userid')
|
||||
weixin_user_orm: WeixinUser = await WeixinUser.filter(userid=follow_userid).first()
|
||||
owner_name = '未知'
|
||||
if weixin_user_orm: owner_name = weixin_user_orm.english_name or weixin_user_orm.username
|
||||
|
||||
qiwei_customer_name = customer_orm.taobao_name or customer_orm.weixin_name
|
||||
|
||||
content = f"{'退款' if is_refund else ''}订单({qiwei_customer_name}) -({owner_name})- 刚{', '.join(order_info)}"
|
||||
logger.info(f'新订单通知:{content}')
|
||||
msg_hash_id = hashlib.md5(content.encode('utf-8')).hexdigest()
|
||||
if msg_hash_id in msg_set: continue
|
||||
msg_set.add(msg_hash_id)
|
||||
|
||||
msg = self.model(
|
||||
hash_id=msg_hash_id,
|
||||
title=qiwei_customer_name,
|
||||
content=content,
|
||||
detail=order_list,
|
||||
type=MsgType.NEW_ORDER,
|
||||
owner_id=follow_userid,
|
||||
owner_name=owner_name
|
||||
)
|
||||
msg_list.append(msg)
|
||||
|
||||
if msg_list:
|
||||
try: await self.model.bulk_create(msg_list)
|
||||
except IntegrityError as e:
|
||||
if "Duplicate entry" in str(e) and "hash_id" in str(e):
|
||||
logger.info("检测到重复消息,已跳过")
|
||||
# 忽略或处理重复
|
||||
else:
|
||||
# 其他完整性错误(如外键失败),应重新抛出
|
||||
raise
|
||||
|
||||
async def get_order_user_days_before(self, days: int):
|
||||
now = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
start_date, otherMemo = (now - timedelta(days=days)).strftime("%Y-%m-%d %H:%M:%S"), "企微"
|
||||
|
||||
# 琪琪——松屿,小小丽——麦子,小小北——大喜,小泉——苏苏,小洋——东东,蓝莓——小满
|
||||
index = 0
|
||||
update_list, add_list = [], []
|
||||
order_iter_list = [
|
||||
(get_order_relative_user(lintao_client, start_date=start_date, otherMemo=otherMemo), ''),
|
||||
(get_order_relative_user(lintao_client, start_date=start_date, customer='琪琪'), '松屿'),
|
||||
(get_order_relative_user(lintao_client, start_date=start_date, customer='小小丽'), '麦子'),
|
||||
(get_order_relative_user(lintao_client, start_date=start_date, customer='小小北'), '大喜'),
|
||||
(get_order_relative_user(lintao_client, start_date=start_date, customer='小泉'), '苏苏'),
|
||||
(get_order_relative_user(lintao_client, start_date=start_date, customer='小洋'), '东东'),
|
||||
(get_order_relative_user(lintao_client, start_date=start_date, customer='蓝莓'), '小满'),
|
||||
]
|
||||
for order_iter, staff_name in order_iter_list:
|
||||
async for order_list in async_split_generator(order_iter, 10):
|
||||
order_dict = {}
|
||||
for order in order_list:
|
||||
designer_list = list(filter(lambda x: x.get("role") == "desiger", order.get("users", [])))
|
||||
designer = designer_list[0] if designer_list else None
|
||||
if not designer: continue
|
||||
|
||||
name = designer.get("name", "")
|
||||
if not name: continue
|
||||
|
||||
key = tuple([order.get("trade_no", ""), name])
|
||||
if key in order_dict: continue
|
||||
order_dict[key] = order
|
||||
index += 1
|
||||
|
||||
_update_list, _add_list = await self.process_order_dict(order_dict, staff_name=staff_name)
|
||||
|
||||
if _update_list and _add_list:
|
||||
logger.info(f'第{index}个订单 处理 {len(_update_list)} 条更新记录, {len(_add_list)} 条新增记录')
|
||||
update_list.extend(_update_list)
|
||||
add_list.extend(_add_list)
|
||||
|
||||
print(start_date)
|
||||
_update_list, _add_list = [], []
|
||||
|
||||
async with _sync_to_feishu_lock:
|
||||
task_id = gen_random_str()
|
||||
_update_list, _add_list = await self.sync_to_feishu(task_id)
|
||||
|
||||
return {
|
||||
"update_count": len(update_list),
|
||||
"add_count": len(add_list),
|
||||
"add_count_to_feishu": len(_add_list),
|
||||
"update_count_to_feishu": len(_update_list),
|
||||
"start_date": start_date,
|
||||
"remark": otherMemo
|
||||
}
|
||||
|
||||
async def process_order_dict(self, order_dict: dict, staff_name: str = ''):
|
||||
async def to_follow_orm(order_info: dict):
|
||||
follow_orm = Follow(**FollowFormErp(**order_info).model_dump())
|
||||
# print(follow_orm.order_id, order_info.get("users", []))
|
||||
|
||||
for user in order_info.get("users", []):
|
||||
if user.get("role") == "desiger":
|
||||
follow_orm.designer_id = user.get("id", "")
|
||||
follow_orm.designer_name = user.get("name", "")
|
||||
elif user.get("role") == "taobao_kefu":
|
||||
follow_orm.kefu_id = user.get("id", "")
|
||||
follow_orm.kefu_name = user.get("name", "")
|
||||
elif user.get("role") == "buyer":
|
||||
follow_orm.customer_taobao_id = user.get("id", "")
|
||||
follow_orm.customer_name = user.get("name", "")
|
||||
|
||||
CustomerUser = await WeixinCustomer.filter(taobao_id=follow_orm.customer_taobao_id).first()
|
||||
if CustomerUser:
|
||||
follow_orm.customer_id = CustomerUser.id
|
||||
|
||||
# 企微员工
|
||||
if staff_name:
|
||||
follow_orm.staff_name = staff_name
|
||||
staff_orm = await WeixinUser.filter(english_name=f"{follow_orm.staff_name}-印刷定制").first()
|
||||
if staff_orm:
|
||||
follow_orm.staff_id = staff_orm.userid
|
||||
else:
|
||||
remark = follow_orm.remark
|
||||
if remark:
|
||||
staff_name_match = re.search(r"企微联系(.*?)拉群", remark)
|
||||
if staff_name_match:
|
||||
follow_orm.staff_name = staff_name_match.group(1).strip()
|
||||
if follow_orm.staff_name:
|
||||
staff_orm = await WeixinUser.filter(english_name=f"{follow_orm.staff_name}-印刷定制").first()
|
||||
if staff_orm:
|
||||
follow_orm.staff_id = staff_orm.userid
|
||||
|
||||
if not staff_name_match:
|
||||
follow_orm.staff_name = remark
|
||||
|
||||
return follow_orm
|
||||
|
||||
update_list = []
|
||||
follow_list = await Follow.filter(order_id__in=[key[0] for key in order_dict.keys()])
|
||||
keys, update_keys = Follow.get_all_keys(exclude_fields=['id', 'updated_at', 'created_at', 'feishu_record_id', 'is_update_to_feishu', 'is_add_to_feishu']), set()
|
||||
for follow in follow_list:
|
||||
key = tuple([follow.order_id, follow.designer_name])
|
||||
if key not in order_dict: continue
|
||||
|
||||
order_info = order_dict.pop(key, None)
|
||||
follow_orm = await to_follow_orm(order_info)
|
||||
# 比较是否一样,不一样的话,更新
|
||||
updated_dict = {}
|
||||
for key in keys:
|
||||
origin_value = str(getattr(follow, key))
|
||||
value = str(getattr(follow_orm, key))
|
||||
if origin_value != value:
|
||||
update_keys.add(key)
|
||||
setattr(follow, key, value)
|
||||
updated_dict[key] = f'{origin_value} -> {value}'
|
||||
|
||||
if updated_dict:
|
||||
follow_orm.is_update_to_feishu = False
|
||||
update_keys.add('is_update_to_feishu')
|
||||
update_list.append(follow)
|
||||
logger.info(f'订单 {follow.order_id} 设计员 {follow.designer_name} 更新字段 {updated_dict}')
|
||||
|
||||
if update_list:
|
||||
await Follow.bulk_update(update_list, fields=update_keys)
|
||||
|
||||
add_list = []
|
||||
for key, order_info in order_dict.items():
|
||||
follow_orm = await to_follow_orm(order_info)
|
||||
add_list.append(follow_orm)
|
||||
|
||||
if add_list:
|
||||
await Follow.bulk_create(add_list)
|
||||
|
||||
return update_list, add_list
|
||||
|
||||
async def sync_to_feishu(self, task_id: str):
|
||||
|
||||
add_list = []
|
||||
# 需要追加的记录
|
||||
follow_list = await Follow.filter(is_update_to_feishu=False, feishu_record_id__isnull=True)
|
||||
if follow_list:
|
||||
for item_list in split_generator(follow_list, 100):
|
||||
result = await create_feishu_records(
|
||||
feishu_client,
|
||||
app_token="HPiNbbW3YaYhBjsjLQychK9gnbf",
|
||||
table_id="tblQGVfmglJgZuBf",
|
||||
records=[{"fields": item.to_feishu_record(fields={"同步任务号": task_id})["fields"]} for item in item_list],
|
||||
)
|
||||
|
||||
_update_list = []
|
||||
result_list = result.get("data", {}).get("records", [])
|
||||
for record, item in zip(result_list, item_list):
|
||||
item.feishu_record_id = record["record_id"]
|
||||
item.is_add_to_feishu = True
|
||||
item.is_update_to_feishu = True
|
||||
_update_list.append(item)
|
||||
|
||||
if _update_list:
|
||||
await Follow.bulk_update(_update_list, fields=['feishu_record_id', 'is_add_to_feishu', 'is_update_to_feishu'])
|
||||
|
||||
logger.info(f'追加 {len(_update_list)} 条记录到飞书')
|
||||
add_list.extend(_update_list)
|
||||
|
||||
update_list = []
|
||||
# 需要更新的记录
|
||||
# follow_list = await Follow.filter(is_update_to_feishu=False, feishu_record_id__isnull=False)
|
||||
# if follow_list:
|
||||
# for item_list in split_generator(follow_list, 100):
|
||||
# await create_feishu_records(
|
||||
# get_feishu_client,
|
||||
# app_token="JTqnbdjb7aEMm3srrLfcidYcnxc",
|
||||
# table_id="tblXq8AmG22uK5n1",
|
||||
# records=item_list,
|
||||
# )
|
||||
|
||||
return update_list, add_list
|
||||
|
||||
async def set_read(self, id: str):
|
||||
await Msg.filter(id=id).update(is_read=True, read_at=datetime.now())
|
||||
|
||||
msg_controller = MsgController()
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
from typing import List
|
||||
|
||||
from app.core.crud import CRUDBase
|
||||
from app.models.admin import Api, Menu, Role
|
||||
from app.schemas.roles import RoleCreate, RoleUpdate
|
||||
|
||||
|
||||
class RoleController(CRUDBase[Role, RoleCreate, RoleUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=Role)
|
||||
|
||||
async def is_exist(self, name: str) -> bool:
|
||||
return await self.model.filter(name=name).exists()
|
||||
|
||||
async def update_roles(self, role: Role, menu_ids: List[int], api_infos: List[dict]) -> None:
|
||||
await role.menus.clear()
|
||||
for menu_id in menu_ids:
|
||||
menu_obj = await Menu.filter(id=menu_id).first()
|
||||
await role.menus.add(menu_obj)
|
||||
|
||||
await role.apis.clear()
|
||||
for item in api_infos:
|
||||
api_obj = await Api.filter(path=item.get("path"), method=item.get("method")).first()
|
||||
await role.apis.add(api_obj)
|
||||
|
||||
|
||||
role_controller = RoleController()
|
||||
@@ -0,0 +1,62 @@
|
||||
from typing import List
|
||||
|
||||
import json
|
||||
from app.core.crud import CRUDBase
|
||||
from app.models.admin import Api, Menu, Codegen
|
||||
from app.schemas.codegen import CodegenCreate, CodegenUpdate
|
||||
from app.utils.db import DatabaseInfo
|
||||
from app.utils.codegen import CodeGenerator
|
||||
|
||||
class CodegenController(CRUDBase[Codegen, CodegenCreate, CodegenUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=Codegen)
|
||||
|
||||
async def is_exist(self, name: str) -> bool:
|
||||
return await self.model.filter(name=name).exists()
|
||||
|
||||
async def import_table(self, connect: str='', importTables: List[str] = None) -> Codegen:
|
||||
|
||||
default_frontend_config = {
|
||||
"editable": True,
|
||||
"listable": True,
|
||||
"sortable": True,
|
||||
"filterable": False,
|
||||
"filter_operator": '=', # = | >= 默认过滤操作符为包含
|
||||
"display_type": 'text', # 默认显示类型为文本
|
||||
}
|
||||
|
||||
cnt = 0
|
||||
|
||||
with DatabaseInfo(
|
||||
host="lt.330770.xyz",
|
||||
port=3307,
|
||||
user="root",
|
||||
password="rap_sky",
|
||||
database="rpa"
|
||||
) as db:
|
||||
|
||||
for table_name in importTables:
|
||||
table = db.get_table_structure(table_name)
|
||||
# result.append(fields)
|
||||
fields = table.get('fields')
|
||||
for field in fields:
|
||||
# if field.name in ['id']:
|
||||
if 'editable' not in field:
|
||||
field['editable'] = field['required']
|
||||
new_data = {**default_frontend_config, **field}
|
||||
field.update(**new_data)
|
||||
description = table.get('tableComment')
|
||||
await self.create(CodegenCreate(name=table_name, description=description , fields=json.dumps(fields,ensure_ascii=False)))
|
||||
cnt += 1
|
||||
|
||||
return cnt
|
||||
|
||||
async def preview(self, table_id):
|
||||
generator = CodeGenerator()
|
||||
config_orm = await self.get(id=table_id)
|
||||
entity_config = await config_orm.to_dict()
|
||||
generated_files = generator.generate_files(entity_config, 'relation-demo', {})
|
||||
return generated_files
|
||||
|
||||
|
||||
codegen_controller = CodegenController()
|
||||
@@ -0,0 +1,60 @@
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi.exceptions import HTTPException
|
||||
|
||||
from app.core.crud import CRUDBase
|
||||
from app.models.admin import User
|
||||
from app.schemas.login import CredentialsSchema
|
||||
from app.schemas.users import UserCreate, UserUpdate
|
||||
from app.utils.password import get_password_hash, verify_password
|
||||
|
||||
from .role import role_controller
|
||||
|
||||
|
||||
class UserController(CRUDBase[User, UserCreate, UserUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=User)
|
||||
|
||||
async def get_by_email(self, email: str) -> Optional[User]:
|
||||
return await self.model.filter(email=email).first()
|
||||
|
||||
async def get_by_username(self, username: str) -> Optional[User]:
|
||||
return await self.model.filter(username=username).first()
|
||||
|
||||
async def create_user(self, obj_in: UserCreate) -> User:
|
||||
obj_in.password = get_password_hash(password=obj_in.password)
|
||||
obj = await self.create(obj_in)
|
||||
return obj
|
||||
|
||||
async def update_last_login(self, id: int) -> None:
|
||||
user = await self.model.get(id=id)
|
||||
user.last_login = datetime.now()
|
||||
await user.save()
|
||||
|
||||
async def authenticate(self, credentials: CredentialsSchema) -> Optional["User"]:
|
||||
user = await self.model.filter(username=credentials.username).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=400, detail="无效的用户名")
|
||||
verified = verify_password(credentials.password, user.password)
|
||||
if not verified:
|
||||
raise HTTPException(status_code=400, detail="密码错误!")
|
||||
if not user.is_active:
|
||||
raise HTTPException(status_code=400, detail="用户已被禁用")
|
||||
return user
|
||||
|
||||
async def update_roles(self, user: User, role_ids: List[int]) -> None:
|
||||
await user.roles.clear()
|
||||
for role_id in role_ids:
|
||||
role_obj = await role_controller.get(id=role_id)
|
||||
await user.roles.add(role_obj)
|
||||
|
||||
async def reset_password(self, user_id: int):
|
||||
user_obj = await self.get(id=user_id)
|
||||
if user_obj.is_superuser:
|
||||
raise HTTPException(status_code=403, detail="不允许重置超级管理员密码")
|
||||
user_obj.password = get_password_hash(password="123456")
|
||||
await user_obj.save()
|
||||
|
||||
|
||||
user_controller = UserController()
|
||||
@@ -0,0 +1,607 @@
|
||||
|
||||
from app.core.crud import CRUDBase
|
||||
from app.schemas.weixin import (
|
||||
WeixinCustomerCreate,
|
||||
WeixinCustomerUpdate,
|
||||
WeixinCustomerXingyunCreate,
|
||||
)
|
||||
from typing import List
|
||||
|
||||
from app.models.weixin import WeixinCustomer, WeixinUser, CustomerGroup
|
||||
from app.schemas.weixin import WeixinUserBindInfo, WeixinOrderBindInfo
|
||||
|
||||
from some_sdk.services.binder import lintao_client, xy_client, wk_client
|
||||
from some_sdk.lintao_sdk.biz.by_order import get_order_relative_user, save_other_memo
|
||||
from some_sdk.xingyun_sdk.apis.customer import bind_user_to_xingyun, list_user_order
|
||||
from some_sdk.xingyun_sdk.apis.work_external_contact import list_all_contact_by_addtime, list_all_contact_by_addtime_and_tag
|
||||
from some_sdk.wk_weixin_sdk.apis.corp_user import from_service_external_userid
|
||||
from some_sdk.wk_weixin_sdk.apis.extern_user import get_external_user_chat_info
|
||||
|
||||
from app.controllers.msg import msg_controller
|
||||
from app.schemas.msg import MsgType
|
||||
|
||||
from app.utils.common import async_split_generator
|
||||
import pickle
|
||||
import hashlib
|
||||
|
||||
import re, os
|
||||
from datetime import datetime, timedelta
|
||||
from app.core.cache import cache_if
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EXCLUDE_SHOP_NAMES = ['领淘文具旗舰店']
|
||||
XINGYUN_USE_TIME = datetime(2025, 6, 18)
|
||||
|
||||
class WeixinCustomerController(CRUDBase[WeixinCustomer, WeixinCustomerCreate, WeixinCustomerUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=WeixinCustomer)
|
||||
|
||||
async def update_from_group(self, external_members: list[dict]):
|
||||
# 同步更新数据库
|
||||
member_dict = {}
|
||||
for member in external_members:
|
||||
member = member.copy()
|
||||
userid = member.pop('userid', None)
|
||||
|
||||
weixin_name = member.pop('name', None)
|
||||
weixin_unionid = member.pop('unionid', None)
|
||||
|
||||
member_info = WeixinCustomer.orm_format(member)
|
||||
member_info['weixin_id'] = userid
|
||||
member_info['weixin_name'] = weixin_name
|
||||
member_info['weixin_unionid'] = weixin_unionid
|
||||
member_dict[userid] = member_info
|
||||
|
||||
member_list = await self.model.filter(weixin_id__in=member_dict.keys())
|
||||
|
||||
update_list = []
|
||||
update_fileds = ['weixin_unionid', 'weixin_id', 'weixin_name', 'order_id', 'taobao_id', 'taobao_name', 'extra', 'need_confirm']
|
||||
|
||||
history = {}
|
||||
for member in member_list:
|
||||
member_info = member_dict.pop(member.weixin_id, history.get(member.weixin_id, {}))
|
||||
if not member_info: continue
|
||||
history[member.weixin_id] = member_info
|
||||
|
||||
member_info['extra'] = {**(member.extra or {}), **member_info['extra']}
|
||||
|
||||
updated = {}
|
||||
for field in update_fileds:
|
||||
old_value = getattr(member, field)
|
||||
if field not in member_info:
|
||||
continue
|
||||
value = member_info.get(field)
|
||||
if old_value != value:
|
||||
setattr(member, field, value)
|
||||
updated[f'{field}:{str(old_value)}'] = value
|
||||
if updated:
|
||||
logger.info(f'用户 {member.weixin_name} 更新关系为 {updated}')
|
||||
update_list.append(member)
|
||||
if update_list:
|
||||
await self.model.bulk_update(update_list, fields=update_fileds)
|
||||
|
||||
# 新增用户
|
||||
create_list = []
|
||||
for userid, member_info in member_dict.items():
|
||||
create_list.append(WeixinCustomer(
|
||||
**member_info,
|
||||
))
|
||||
if create_list:
|
||||
await self.model.bulk_create(create_list)
|
||||
|
||||
return {
|
||||
'update_count': len(update_list),
|
||||
'create_count': len(create_list),
|
||||
}
|
||||
|
||||
async def bind_user(self, user_in: WeixinUserBindInfo):
|
||||
if not user_in.taobao_id:
|
||||
raise ValueError("淘宝ID不能为空")
|
||||
await self.model.filter(taobao_id=user_in.taobao_id).update_from_dict(user_in.model_dump(exclude_unset=True))
|
||||
|
||||
async def remark_order(self, order: dict, remark: str):
|
||||
ctid = order.get("ctid") or order.get("trade_no")
|
||||
# if remark in (order.get("remark") or ''):
|
||||
if '企微' in (order.get("remark") or ''):
|
||||
logger.info(f'订单 {ctid} 备注中已包含 企微 备注,无需重复添加')
|
||||
return
|
||||
remark_list = [order.get("remark", ""), remark]
|
||||
append_remark = ';'.join(filter(lambda x: x, remark_list))
|
||||
|
||||
if os.getenv("APP_ENV") == "prod":
|
||||
result = await save_other_memo(lintao_client, order_id=ctid, other_memo=append_remark)
|
||||
assert result["type"] == "success", f"保存备注失败,响应: {result}"
|
||||
logger.info(f'成功为订单 {ctid} 打上备注 {append_remark}')
|
||||
else:
|
||||
logger.info(f'非生产环境,不实际为订单 {ctid} 打上备注 {append_remark}')
|
||||
|
||||
async def remark_order_by_order_id(self, remark: str, order_id: str=None, orders: list=None):
|
||||
if not orders:
|
||||
assert order_id, "订单ID不能为空"
|
||||
orders = [o async for o in get_order_relative_user(lintao_client, trade_no=order_id)]
|
||||
assert orders, f"订单 {order_id} 不存在"
|
||||
|
||||
# 打上备注
|
||||
logger.info(f'即将为以下订单 {[o.get("trade_no") for o in orders]} 打上备注 {remark}')
|
||||
need_monitor_order_list = []
|
||||
for order in orders:
|
||||
if not order.get('title'):
|
||||
# 未领单的订单,需要添加到监控列表中进行监控,防止被刷掉
|
||||
need_monitor_order_list.append(order)
|
||||
logger.info(f'订单 {order.get("trade_no")} 未领单,需要添加到监控列表中进行监控,防止被刷掉')
|
||||
|
||||
try:
|
||||
await self.remark_order(order, remark=remark)
|
||||
except Exception as e:
|
||||
logger.error(f'为订单 {order.get("trade_no")} 打上备注 {remark} 失败,异常: {e}')
|
||||
need_monitor_order_list.append(order)
|
||||
|
||||
return need_monitor_order_list
|
||||
|
||||
|
||||
async def bind_order(self, bind_in: WeixinOrderBindInfo):
|
||||
orders = [o async for o in get_order_relative_user(lintao_client, trade_no=bind_in.order_id)]
|
||||
assert orders, f"订单 {bind_in.order_id} 不存在"
|
||||
|
||||
orders = [o for o in orders if o.get('trade_no') == bind_in.order_id]
|
||||
order = orders[0]
|
||||
|
||||
buyer_ids = []
|
||||
for user in order.get('users', []):
|
||||
taobao_id = user.get('id', '')
|
||||
if not taobao_id or re.match(r'^\d+$', taobao_id): continue
|
||||
buyer_ids.append(taobao_id)
|
||||
|
||||
# 更新还是新建?
|
||||
orms = await self.model.filter(weixin_id=bind_in.userid).all()
|
||||
logger.info(f'用户 {bind_in.userid} 准备绑定订单 {len(orms)} {[orm.weixin_name for orm in orms]}')
|
||||
|
||||
update_list = []
|
||||
for orm in orms:
|
||||
if orm.order_id and orm.order_id != order.get('trade_no'):
|
||||
logger.warning(f'用户 {orm.weixin_name} 已绑定订单 {orm.order_id},即将覆盖为 {order.get("trade_no")}')
|
||||
|
||||
orm.weixin_id = bind_in.userid
|
||||
orm.order_id = order.get('trade_no')
|
||||
orm.shop_name = order.get('shop_name', '')
|
||||
orm.taobao_id = user.get('id', '')
|
||||
orm.taobao_name = user.get('name', '')
|
||||
update_list.append(orm)
|
||||
|
||||
if update_list:
|
||||
logger.info(f'更新用户 {bind_in.userid} 绑定订单 {order.get("trade_no")} 中的用户 {user}')
|
||||
await self.model.bulk_update(update_list, fields=['order_id', 'weixin_id', 'taobao_id', 'taobao_name'])
|
||||
return True, orders
|
||||
|
||||
if not orms:
|
||||
logger.info(f'绑定用户 {bind_in.userid} 绑定订单 {order.get("trade_no")} 中的用户 {user}')
|
||||
orm_user = WeixinCustomer(
|
||||
weixin_id=bind_in.userid,
|
||||
order_id=order.get('trade_no'),
|
||||
taobao_id=user.get('id', ''),
|
||||
taobao_name=user.get('name', ''),
|
||||
)
|
||||
await orm_user.save()
|
||||
return True, orders
|
||||
|
||||
return False, orders
|
||||
|
||||
async def bind_xingyun_user(self, contact_orm: WeixinCustomer):
|
||||
assert contact_orm.xingyun_id, "星云用户ID不能为空"
|
||||
if contact_orm.shop_name in EXCLUDE_SHOP_NAMES:
|
||||
contact_orm.xingyun_sync = True
|
||||
contact_orm.extra = contact_orm.extra or {}
|
||||
contact_orm.extra['system_remark'] = f'店铺名称: {contact_orm.shop_name},没有开通星云有客服务,故此跳过。'
|
||||
return
|
||||
|
||||
order_id = contact_orm.order_id.split('_')[-1]
|
||||
logger.info(f'同步客户订单信息到星云: {contact_orm.xingyun_id} -》 {order_id}')
|
||||
try:
|
||||
await bind_user_to_xingyun(xy_client, contact_orm.xingyun_id, order_id)
|
||||
except:
|
||||
logger.error(f'同步客户订单信息到星云失败: {contact_orm.xingyun_id} -》 {order_id}, 尝试寻找同用户的其他订单来绑定')
|
||||
shop_name = ''
|
||||
async for order in get_order_relative_user(lintao_client, buyer_nick=contact_orm.taobao_name):
|
||||
shop_name = order.get('shop_name', '')
|
||||
if shop_name in EXCLUDE_SHOP_NAMES:
|
||||
continue
|
||||
|
||||
if not order.get('create_time'): continue
|
||||
create_time = order.get('create_time')
|
||||
create_time = datetime.fromisoformat(create_time.replace("Z", "+00:00"))
|
||||
if create_time < XINGYUN_USE_TIME:
|
||||
continue
|
||||
|
||||
order_id = order.get('trade_no').split('_')[-1]
|
||||
await bind_user_to_xingyun(xy_client, contact_orm.xingyun_id, order_id)
|
||||
logger.info(f'通过同用户的其他订单来同步客户订单信息到星云成功: {contact_orm.xingyun_id} -》 {order_id}')
|
||||
break
|
||||
|
||||
if shop_name and not contact_orm.shop_name:
|
||||
contact_orm.shop_name = order.get('shop_name', '')
|
||||
await contact_orm.save()
|
||||
|
||||
contact_orm.xingyun_sync = True
|
||||
|
||||
async def load_all_user_from_xingyun(self, task_id: str, add_time_start: datetime, add_time_end: datetime):
|
||||
logger.info(f'从星云加载客户数据,时间范围 {task_id} {add_time_start} - {add_time_end}')
|
||||
contact_iter = list_all_contact_by_addtime_and_tag(xy_client, task_id=task_id, keyword='', add_time_start=add_time_start, add_time_end=add_time_end)
|
||||
|
||||
return await self._load_user_from_xingyun(contact_iter, eager_load=True)
|
||||
|
||||
async def load_user_from_xingyun(self, add_time_start: datetime, add_time_end: datetime):
|
||||
logger.info(f'从星云加载客户数据,时间范围 {add_time_start} - {add_time_end}')
|
||||
contact_iter = list_all_contact_by_addtime(xy_client, keyword='', add_time_start=add_time_start, add_time_end=add_time_end)
|
||||
|
||||
return await self._load_user_from_xingyun(contact_iter)
|
||||
|
||||
async def _load_user_from_xingyun(self, contact_iter: list, eager_load: bool = False):
|
||||
logger.info(f'从星云加载客户数据,开始同步')
|
||||
load_result = {"update_count": 0, "add_count": 0}
|
||||
async for contact_list in async_split_generator(contact_iter, 10):
|
||||
try: result = await self.save_xingyun_contact_info(contact_list)
|
||||
except Exception as e:
|
||||
logger.error(f'从星云加载客户数据,同步失败 {e}')
|
||||
logger.exception(e)
|
||||
raise e
|
||||
|
||||
# 全部都已经同步完了
|
||||
if not eager_load and result.get("update_count", 0) + result.get("add_count", 0) == 0:
|
||||
logger.info(f'从星云加载客户数据,全部同步完成')
|
||||
break
|
||||
|
||||
load_result['update_count'] += result.get("update_count", 0)
|
||||
load_result['add_count'] += result.get("add_count", 0)
|
||||
logger.info(f'从星云加载客户数据{eager_load},更新 {result.get("update_count", 0)} 条,新增 {result.get("add_count", 0)} 条')
|
||||
logger.info(f'从星云加载客户数据,总 {load_result["update_count"] + load_result["add_count"]} 条,更新 {load_result["update_count"]} 条,新增 {load_result["add_count"]} 条')
|
||||
await self.sync_xingyun_contact_info()
|
||||
|
||||
return load_result
|
||||
|
||||
async def save_xingyun_contact_info(self, contact_list: list):
|
||||
contact_dict = {contact.get('cid'): contact for contact in contact_list}
|
||||
contact_orm_list: List[WeixinCustomer] = await self.model.filter(xingyun_id__in=contact_dict.keys())
|
||||
|
||||
field_names = set()
|
||||
update_list = []
|
||||
|
||||
async def update(contact_dict, contact_orm_list, key):
|
||||
history = {}
|
||||
for contact_orm in contact_orm_list:
|
||||
value = getattr(contact_orm, key)
|
||||
contact = contact_dict.pop(value, history.get(value, {}))
|
||||
if not contact: continue
|
||||
history[value] = contact
|
||||
|
||||
try:
|
||||
contact_in = WeixinCustomerXingyunCreate(**contact).transform_to_extra()
|
||||
except Exception as e:
|
||||
logger.error(f'客户数据转换错误,chat_id: {contact}, {e}')
|
||||
continue
|
||||
# print(type(contact_in), contact_in)
|
||||
updated = False
|
||||
|
||||
contact_in_dict = contact_in.model_dump(exclude_unset=True)
|
||||
field_names.update(contact_in_dict.keys())
|
||||
|
||||
for field_name in field_names:
|
||||
|
||||
old_value = getattr(contact_orm, field_name)
|
||||
value = contact_in_dict.get(field_name)
|
||||
if old_value != value:
|
||||
setattr(contact_orm, field_name, value)
|
||||
updated = True
|
||||
|
||||
new_extra = {**(contact_orm.extra or {}), **contact_in.extra}
|
||||
if contact_in.extra != new_extra:
|
||||
if new_extra != contact_orm.extra:
|
||||
contact_orm.extra = new_extra
|
||||
updated = True
|
||||
|
||||
# 触发同步信息到星云
|
||||
if updated and not contact_orm.xingyun_sync and contact_orm.xingyun_id:
|
||||
try:
|
||||
await self.bind_xingyun_user(contact_orm)
|
||||
update_list.append(contact_orm)
|
||||
except Exception as e:
|
||||
logger.error(f'客户数据同步错误,xingyun_id: {contact_orm.xingyun_id}, {e}')
|
||||
continue
|
||||
|
||||
if update_list:
|
||||
await self.model.bulk_update(update_list, list(field_names) + ['extra', 'xingyun_sync'])
|
||||
|
||||
await update(contact_dict, contact_orm_list, 'xingyun_id')
|
||||
|
||||
add_dict = {}
|
||||
for contact in contact_dict.values():
|
||||
externalUserid = contact.get('externalUserid', None)
|
||||
if not externalUserid:
|
||||
continue
|
||||
try:
|
||||
resp = await from_service_external_userid(wk_client, externalUserid)
|
||||
except Exception as e:
|
||||
logger.error(f'从星云加载客户数据,获取用户ID失败,externalUserid: {externalUserid}, {e}')
|
||||
continue
|
||||
if not resp or not resp.get('external_userid'):
|
||||
continue
|
||||
|
||||
external_userid = resp.get('external_userid')
|
||||
contact['external_userid'] = external_userid
|
||||
add_dict[external_userid] = contact
|
||||
|
||||
contact_orm_list = await self.model.filter(weixin_id__in=add_dict.keys())
|
||||
await update(add_dict, contact_orm_list, 'weixin_id')
|
||||
|
||||
add_list = []
|
||||
for contact in add_dict.values():
|
||||
contact_in = WeixinCustomerXingyunCreate(**contact).transform_to_extra()
|
||||
add_list.append(self.model(**contact_in.model_dump(exclude_unset=True)))
|
||||
|
||||
if add_list:
|
||||
await self.model.bulk_create(add_list)
|
||||
|
||||
return {
|
||||
'update_count': len(update_list),
|
||||
'add_count': len(add_list),
|
||||
}
|
||||
|
||||
async def bind_user(self, bind_info: WeixinUserBindInfo):
|
||||
return await self.bind_user(bind_info)
|
||||
|
||||
async def monitor_erp_order(self):
|
||||
# 从领淘ERP加载订单相关用户
|
||||
# 获取上一个订单的创建时间
|
||||
cache_file = 'env/order_cache.pkl'
|
||||
newest_order_create_time = None
|
||||
today_end = datetime.now().replace(hour=23, minute=59, second=59, microsecond=0)
|
||||
try:
|
||||
with open(cache_file, 'rb') as f:
|
||||
last_order = pickle.load(f)
|
||||
newest_order_create_time = last_order.get('create_time')
|
||||
logger.info(f'从缓存文件加载最新订单创建时间,{newest_order_create_time}')
|
||||
except FileNotFoundError:
|
||||
newest_order_create_time = today_end.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
newest_order_create_time = newest_order_create_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
order_list = [o async for o in get_order_relative_user(lintao_client, start_date=newest_order_create_time, end_date=today_end.strftime('%Y-%m-%d %H:%M:%S'))]
|
||||
msg_list, newest_order = await self.find_bind_order(order_list)
|
||||
|
||||
# 保存最新订单到缓存文件
|
||||
if newest_order:
|
||||
newest_order['create_time'] = newest_order['create_time'].replace('T', ' ')
|
||||
with open(cache_file, 'wb') as f:
|
||||
pickle.dump(newest_order, f)
|
||||
|
||||
return msg_list
|
||||
|
||||
async def find_bind_order(self, order_list: list):
|
||||
|
||||
buyer_order_dict = {}
|
||||
newest_order = None
|
||||
last_erp_id = 0
|
||||
|
||||
order_id_set = set()
|
||||
for order in order_list:
|
||||
erp_id = order.get('id')
|
||||
if erp_id <= 6368700: break
|
||||
|
||||
create_time = order.get('create_time')
|
||||
if last_erp_id < erp_id:
|
||||
if create_time: newest_order = order
|
||||
last_erp_id = erp_id
|
||||
|
||||
order_id = order.get('trade_no')
|
||||
if order_id in order_id_set: continue
|
||||
order_id_set.add(order_id)
|
||||
|
||||
users = order.get("users", [])
|
||||
buyer_id_set = set()
|
||||
for user in users:
|
||||
role = user.get('role')
|
||||
if role != 'buyer': continue
|
||||
buyer_id = user.get('id')
|
||||
if not buyer_id: continue
|
||||
if buyer_id in buyer_id_set: continue
|
||||
buyer_id_set.add(buyer_id)
|
||||
|
||||
buyer_order_dict.setdefault(buyer_id, []).append(order)
|
||||
|
||||
customer_list = await self.model.filter(taobao_id__in=buyer_order_dict.keys()).all()
|
||||
|
||||
# 通知新订单
|
||||
# (客户id)-(所属企微客服)刚在(店铺名)下单-(金额)
|
||||
msg_set = set()
|
||||
msg_list = []
|
||||
for customer_orm in customer_list:
|
||||
order_list = buyer_order_dict.get(customer_orm.taobao_id, [])
|
||||
|
||||
order_info = [
|
||||
f"在({order.get('shop_name')})下单{order.get('price')}元,单号({order.get('trade_no')})"
|
||||
for order in order_list
|
||||
]
|
||||
chat_info = await get_external_user_chat_info(customer_orm.weixin_id)
|
||||
follow_user_list = chat_info.get('follow_user', [])
|
||||
|
||||
for follow_user in follow_user_list:
|
||||
follow_userid = follow_user.get('userid')
|
||||
weixin_user_orm: WeixinUser = await WeixinUser.filter(userid=follow_userid).first()
|
||||
owner_name = '未知'
|
||||
if weixin_user_orm: owner_name = weixin_user_orm.english_name or weixin_user_orm.username
|
||||
qiwei_customer_name = customer_orm.taobao_name or customer_orm.weixin_name
|
||||
|
||||
content = f"({qiwei_customer_name}) -({owner_name})- 刚{', '.join(order_info)}"
|
||||
msg_hash_id = hashlib.md5(content.encode('utf-8')).hexdigest()
|
||||
if msg_hash_id in msg_set: continue
|
||||
msg_set.add(msg_hash_id)
|
||||
|
||||
# 检查消息是否已存在
|
||||
msg_orm = await msg_controller.model.filter(hash_id=msg_hash_id).first()
|
||||
if msg_orm: continue
|
||||
|
||||
msg = msg_controller.model(
|
||||
hash_id=msg_hash_id,
|
||||
title=qiwei_customer_name,
|
||||
content=content,
|
||||
detail=order_list,
|
||||
type=MsgType.NEW_ORDER,
|
||||
owner_id=follow_userid,
|
||||
owner_name=owner_name
|
||||
)
|
||||
logger.info(f'创建新订单通知消息,{msg.content}')
|
||||
msg_list.append(msg)
|
||||
|
||||
if msg_list:
|
||||
await msg_controller.model.bulk_create(msg_list)
|
||||
|
||||
return msg_list, newest_order
|
||||
|
||||
async def get_customer_by_weixin_id(self, weixin_id: str):
|
||||
async with cache_if(f'customer:user_datail_0:{weixin_id}', ttl=3600*24) as cache:
|
||||
if cache.hit:
|
||||
result = cache.value
|
||||
return result
|
||||
else:
|
||||
result = await self._get_customer_by_weixin_id(weixin_id)
|
||||
cache.set(result)
|
||||
return result
|
||||
|
||||
async def _get_customer_by_weixin_id(self, weixin_id: str):
|
||||
chat_info = await get_external_user_chat_info(weixin_id)
|
||||
follow_user_list = chat_info.get('follow_user', [])
|
||||
external_contact = chat_info.get('external_contact', {})
|
||||
|
||||
if external_contact:
|
||||
customer_orm = await self.model.filter(weixin_id=weixin_id).first()
|
||||
if customer_orm:
|
||||
customer_orm.weixin_avatar = external_contact.get('avatar')
|
||||
customer_orm.weixin_unionid = external_contact.get('unionid')
|
||||
customer_orm.weixin_name = external_contact.get('name')
|
||||
await customer_orm.save()
|
||||
|
||||
user_ids = [follow_user.get('userid') for follow_user in follow_user_list]
|
||||
weixin_user_orm_list = await WeixinUser.filter(userid__in=user_ids).all()
|
||||
weixin_user_orm_dict = {weixin_user_orm.userid: weixin_user_orm for weixin_user_orm in weixin_user_orm_list}
|
||||
for follow_user in follow_user_list:
|
||||
follow_userid = follow_user.get('userid')
|
||||
weixin_user_orm = weixin_user_orm_dict.get(follow_userid)
|
||||
|
||||
if weixin_user_orm:
|
||||
follow_user['username'] = weixin_user_orm.english_name or weixin_user_orm.username
|
||||
|
||||
return {
|
||||
**external_contact,
|
||||
'follow_user_list': follow_user_list,
|
||||
}
|
||||
|
||||
# for follow_user in follow_user_list:
|
||||
# follow_userid = follow_user.get('userid')
|
||||
|
||||
async def get_user_detail(self, weixin_id: str):
|
||||
async with cache_if(f'customer:user_datail:{weixin_id}', ttl=3600) as cache:
|
||||
if cache.hit:
|
||||
result = cache.value
|
||||
return result
|
||||
else:
|
||||
customer = await weixin_customer_controller.model.get_or_none(weixin_id=weixin_id)
|
||||
# if customer: return customer.to_dict()
|
||||
print(f'get_user_detail, weixin_id: {weixin_id}, customer: {customer}')
|
||||
|
||||
# 通过中间表 CustomerGroup 查询其加入的所有群
|
||||
memberships = await CustomerGroup.filter(
|
||||
customer=customer
|
||||
).prefetch_related('group') # 预加载 group 信息
|
||||
|
||||
group_list = []
|
||||
result = {'group_list': group_list}
|
||||
for m in memberships:
|
||||
data = await m.to_dict()
|
||||
# data['group'] = await m.group.to_dict()
|
||||
data['chat_name'] = m.group.name
|
||||
data['create_time'] = m.group.create_time
|
||||
group_list.append(data)
|
||||
|
||||
chat_info = await self.get_customer_by_weixin_id(weixin_id)
|
||||
result['user_detail'] = chat_info
|
||||
cache.set(result)
|
||||
|
||||
return result
|
||||
|
||||
# 同步星云和数据库中的客户信息: 同步绑定信息到星云、从星云中加载订单信息
|
||||
async def sync_xingyun_contact_info(self, eager_load: bool = False):
|
||||
# 从星云同步绑定信息到本地
|
||||
await self._sync_xingyun_contact_info(eager_load=eager_load, order_id__isnull=False)
|
||||
# 从本地同步订单信息到星云
|
||||
await self._sync_xingyun_contact_info(eager_load=eager_load, order_id__isnull=True, limit=10)
|
||||
|
||||
async def _sync_xingyun_contact_info(self, eager_load: bool = False, max_try: int = 2, limit: int = 20, **query_kwargs):
|
||||
query_kwargs = query_kwargs or {}
|
||||
|
||||
last_id = 0
|
||||
while eager_load or max_try > 0:
|
||||
max_try -= 1
|
||||
if last_id:
|
||||
contact_orm_list = await self.model.filter(xingyun_sync=False, xingyun_id__isnull=False, id__lt=last_id, **query_kwargs).order_by('-id').limit(limit).all()
|
||||
else:
|
||||
contact_orm_list = await self.model.filter(xingyun_sync=False, xingyun_id__isnull=False, **query_kwargs).order_by('-id').limit(limit).all()
|
||||
|
||||
if not contact_orm_list: break
|
||||
logger.debug(f'同步客户数据,{len(contact_orm_list)}')
|
||||
|
||||
has_new = False
|
||||
update_list = []
|
||||
for contact_orm in contact_orm_list:
|
||||
last_id = contact_orm.id
|
||||
has_new = True
|
||||
|
||||
if contact_orm.need_confirm:
|
||||
logger.info(f'客户需要确认绑定: {contact_orm}')
|
||||
continue
|
||||
|
||||
if contact_orm.order_id:
|
||||
try:
|
||||
await self.bind_xingyun_user(contact_orm)
|
||||
update_list.append(contact_orm)
|
||||
except Exception as e:
|
||||
logger.error(f'同步客户订单信息到星云错误,xingyun_id: {contact_orm.xingyun_id}, {e}')
|
||||
continue
|
||||
else:
|
||||
# list_user_order
|
||||
try:
|
||||
order_list = list_user_order(xy_client, cid=contact_orm.xingyun_id)
|
||||
except Exception as e:
|
||||
logger.error(f'客户数据同步错误,xingyun_id: {contact_orm.xingyun_id}, {e}')
|
||||
logger.exception(e)
|
||||
continue
|
||||
|
||||
shopNameSet, order_id_list = set(), []
|
||||
async for order in order_list:
|
||||
shopName = order.get('shopName')
|
||||
if shopName in shopNameSet:
|
||||
continue
|
||||
shopNameSet.add(shopName)
|
||||
order_id_list.append(order.get('orderId'))
|
||||
# 多店铺用的用的名称和ID都一样,所以不区分
|
||||
break
|
||||
|
||||
for order_id in order_id_list:
|
||||
try:
|
||||
await self.bind_order(WeixinOrderBindInfo(
|
||||
userid=contact_orm.weixin_id,
|
||||
order_id=order_id,
|
||||
))
|
||||
except AssertionError as e:
|
||||
logger.error(f'同步客户订单信息到本地错误,xingyun_id: {contact_orm.xingyun_id}, {e}')
|
||||
logger.exception(e)
|
||||
continue
|
||||
contact_orm.xingyun_sync = True
|
||||
update_list.append(contact_orm)
|
||||
logger.info(f'同步客户订单信息到本地: {contact_orm.xingyun_id} -》 {order_id}')
|
||||
|
||||
if update_list:
|
||||
await self.model.bulk_update(update_list, ['xingyun_sync'])
|
||||
|
||||
if not has_new: break
|
||||
|
||||
weixin_customer_controller = WeixinCustomerController()
|
||||
@@ -0,0 +1,408 @@
|
||||
from tortoise.expressions import Q
|
||||
|
||||
from app.core.crud import CRUDBase
|
||||
from app.schemas.weixin import (
|
||||
WeixinGroupChatCreate,
|
||||
WeixinGroupChatUpdate,
|
||||
WeixinGroupChatXingyunCreate,
|
||||
)
|
||||
|
||||
from some_sdk.services.binder import wk_client, lintao_client, xy_client
|
||||
from some_sdk.wk_weixin_sdk.apis import corp_group
|
||||
from some_sdk.wk_weixin_sdk.apis.extern_user import get_external_user_chat_info
|
||||
from some_sdk.xingyun_sdk.apis.work_user import list_work_group
|
||||
from some_sdk.xingyun_sdk.apis.customer import list_user_join_group
|
||||
|
||||
from some_sdk.lintao_sdk.biz.by_order import get_order_relative_user
|
||||
from some_sdk.lintao_sdk.apis.orderlist import list_product_raw, get_order_log
|
||||
|
||||
from app.models.weixin import WeixinGroupChat, RoleType, CustomerGroup
|
||||
from app.controllers.weixin.user import weixin_user_controller
|
||||
from app.controllers.weixin.customer import weixin_customer_controller
|
||||
from app.utils.common import async_split_generator, async_generator_to_list
|
||||
from app.core.cache import cache_if, invalidate_cache
|
||||
|
||||
import re
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ROLE_LIST = [role.value for role in RoleType]
|
||||
|
||||
class WeixinGroupChatController(CRUDBase[WeixinGroupChat, WeixinGroupChatCreate, WeixinGroupChatUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=WeixinGroupChat)
|
||||
|
||||
async def get_external_group_chat_info(self, chat_id: str):
|
||||
group_info = await corp_group.get_external_group_chat_info(wk_client, chat_id)
|
||||
|
||||
internal_member_list = group_info['internal_member_list']
|
||||
external_member_list = group_info['external_member_list']
|
||||
group_name = group_info.get('name')
|
||||
|
||||
external_member_list.sort(key=lambda user: user.get('join_time'))
|
||||
|
||||
unique_dict = {
|
||||
'name': group_name,
|
||||
'external_user_count': len(external_member_list),
|
||||
'internal_member_count': len(internal_member_list),
|
||||
}
|
||||
group_info.update(unique_dict)
|
||||
# 同步更新数据库
|
||||
status, obj = await self.create_or_update(group_info, query_kwargs={'chat_id': chat_id}, update_kwargs=unique_dict)
|
||||
if not status:
|
||||
logger.debug(f'群聊数据不变,不用更新,chat_id: {chat_id}')
|
||||
# elif status == 'create':
|
||||
# await self.create_customer_group(chat_id)
|
||||
else:
|
||||
# 群更新:触发数据同步事件
|
||||
# event_manager.subscribe("group_chat_updated", update_cache_handler, EventPriority.MEDIUM)
|
||||
|
||||
# 只保存准确无误的数据
|
||||
# buyer_nick = get_buyer_nick_from_group_name(group_name)
|
||||
# logger.debug(f'group_name: {group_name}; buyer_nick: {buyer_nick}')
|
||||
# if not buyer_nick:
|
||||
# logger.error(f'群聊未命名,无法绑定订单,chat_id: {chat_id}')
|
||||
# return group_info
|
||||
|
||||
# self.load_erp_order_info_by_group(buyer_nick, internal_member_list, external_member_list)
|
||||
|
||||
update_result1 = await weixin_user_controller.update_from_group([*internal_member_list])
|
||||
update_result2 = await weixin_customer_controller.update_from_group([*external_member_list])
|
||||
logger.debug(f'群聊用户数据更新结果,chat_id: {chat_id}, {update_result1}')
|
||||
logger.debug(f'群聊客户数据更新结果,chat_id: {chat_id}, {update_result2}')
|
||||
# 刷新用户缓存
|
||||
for user in [*internal_member_list, *external_member_list]:
|
||||
await self.expire_user_cache(user.get('userid'))
|
||||
|
||||
group_info['is_create'] = status == 'create'
|
||||
group_info['detect_update'] = status != False
|
||||
return group_info
|
||||
|
||||
# 从星云有客中加载群聊信息
|
||||
async def load_xingyun_group_info(self, chat_name: str=''):
|
||||
# 检索出最新的记录,只加载最新的记录
|
||||
newest_group = await self.model.filter(xingyun_chat_id__isnull=False).order_by('-create_time').first()
|
||||
newest_time = newest_group.create_time if newest_group else 1735660800 # 2025-01-01 00:00:00
|
||||
|
||||
load_result = {"update_count": 0, "add_count": 0}
|
||||
group_iter = list_work_group(xy_client, chatName=chat_name)
|
||||
async for group_list in async_split_generator(group_iter):
|
||||
if group_list[-1].get('createTime') < newest_time:
|
||||
break
|
||||
logger.debug(f'处理群聊信息,chat_name: {chat_name}, {len(group_list)}')
|
||||
result = await self.save_xingyun_group_info(group_list)
|
||||
load_result['update_count'] += result['update_count']
|
||||
load_result['add_count'] += result['add_count']
|
||||
logger.debug(f'群聊数据更新结果,chat_name: {chat_name}, {result}')
|
||||
|
||||
if load_result['add_count'] + load_result['update_count'] > 300:
|
||||
break
|
||||
|
||||
await self.refresh_xingyun_group_info()
|
||||
return load_result
|
||||
|
||||
async def save_xingyun_group_info(self, group_list: list):
|
||||
group_dict = {group.get('groupChatId'): group for group in group_list}
|
||||
group_orm_list = await self.model.filter(chat_id__in=group_dict.keys())
|
||||
|
||||
update_list = []
|
||||
history = {}
|
||||
for group_orm in group_orm_list:
|
||||
group = group_dict.pop(group_orm.chat_id, history.get(group_orm.chat_id, {}))
|
||||
if not group: continue
|
||||
history[group_orm.chat_id] = group
|
||||
|
||||
try:
|
||||
group_in = WeixinGroupChatXingyunCreate(**group)
|
||||
except Exception as e:
|
||||
logger.error(f'群聊数据转换错误,chat_id: {group}, {e}')
|
||||
continue
|
||||
updated = False
|
||||
if group_in.avatars:
|
||||
group_orm.avatars = group_in.avatars
|
||||
updated = True
|
||||
if group_orm.xingyun_chat_id != group_in.xingyun_chat_id:
|
||||
group_orm.xingyun_chat_id = group_in.xingyun_chat_id
|
||||
updated = True
|
||||
if updated:
|
||||
update_list.append(group_orm)
|
||||
if update_list:
|
||||
await self.model.bulk_update(update_list, ['avatars', 'xingyun_chat_id'])
|
||||
|
||||
add_list = []
|
||||
for chat_id, group in group_dict.items():
|
||||
group_in = WeixinGroupChatXingyunCreate(**group)
|
||||
add_list.append(self.model(**group_in.model_dump(exclude_unset=True)))
|
||||
|
||||
if add_list:
|
||||
await self.model.bulk_create(add_list)
|
||||
|
||||
return {
|
||||
'update_count': len(update_list),
|
||||
'add_count': len(add_list),
|
||||
}
|
||||
|
||||
# 刷新数据库中的群聊信息
|
||||
async def refresh_xingyun_group_info(self):
|
||||
|
||||
for _ in range(10):
|
||||
group_orm_list = await self.model.filter(xingyun_chat_id__isnull=False, external_user_count__isnull=True).order_by('-create_time').limit(20).all()
|
||||
if not group_orm_list: break
|
||||
logger.debug(f'刷新群聊数据,{len(group_orm_list)}')
|
||||
|
||||
for group_orm in group_orm_list:
|
||||
try:
|
||||
await self.get_external_group_chat_info(group_orm.chat_id)
|
||||
except Exception as e:
|
||||
logger.error(f'刷新群聊数据错误,chat_id: {group_orm.chat_id}, {e}')
|
||||
continue
|
||||
logger.debug(f'刷新群聊数据结果,chat_id: {group_orm.name} 已更新')
|
||||
|
||||
# 同步erp中的用户和客户信息到数据库
|
||||
async def load_erp_order_info_by_group(self, buyer_nick: str, internal_member_list: list, external_member_list: list):
|
||||
if not buyer_nick:
|
||||
logger.error(f'群聊未命名,无法绑定订单,buyer_nick: {buyer_nick}')
|
||||
return
|
||||
|
||||
orders = await async_generator_to_list(get_order_relative_user(lintao_client, buyer_nick=buyer_nick))
|
||||
assert orders, f'未找到订单,buyer_nick: {buyer_nick}'
|
||||
|
||||
internal_member_dict = {user.get('name'): user for user in internal_member_list}
|
||||
|
||||
order = orders[0]
|
||||
|
||||
order_relative_user = order.get("users", [])
|
||||
order_relative_user_dict = {user.get('name'): user for user in order_relative_user}
|
||||
|
||||
for name, user in order_relative_user_dict.items():
|
||||
erp_id = user.get('id', '')
|
||||
if not erp_id: continue
|
||||
if re.match(r'^\d+$', erp_id) is None:
|
||||
# 客户
|
||||
if not external_member_list:
|
||||
logger.error(f'客户群聊用户数为0,无法绑定客户, name: {name}, erp_id: {erp_id}')
|
||||
continue
|
||||
if name != buyer_nick:
|
||||
logger.error(f'客户群聊用户与ERP卖家名称不一致,无法绑定客户, name: {name}, erp_id: {erp_id}')
|
||||
continue
|
||||
if len(external_member_list) != 1:
|
||||
external_member_list.sort(key=lambda user: user.get('join_time'))
|
||||
logger.warning(f'客户群聊用户数不是1个,将选取最先入群的用户作为客户,name: {name}, erp_id: {erp_id}')
|
||||
|
||||
for member in external_member_list:
|
||||
member['order_id'] = order.get('trade_no')
|
||||
|
||||
weixin_user = external_member_list[0]
|
||||
weixin_user.update({
|
||||
'order_id': order.get('trade_no'),
|
||||
'taobao_id': user.get('id', ''),
|
||||
'taobao_name': user.get('name', ''),
|
||||
'erp_message': user.get('message', ''),
|
||||
'erp_remark': user.get('memo', ''),
|
||||
'need_confirm': len(external_member_list) > 1,
|
||||
})
|
||||
|
||||
continue
|
||||
else:
|
||||
# 员工
|
||||
erp_id = int(erp_id)
|
||||
weixin_user = internal_member_dict.get(name)
|
||||
logger.debug(f'name: {name}, erp_id: {erp_id}, weixin_user: {weixin_user}')
|
||||
if not weixin_user:
|
||||
logger.error(f'{internal_member_dict.keys()} {name}')
|
||||
logger.error(f'客户群聊用户与ERP员工名称不一致,无法绑定员工, name: {name}, erp_id: {erp_id}')
|
||||
continue
|
||||
weixin_user.update({
|
||||
'erp_id': erp_id,
|
||||
'erp_name': name,
|
||||
'role': user.get('role', ''),
|
||||
})
|
||||
continue
|
||||
|
||||
# 通过客户ID获取客户的所有订单
|
||||
async def get_order_relative_user_list_by_weixin_userid(self, user_id: str):
|
||||
customers = await weixin_customer_controller.model.filter(weixin_id=user_id)
|
||||
if not customers:
|
||||
await weixin_customer_controller.create({
|
||||
"weixin_id": user_id,
|
||||
})
|
||||
# 新用户:触发数据同步事件
|
||||
return False, []
|
||||
|
||||
order_list = []
|
||||
buyer_set = set()
|
||||
for customer in customers:
|
||||
buyer_nick = customer.taobao_name
|
||||
buyer_id = customer.taobao_id
|
||||
if buyer_nick:
|
||||
logger.debug(f'name: {user_id}; buyer_nick: {buyer_nick}')
|
||||
if buyer_nick in buyer_set: continue
|
||||
buyer_set.add(buyer_nick)
|
||||
orders = await async_generator_to_list(get_order_relative_user(lintao_client, buyer_nick=buyer_nick))
|
||||
elif buyer_id:
|
||||
if buyer_id in buyer_set: continue
|
||||
buyer_set.add(buyer_id)
|
||||
orders = await async_generator_to_list(get_order_relative_user(lintao_client, buyer_id=buyer_id))
|
||||
else:
|
||||
assert buyer_nick, "当前客户未绑定淘宝账号"
|
||||
|
||||
logger.debug(f'orders.length: {len(orders)}')
|
||||
order_list.extend(orders)
|
||||
|
||||
# for order in orders:
|
||||
# 原地按照id排序
|
||||
order_list.sort(key=lambda order: order.get('id'), reverse=True)
|
||||
if order_list:
|
||||
await self.get_user_detail_by_order(order=order_list[0])
|
||||
|
||||
return True, order_list
|
||||
|
||||
# 根据客户名字,获取群聊中客户的所有订单
|
||||
async def get_order_relative_user_list_by_weixin_group_name(self, buyer_nick: str):
|
||||
async with cache_if(f'order:relative_user:{buyer_nick}', ttl=10) as cache:
|
||||
if cache.hit:
|
||||
order_list = cache.value
|
||||
logger.debug(f'{cache.key} hit')
|
||||
else:
|
||||
logger.debug(f'{cache.key} not hit')
|
||||
order_list = await async_generator_to_list(get_order_relative_user(lintao_client, buyer_nick=buyer_nick))
|
||||
cache.set(order_list)
|
||||
logger.debug(f'order_list.length: {len(order_list)}')
|
||||
|
||||
# for order in order_list:
|
||||
order_list.sort(key=lambda order: order.get('id'), reverse=True)
|
||||
if order_list:
|
||||
await self.get_user_detail_by_order(order=order_list[0])
|
||||
order_list[0]['__type__'] = 'load_order_by_weixin_group_name'
|
||||
|
||||
return order_list
|
||||
|
||||
# 根据客户ID,获取群聊中客户的所有订单
|
||||
async def get_order_relative_user_list_by_weixin_group_buyer_id(self, buyer_id: str):
|
||||
|
||||
orders = await async_generator_to_list(get_order_relative_user(lintao_client, buyer_id=buyer_id))
|
||||
logger.debug(f'orders.length: {len(orders)}')
|
||||
|
||||
# for order in orders:
|
||||
if orders:
|
||||
await self.get_user_detail_by_order(order=orders[0])
|
||||
|
||||
return orders
|
||||
|
||||
async def get_user_detail_by_order(self, order_id: str='', order: dict = None):
|
||||
if not order:
|
||||
orders = await async_generator_to_list(get_order_relative_user(lintao_client, trade_no=order_id))
|
||||
if not orders: return
|
||||
_orders = [o for o in orders if o.get('ctid') == order_id]
|
||||
# assert len(orders) == 1, f'订单号 {order_id} 对应多个订单'
|
||||
if not _orders:
|
||||
for order in orders:
|
||||
logger.warning(f'订单号 {order_id} 对应多个订单,{order}')
|
||||
# return {}
|
||||
_orders = orders
|
||||
|
||||
order = _orders[0]
|
||||
|
||||
# 缓存订单产品信息
|
||||
tid = order.get("ctid")
|
||||
async with cache_if(f'order:product:{tid}', ttl=3600*24) as cache:
|
||||
if cache.hit:
|
||||
product_resp = cache.value
|
||||
else:
|
||||
product_resp = (await list_product_raw(lintao_client, tid=tid)).get('data', {})
|
||||
if product_resp: cache.set(product_resp)
|
||||
|
||||
if product_resp:
|
||||
order['product_title'] = product_resp[0].get("title")
|
||||
order['product_pic'] = product_resp[0].get("pic_path")
|
||||
|
||||
order['has_fetched_user_detail'] = True
|
||||
user_list = order.get("users", [])
|
||||
|
||||
async with cache_if(f'user:auto_relate', ttl=600) as cache:
|
||||
if cache.hit:
|
||||
default_user_list = cache.value
|
||||
else:
|
||||
default_user_list = await weixin_user_controller.all(Q(auto_relate=True))
|
||||
default_user_list = [user.to_dict() for user in default_user_list]
|
||||
cache.set(default_user_list)
|
||||
default_user_set = {str(user.get('erp_id')) for user in default_user_list}
|
||||
|
||||
for user in user_list:
|
||||
erp_id = user.get('id', '')
|
||||
if erp_id in default_user_set: continue
|
||||
await self.get_order_user_info(user)
|
||||
|
||||
order['users'].extend(default_user_list)
|
||||
|
||||
# 根据 role_list 中的顺序进行排序
|
||||
role_order = {role: index for index, role in enumerate(ROLE_LIST)}
|
||||
order['users'].sort(key=lambda user: role_order.get(user['role'], len(ROLE_LIST)))
|
||||
|
||||
return order
|
||||
|
||||
async def get_order_user_info(self, user: dict=None):
|
||||
if not user: return
|
||||
erp_id = user.get('id', '')
|
||||
if not erp_id: return
|
||||
if re.match(r'^\d+$', erp_id) is None:
|
||||
# logger.debug(f'淘宝客户, erp_id: {erp_id}, user: {user}')
|
||||
# 客户
|
||||
taobao_id = user.get('id', '')
|
||||
query = Q(taobao_id=taobao_id)
|
||||
weixin_customer_list = await weixin_customer_controller.all(query)
|
||||
if not weixin_customer_list:
|
||||
logger.error(f'客户未绑定订单, taobao_id: {taobao_id}, user: {user}')
|
||||
return
|
||||
weixin_customer = weixin_customer_list[0]
|
||||
buyer = weixin_customer.to_dict()
|
||||
if not buyer.get('name') and user.get('name'):
|
||||
weixin_customer.taobao_name = user.get('name')
|
||||
await weixin_customer.save()
|
||||
user.update({**buyer, **user})
|
||||
else:
|
||||
# 员工
|
||||
erp_id = int(erp_id)
|
||||
# logger.debug(f'员工, erp_id: {erp_id}, user: {user}')
|
||||
query = Q(erp_id=erp_id)
|
||||
weixin_user_list = await weixin_user_controller.all(query)
|
||||
if not weixin_user_list:
|
||||
user['erp_id'] = erp_id
|
||||
logger.error(f'员工未绑定ERP, erp_id: {erp_id}, user: {user}')
|
||||
return
|
||||
weixin_user = weixin_user_list[0]
|
||||
user.update(weixin_user.to_dict())
|
||||
|
||||
# 获取erp日志
|
||||
async def get_erp_order_log(self, order_id: str):
|
||||
return await get_order_log(lintao_client, order_id)
|
||||
|
||||
async def expire_user_cache(self, weixin_id: str):
|
||||
await invalidate_cache(f'customer:user_datail:{weixin_id}')
|
||||
await invalidate_cache(f'customer:user_datail_0:{weixin_id}')
|
||||
|
||||
# async def list_user_join_group(self, xingyun_id: int):
|
||||
# assert xingyun_id, 'id 不能为空'
|
||||
# result = await async_generator_to_list(list_user_join_group(xy_client, cid=xingyun_id))
|
||||
|
||||
# xingyun_chat_ids = [item.get('id') for item in result]
|
||||
# all_group_chat = await self.model.filter(xingyun_chat_id__in=xingyun_chat_ids)
|
||||
# group_chat_dict = {item.xingyun_chat_id: item for item in all_group_chat}
|
||||
# logger.debug(f'list_user_join_group, xingyun_chat_ids: {xingyun_chat_ids}')
|
||||
|
||||
# remain_chat_ids = set()
|
||||
# for item in result:
|
||||
# group_chat_id = item.get('id')
|
||||
# if group_chat_id in group_chat_dict: continue
|
||||
# remain_chat_ids.add(group_chat_id)
|
||||
|
||||
# chat_name = item.get('chatName')
|
||||
# if not chat_name: continue
|
||||
# await self.load_xingyun_group_info(chat_name=chat_name)
|
||||
|
||||
# remain_chat_list = await self.model.filter(xingyun_chat_id__in=remain_chat_ids)
|
||||
# all_group_chat.extend(remain_chat_list)
|
||||
# return all_group_chat
|
||||
|
||||
weixin_group_chat_controller = WeixinGroupChatController()
|
||||
@@ -0,0 +1,169 @@
|
||||
from .customer import weixin_customer_controller
|
||||
from .user import weixin_user_controller
|
||||
from app.controllers.msg import msg_controller
|
||||
from app.controllers.action import action_controller
|
||||
from app.schemas.weixin import WeixinGroupChatEvent, BindOrderResultEvent, CustomerRepeatPurchaseEvent, CustomerAssignOrderEvent, CustomerRefundOrderEvent
|
||||
from app.utils.common import transform_pydantic_to_list
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from app.utils.event_task import event_manager, EventType
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
from typing import Any
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler as BackgroundScheduler
|
||||
|
||||
from app.controllers.automation.scenario import automation_scenario_controller
|
||||
from app.controllers.automation.task import task_controller
|
||||
|
||||
|
||||
sync_lock = asyncio.Lock()
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def sync_xingyun_contact_info_async(event_type, *args, **kwargs):
|
||||
if os.getenv("APP_ENV") != "prod":
|
||||
logger.info(f'非生产环境,不实际同步客户数据,{args} {kwargs}')
|
||||
return
|
||||
|
||||
logger.info(f'同步客户数据,{args} {kwargs}')
|
||||
now_time = datetime.now()
|
||||
start_time = (now_time - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end_time = now_time + timedelta(days=1)
|
||||
await weixin_customer_controller.load_user_from_xingyun(add_time_start=start_time, add_time_end=end_time)
|
||||
|
||||
async def load_all_user_from_xingyun(event_type, *args, **kwargs):
|
||||
if os.getenv("APP_ENV") != "prod":
|
||||
logger.info(f'非生产环境,不实际从星云加载全量客户数据,{args} {kwargs}')
|
||||
return
|
||||
|
||||
logger.info(f'从星云加载全量客户数据,{args} {kwargs}')
|
||||
start_time = datetime(2025, 6, 1)
|
||||
end_time = datetime.now() + timedelta(days=1)
|
||||
return await weixin_customer_controller.load_all_user_from_xingyun(task_id='load_all_user_from_xingyun', add_time_start=start_time, add_time_end=end_time)
|
||||
|
||||
async def monitor_erp_order(event_type, *args, **kwargs):
|
||||
if os.getenv("APP_ENV") != "prod":
|
||||
logger.info(f'非生产环境,不实际从星云加载全量客户数据,{args} {kwargs}')
|
||||
return
|
||||
|
||||
now_time = datetime.now()
|
||||
logger.info(f'监控ERP订单,{args} {kwargs}')
|
||||
new_order_list = [1]
|
||||
new_order_list = await weixin_customer_controller.monitor_erp_order()
|
||||
if new_order_list:
|
||||
logger.info(f'监控到新订单,{new_order_list}')
|
||||
await msg_controller.sync_msg()
|
||||
logger.debug(f'耗时:{datetime.now() - now_time}')
|
||||
|
||||
async def update_erp_order(*args, **kwargs):
|
||||
logger.info(f'更新ERP订单,{args} {kwargs}')
|
||||
await msg_controller.get_order_user_days_before(2)
|
||||
|
||||
async def check_remark_action_is_done(*args, **kwargs):
|
||||
if os.getenv("APP_ENV") != "prod":
|
||||
logger.info(f'非生产环境,不实际检查备注操作是否完成,{args} {kwargs}')
|
||||
return
|
||||
|
||||
logger.info(f'检查备注操作是否完成,{args} {kwargs}')
|
||||
await action_controller.check_remark_action_is_done()
|
||||
|
||||
# 每天到点执行一次
|
||||
today = datetime.now()
|
||||
todayStart = today.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
if today.hour in [23] and today.minute >= 50:
|
||||
todayStart += timedelta(days=1)
|
||||
class_type = '早班'
|
||||
elif today.hour in [0, 1] and today.minute <= 10:
|
||||
class_type = '早班'
|
||||
elif today.hour in [16] and today.minute >= 50 or today.hour in [17,18] and today.minute <= 10:
|
||||
class_type = '晚班'
|
||||
else:
|
||||
return
|
||||
|
||||
date = todayStart.strftime('%Y-%m-%d')
|
||||
title = f'设置 {date} {class_type} 的员工活码'
|
||||
success_title = title+' 成功'
|
||||
fail_title = title+' 失败'
|
||||
|
||||
async with sync_lock:
|
||||
has_set = await msg_controller.model.filter(title__in=[success_title]).first()
|
||||
if has_set:
|
||||
logger.info(f'【已操作】{title}')
|
||||
return
|
||||
|
||||
logger.info(f'【开始】{title}')
|
||||
try:
|
||||
result, next_class = await weixin_user_controller.set_user_online(class_type=class_type, date=todayStart)
|
||||
except Exception as e:
|
||||
logger.exception(f'设置 {date} {class_type} 的员工活码失败,{e}')
|
||||
result, next_class = {}, None
|
||||
|
||||
is_failed = bool(list(filter(lambda x: x.get('fail') != 0, result.values()))) or not result
|
||||
logger.info(f'【完成】{title},结果:{result} {"失败" if is_failed else "成功"}')
|
||||
|
||||
result_str = '\n'.join([f'{k}:{v}' for k, v in result.items()])
|
||||
|
||||
logger.info(f'{success_title if not is_failed else fail_title},{result_str}')
|
||||
content = f'\n\n{result_str}'
|
||||
if result and not next_class:
|
||||
content += f'\n\n⚠️ 未设置下一班值班人员,请及时设置'
|
||||
logger.info(f'消息通知:{content}')
|
||||
await msg_controller.new_system_msg(title=success_title if not is_failed else fail_title, content=content)
|
||||
await msg_controller.sync_msg()
|
||||
|
||||
async def group_chat_update(event_type, group_info, *args):
|
||||
if os.getenv("APP_ENV") != "prod":
|
||||
logger.info(f'非生产环境,不实际检查群聊更新,{group_info} {args}')
|
||||
return
|
||||
|
||||
group_event = WeixinGroupChatEvent(**group_info)
|
||||
await sync_xingyun_contact_info_async()
|
||||
|
||||
|
||||
async def test_event(event_type, event_data=None, *args, **kwargs):
|
||||
# logger.info(f'测试事件,{args} {kwargs}')
|
||||
# scenario_list = await automation_scenario_controller.find_active_scenarios(event_type, event_data=event_data)
|
||||
# logger.info(f'触发事件,{len(scenario_list)}')
|
||||
# for scenario in scenario_list:
|
||||
# task = await task_controller.create_from_scenario(scenario=scenario, event_data=event_data)
|
||||
# logger.info(f'创建任务,{task}')
|
||||
pass
|
||||
|
||||
async def trigger_event(event_type: str, event_data: Any):
|
||||
"""解析事件数据"""
|
||||
logger.info(f'测试事件,{event_type} {event_data}')
|
||||
scenario_list = await automation_scenario_controller.find_active_scenarios(event_type, event_data=event_data)
|
||||
logger.info(f'触发事件,{len(scenario_list)}')
|
||||
for scenario, reason in scenario_list:
|
||||
logger.info(f'触发场景,{scenario},{reason}')
|
||||
try:
|
||||
task = await task_controller.create_from_scenario(scenario=scenario, event_data=event_data, reason=reason)
|
||||
logger.info(f'创建任务,{task}')
|
||||
except Exception as e:
|
||||
logger.error(f'创建任务失败,{e}')
|
||||
|
||||
# print(f'事件订阅:{transform_pydantic_to_list(WeixinGroupChatEvent)}')
|
||||
|
||||
event_manager.subscribe(EventType.SYNC_XINGYUN_CONTACT_INFO, load_all_user_from_xingyun)
|
||||
event_manager.subscribe(EventType.OPEN_PERSONAL_CHAT, sync_xingyun_contact_info_async)
|
||||
event_manager.subscribe(EventType.GROUP_CHAT_UPDATED, group_chat_update, input_model=transform_pydantic_to_list(WeixinGroupChatEvent))
|
||||
event_manager.subscribe(EventType.BIND_ORDER_FOR_USER, sync_xingyun_contact_info_async, input_model=transform_pydantic_to_list(BindOrderResultEvent))
|
||||
event_manager.subscribe(EventType.OLD_CUSTOMER_REPEAT_PURCHASE, test_event, input_model=transform_pydantic_to_list(CustomerRepeatPurchaseEvent))
|
||||
event_manager.subscribe(EventType.CUSTOMER_SERVICE_ASSIGN_ORDER, test_event, input_model=transform_pydantic_to_list(CustomerAssignOrderEvent))
|
||||
event_manager.subscribe(EventType.DESIGNER_ASSIGN_ORDER, test_event, input_model=transform_pydantic_to_list(CustomerAssignOrderEvent))
|
||||
event_manager.subscribe(EventType.CUSTOMER_RETURN_ORDER, test_event, input_model=transform_pydantic_to_list(CustomerRefundOrderEvent))
|
||||
event_manager.subscribe(EventType.DESIGNER_UPLOAD_DESIGN, test_event)
|
||||
|
||||
event_manager.register_pretask(trigger_event)
|
||||
|
||||
if os.getenv("APP_ENV") == "prod":
|
||||
logger.info('生产环境,启动定时任务')
|
||||
# 创建调度器
|
||||
scheduler = BackgroundScheduler()
|
||||
|
||||
# scheduler.add_job(monitor_erp_order, "interval", seconds=60) # 每90秒执行一次
|
||||
scheduler.add_job(update_erp_order, "interval", seconds=240) # 每90秒执行一次
|
||||
scheduler.add_job(check_remark_action_is_done, "interval", seconds=180) # 每90秒执行一次
|
||||
scheduler.start()
|
||||
@@ -0,0 +1,201 @@
|
||||
|
||||
from app.core.crud import CRUDBase
|
||||
from app.schemas.weixin import (
|
||||
WeixinUserCreate,
|
||||
WeixinUserUpdate,
|
||||
)
|
||||
|
||||
from app.models.weixin import WeixinUser
|
||||
from app.schemas.weixin import WeixinUserBindInfo
|
||||
|
||||
from some_sdk.services.binder import feishu_client, xy_client
|
||||
from some_sdk.feishu_sdk.biz.doc import search_file_record
|
||||
from some_sdk.xingyun_sdk.apis.channel import list_channel_group, list_channel_group_user_list, set_online_staff
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
def split_generator(iterable, chunk_size=50):
|
||||
"""
|
||||
将可迭代对象拆分为多个子列表
|
||||
"""
|
||||
chunk = []
|
||||
for item in iterable:
|
||||
chunk.append(item)
|
||||
if len(chunk) == chunk_size:
|
||||
yield chunk
|
||||
chunk = []
|
||||
if chunk:
|
||||
yield chunk
|
||||
|
||||
class WeixinUserController(CRUDBase[WeixinUser, WeixinUserCreate, WeixinUserUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=WeixinUser)
|
||||
|
||||
async def update_from_group(self, internal_members: list[dict]):
|
||||
# 同步更新数据库
|
||||
member_dict = {}
|
||||
for member in internal_members:
|
||||
member = member.copy()
|
||||
userid = member.get('userid', None)
|
||||
member['alias'] = member.pop('name', None)
|
||||
member_dict[userid] = member
|
||||
|
||||
member_list = await self.model.filter(userid__in=member_dict.keys())
|
||||
|
||||
update_list = []
|
||||
update_fileds = ['alias', 'role', 'erp_id', 'erp_name']
|
||||
|
||||
history = {}
|
||||
for member in member_list:
|
||||
member_info = member_dict.pop(member.userid, history.get(member.userid, {}))
|
||||
if member.role in ['owner', 'admin', 'follower']:
|
||||
continue
|
||||
if not member_info: continue
|
||||
history[member.userid] = member_info
|
||||
|
||||
updated = {}
|
||||
for field in update_fileds:
|
||||
old_value = getattr(member, field)
|
||||
if field not in member_info:
|
||||
continue
|
||||
value = member_info.get(field)
|
||||
if old_value != value:
|
||||
setattr(member, field, value)
|
||||
updated[f'{field}:{str(old_value)}'] = value
|
||||
if updated:
|
||||
logger.info(f'用户 {member.userid} 更新关系为 {updated}')
|
||||
update_list.append(member)
|
||||
if update_list:
|
||||
await self.model.bulk_update(update_list, fields=update_fileds)
|
||||
|
||||
# 新增用户
|
||||
create_list = []
|
||||
for userid, member_info in member_dict.items():
|
||||
create_list.append(WeixinUser(
|
||||
**member_info,
|
||||
name=member_info.get('alias'),
|
||||
))
|
||||
if create_list:
|
||||
await self.model.bulk_create(create_list)
|
||||
|
||||
return {
|
||||
'update_count': len(update_list),
|
||||
'create_count': len(create_list),
|
||||
}
|
||||
|
||||
|
||||
async def bind_user(self, user_in: WeixinUserBindInfo):
|
||||
if not user_in.userid:
|
||||
raise ValueError("用户ID不能为空")
|
||||
if user_in.erp_id == 0:
|
||||
del user_in.erp_id
|
||||
|
||||
user = await self.model.filter(userid=user_in.userid).first()
|
||||
if not user:
|
||||
await self.create({**user_in.model_dump(exclude_unset=True), 'name': user_in.username})
|
||||
return {}
|
||||
await self.model.filter(userid=user_in.userid).update(**user_in.model_dump(exclude_unset=True))
|
||||
|
||||
async def set_user_online(self, class_type: str, date: datetime):
|
||||
daysTimestamp = lambda nowStamp, days: nowStamp + days * 24 * 60 * 60 * 1000
|
||||
formatDate = lambda ts: datetime.fromtimestamp(ts / 1000).strftime('%Y-%m-%d')
|
||||
|
||||
today = int(date.timestamp() * 1000)
|
||||
logger.info(f'设置 {formatDate(today)} 班次为 {class_type} 的员工为在线状态')
|
||||
yestoday = daysTimestamp(today, -1)
|
||||
tomorrow = daysTimestamp(today, 1)
|
||||
|
||||
# 查询日期为 yestoday 到 后天 之间的所有记录
|
||||
resp = search_file_record(
|
||||
feishu_client,
|
||||
app_token="V4qebMG2kamCWMslJmdcQMbtngW",
|
||||
table_id="tblKgjc3F2jZ2inC",
|
||||
filter={
|
||||
"conjunction": "and",
|
||||
"conditions": [{
|
||||
"field_name": "日期",
|
||||
"operator": "isGreater",
|
||||
"value": ["ExactDate", yestoday]
|
||||
},{
|
||||
"field_name": "日期",
|
||||
"operator": "isLess",
|
||||
"value": ["ExactDate", daysTimestamp(today, 2)]
|
||||
}]
|
||||
},
|
||||
)
|
||||
|
||||
set_result = {}
|
||||
current_class = (class_type, today)
|
||||
next_class = ('早班', tomorrow) if class_type == '晚班' else ('晚班', today)
|
||||
|
||||
current_class_mapping = {}
|
||||
next_class_mapping = {}
|
||||
async for data in resp:
|
||||
user = data.get('data', {})
|
||||
user_online = ((user.get('班次') or '')[:2], user.get('日期', ''))
|
||||
# logger.info(f'用户{user.get("接量成员", "")} 在线时间为{user_online} 当前班次为{current_class} 下一班次为{next_class}')
|
||||
class_group = user.get('组别', '')
|
||||
class_mapping = None
|
||||
# 当前班次
|
||||
if user_online == current_class:
|
||||
class_mapping = current_class_mapping
|
||||
set_result[class_group] = {"接量成员": user.get('接量成员', '')}
|
||||
# 下一班次
|
||||
elif user_online == next_class:
|
||||
class_mapping = next_class_mapping
|
||||
if class_mapping is None:
|
||||
continue
|
||||
|
||||
if user.get('id', ''):
|
||||
class_mapping.setdefault(class_group, [])
|
||||
class_mapping[class_group].append(user.get('id', ''))
|
||||
|
||||
assert current_class_mapping, f"根据日期{current_class[1]} {formatDate(current_class[1])}没有找到班次为{class_type}的员工"
|
||||
logger.info(f'根据日期{current_class[1]} {formatDate(current_class[1])} 找到班次为{class_type}的员工: {current_class_mapping} {set_result}')
|
||||
if next_class_mapping:
|
||||
logger.info(f'根据日期{next_class[1]} {formatDate(next_class[1])} 找到班次为{next_class[0]}的员工: {next_class_mapping}')
|
||||
|
||||
group_list = list_channel_group(xy_client)
|
||||
async for group in group_list:
|
||||
title = group.get("title", "")
|
||||
if title not in current_class_mapping:
|
||||
continue
|
||||
|
||||
set_result[title] = set_result.get(title) or {}
|
||||
try:
|
||||
user_list = list_channel_group_user_list(xy_client, groupId=group.get("id", ""))
|
||||
|
||||
huomaList = []
|
||||
async for user in user_list:
|
||||
huomaList.append(user.get('id'))
|
||||
|
||||
if not huomaList:
|
||||
logger.error(f'组 {title} {group.get("id", "")} 中没有员工')
|
||||
continue
|
||||
|
||||
is_faild, is_success = [], []
|
||||
# 按照50个一组设置在线状态
|
||||
for i in range(0, len(huomaList), 50):
|
||||
result = await set_online_staff(xy_client, allDayUserIds=','.join(current_class_mapping[title]), huomaList=huomaList[i:i+50])
|
||||
|
||||
is_faild += list(filter(lambda x: x.get('errorMsg', ''), result.get('data', [])))
|
||||
is_success += list(filter(lambda x: x.get('success', True), result.get('data', [])))
|
||||
|
||||
is_faild = set(is_faild)
|
||||
if is_faild: logger.error(f'设置失败的员工: {is_faild}')
|
||||
else: logger.info(f'全部员工设置成功')
|
||||
|
||||
set_result[title] = {
|
||||
'success': len(is_success),
|
||||
'fail': len(is_faild),
|
||||
**set_result[title]
|
||||
}
|
||||
except Exception as e:
|
||||
logger.exception(f'设置群 {group.get("id", "")} 中员工 {huomaList} 为在线状态失败: {e}')
|
||||
|
||||
return set_result, next_class_mapping
|
||||
|
||||
weixin_user_controller = WeixinUserController()
|
||||
@@ -0,0 +1,4 @@
|
||||
def get_buyer_nick_from_group_name(group_name: str):
|
||||
buyer_nick = group_name.rsplit('-', 1)[0] if '服务群' in group_name else group_name
|
||||
return buyer_nick
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from some_sdk.weixin_sdk.session import get_logged_in_client
|
||||
from some_sdk.weixin_sdk.apis.userlist import list_department_user, get_user_detail_by_vid
|
||||
|
||||
# 首次调用:登录并创建 client
|
||||
client = get_logged_in_client()
|
||||
|
||||
def test_list_department_user(partyid: str):
|
||||
department_list = list_department_user(client, partyid)
|
||||
for index, department in enumerate(department_list, 1):
|
||||
print(f'{index}. {department}')
|
||||
print('==== ' * 10)
|
||||
|
||||
return
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_list_department_user('1688852859949799')
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
from some_sdk.xingyun_sdk.session import get_logged_in_client
|
||||
from some_sdk.xingyun_sdk.biz.by_user import get_user_order
|
||||
from .wechat_sdk import get_external_group_chat_info
|
||||
from app.models.weixin import WeixinUser
|
||||
|
||||
# 首次调用:登录并创建 client
|
||||
client = get_logged_in_client()
|
||||
|
||||
def get_user_order_list(group_name: str="", keyword: str="", regrex: str=None, userIds: list = None):
|
||||
result = get_user_order(client, keyword=keyword, group_name=group_name, regrex=regrex, userIds=userIds)
|
||||
if not result:
|
||||
return {}
|
||||
|
||||
order_list = result.pop("order_list", [])
|
||||
print(f'在群聊 {result["group_name"]} 中的客户 {result["name"]}({result["keyword"]}) 有订单数: {len(order_list)}')
|
||||
|
||||
subTradeNo_list = []
|
||||
for order in order_list:
|
||||
result_order = dict(tradeNo='', tradeTime=order.get("tradeTime", ''), payTime=order.get("payTime", ''))
|
||||
orderList = order.get("orderList", [])
|
||||
|
||||
for item in orderList:
|
||||
result_order['shopName'] = order.get("shopName", "")
|
||||
result_order['goodsName'] = item.get("goodsName", "")
|
||||
result_order['actuPayment'] = order.get("actuPayment", "")
|
||||
result_order['goodsCount'] = order.get("goodsCount", "")
|
||||
result_order['tradeNo'] = item.get("subTradeNo", "")
|
||||
result_order['pic'] = item.get("pic", "")
|
||||
result_order['other_status'] = order.get("orderStatus", 0)
|
||||
result_order['other_status_name'] = order.get("status_name", "")
|
||||
subTradeNo_list.append(result_order.copy())
|
||||
|
||||
import json; print(json.dumps(subTradeNo_list, indent=4, ensure_ascii=False))
|
||||
result['order_list'] = subTradeNo_list
|
||||
return result
|
||||
|
||||
async def get_user_order_list_by_chatid(chat_id: str):
|
||||
group_chat_info = await get_external_group_chat_info(chat_id)
|
||||
external_member_list = group_chat_info.get('external_member_list', [])
|
||||
|
||||
trade_list = []
|
||||
group_chat_info['trade_list'] = trade_list
|
||||
|
||||
invitor = group_chat_info.get('owner')
|
||||
qiwei_kefu = await WeixinUser.filter(userid=invitor).first()
|
||||
print(f'邀请人:{invitor} {qiwei_kefu.username} {qiwei_kefu.crmid}')
|
||||
|
||||
group_name = group_chat_info.get('name')
|
||||
# if not group_name:
|
||||
# group_name = '未知群聊'
|
||||
|
||||
print(f'群聊名称{group_name},有{len(external_member_list)}位客户', flush=True)
|
||||
for user in external_member_list:
|
||||
print(f'正在处理客户:{user.get("name")}', flush=True)
|
||||
result = get_user_order_list(group_name=group_name, keyword=user.get('name'), userIds=[qiwei_kefu.crmid] if qiwei_kefu.crmid else None)
|
||||
if not result:
|
||||
continue
|
||||
|
||||
trade_list.append(result)
|
||||
|
||||
return group_chat_info
|
||||
|
||||
if __name__ == "__main__":
|
||||
get_user_order("fuyixuan0628-印刷vip客户服务群", "去看海吗")
|
||||
get_user_order("小旋honey-印刷vip客户服务群@喜印说", "宇宙大女神")
|
||||
|
||||
Reference in New Issue
Block a user