first commit

This commit is contained in:
2026-06-25 17:41:06 +08:00
commit b2f23a933b
370 changed files with 30526 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
from .role import role_controller as role_controller
from .user import user_controller as user_controller
+109
View File
@@ -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()
+46
View File
@@ -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()
+82
View File
@@ -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()
+222
View File
@@ -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()
+77
View File
@@ -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()
+235
View File
@@ -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()
+41
View File
@@ -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()
+86
View File
@@ -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()
+43
View File
@@ -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
+16
View File
@@ -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()
+461
View File
@@ -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()
+27
View File
@@ -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()
+62
View File
@@ -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()
+60
View File
@@ -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()
+607
View File
@@ -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()
+408
View File
@@ -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()
+169
View File
@@ -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()
+201
View File
@@ -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()
+4
View File
@@ -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
+20
View File
@@ -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')
+66
View File
@@ -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客户服务群@喜印说", "宇宙大女神")