Files
vue-fastapi-admin/app/controllers/weixin/customer.py
T
2026-06-25 17:41:06 +08:00

608 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()