first commit
This commit is contained in:
@@ -0,0 +1,607 @@
|
||||
|
||||
from app.core.crud import CRUDBase
|
||||
from app.schemas.weixin import (
|
||||
WeixinCustomerCreate,
|
||||
WeixinCustomerUpdate,
|
||||
WeixinCustomerXingyunCreate,
|
||||
)
|
||||
from typing import List
|
||||
|
||||
from app.models.weixin import WeixinCustomer, WeixinUser, CustomerGroup
|
||||
from app.schemas.weixin import WeixinUserBindInfo, WeixinOrderBindInfo
|
||||
|
||||
from some_sdk.services.binder import lintao_client, xy_client, wk_client
|
||||
from some_sdk.lintao_sdk.biz.by_order import get_order_relative_user, save_other_memo
|
||||
from some_sdk.xingyun_sdk.apis.customer import bind_user_to_xingyun, list_user_order
|
||||
from some_sdk.xingyun_sdk.apis.work_external_contact import list_all_contact_by_addtime, list_all_contact_by_addtime_and_tag
|
||||
from some_sdk.wk_weixin_sdk.apis.corp_user import from_service_external_userid
|
||||
from some_sdk.wk_weixin_sdk.apis.extern_user import get_external_user_chat_info
|
||||
|
||||
from app.controllers.msg import msg_controller
|
||||
from app.schemas.msg import MsgType
|
||||
|
||||
from app.utils.common import async_split_generator
|
||||
import pickle
|
||||
import hashlib
|
||||
|
||||
import re, os
|
||||
from datetime import datetime, timedelta
|
||||
from app.core.cache import cache_if
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EXCLUDE_SHOP_NAMES = ['领淘文具旗舰店']
|
||||
XINGYUN_USE_TIME = datetime(2025, 6, 18)
|
||||
|
||||
class WeixinCustomerController(CRUDBase[WeixinCustomer, WeixinCustomerCreate, WeixinCustomerUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=WeixinCustomer)
|
||||
|
||||
async def update_from_group(self, external_members: list[dict]):
|
||||
# 同步更新数据库
|
||||
member_dict = {}
|
||||
for member in external_members:
|
||||
member = member.copy()
|
||||
userid = member.pop('userid', None)
|
||||
|
||||
weixin_name = member.pop('name', None)
|
||||
weixin_unionid = member.pop('unionid', None)
|
||||
|
||||
member_info = WeixinCustomer.orm_format(member)
|
||||
member_info['weixin_id'] = userid
|
||||
member_info['weixin_name'] = weixin_name
|
||||
member_info['weixin_unionid'] = weixin_unionid
|
||||
member_dict[userid] = member_info
|
||||
|
||||
member_list = await self.model.filter(weixin_id__in=member_dict.keys())
|
||||
|
||||
update_list = []
|
||||
update_fileds = ['weixin_unionid', 'weixin_id', 'weixin_name', 'order_id', 'taobao_id', 'taobao_name', 'extra', 'need_confirm']
|
||||
|
||||
history = {}
|
||||
for member in member_list:
|
||||
member_info = member_dict.pop(member.weixin_id, history.get(member.weixin_id, {}))
|
||||
if not member_info: continue
|
||||
history[member.weixin_id] = member_info
|
||||
|
||||
member_info['extra'] = {**(member.extra or {}), **member_info['extra']}
|
||||
|
||||
updated = {}
|
||||
for field in update_fileds:
|
||||
old_value = getattr(member, field)
|
||||
if field not in member_info:
|
||||
continue
|
||||
value = member_info.get(field)
|
||||
if old_value != value:
|
||||
setattr(member, field, value)
|
||||
updated[f'{field}:{str(old_value)}'] = value
|
||||
if updated:
|
||||
logger.info(f'用户 {member.weixin_name} 更新关系为 {updated}')
|
||||
update_list.append(member)
|
||||
if update_list:
|
||||
await self.model.bulk_update(update_list, fields=update_fileds)
|
||||
|
||||
# 新增用户
|
||||
create_list = []
|
||||
for userid, member_info in member_dict.items():
|
||||
create_list.append(WeixinCustomer(
|
||||
**member_info,
|
||||
))
|
||||
if create_list:
|
||||
await self.model.bulk_create(create_list)
|
||||
|
||||
return {
|
||||
'update_count': len(update_list),
|
||||
'create_count': len(create_list),
|
||||
}
|
||||
|
||||
async def bind_user(self, user_in: WeixinUserBindInfo):
|
||||
if not user_in.taobao_id:
|
||||
raise ValueError("淘宝ID不能为空")
|
||||
await self.model.filter(taobao_id=user_in.taobao_id).update_from_dict(user_in.model_dump(exclude_unset=True))
|
||||
|
||||
async def remark_order(self, order: dict, remark: str):
|
||||
ctid = order.get("ctid") or order.get("trade_no")
|
||||
# if remark in (order.get("remark") or ''):
|
||||
if '企微' in (order.get("remark") or ''):
|
||||
logger.info(f'订单 {ctid} 备注中已包含 企微 备注,无需重复添加')
|
||||
return
|
||||
remark_list = [order.get("remark", ""), remark]
|
||||
append_remark = ';'.join(filter(lambda x: x, remark_list))
|
||||
|
||||
if os.getenv("APP_ENV") == "prod":
|
||||
result = await save_other_memo(lintao_client, order_id=ctid, other_memo=append_remark)
|
||||
assert result["type"] == "success", f"保存备注失败,响应: {result}"
|
||||
logger.info(f'成功为订单 {ctid} 打上备注 {append_remark}')
|
||||
else:
|
||||
logger.info(f'非生产环境,不实际为订单 {ctid} 打上备注 {append_remark}')
|
||||
|
||||
async def remark_order_by_order_id(self, remark: str, order_id: str=None, orders: list=None):
|
||||
if not orders:
|
||||
assert order_id, "订单ID不能为空"
|
||||
orders = [o async for o in get_order_relative_user(lintao_client, trade_no=order_id)]
|
||||
assert orders, f"订单 {order_id} 不存在"
|
||||
|
||||
# 打上备注
|
||||
logger.info(f'即将为以下订单 {[o.get("trade_no") for o in orders]} 打上备注 {remark}')
|
||||
need_monitor_order_list = []
|
||||
for order in orders:
|
||||
if not order.get('title'):
|
||||
# 未领单的订单,需要添加到监控列表中进行监控,防止被刷掉
|
||||
need_monitor_order_list.append(order)
|
||||
logger.info(f'订单 {order.get("trade_no")} 未领单,需要添加到监控列表中进行监控,防止被刷掉')
|
||||
|
||||
try:
|
||||
await self.remark_order(order, remark=remark)
|
||||
except Exception as e:
|
||||
logger.error(f'为订单 {order.get("trade_no")} 打上备注 {remark} 失败,异常: {e}')
|
||||
need_monitor_order_list.append(order)
|
||||
|
||||
return need_monitor_order_list
|
||||
|
||||
|
||||
async def bind_order(self, bind_in: WeixinOrderBindInfo):
|
||||
orders = [o async for o in get_order_relative_user(lintao_client, trade_no=bind_in.order_id)]
|
||||
assert orders, f"订单 {bind_in.order_id} 不存在"
|
||||
|
||||
orders = [o for o in orders if o.get('trade_no') == bind_in.order_id]
|
||||
order = orders[0]
|
||||
|
||||
buyer_ids = []
|
||||
for user in order.get('users', []):
|
||||
taobao_id = user.get('id', '')
|
||||
if not taobao_id or re.match(r'^\d+$', taobao_id): continue
|
||||
buyer_ids.append(taobao_id)
|
||||
|
||||
# 更新还是新建?
|
||||
orms = await self.model.filter(weixin_id=bind_in.userid).all()
|
||||
logger.info(f'用户 {bind_in.userid} 准备绑定订单 {len(orms)} {[orm.weixin_name for orm in orms]}')
|
||||
|
||||
update_list = []
|
||||
for orm in orms:
|
||||
if orm.order_id and orm.order_id != order.get('trade_no'):
|
||||
logger.warning(f'用户 {orm.weixin_name} 已绑定订单 {orm.order_id},即将覆盖为 {order.get("trade_no")}')
|
||||
|
||||
orm.weixin_id = bind_in.userid
|
||||
orm.order_id = order.get('trade_no')
|
||||
orm.shop_name = order.get('shop_name', '')
|
||||
orm.taobao_id = user.get('id', '')
|
||||
orm.taobao_name = user.get('name', '')
|
||||
update_list.append(orm)
|
||||
|
||||
if update_list:
|
||||
logger.info(f'更新用户 {bind_in.userid} 绑定订单 {order.get("trade_no")} 中的用户 {user}')
|
||||
await self.model.bulk_update(update_list, fields=['order_id', 'weixin_id', 'taobao_id', 'taobao_name'])
|
||||
return True, orders
|
||||
|
||||
if not orms:
|
||||
logger.info(f'绑定用户 {bind_in.userid} 绑定订单 {order.get("trade_no")} 中的用户 {user}')
|
||||
orm_user = WeixinCustomer(
|
||||
weixin_id=bind_in.userid,
|
||||
order_id=order.get('trade_no'),
|
||||
taobao_id=user.get('id', ''),
|
||||
taobao_name=user.get('name', ''),
|
||||
)
|
||||
await orm_user.save()
|
||||
return True, orders
|
||||
|
||||
return False, orders
|
||||
|
||||
async def bind_xingyun_user(self, contact_orm: WeixinCustomer):
|
||||
assert contact_orm.xingyun_id, "星云用户ID不能为空"
|
||||
if contact_orm.shop_name in EXCLUDE_SHOP_NAMES:
|
||||
contact_orm.xingyun_sync = True
|
||||
contact_orm.extra = contact_orm.extra or {}
|
||||
contact_orm.extra['system_remark'] = f'店铺名称: {contact_orm.shop_name},没有开通星云有客服务,故此跳过。'
|
||||
return
|
||||
|
||||
order_id = contact_orm.order_id.split('_')[-1]
|
||||
logger.info(f'同步客户订单信息到星云: {contact_orm.xingyun_id} -》 {order_id}')
|
||||
try:
|
||||
await bind_user_to_xingyun(xy_client, contact_orm.xingyun_id, order_id)
|
||||
except:
|
||||
logger.error(f'同步客户订单信息到星云失败: {contact_orm.xingyun_id} -》 {order_id}, 尝试寻找同用户的其他订单来绑定')
|
||||
shop_name = ''
|
||||
async for order in get_order_relative_user(lintao_client, buyer_nick=contact_orm.taobao_name):
|
||||
shop_name = order.get('shop_name', '')
|
||||
if shop_name in EXCLUDE_SHOP_NAMES:
|
||||
continue
|
||||
|
||||
if not order.get('create_time'): continue
|
||||
create_time = order.get('create_time')
|
||||
create_time = datetime.fromisoformat(create_time.replace("Z", "+00:00"))
|
||||
if create_time < XINGYUN_USE_TIME:
|
||||
continue
|
||||
|
||||
order_id = order.get('trade_no').split('_')[-1]
|
||||
await bind_user_to_xingyun(xy_client, contact_orm.xingyun_id, order_id)
|
||||
logger.info(f'通过同用户的其他订单来同步客户订单信息到星云成功: {contact_orm.xingyun_id} -》 {order_id}')
|
||||
break
|
||||
|
||||
if shop_name and not contact_orm.shop_name:
|
||||
contact_orm.shop_name = order.get('shop_name', '')
|
||||
await contact_orm.save()
|
||||
|
||||
contact_orm.xingyun_sync = True
|
||||
|
||||
async def load_all_user_from_xingyun(self, task_id: str, add_time_start: datetime, add_time_end: datetime):
|
||||
logger.info(f'从星云加载客户数据,时间范围 {task_id} {add_time_start} - {add_time_end}')
|
||||
contact_iter = list_all_contact_by_addtime_and_tag(xy_client, task_id=task_id, keyword='', add_time_start=add_time_start, add_time_end=add_time_end)
|
||||
|
||||
return await self._load_user_from_xingyun(contact_iter, eager_load=True)
|
||||
|
||||
async def load_user_from_xingyun(self, add_time_start: datetime, add_time_end: datetime):
|
||||
logger.info(f'从星云加载客户数据,时间范围 {add_time_start} - {add_time_end}')
|
||||
contact_iter = list_all_contact_by_addtime(xy_client, keyword='', add_time_start=add_time_start, add_time_end=add_time_end)
|
||||
|
||||
return await self._load_user_from_xingyun(contact_iter)
|
||||
|
||||
async def _load_user_from_xingyun(self, contact_iter: list, eager_load: bool = False):
|
||||
logger.info(f'从星云加载客户数据,开始同步')
|
||||
load_result = {"update_count": 0, "add_count": 0}
|
||||
async for contact_list in async_split_generator(contact_iter, 10):
|
||||
try: result = await self.save_xingyun_contact_info(contact_list)
|
||||
except Exception as e:
|
||||
logger.error(f'从星云加载客户数据,同步失败 {e}')
|
||||
logger.exception(e)
|
||||
raise e
|
||||
|
||||
# 全部都已经同步完了
|
||||
if not eager_load and result.get("update_count", 0) + result.get("add_count", 0) == 0:
|
||||
logger.info(f'从星云加载客户数据,全部同步完成')
|
||||
break
|
||||
|
||||
load_result['update_count'] += result.get("update_count", 0)
|
||||
load_result['add_count'] += result.get("add_count", 0)
|
||||
logger.info(f'从星云加载客户数据{eager_load},更新 {result.get("update_count", 0)} 条,新增 {result.get("add_count", 0)} 条')
|
||||
logger.info(f'从星云加载客户数据,总 {load_result["update_count"] + load_result["add_count"]} 条,更新 {load_result["update_count"]} 条,新增 {load_result["add_count"]} 条')
|
||||
await self.sync_xingyun_contact_info()
|
||||
|
||||
return load_result
|
||||
|
||||
async def save_xingyun_contact_info(self, contact_list: list):
|
||||
contact_dict = {contact.get('cid'): contact for contact in contact_list}
|
||||
contact_orm_list: List[WeixinCustomer] = await self.model.filter(xingyun_id__in=contact_dict.keys())
|
||||
|
||||
field_names = set()
|
||||
update_list = []
|
||||
|
||||
async def update(contact_dict, contact_orm_list, key):
|
||||
history = {}
|
||||
for contact_orm in contact_orm_list:
|
||||
value = getattr(contact_orm, key)
|
||||
contact = contact_dict.pop(value, history.get(value, {}))
|
||||
if not contact: continue
|
||||
history[value] = contact
|
||||
|
||||
try:
|
||||
contact_in = WeixinCustomerXingyunCreate(**contact).transform_to_extra()
|
||||
except Exception as e:
|
||||
logger.error(f'客户数据转换错误,chat_id: {contact}, {e}')
|
||||
continue
|
||||
# print(type(contact_in), contact_in)
|
||||
updated = False
|
||||
|
||||
contact_in_dict = contact_in.model_dump(exclude_unset=True)
|
||||
field_names.update(contact_in_dict.keys())
|
||||
|
||||
for field_name in field_names:
|
||||
|
||||
old_value = getattr(contact_orm, field_name)
|
||||
value = contact_in_dict.get(field_name)
|
||||
if old_value != value:
|
||||
setattr(contact_orm, field_name, value)
|
||||
updated = True
|
||||
|
||||
new_extra = {**(contact_orm.extra or {}), **contact_in.extra}
|
||||
if contact_in.extra != new_extra:
|
||||
if new_extra != contact_orm.extra:
|
||||
contact_orm.extra = new_extra
|
||||
updated = True
|
||||
|
||||
# 触发同步信息到星云
|
||||
if updated and not contact_orm.xingyun_sync and contact_orm.xingyun_id:
|
||||
try:
|
||||
await self.bind_xingyun_user(contact_orm)
|
||||
update_list.append(contact_orm)
|
||||
except Exception as e:
|
||||
logger.error(f'客户数据同步错误,xingyun_id: {contact_orm.xingyun_id}, {e}')
|
||||
continue
|
||||
|
||||
if update_list:
|
||||
await self.model.bulk_update(update_list, list(field_names) + ['extra', 'xingyun_sync'])
|
||||
|
||||
await update(contact_dict, contact_orm_list, 'xingyun_id')
|
||||
|
||||
add_dict = {}
|
||||
for contact in contact_dict.values():
|
||||
externalUserid = contact.get('externalUserid', None)
|
||||
if not externalUserid:
|
||||
continue
|
||||
try:
|
||||
resp = await from_service_external_userid(wk_client, externalUserid)
|
||||
except Exception as e:
|
||||
logger.error(f'从星云加载客户数据,获取用户ID失败,externalUserid: {externalUserid}, {e}')
|
||||
continue
|
||||
if not resp or not resp.get('external_userid'):
|
||||
continue
|
||||
|
||||
external_userid = resp.get('external_userid')
|
||||
contact['external_userid'] = external_userid
|
||||
add_dict[external_userid] = contact
|
||||
|
||||
contact_orm_list = await self.model.filter(weixin_id__in=add_dict.keys())
|
||||
await update(add_dict, contact_orm_list, 'weixin_id')
|
||||
|
||||
add_list = []
|
||||
for contact in add_dict.values():
|
||||
contact_in = WeixinCustomerXingyunCreate(**contact).transform_to_extra()
|
||||
add_list.append(self.model(**contact_in.model_dump(exclude_unset=True)))
|
||||
|
||||
if add_list:
|
||||
await self.model.bulk_create(add_list)
|
||||
|
||||
return {
|
||||
'update_count': len(update_list),
|
||||
'add_count': len(add_list),
|
||||
}
|
||||
|
||||
async def bind_user(self, bind_info: WeixinUserBindInfo):
|
||||
return await self.bind_user(bind_info)
|
||||
|
||||
async def monitor_erp_order(self):
|
||||
# 从领淘ERP加载订单相关用户
|
||||
# 获取上一个订单的创建时间
|
||||
cache_file = 'env/order_cache.pkl'
|
||||
newest_order_create_time = None
|
||||
today_end = datetime.now().replace(hour=23, minute=59, second=59, microsecond=0)
|
||||
try:
|
||||
with open(cache_file, 'rb') as f:
|
||||
last_order = pickle.load(f)
|
||||
newest_order_create_time = last_order.get('create_time')
|
||||
logger.info(f'从缓存文件加载最新订单创建时间,{newest_order_create_time}')
|
||||
except FileNotFoundError:
|
||||
newest_order_create_time = today_end.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
newest_order_create_time = newest_order_create_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
order_list = [o async for o in get_order_relative_user(lintao_client, start_date=newest_order_create_time, end_date=today_end.strftime('%Y-%m-%d %H:%M:%S'))]
|
||||
msg_list, newest_order = await self.find_bind_order(order_list)
|
||||
|
||||
# 保存最新订单到缓存文件
|
||||
if newest_order:
|
||||
newest_order['create_time'] = newest_order['create_time'].replace('T', ' ')
|
||||
with open(cache_file, 'wb') as f:
|
||||
pickle.dump(newest_order, f)
|
||||
|
||||
return msg_list
|
||||
|
||||
async def find_bind_order(self, order_list: list):
|
||||
|
||||
buyer_order_dict = {}
|
||||
newest_order = None
|
||||
last_erp_id = 0
|
||||
|
||||
order_id_set = set()
|
||||
for order in order_list:
|
||||
erp_id = order.get('id')
|
||||
if erp_id <= 6368700: break
|
||||
|
||||
create_time = order.get('create_time')
|
||||
if last_erp_id < erp_id:
|
||||
if create_time: newest_order = order
|
||||
last_erp_id = erp_id
|
||||
|
||||
order_id = order.get('trade_no')
|
||||
if order_id in order_id_set: continue
|
||||
order_id_set.add(order_id)
|
||||
|
||||
users = order.get("users", [])
|
||||
buyer_id_set = set()
|
||||
for user in users:
|
||||
role = user.get('role')
|
||||
if role != 'buyer': continue
|
||||
buyer_id = user.get('id')
|
||||
if not buyer_id: continue
|
||||
if buyer_id in buyer_id_set: continue
|
||||
buyer_id_set.add(buyer_id)
|
||||
|
||||
buyer_order_dict.setdefault(buyer_id, []).append(order)
|
||||
|
||||
customer_list = await self.model.filter(taobao_id__in=buyer_order_dict.keys()).all()
|
||||
|
||||
# 通知新订单
|
||||
# (客户id)-(所属企微客服)刚在(店铺名)下单-(金额)
|
||||
msg_set = set()
|
||||
msg_list = []
|
||||
for customer_orm in customer_list:
|
||||
order_list = buyer_order_dict.get(customer_orm.taobao_id, [])
|
||||
|
||||
order_info = [
|
||||
f"在({order.get('shop_name')})下单{order.get('price')}元,单号({order.get('trade_no')})"
|
||||
for order in order_list
|
||||
]
|
||||
chat_info = await get_external_user_chat_info(customer_orm.weixin_id)
|
||||
follow_user_list = chat_info.get('follow_user', [])
|
||||
|
||||
for follow_user in follow_user_list:
|
||||
follow_userid = follow_user.get('userid')
|
||||
weixin_user_orm: WeixinUser = await WeixinUser.filter(userid=follow_userid).first()
|
||||
owner_name = '未知'
|
||||
if weixin_user_orm: owner_name = weixin_user_orm.english_name or weixin_user_orm.username
|
||||
qiwei_customer_name = customer_orm.taobao_name or customer_orm.weixin_name
|
||||
|
||||
content = f"({qiwei_customer_name}) -({owner_name})- 刚{', '.join(order_info)}"
|
||||
msg_hash_id = hashlib.md5(content.encode('utf-8')).hexdigest()
|
||||
if msg_hash_id in msg_set: continue
|
||||
msg_set.add(msg_hash_id)
|
||||
|
||||
# 检查消息是否已存在
|
||||
msg_orm = await msg_controller.model.filter(hash_id=msg_hash_id).first()
|
||||
if msg_orm: continue
|
||||
|
||||
msg = msg_controller.model(
|
||||
hash_id=msg_hash_id,
|
||||
title=qiwei_customer_name,
|
||||
content=content,
|
||||
detail=order_list,
|
||||
type=MsgType.NEW_ORDER,
|
||||
owner_id=follow_userid,
|
||||
owner_name=owner_name
|
||||
)
|
||||
logger.info(f'创建新订单通知消息,{msg.content}')
|
||||
msg_list.append(msg)
|
||||
|
||||
if msg_list:
|
||||
await msg_controller.model.bulk_create(msg_list)
|
||||
|
||||
return msg_list, newest_order
|
||||
|
||||
async def get_customer_by_weixin_id(self, weixin_id: str):
|
||||
async with cache_if(f'customer:user_datail_0:{weixin_id}', ttl=3600*24) as cache:
|
||||
if cache.hit:
|
||||
result = cache.value
|
||||
return result
|
||||
else:
|
||||
result = await self._get_customer_by_weixin_id(weixin_id)
|
||||
cache.set(result)
|
||||
return result
|
||||
|
||||
async def _get_customer_by_weixin_id(self, weixin_id: str):
|
||||
chat_info = await get_external_user_chat_info(weixin_id)
|
||||
follow_user_list = chat_info.get('follow_user', [])
|
||||
external_contact = chat_info.get('external_contact', {})
|
||||
|
||||
if external_contact:
|
||||
customer_orm = await self.model.filter(weixin_id=weixin_id).first()
|
||||
if customer_orm:
|
||||
customer_orm.weixin_avatar = external_contact.get('avatar')
|
||||
customer_orm.weixin_unionid = external_contact.get('unionid')
|
||||
customer_orm.weixin_name = external_contact.get('name')
|
||||
await customer_orm.save()
|
||||
|
||||
user_ids = [follow_user.get('userid') for follow_user in follow_user_list]
|
||||
weixin_user_orm_list = await WeixinUser.filter(userid__in=user_ids).all()
|
||||
weixin_user_orm_dict = {weixin_user_orm.userid: weixin_user_orm for weixin_user_orm in weixin_user_orm_list}
|
||||
for follow_user in follow_user_list:
|
||||
follow_userid = follow_user.get('userid')
|
||||
weixin_user_orm = weixin_user_orm_dict.get(follow_userid)
|
||||
|
||||
if weixin_user_orm:
|
||||
follow_user['username'] = weixin_user_orm.english_name or weixin_user_orm.username
|
||||
|
||||
return {
|
||||
**external_contact,
|
||||
'follow_user_list': follow_user_list,
|
||||
}
|
||||
|
||||
# for follow_user in follow_user_list:
|
||||
# follow_userid = follow_user.get('userid')
|
||||
|
||||
async def get_user_detail(self, weixin_id: str):
|
||||
async with cache_if(f'customer:user_datail:{weixin_id}', ttl=3600) as cache:
|
||||
if cache.hit:
|
||||
result = cache.value
|
||||
return result
|
||||
else:
|
||||
customer = await weixin_customer_controller.model.get_or_none(weixin_id=weixin_id)
|
||||
# if customer: return customer.to_dict()
|
||||
print(f'get_user_detail, weixin_id: {weixin_id}, customer: {customer}')
|
||||
|
||||
# 通过中间表 CustomerGroup 查询其加入的所有群
|
||||
memberships = await CustomerGroup.filter(
|
||||
customer=customer
|
||||
).prefetch_related('group') # 预加载 group 信息
|
||||
|
||||
group_list = []
|
||||
result = {'group_list': group_list}
|
||||
for m in memberships:
|
||||
data = await m.to_dict()
|
||||
# data['group'] = await m.group.to_dict()
|
||||
data['chat_name'] = m.group.name
|
||||
data['create_time'] = m.group.create_time
|
||||
group_list.append(data)
|
||||
|
||||
chat_info = await self.get_customer_by_weixin_id(weixin_id)
|
||||
result['user_detail'] = chat_info
|
||||
cache.set(result)
|
||||
|
||||
return result
|
||||
|
||||
# 同步星云和数据库中的客户信息: 同步绑定信息到星云、从星云中加载订单信息
|
||||
async def sync_xingyun_contact_info(self, eager_load: bool = False):
|
||||
# 从星云同步绑定信息到本地
|
||||
await self._sync_xingyun_contact_info(eager_load=eager_load, order_id__isnull=False)
|
||||
# 从本地同步订单信息到星云
|
||||
await self._sync_xingyun_contact_info(eager_load=eager_load, order_id__isnull=True, limit=10)
|
||||
|
||||
async def _sync_xingyun_contact_info(self, eager_load: bool = False, max_try: int = 2, limit: int = 20, **query_kwargs):
|
||||
query_kwargs = query_kwargs or {}
|
||||
|
||||
last_id = 0
|
||||
while eager_load or max_try > 0:
|
||||
max_try -= 1
|
||||
if last_id:
|
||||
contact_orm_list = await self.model.filter(xingyun_sync=False, xingyun_id__isnull=False, id__lt=last_id, **query_kwargs).order_by('-id').limit(limit).all()
|
||||
else:
|
||||
contact_orm_list = await self.model.filter(xingyun_sync=False, xingyun_id__isnull=False, **query_kwargs).order_by('-id').limit(limit).all()
|
||||
|
||||
if not contact_orm_list: break
|
||||
logger.debug(f'同步客户数据,{len(contact_orm_list)}')
|
||||
|
||||
has_new = False
|
||||
update_list = []
|
||||
for contact_orm in contact_orm_list:
|
||||
last_id = contact_orm.id
|
||||
has_new = True
|
||||
|
||||
if contact_orm.need_confirm:
|
||||
logger.info(f'客户需要确认绑定: {contact_orm}')
|
||||
continue
|
||||
|
||||
if contact_orm.order_id:
|
||||
try:
|
||||
await self.bind_xingyun_user(contact_orm)
|
||||
update_list.append(contact_orm)
|
||||
except Exception as e:
|
||||
logger.error(f'同步客户订单信息到星云错误,xingyun_id: {contact_orm.xingyun_id}, {e}')
|
||||
continue
|
||||
else:
|
||||
# list_user_order
|
||||
try:
|
||||
order_list = list_user_order(xy_client, cid=contact_orm.xingyun_id)
|
||||
except Exception as e:
|
||||
logger.error(f'客户数据同步错误,xingyun_id: {contact_orm.xingyun_id}, {e}')
|
||||
logger.exception(e)
|
||||
continue
|
||||
|
||||
shopNameSet, order_id_list = set(), []
|
||||
async for order in order_list:
|
||||
shopName = order.get('shopName')
|
||||
if shopName in shopNameSet:
|
||||
continue
|
||||
shopNameSet.add(shopName)
|
||||
order_id_list.append(order.get('orderId'))
|
||||
# 多店铺用的用的名称和ID都一样,所以不区分
|
||||
break
|
||||
|
||||
for order_id in order_id_list:
|
||||
try:
|
||||
await self.bind_order(WeixinOrderBindInfo(
|
||||
userid=contact_orm.weixin_id,
|
||||
order_id=order_id,
|
||||
))
|
||||
except AssertionError as e:
|
||||
logger.error(f'同步客户订单信息到本地错误,xingyun_id: {contact_orm.xingyun_id}, {e}')
|
||||
logger.exception(e)
|
||||
continue
|
||||
contact_orm.xingyun_sync = True
|
||||
update_list.append(contact_orm)
|
||||
logger.info(f'同步客户订单信息到本地: {contact_orm.xingyun_id} -》 {order_id}')
|
||||
|
||||
if update_list:
|
||||
await self.model.bulk_update(update_list, ['xingyun_sync'])
|
||||
|
||||
if not has_new: break
|
||||
|
||||
weixin_customer_controller = WeixinCustomerController()
|
||||
@@ -0,0 +1,408 @@
|
||||
from tortoise.expressions import Q
|
||||
|
||||
from app.core.crud import CRUDBase
|
||||
from app.schemas.weixin import (
|
||||
WeixinGroupChatCreate,
|
||||
WeixinGroupChatUpdate,
|
||||
WeixinGroupChatXingyunCreate,
|
||||
)
|
||||
|
||||
from some_sdk.services.binder import wk_client, lintao_client, xy_client
|
||||
from some_sdk.wk_weixin_sdk.apis import corp_group
|
||||
from some_sdk.wk_weixin_sdk.apis.extern_user import get_external_user_chat_info
|
||||
from some_sdk.xingyun_sdk.apis.work_user import list_work_group
|
||||
from some_sdk.xingyun_sdk.apis.customer import list_user_join_group
|
||||
|
||||
from some_sdk.lintao_sdk.biz.by_order import get_order_relative_user
|
||||
from some_sdk.lintao_sdk.apis.orderlist import list_product_raw, get_order_log
|
||||
|
||||
from app.models.weixin import WeixinGroupChat, RoleType, CustomerGroup
|
||||
from app.controllers.weixin.user import weixin_user_controller
|
||||
from app.controllers.weixin.customer import weixin_customer_controller
|
||||
from app.utils.common import async_split_generator, async_generator_to_list
|
||||
from app.core.cache import cache_if, invalidate_cache
|
||||
|
||||
import re
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ROLE_LIST = [role.value for role in RoleType]
|
||||
|
||||
class WeixinGroupChatController(CRUDBase[WeixinGroupChat, WeixinGroupChatCreate, WeixinGroupChatUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=WeixinGroupChat)
|
||||
|
||||
async def get_external_group_chat_info(self, chat_id: str):
|
||||
group_info = await corp_group.get_external_group_chat_info(wk_client, chat_id)
|
||||
|
||||
internal_member_list = group_info['internal_member_list']
|
||||
external_member_list = group_info['external_member_list']
|
||||
group_name = group_info.get('name')
|
||||
|
||||
external_member_list.sort(key=lambda user: user.get('join_time'))
|
||||
|
||||
unique_dict = {
|
||||
'name': group_name,
|
||||
'external_user_count': len(external_member_list),
|
||||
'internal_member_count': len(internal_member_list),
|
||||
}
|
||||
group_info.update(unique_dict)
|
||||
# 同步更新数据库
|
||||
status, obj = await self.create_or_update(group_info, query_kwargs={'chat_id': chat_id}, update_kwargs=unique_dict)
|
||||
if not status:
|
||||
logger.debug(f'群聊数据不变,不用更新,chat_id: {chat_id}')
|
||||
# elif status == 'create':
|
||||
# await self.create_customer_group(chat_id)
|
||||
else:
|
||||
# 群更新:触发数据同步事件
|
||||
# event_manager.subscribe("group_chat_updated", update_cache_handler, EventPriority.MEDIUM)
|
||||
|
||||
# 只保存准确无误的数据
|
||||
# buyer_nick = get_buyer_nick_from_group_name(group_name)
|
||||
# logger.debug(f'group_name: {group_name}; buyer_nick: {buyer_nick}')
|
||||
# if not buyer_nick:
|
||||
# logger.error(f'群聊未命名,无法绑定订单,chat_id: {chat_id}')
|
||||
# return group_info
|
||||
|
||||
# self.load_erp_order_info_by_group(buyer_nick, internal_member_list, external_member_list)
|
||||
|
||||
update_result1 = await weixin_user_controller.update_from_group([*internal_member_list])
|
||||
update_result2 = await weixin_customer_controller.update_from_group([*external_member_list])
|
||||
logger.debug(f'群聊用户数据更新结果,chat_id: {chat_id}, {update_result1}')
|
||||
logger.debug(f'群聊客户数据更新结果,chat_id: {chat_id}, {update_result2}')
|
||||
# 刷新用户缓存
|
||||
for user in [*internal_member_list, *external_member_list]:
|
||||
await self.expire_user_cache(user.get('userid'))
|
||||
|
||||
group_info['is_create'] = status == 'create'
|
||||
group_info['detect_update'] = status != False
|
||||
return group_info
|
||||
|
||||
# 从星云有客中加载群聊信息
|
||||
async def load_xingyun_group_info(self, chat_name: str=''):
|
||||
# 检索出最新的记录,只加载最新的记录
|
||||
newest_group = await self.model.filter(xingyun_chat_id__isnull=False).order_by('-create_time').first()
|
||||
newest_time = newest_group.create_time if newest_group else 1735660800 # 2025-01-01 00:00:00
|
||||
|
||||
load_result = {"update_count": 0, "add_count": 0}
|
||||
group_iter = list_work_group(xy_client, chatName=chat_name)
|
||||
async for group_list in async_split_generator(group_iter):
|
||||
if group_list[-1].get('createTime') < newest_time:
|
||||
break
|
||||
logger.debug(f'处理群聊信息,chat_name: {chat_name}, {len(group_list)}')
|
||||
result = await self.save_xingyun_group_info(group_list)
|
||||
load_result['update_count'] += result['update_count']
|
||||
load_result['add_count'] += result['add_count']
|
||||
logger.debug(f'群聊数据更新结果,chat_name: {chat_name}, {result}')
|
||||
|
||||
if load_result['add_count'] + load_result['update_count'] > 300:
|
||||
break
|
||||
|
||||
await self.refresh_xingyun_group_info()
|
||||
return load_result
|
||||
|
||||
async def save_xingyun_group_info(self, group_list: list):
|
||||
group_dict = {group.get('groupChatId'): group for group in group_list}
|
||||
group_orm_list = await self.model.filter(chat_id__in=group_dict.keys())
|
||||
|
||||
update_list = []
|
||||
history = {}
|
||||
for group_orm in group_orm_list:
|
||||
group = group_dict.pop(group_orm.chat_id, history.get(group_orm.chat_id, {}))
|
||||
if not group: continue
|
||||
history[group_orm.chat_id] = group
|
||||
|
||||
try:
|
||||
group_in = WeixinGroupChatXingyunCreate(**group)
|
||||
except Exception as e:
|
||||
logger.error(f'群聊数据转换错误,chat_id: {group}, {e}')
|
||||
continue
|
||||
updated = False
|
||||
if group_in.avatars:
|
||||
group_orm.avatars = group_in.avatars
|
||||
updated = True
|
||||
if group_orm.xingyun_chat_id != group_in.xingyun_chat_id:
|
||||
group_orm.xingyun_chat_id = group_in.xingyun_chat_id
|
||||
updated = True
|
||||
if updated:
|
||||
update_list.append(group_orm)
|
||||
if update_list:
|
||||
await self.model.bulk_update(update_list, ['avatars', 'xingyun_chat_id'])
|
||||
|
||||
add_list = []
|
||||
for chat_id, group in group_dict.items():
|
||||
group_in = WeixinGroupChatXingyunCreate(**group)
|
||||
add_list.append(self.model(**group_in.model_dump(exclude_unset=True)))
|
||||
|
||||
if add_list:
|
||||
await self.model.bulk_create(add_list)
|
||||
|
||||
return {
|
||||
'update_count': len(update_list),
|
||||
'add_count': len(add_list),
|
||||
}
|
||||
|
||||
# 刷新数据库中的群聊信息
|
||||
async def refresh_xingyun_group_info(self):
|
||||
|
||||
for _ in range(10):
|
||||
group_orm_list = await self.model.filter(xingyun_chat_id__isnull=False, external_user_count__isnull=True).order_by('-create_time').limit(20).all()
|
||||
if not group_orm_list: break
|
||||
logger.debug(f'刷新群聊数据,{len(group_orm_list)}')
|
||||
|
||||
for group_orm in group_orm_list:
|
||||
try:
|
||||
await self.get_external_group_chat_info(group_orm.chat_id)
|
||||
except Exception as e:
|
||||
logger.error(f'刷新群聊数据错误,chat_id: {group_orm.chat_id}, {e}')
|
||||
continue
|
||||
logger.debug(f'刷新群聊数据结果,chat_id: {group_orm.name} 已更新')
|
||||
|
||||
# 同步erp中的用户和客户信息到数据库
|
||||
async def load_erp_order_info_by_group(self, buyer_nick: str, internal_member_list: list, external_member_list: list):
|
||||
if not buyer_nick:
|
||||
logger.error(f'群聊未命名,无法绑定订单,buyer_nick: {buyer_nick}')
|
||||
return
|
||||
|
||||
orders = await async_generator_to_list(get_order_relative_user(lintao_client, buyer_nick=buyer_nick))
|
||||
assert orders, f'未找到订单,buyer_nick: {buyer_nick}'
|
||||
|
||||
internal_member_dict = {user.get('name'): user for user in internal_member_list}
|
||||
|
||||
order = orders[0]
|
||||
|
||||
order_relative_user = order.get("users", [])
|
||||
order_relative_user_dict = {user.get('name'): user for user in order_relative_user}
|
||||
|
||||
for name, user in order_relative_user_dict.items():
|
||||
erp_id = user.get('id', '')
|
||||
if not erp_id: continue
|
||||
if re.match(r'^\d+$', erp_id) is None:
|
||||
# 客户
|
||||
if not external_member_list:
|
||||
logger.error(f'客户群聊用户数为0,无法绑定客户, name: {name}, erp_id: {erp_id}')
|
||||
continue
|
||||
if name != buyer_nick:
|
||||
logger.error(f'客户群聊用户与ERP卖家名称不一致,无法绑定客户, name: {name}, erp_id: {erp_id}')
|
||||
continue
|
||||
if len(external_member_list) != 1:
|
||||
external_member_list.sort(key=lambda user: user.get('join_time'))
|
||||
logger.warning(f'客户群聊用户数不是1个,将选取最先入群的用户作为客户,name: {name}, erp_id: {erp_id}')
|
||||
|
||||
for member in external_member_list:
|
||||
member['order_id'] = order.get('trade_no')
|
||||
|
||||
weixin_user = external_member_list[0]
|
||||
weixin_user.update({
|
||||
'order_id': order.get('trade_no'),
|
||||
'taobao_id': user.get('id', ''),
|
||||
'taobao_name': user.get('name', ''),
|
||||
'erp_message': user.get('message', ''),
|
||||
'erp_remark': user.get('memo', ''),
|
||||
'need_confirm': len(external_member_list) > 1,
|
||||
})
|
||||
|
||||
continue
|
||||
else:
|
||||
# 员工
|
||||
erp_id = int(erp_id)
|
||||
weixin_user = internal_member_dict.get(name)
|
||||
logger.debug(f'name: {name}, erp_id: {erp_id}, weixin_user: {weixin_user}')
|
||||
if not weixin_user:
|
||||
logger.error(f'{internal_member_dict.keys()} {name}')
|
||||
logger.error(f'客户群聊用户与ERP员工名称不一致,无法绑定员工, name: {name}, erp_id: {erp_id}')
|
||||
continue
|
||||
weixin_user.update({
|
||||
'erp_id': erp_id,
|
||||
'erp_name': name,
|
||||
'role': user.get('role', ''),
|
||||
})
|
||||
continue
|
||||
|
||||
# 通过客户ID获取客户的所有订单
|
||||
async def get_order_relative_user_list_by_weixin_userid(self, user_id: str):
|
||||
customers = await weixin_customer_controller.model.filter(weixin_id=user_id)
|
||||
if not customers:
|
||||
await weixin_customer_controller.create({
|
||||
"weixin_id": user_id,
|
||||
})
|
||||
# 新用户:触发数据同步事件
|
||||
return False, []
|
||||
|
||||
order_list = []
|
||||
buyer_set = set()
|
||||
for customer in customers:
|
||||
buyer_nick = customer.taobao_name
|
||||
buyer_id = customer.taobao_id
|
||||
if buyer_nick:
|
||||
logger.debug(f'name: {user_id}; buyer_nick: {buyer_nick}')
|
||||
if buyer_nick in buyer_set: continue
|
||||
buyer_set.add(buyer_nick)
|
||||
orders = await async_generator_to_list(get_order_relative_user(lintao_client, buyer_nick=buyer_nick))
|
||||
elif buyer_id:
|
||||
if buyer_id in buyer_set: continue
|
||||
buyer_set.add(buyer_id)
|
||||
orders = await async_generator_to_list(get_order_relative_user(lintao_client, buyer_id=buyer_id))
|
||||
else:
|
||||
assert buyer_nick, "当前客户未绑定淘宝账号"
|
||||
|
||||
logger.debug(f'orders.length: {len(orders)}')
|
||||
order_list.extend(orders)
|
||||
|
||||
# for order in orders:
|
||||
# 原地按照id排序
|
||||
order_list.sort(key=lambda order: order.get('id'), reverse=True)
|
||||
if order_list:
|
||||
await self.get_user_detail_by_order(order=order_list[0])
|
||||
|
||||
return True, order_list
|
||||
|
||||
# 根据客户名字,获取群聊中客户的所有订单
|
||||
async def get_order_relative_user_list_by_weixin_group_name(self, buyer_nick: str):
|
||||
async with cache_if(f'order:relative_user:{buyer_nick}', ttl=10) as cache:
|
||||
if cache.hit:
|
||||
order_list = cache.value
|
||||
logger.debug(f'{cache.key} hit')
|
||||
else:
|
||||
logger.debug(f'{cache.key} not hit')
|
||||
order_list = await async_generator_to_list(get_order_relative_user(lintao_client, buyer_nick=buyer_nick))
|
||||
cache.set(order_list)
|
||||
logger.debug(f'order_list.length: {len(order_list)}')
|
||||
|
||||
# for order in order_list:
|
||||
order_list.sort(key=lambda order: order.get('id'), reverse=True)
|
||||
if order_list:
|
||||
await self.get_user_detail_by_order(order=order_list[0])
|
||||
order_list[0]['__type__'] = 'load_order_by_weixin_group_name'
|
||||
|
||||
return order_list
|
||||
|
||||
# 根据客户ID,获取群聊中客户的所有订单
|
||||
async def get_order_relative_user_list_by_weixin_group_buyer_id(self, buyer_id: str):
|
||||
|
||||
orders = await async_generator_to_list(get_order_relative_user(lintao_client, buyer_id=buyer_id))
|
||||
logger.debug(f'orders.length: {len(orders)}')
|
||||
|
||||
# for order in orders:
|
||||
if orders:
|
||||
await self.get_user_detail_by_order(order=orders[0])
|
||||
|
||||
return orders
|
||||
|
||||
async def get_user_detail_by_order(self, order_id: str='', order: dict = None):
|
||||
if not order:
|
||||
orders = await async_generator_to_list(get_order_relative_user(lintao_client, trade_no=order_id))
|
||||
if not orders: return
|
||||
_orders = [o for o in orders if o.get('ctid') == order_id]
|
||||
# assert len(orders) == 1, f'订单号 {order_id} 对应多个订单'
|
||||
if not _orders:
|
||||
for order in orders:
|
||||
logger.warning(f'订单号 {order_id} 对应多个订单,{order}')
|
||||
# return {}
|
||||
_orders = orders
|
||||
|
||||
order = _orders[0]
|
||||
|
||||
# 缓存订单产品信息
|
||||
tid = order.get("ctid")
|
||||
async with cache_if(f'order:product:{tid}', ttl=3600*24) as cache:
|
||||
if cache.hit:
|
||||
product_resp = cache.value
|
||||
else:
|
||||
product_resp = (await list_product_raw(lintao_client, tid=tid)).get('data', {})
|
||||
if product_resp: cache.set(product_resp)
|
||||
|
||||
if product_resp:
|
||||
order['product_title'] = product_resp[0].get("title")
|
||||
order['product_pic'] = product_resp[0].get("pic_path")
|
||||
|
||||
order['has_fetched_user_detail'] = True
|
||||
user_list = order.get("users", [])
|
||||
|
||||
async with cache_if(f'user:auto_relate', ttl=600) as cache:
|
||||
if cache.hit:
|
||||
default_user_list = cache.value
|
||||
else:
|
||||
default_user_list = await weixin_user_controller.all(Q(auto_relate=True))
|
||||
default_user_list = [user.to_dict() for user in default_user_list]
|
||||
cache.set(default_user_list)
|
||||
default_user_set = {str(user.get('erp_id')) for user in default_user_list}
|
||||
|
||||
for user in user_list:
|
||||
erp_id = user.get('id', '')
|
||||
if erp_id in default_user_set: continue
|
||||
await self.get_order_user_info(user)
|
||||
|
||||
order['users'].extend(default_user_list)
|
||||
|
||||
# 根据 role_list 中的顺序进行排序
|
||||
role_order = {role: index for index, role in enumerate(ROLE_LIST)}
|
||||
order['users'].sort(key=lambda user: role_order.get(user['role'], len(ROLE_LIST)))
|
||||
|
||||
return order
|
||||
|
||||
async def get_order_user_info(self, user: dict=None):
|
||||
if not user: return
|
||||
erp_id = user.get('id', '')
|
||||
if not erp_id: return
|
||||
if re.match(r'^\d+$', erp_id) is None:
|
||||
# logger.debug(f'淘宝客户, erp_id: {erp_id}, user: {user}')
|
||||
# 客户
|
||||
taobao_id = user.get('id', '')
|
||||
query = Q(taobao_id=taobao_id)
|
||||
weixin_customer_list = await weixin_customer_controller.all(query)
|
||||
if not weixin_customer_list:
|
||||
logger.error(f'客户未绑定订单, taobao_id: {taobao_id}, user: {user}')
|
||||
return
|
||||
weixin_customer = weixin_customer_list[0]
|
||||
buyer = weixin_customer.to_dict()
|
||||
if not buyer.get('name') and user.get('name'):
|
||||
weixin_customer.taobao_name = user.get('name')
|
||||
await weixin_customer.save()
|
||||
user.update({**buyer, **user})
|
||||
else:
|
||||
# 员工
|
||||
erp_id = int(erp_id)
|
||||
# logger.debug(f'员工, erp_id: {erp_id}, user: {user}')
|
||||
query = Q(erp_id=erp_id)
|
||||
weixin_user_list = await weixin_user_controller.all(query)
|
||||
if not weixin_user_list:
|
||||
user['erp_id'] = erp_id
|
||||
logger.error(f'员工未绑定ERP, erp_id: {erp_id}, user: {user}')
|
||||
return
|
||||
weixin_user = weixin_user_list[0]
|
||||
user.update(weixin_user.to_dict())
|
||||
|
||||
# 获取erp日志
|
||||
async def get_erp_order_log(self, order_id: str):
|
||||
return await get_order_log(lintao_client, order_id)
|
||||
|
||||
async def expire_user_cache(self, weixin_id: str):
|
||||
await invalidate_cache(f'customer:user_datail:{weixin_id}')
|
||||
await invalidate_cache(f'customer:user_datail_0:{weixin_id}')
|
||||
|
||||
# async def list_user_join_group(self, xingyun_id: int):
|
||||
# assert xingyun_id, 'id 不能为空'
|
||||
# result = await async_generator_to_list(list_user_join_group(xy_client, cid=xingyun_id))
|
||||
|
||||
# xingyun_chat_ids = [item.get('id') for item in result]
|
||||
# all_group_chat = await self.model.filter(xingyun_chat_id__in=xingyun_chat_ids)
|
||||
# group_chat_dict = {item.xingyun_chat_id: item for item in all_group_chat}
|
||||
# logger.debug(f'list_user_join_group, xingyun_chat_ids: {xingyun_chat_ids}')
|
||||
|
||||
# remain_chat_ids = set()
|
||||
# for item in result:
|
||||
# group_chat_id = item.get('id')
|
||||
# if group_chat_id in group_chat_dict: continue
|
||||
# remain_chat_ids.add(group_chat_id)
|
||||
|
||||
# chat_name = item.get('chatName')
|
||||
# if not chat_name: continue
|
||||
# await self.load_xingyun_group_info(chat_name=chat_name)
|
||||
|
||||
# remain_chat_list = await self.model.filter(xingyun_chat_id__in=remain_chat_ids)
|
||||
# all_group_chat.extend(remain_chat_list)
|
||||
# return all_group_chat
|
||||
|
||||
weixin_group_chat_controller = WeixinGroupChatController()
|
||||
@@ -0,0 +1,169 @@
|
||||
from .customer import weixin_customer_controller
|
||||
from .user import weixin_user_controller
|
||||
from app.controllers.msg import msg_controller
|
||||
from app.controllers.action import action_controller
|
||||
from app.schemas.weixin import WeixinGroupChatEvent, BindOrderResultEvent, CustomerRepeatPurchaseEvent, CustomerAssignOrderEvent, CustomerRefundOrderEvent
|
||||
from app.utils.common import transform_pydantic_to_list
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from app.utils.event_task import event_manager, EventType
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
from typing import Any
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler as BackgroundScheduler
|
||||
|
||||
from app.controllers.automation.scenario import automation_scenario_controller
|
||||
from app.controllers.automation.task import task_controller
|
||||
|
||||
|
||||
sync_lock = asyncio.Lock()
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def sync_xingyun_contact_info_async(event_type, *args, **kwargs):
|
||||
if os.getenv("APP_ENV") != "prod":
|
||||
logger.info(f'非生产环境,不实际同步客户数据,{args} {kwargs}')
|
||||
return
|
||||
|
||||
logger.info(f'同步客户数据,{args} {kwargs}')
|
||||
now_time = datetime.now()
|
||||
start_time = (now_time - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end_time = now_time + timedelta(days=1)
|
||||
await weixin_customer_controller.load_user_from_xingyun(add_time_start=start_time, add_time_end=end_time)
|
||||
|
||||
async def load_all_user_from_xingyun(event_type, *args, **kwargs):
|
||||
if os.getenv("APP_ENV") != "prod":
|
||||
logger.info(f'非生产环境,不实际从星云加载全量客户数据,{args} {kwargs}')
|
||||
return
|
||||
|
||||
logger.info(f'从星云加载全量客户数据,{args} {kwargs}')
|
||||
start_time = datetime(2025, 6, 1)
|
||||
end_time = datetime.now() + timedelta(days=1)
|
||||
return await weixin_customer_controller.load_all_user_from_xingyun(task_id='load_all_user_from_xingyun', add_time_start=start_time, add_time_end=end_time)
|
||||
|
||||
async def monitor_erp_order(event_type, *args, **kwargs):
|
||||
if os.getenv("APP_ENV") != "prod":
|
||||
logger.info(f'非生产环境,不实际从星云加载全量客户数据,{args} {kwargs}')
|
||||
return
|
||||
|
||||
now_time = datetime.now()
|
||||
logger.info(f'监控ERP订单,{args} {kwargs}')
|
||||
new_order_list = [1]
|
||||
new_order_list = await weixin_customer_controller.monitor_erp_order()
|
||||
if new_order_list:
|
||||
logger.info(f'监控到新订单,{new_order_list}')
|
||||
await msg_controller.sync_msg()
|
||||
logger.debug(f'耗时:{datetime.now() - now_time}')
|
||||
|
||||
async def update_erp_order(*args, **kwargs):
|
||||
logger.info(f'更新ERP订单,{args} {kwargs}')
|
||||
await msg_controller.get_order_user_days_before(2)
|
||||
|
||||
async def check_remark_action_is_done(*args, **kwargs):
|
||||
if os.getenv("APP_ENV") != "prod":
|
||||
logger.info(f'非生产环境,不实际检查备注操作是否完成,{args} {kwargs}')
|
||||
return
|
||||
|
||||
logger.info(f'检查备注操作是否完成,{args} {kwargs}')
|
||||
await action_controller.check_remark_action_is_done()
|
||||
|
||||
# 每天到点执行一次
|
||||
today = datetime.now()
|
||||
todayStart = today.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
if today.hour in [23] and today.minute >= 50:
|
||||
todayStart += timedelta(days=1)
|
||||
class_type = '早班'
|
||||
elif today.hour in [0, 1] and today.minute <= 10:
|
||||
class_type = '早班'
|
||||
elif today.hour in [16] and today.minute >= 50 or today.hour in [17,18] and today.minute <= 10:
|
||||
class_type = '晚班'
|
||||
else:
|
||||
return
|
||||
|
||||
date = todayStart.strftime('%Y-%m-%d')
|
||||
title = f'设置 {date} {class_type} 的员工活码'
|
||||
success_title = title+' 成功'
|
||||
fail_title = title+' 失败'
|
||||
|
||||
async with sync_lock:
|
||||
has_set = await msg_controller.model.filter(title__in=[success_title]).first()
|
||||
if has_set:
|
||||
logger.info(f'【已操作】{title}')
|
||||
return
|
||||
|
||||
logger.info(f'【开始】{title}')
|
||||
try:
|
||||
result, next_class = await weixin_user_controller.set_user_online(class_type=class_type, date=todayStart)
|
||||
except Exception as e:
|
||||
logger.exception(f'设置 {date} {class_type} 的员工活码失败,{e}')
|
||||
result, next_class = {}, None
|
||||
|
||||
is_failed = bool(list(filter(lambda x: x.get('fail') != 0, result.values()))) or not result
|
||||
logger.info(f'【完成】{title},结果:{result} {"失败" if is_failed else "成功"}')
|
||||
|
||||
result_str = '\n'.join([f'{k}:{v}' for k, v in result.items()])
|
||||
|
||||
logger.info(f'{success_title if not is_failed else fail_title},{result_str}')
|
||||
content = f'\n\n{result_str}'
|
||||
if result and not next_class:
|
||||
content += f'\n\n⚠️ 未设置下一班值班人员,请及时设置'
|
||||
logger.info(f'消息通知:{content}')
|
||||
await msg_controller.new_system_msg(title=success_title if not is_failed else fail_title, content=content)
|
||||
await msg_controller.sync_msg()
|
||||
|
||||
async def group_chat_update(event_type, group_info, *args):
|
||||
if os.getenv("APP_ENV") != "prod":
|
||||
logger.info(f'非生产环境,不实际检查群聊更新,{group_info} {args}')
|
||||
return
|
||||
|
||||
group_event = WeixinGroupChatEvent(**group_info)
|
||||
await sync_xingyun_contact_info_async()
|
||||
|
||||
|
||||
async def test_event(event_type, event_data=None, *args, **kwargs):
|
||||
# logger.info(f'测试事件,{args} {kwargs}')
|
||||
# scenario_list = await automation_scenario_controller.find_active_scenarios(event_type, event_data=event_data)
|
||||
# logger.info(f'触发事件,{len(scenario_list)}')
|
||||
# for scenario in scenario_list:
|
||||
# task = await task_controller.create_from_scenario(scenario=scenario, event_data=event_data)
|
||||
# logger.info(f'创建任务,{task}')
|
||||
pass
|
||||
|
||||
async def trigger_event(event_type: str, event_data: Any):
|
||||
"""解析事件数据"""
|
||||
logger.info(f'测试事件,{event_type} {event_data}')
|
||||
scenario_list = await automation_scenario_controller.find_active_scenarios(event_type, event_data=event_data)
|
||||
logger.info(f'触发事件,{len(scenario_list)}')
|
||||
for scenario, reason in scenario_list:
|
||||
logger.info(f'触发场景,{scenario},{reason}')
|
||||
try:
|
||||
task = await task_controller.create_from_scenario(scenario=scenario, event_data=event_data, reason=reason)
|
||||
logger.info(f'创建任务,{task}')
|
||||
except Exception as e:
|
||||
logger.error(f'创建任务失败,{e}')
|
||||
|
||||
# print(f'事件订阅:{transform_pydantic_to_list(WeixinGroupChatEvent)}')
|
||||
|
||||
event_manager.subscribe(EventType.SYNC_XINGYUN_CONTACT_INFO, load_all_user_from_xingyun)
|
||||
event_manager.subscribe(EventType.OPEN_PERSONAL_CHAT, sync_xingyun_contact_info_async)
|
||||
event_manager.subscribe(EventType.GROUP_CHAT_UPDATED, group_chat_update, input_model=transform_pydantic_to_list(WeixinGroupChatEvent))
|
||||
event_manager.subscribe(EventType.BIND_ORDER_FOR_USER, sync_xingyun_contact_info_async, input_model=transform_pydantic_to_list(BindOrderResultEvent))
|
||||
event_manager.subscribe(EventType.OLD_CUSTOMER_REPEAT_PURCHASE, test_event, input_model=transform_pydantic_to_list(CustomerRepeatPurchaseEvent))
|
||||
event_manager.subscribe(EventType.CUSTOMER_SERVICE_ASSIGN_ORDER, test_event, input_model=transform_pydantic_to_list(CustomerAssignOrderEvent))
|
||||
event_manager.subscribe(EventType.DESIGNER_ASSIGN_ORDER, test_event, input_model=transform_pydantic_to_list(CustomerAssignOrderEvent))
|
||||
event_manager.subscribe(EventType.CUSTOMER_RETURN_ORDER, test_event, input_model=transform_pydantic_to_list(CustomerRefundOrderEvent))
|
||||
event_manager.subscribe(EventType.DESIGNER_UPLOAD_DESIGN, test_event)
|
||||
|
||||
event_manager.register_pretask(trigger_event)
|
||||
|
||||
if os.getenv("APP_ENV") == "prod":
|
||||
logger.info('生产环境,启动定时任务')
|
||||
# 创建调度器
|
||||
scheduler = BackgroundScheduler()
|
||||
|
||||
# scheduler.add_job(monitor_erp_order, "interval", seconds=60) # 每90秒执行一次
|
||||
scheduler.add_job(update_erp_order, "interval", seconds=240) # 每90秒执行一次
|
||||
scheduler.add_job(check_remark_action_is_done, "interval", seconds=180) # 每90秒执行一次
|
||||
scheduler.start()
|
||||
@@ -0,0 +1,201 @@
|
||||
|
||||
from app.core.crud import CRUDBase
|
||||
from app.schemas.weixin import (
|
||||
WeixinUserCreate,
|
||||
WeixinUserUpdate,
|
||||
)
|
||||
|
||||
from app.models.weixin import WeixinUser
|
||||
from app.schemas.weixin import WeixinUserBindInfo
|
||||
|
||||
from some_sdk.services.binder import feishu_client, xy_client
|
||||
from some_sdk.feishu_sdk.biz.doc import search_file_record
|
||||
from some_sdk.xingyun_sdk.apis.channel import list_channel_group, list_channel_group_user_list, set_online_staff
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
def split_generator(iterable, chunk_size=50):
|
||||
"""
|
||||
将可迭代对象拆分为多个子列表
|
||||
"""
|
||||
chunk = []
|
||||
for item in iterable:
|
||||
chunk.append(item)
|
||||
if len(chunk) == chunk_size:
|
||||
yield chunk
|
||||
chunk = []
|
||||
if chunk:
|
||||
yield chunk
|
||||
|
||||
class WeixinUserController(CRUDBase[WeixinUser, WeixinUserCreate, WeixinUserUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=WeixinUser)
|
||||
|
||||
async def update_from_group(self, internal_members: list[dict]):
|
||||
# 同步更新数据库
|
||||
member_dict = {}
|
||||
for member in internal_members:
|
||||
member = member.copy()
|
||||
userid = member.get('userid', None)
|
||||
member['alias'] = member.pop('name', None)
|
||||
member_dict[userid] = member
|
||||
|
||||
member_list = await self.model.filter(userid__in=member_dict.keys())
|
||||
|
||||
update_list = []
|
||||
update_fileds = ['alias', 'role', 'erp_id', 'erp_name']
|
||||
|
||||
history = {}
|
||||
for member in member_list:
|
||||
member_info = member_dict.pop(member.userid, history.get(member.userid, {}))
|
||||
if member.role in ['owner', 'admin', 'follower']:
|
||||
continue
|
||||
if not member_info: continue
|
||||
history[member.userid] = member_info
|
||||
|
||||
updated = {}
|
||||
for field in update_fileds:
|
||||
old_value = getattr(member, field)
|
||||
if field not in member_info:
|
||||
continue
|
||||
value = member_info.get(field)
|
||||
if old_value != value:
|
||||
setattr(member, field, value)
|
||||
updated[f'{field}:{str(old_value)}'] = value
|
||||
if updated:
|
||||
logger.info(f'用户 {member.userid} 更新关系为 {updated}')
|
||||
update_list.append(member)
|
||||
if update_list:
|
||||
await self.model.bulk_update(update_list, fields=update_fileds)
|
||||
|
||||
# 新增用户
|
||||
create_list = []
|
||||
for userid, member_info in member_dict.items():
|
||||
create_list.append(WeixinUser(
|
||||
**member_info,
|
||||
name=member_info.get('alias'),
|
||||
))
|
||||
if create_list:
|
||||
await self.model.bulk_create(create_list)
|
||||
|
||||
return {
|
||||
'update_count': len(update_list),
|
||||
'create_count': len(create_list),
|
||||
}
|
||||
|
||||
|
||||
async def bind_user(self, user_in: WeixinUserBindInfo):
|
||||
if not user_in.userid:
|
||||
raise ValueError("用户ID不能为空")
|
||||
if user_in.erp_id == 0:
|
||||
del user_in.erp_id
|
||||
|
||||
user = await self.model.filter(userid=user_in.userid).first()
|
||||
if not user:
|
||||
await self.create({**user_in.model_dump(exclude_unset=True), 'name': user_in.username})
|
||||
return {}
|
||||
await self.model.filter(userid=user_in.userid).update(**user_in.model_dump(exclude_unset=True))
|
||||
|
||||
async def set_user_online(self, class_type: str, date: datetime):
|
||||
daysTimestamp = lambda nowStamp, days: nowStamp + days * 24 * 60 * 60 * 1000
|
||||
formatDate = lambda ts: datetime.fromtimestamp(ts / 1000).strftime('%Y-%m-%d')
|
||||
|
||||
today = int(date.timestamp() * 1000)
|
||||
logger.info(f'设置 {formatDate(today)} 班次为 {class_type} 的员工为在线状态')
|
||||
yestoday = daysTimestamp(today, -1)
|
||||
tomorrow = daysTimestamp(today, 1)
|
||||
|
||||
# 查询日期为 yestoday 到 后天 之间的所有记录
|
||||
resp = search_file_record(
|
||||
feishu_client,
|
||||
app_token="V4qebMG2kamCWMslJmdcQMbtngW",
|
||||
table_id="tblKgjc3F2jZ2inC",
|
||||
filter={
|
||||
"conjunction": "and",
|
||||
"conditions": [{
|
||||
"field_name": "日期",
|
||||
"operator": "isGreater",
|
||||
"value": ["ExactDate", yestoday]
|
||||
},{
|
||||
"field_name": "日期",
|
||||
"operator": "isLess",
|
||||
"value": ["ExactDate", daysTimestamp(today, 2)]
|
||||
}]
|
||||
},
|
||||
)
|
||||
|
||||
set_result = {}
|
||||
current_class = (class_type, today)
|
||||
next_class = ('早班', tomorrow) if class_type == '晚班' else ('晚班', today)
|
||||
|
||||
current_class_mapping = {}
|
||||
next_class_mapping = {}
|
||||
async for data in resp:
|
||||
user = data.get('data', {})
|
||||
user_online = ((user.get('班次') or '')[:2], user.get('日期', ''))
|
||||
# logger.info(f'用户{user.get("接量成员", "")} 在线时间为{user_online} 当前班次为{current_class} 下一班次为{next_class}')
|
||||
class_group = user.get('组别', '')
|
||||
class_mapping = None
|
||||
# 当前班次
|
||||
if user_online == current_class:
|
||||
class_mapping = current_class_mapping
|
||||
set_result[class_group] = {"接量成员": user.get('接量成员', '')}
|
||||
# 下一班次
|
||||
elif user_online == next_class:
|
||||
class_mapping = next_class_mapping
|
||||
if class_mapping is None:
|
||||
continue
|
||||
|
||||
if user.get('id', ''):
|
||||
class_mapping.setdefault(class_group, [])
|
||||
class_mapping[class_group].append(user.get('id', ''))
|
||||
|
||||
assert current_class_mapping, f"根据日期{current_class[1]} {formatDate(current_class[1])}没有找到班次为{class_type}的员工"
|
||||
logger.info(f'根据日期{current_class[1]} {formatDate(current_class[1])} 找到班次为{class_type}的员工: {current_class_mapping} {set_result}')
|
||||
if next_class_mapping:
|
||||
logger.info(f'根据日期{next_class[1]} {formatDate(next_class[1])} 找到班次为{next_class[0]}的员工: {next_class_mapping}')
|
||||
|
||||
group_list = list_channel_group(xy_client)
|
||||
async for group in group_list:
|
||||
title = group.get("title", "")
|
||||
if title not in current_class_mapping:
|
||||
continue
|
||||
|
||||
set_result[title] = set_result.get(title) or {}
|
||||
try:
|
||||
user_list = list_channel_group_user_list(xy_client, groupId=group.get("id", ""))
|
||||
|
||||
huomaList = []
|
||||
async for user in user_list:
|
||||
huomaList.append(user.get('id'))
|
||||
|
||||
if not huomaList:
|
||||
logger.error(f'组 {title} {group.get("id", "")} 中没有员工')
|
||||
continue
|
||||
|
||||
is_faild, is_success = [], []
|
||||
# 按照50个一组设置在线状态
|
||||
for i in range(0, len(huomaList), 50):
|
||||
result = await set_online_staff(xy_client, allDayUserIds=','.join(current_class_mapping[title]), huomaList=huomaList[i:i+50])
|
||||
|
||||
is_faild += list(filter(lambda x: x.get('errorMsg', ''), result.get('data', [])))
|
||||
is_success += list(filter(lambda x: x.get('success', True), result.get('data', [])))
|
||||
|
||||
is_faild = set(is_faild)
|
||||
if is_faild: logger.error(f'设置失败的员工: {is_faild}')
|
||||
else: logger.info(f'全部员工设置成功')
|
||||
|
||||
set_result[title] = {
|
||||
'success': len(is_success),
|
||||
'fail': len(is_faild),
|
||||
**set_result[title]
|
||||
}
|
||||
except Exception as e:
|
||||
logger.exception(f'设置群 {group.get("id", "")} 中员工 {huomaList} 为在线状态失败: {e}')
|
||||
|
||||
return set_result, next_class_mapping
|
||||
|
||||
weixin_user_controller = WeixinUserController()
|
||||
@@ -0,0 +1,4 @@
|
||||
def get_buyer_nick_from_group_name(group_name: str):
|
||||
buyer_nick = group_name.rsplit('-', 1)[0] if '服务群' in group_name else group_name
|
||||
return buyer_nick
|
||||
|
||||
Reference in New Issue
Block a user