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()