From a985b3205be532e86d7af2ee874f0378e5b7a73f Mon Sep 17 00:00:00 2001 From: aaron <> Date: Fri, 28 Mar 2025 19:28:43 +0800 Subject: [PATCH 01/21] update --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 50e2816..8e8a685 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,8 @@ ENV/ # Local development .env.local -*.db \ No newline at end of file +*.db + +# sqlite +*.sqlite +*.sqlite3 \ No newline at end of file From fcc0e6a43dc782bd51cbde3caca56360e3b44df6 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Fri, 28 Mar 2025 20:17:30 +0800 Subject: [PATCH 02/21] update --- app/api/endpoints/merchant_order.py | 6 +++--- jobs.sqlite | Bin 24576 -> 24576 bytes 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/api/endpoints/merchant_order.py b/app/api/endpoints/merchant_order.py index f9a5c7b..28972e1 100644 --- a/app/api/endpoints/merchant_order.py +++ b/app/api/endpoints/merchant_order.py @@ -494,18 +494,18 @@ async def verify_order( # 如果有积分奖励,赠送积分 - if order.MerchantProductDB.gift_points > 0: + if order.MerchantOrderDB.gift_points > 0: point_manager = PointManager(db) point_manager.add_points( user_id=order.MerchantOrderDB.user_id, - points=order.MerchantProductDB.gift_points, + points=order.MerchantOrderDB.gift_points, description=f"团购券核销奖励", order_id=order.MerchantOrderDB.order_id ) # 对商家进行结算 account_manager = AccountManager(db) - settlement_amount = float(order.MerchantProductDB.settlement_amount) + settlement_amount = float(order.MerchantProductDB.settlement_amount) * order.MerchantOrderDB.qty if settlement_amount > 0: account_manager.change_balance( user_id=order.MerchantDB.user_id, diff --git a/jobs.sqlite b/jobs.sqlite index 0c648b9ff27baa3308218d80ca670767556a94d0..eafa61cb9bca43943cf282b0cffbd5f367cc6d7c 100644 GIT binary patch delta 19 bcmZoTz}Rqrae_4C(TOt7j7K*nEQkjHOY;Z1 delta 19 bcmZoTz}Rqrae_4C{)sZqjQckxEQkjHOPL3* From 87dda933e252efe987901769cfe1becc0b5f88d6 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Fri, 28 Mar 2025 22:25:36 +0800 Subject: [PATCH 03/21] update --- app/api/endpoints/order.py | 1 + jobs.sqlite | Bin 24576 -> 24576 bytes 2 files changed, 1 insertion(+) diff --git a/app/api/endpoints/order.py b/app/api/endpoints/order.py index 39e7740..571ee88 100644 --- a/app/api/endpoints/order.py +++ b/app/api/endpoints/order.py @@ -1524,6 +1524,7 @@ async def get_orders( "coupon_discount_amount": order.coupon_discount_amount, "point_discount_amount": order.point_discount_amount, "cancel_reason": order.cancel_reason, + "additional_fee_amount": order.additional_fee_amount, "is_delivery_cancel": order.cancel_user_id == order.deliveryman_user_id, "complete_images": order.optimized_complete_images, "completed_time": order.completed_time, diff --git a/jobs.sqlite b/jobs.sqlite index eafa61cb9bca43943cf282b0cffbd5f367cc6d7c..b2bf440d2cf85e449ee3a028baff6c39cff24fab 100644 GIT binary patch delta 19 bcmZoTz}Rqrae_4C>4`GVjHfpyEQkjHOic&I delta 19 bcmZoTz}Rqrae_4C(TOt7j7K*nEQkjHOY;Z1 From be8a478676af74e8b1896d09ba5161ad7c9334d2 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Sat, 29 Mar 2025 22:08:02 +0800 Subject: [PATCH 04/21] update --- app/api/endpoints/order.py | 1 + app/core/config.py | 6 ++ app/core/unisms.py | 144 +++++++++++++++++++++++++++++++++++++ jobs.sqlite | Bin 24576 -> 24576 bytes 4 files changed, 151 insertions(+) create mode 100644 app/core/unisms.py diff --git a/app/api/endpoints/order.py b/app/api/endpoints/order.py index 571ee88..4dd1e11 100644 --- a/app/api/endpoints/order.py +++ b/app/api/endpoints/order.py @@ -1529,6 +1529,7 @@ async def get_orders( "complete_images": order.optimized_complete_images, "completed_time": order.completed_time, "final_amount": order.final_amount, + "is_first_order": order.is_first_order, "packages": package_list, "address": { "name": order.address_customer_name, diff --git a/app/core/config.py b/app/core/config.py index e88ad32..7d4704e 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -133,6 +133,12 @@ class Settings(BaseSettings): # 千问 API 配置 QWEN_API_KEY: str = "sk-caa199589f1c451aaac471fad2986e28" QWEN_API_URL: str = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation" + + # UniSMS 配置 + UNISMS_ACCESS_KEY_ID: str = "xxxxxx" # 替换为您的 UniSMS Access Key ID + UNISMS_ACCESS_KEY_SECRET: str = "xxxxxx" # 替换为您的 UniSMS Access Key Secret + UNISMS_SIGNATURE: str = "蜂快到家" # 短信签名 + UNISMS_VERIFICATION_TEMPLATE_ID: str = "pub_verif_8dgk" # 验证码短信模板ID class Config: case_sensitive = True diff --git a/app/core/unisms.py b/app/core/unisms.py new file mode 100644 index 0000000..b5308ea --- /dev/null +++ b/app/core/unisms.py @@ -0,0 +1,144 @@ +import json +import time +import hmac +import hashlib +import base64 +import uuid +import logging +import aiohttp +from typing import List, Dict, Any, Optional +from app.core.config import settings + +class UniSMSClient: + """UniSMS短信客户端""" + + def __init__(self): + self.access_key_id = settings.UNISMS_ACCESS_KEY_ID + self.access_key_secret = settings.UNISMS_ACCESS_KEY_SECRET + self.base_url = "https://uni.apistd.com" + self.endpoint = "/2022-12-28/sms/send" + self.signature_method = "HMAC-SHA256" + self.signature_version = "1" + + def _generate_signature(self, string_to_sign: str) -> str: + """生成签名""" + key = self.access_key_secret.encode('utf-8') + message = string_to_sign.encode('utf-8') + sign = hmac.new(key, message, digestmod=hashlib.sha256).digest() + return base64.b64encode(sign).decode('utf-8') + + def _build_request_headers(self, body: Dict[str, Any]) -> Dict[str, str]: + """构建请求头""" + timestamp = str(int(time.time())) + nonce = str(uuid.uuid4()).replace('-', '') + content_md5 = hashlib.md5(json.dumps(body).encode('utf-8')).hexdigest() + + # 构建待签名字符串 + string_to_sign = "\n".join([ + self.endpoint, + timestamp, + nonce, + content_md5 + ]) + + # 生成签名 + signature = self._generate_signature(string_to_sign) + + # 构建请求头 + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "X-Uni-Timestamp": timestamp, + "X-Uni-Nonce": nonce, + "X-Uni-Content-MD5": content_md5, + "X-Uni-Signature-Method": self.signature_method, + "X-Uni-Signature-Version": self.signature_version, + "X-Uni-Signature": signature, + "X-Uni-AccessKeyId": self.access_key_id, + } + + return headers + + async def send_sms( + self, + to: str, + signature: str, + template_id: str, + template_data: Dict[str, Any] = None + ) -> Dict[str, Any]: + """ + 发送短信 + + Args: + to: 接收短信的手机号码 + signature: 短信签名 + template_id: 短信模板ID + template_data: 短信模板参数 + + Returns: + Dict[str, Any]: 发送结果 + """ + try: + # 构建请求体 + body = { + "to": to, + "signature": signature, + "templateId": template_id, + } + + if template_data: + body["templateData"] = template_data + + # 构建请求头 + headers = self._build_request_headers(body) + + # 发送请求 + url = f"{self.base_url}{self.endpoint}" + + async with aiohttp.ClientSession() as session: + async with session.post(url, json=body, headers=headers) as response: + result = await response.json() + logging.info(f"UniSMS响应: {result}") + + if result.get("status") != "success": + logging.error(f"发送短信失败: {result}") + return { + "success": False, + "error": result.get("message", "发送短信失败") + } + + return { + "success": True, + "message_id": result.get("data", {}).get("messageId", ""), + "fee": result.get("data", {}).get("fee", 0) + } + + except Exception as e: + logging.error(f"发送短信异常: {str(e)}") + return { + "success": False, + "error": f"发送短信异常: {str(e)}" + } + + async def send_verification_code(self, phone: str, code: str) -> Dict[str, Any]: + """ + 发送验证码短信 + + Args: + phone: 手机号码 + code: 验证码 + + Returns: + Dict[str, Any]: 发送结果 + """ + # 使用验证码短信模板 + template_id = settings.UNISMS_VERIFICATION_TEMPLATE_ID + signature = settings.UNISMS_SIGNATURE + template_data = {"code": code} + + return await self.send_sms( + to=phone, + signature=signature, + template_id=template_id, + template_data=template_data + ) \ No newline at end of file diff --git a/jobs.sqlite b/jobs.sqlite index b2bf440d2cf85e449ee3a028baff6c39cff24fab..827a6df22d36fbc6d1c91dc1219b976eafcea620 100644 GIT binary patch delta 82 zcmZoTz}Rqrae_4C{fRQpjQ2Mtn_W@`MszyBct4AW$|7X_7@5q4Xjh0 jCOf*zY`!cl%K{YtD9@z80~BZGU}j-V2kH86&!_+ZgIO9N delta 82 zcmZoTz}Rqrae_4C>4`GVjHfpy Date: Sat, 29 Mar 2025 22:15:09 +0800 Subject: [PATCH 05/21] update --- app/api/endpoints/order.py | 20 ++++++++++---------- jobs.sqlite | Bin 24576 -> 24576 bytes 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/api/endpoints/order.py b/app/api/endpoints/order.py index 4dd1e11..bd3921c 100644 --- a/app/api/endpoints/order.py +++ b/app/api/endpoints/order.py @@ -658,10 +658,10 @@ async def deliveryman_get_order_status_count( ) # 待接单的订单,只显示今天以及今天以前的订单 - if status == OrderStatus.CREATED: - query = query.filter( - ShippingOrderDB.delivery_date <= datetime.now().date() - ) + # if status == OrderStatus.CREATED: + # query = query.filter( + # ShippingOrderDB.delivery_date <= datetime.now().date() + # ) count = query.count() result.append({ @@ -718,10 +718,10 @@ async def deliveryman_get_community_building_order_count( ) # 待接单的订单,只显示今天以及今天以前的订单 - if OrderStatus.CREATED in status: - query = query.filter( - ShippingOrderDB.delivery_date <= datetime.now().date() - ) + # if OrderStatus.CREATED in status: + # query = query.filter( + # ShippingOrderDB.delivery_date <= datetime.now().date() + # ) building_order_count = query.group_by( ShippingOrderDB.address_community_building_id @@ -977,8 +977,8 @@ async def deliveryman_orders( query = query.filter(ShippingOrderDB.deliveryman_user_id == deliveryman.userid) # 待接单的订单,只显示今天以及今天以前的订单 - if OrderStatus.CREATED in statuses: - query = query.filter(ShippingOrderDB.delivery_date <= datetime.now().date()) + # if OrderStatus.CREATED in statuses: + # query = query.filter(ShippingOrderDB.delivery_date <= datetime.now().date()) # 楼栋筛选 if building_id: diff --git a/jobs.sqlite b/jobs.sqlite index 827a6df22d36fbc6d1c91dc1219b976eafcea620..847b33c112961acad33a0c509fe99a25eb44ba8c 100644 GIT binary patch delta 19 bcmZoTz}Rqrae_4C^NBLfjL$bFEQkjHP4Nf* delta 19 bcmZoTz}Rqrae_4C{fRQpjQ2MtEQkjHO?e0I From 468b51ab0a2bf9d45c53e16d74712801f5f849e0 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Sat, 29 Mar 2025 22:17:34 +0800 Subject: [PATCH 06/21] update --- app/core/config.py | 2 +- jobs.sqlite | Bin 24576 -> 24576 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/config.py b/app/core/config.py index 7d4704e..11ec508 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -61,7 +61,7 @@ class Settings(BaseSettings): TENCENT_SECRET_KEY: str = "ta6PXTMBsX7dzA7IN6uYUFn8F9uTovoU" TENCENT_REGION: str = "ap-guangzhou" # 接口地域 SMS_SDK_APP_ID: str = "1400961527" - SMS_SIGN_NAME: str = "蜂快到家公众号" + SMS_SIGN_NAME: str = "成都爱嘉辰科技" SMS_TEMPLATE_ID: str = "2353143" # 验证码短信模板ID SMS_TEMPLATE_ID_ADDITIONAL_FEE: str = "2375181" # 加价短信模板ID SMS_TEMPLATE_ID_ORDER_COMPLETE: str = "2382882" # 订单完成短信模板ID diff --git a/jobs.sqlite b/jobs.sqlite index 847b33c112961acad33a0c509fe99a25eb44ba8c..78634a97cda25440cf4ce55b3566b20ea19ae0fa 100644 GIT binary patch delta 19 bcmZoTz}Rqrae_4C>xnYXjITE)EQkjHPAv!q delta 19 bcmZoTz}Rqrae_4C^NBLfjL$bFEQkjHP4Nf* From 8c489288eb90fc64ff32fb8be0e9475f24d21cf5 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Sat, 29 Mar 2025 22:22:44 +0800 Subject: [PATCH 07/21] update --- app/core/config.py | 2 +- jobs.sqlite | Bin 24576 -> 24576 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/config.py b/app/core/config.py index 11ec508..7d4704e 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -61,7 +61,7 @@ class Settings(BaseSettings): TENCENT_SECRET_KEY: str = "ta6PXTMBsX7dzA7IN6uYUFn8F9uTovoU" TENCENT_REGION: str = "ap-guangzhou" # 接口地域 SMS_SDK_APP_ID: str = "1400961527" - SMS_SIGN_NAME: str = "成都爱嘉辰科技" + SMS_SIGN_NAME: str = "蜂快到家公众号" SMS_TEMPLATE_ID: str = "2353143" # 验证码短信模板ID SMS_TEMPLATE_ID_ADDITIONAL_FEE: str = "2375181" # 加价短信模板ID SMS_TEMPLATE_ID_ORDER_COMPLETE: str = "2382882" # 订单完成短信模板ID diff --git a/jobs.sqlite b/jobs.sqlite index 78634a97cda25440cf4ce55b3566b20ea19ae0fa..70eb5f2cb16aace87c4ed0a9240de61428a3c9a8 100644 GIT binary patch delta 19 bcmZoTz}Rqrae_4C+leyHjBht4EQkjHPD=<1 delta 19 bcmZoTz}Rqrae_4C>xnYXjITE)EQkjHPAv!q From 5448a04be496ff08f5da32fa13820389ea5e720b Mon Sep 17 00:00:00 2001 From: aaron <> Date: Sat, 29 Mar 2025 22:40:21 +0800 Subject: [PATCH 08/21] update --- app/api/endpoints/merchant_order.py | 78 +++++++++++++------------- app/api/endpoints/merchant_product.py | 33 ++++++++++- jobs.sqlite | Bin 24576 -> 24576 bytes 3 files changed, 70 insertions(+), 41 deletions(-) diff --git a/app/api/endpoints/merchant_order.py b/app/api/endpoints/merchant_order.py index 28972e1..8a855b8 100644 --- a/app/api/endpoints/merchant_order.py +++ b/app/api/endpoints/merchant_order.py @@ -310,6 +310,9 @@ async def complete_order( if not order: return error_response(code=404, message="订单不存在") + if merchant_user.userid != order.merchant.user_id: + return error_response(code=403, message="不是你的订单,无权限完成") + if order.status not in [MerchantOrderStatus.DELIVERING, MerchantOrderStatus.PICKUP_READY]: return error_response(code=400, message="订单状态不正确") @@ -326,24 +329,31 @@ async def complete_order( order_id = order.order_id ) + # 对商家进行结算 + account_manager = AccountManager(db) + settlement_amount = float(order.MerchantProductDB.settlement_amount) * order.MerchantOrderDB.qty + if settlement_amount > 0: + account_manager.change_balance( + user_id=order.merchant.user_id, + amount=settlement_amount, + description=order.merchant_product.name, + transaction_id=order.order_id + ) - user = db.query(UserDB).filter( - UserDB.userid == order.user_id - ).first() - - product = db.query(MerchantProductDB).filter( - MerchantProductDB.id == order.merchant_product_id - ).first() - if product: - product.sold_total += order.qty + # 更新商品销量 + if order.merchant_product: + order.merchant_product.sold_total += order.qty db.commit() # 发送商家订单完成消息 + user = db.query(UserDB).filter( + UserDB.userid == order.user_id + ).first() if user and user.mp_openid: data={ "character_string7": order_id, - "thing5": product.name, + "thing5": order.merchant_product.name, "character_string24": order.qty, "time10": datetime.now().strftime("%Y-%m-%d %H:%M:%S") } @@ -467,61 +477,53 @@ async def verify_order( ): """核销订单""" # 查询订单及相关信息 - order = db.query( - MerchantOrderDB, - MerchantProductDB, - MerchantDB - ).join( - MerchantProductDB, - MerchantOrderDB.merchant_product_id == MerchantProductDB.id - ).join( - MerchantDB, - MerchantProductDB.merchant_id == MerchantDB.id - ).filter( + order = db.query(MerchantOrderDB).filter( MerchantOrderDB.order_verify_code == request.verify_code, - MerchantOrderDB.status == MerchantOrderStatus.PICKUP_READY, - MerchantDB.user_id == merchant_user.userid + MerchantOrderDB.status == MerchantOrderStatus.PICKUP_READY ).first() if not order: return error_response(code=404, message="订单不存在或已核销") + if merchant_user.userid != order.merchant.user_id: + return error_response(code=403, message="不是你的订单,无权限核销") + try: # 更新核销时间和核销用户 - order.MerchantOrderDB.verify_time = datetime.now(timezone.utc) - order.MerchantOrderDB.verify_user_id = merchant_user.userid - order.MerchantOrderDB.status = MerchantOrderStatus.COMPLETED + order.verify_time = datetime.now(timezone.utc) + order.verify_user_id = merchant_user.userid + order.status = MerchantOrderStatus.COMPLETED # 如果有积分奖励,赠送积分 - if order.MerchantOrderDB.gift_points > 0: + if order.gift_points > 0: point_manager = PointManager(db) point_manager.add_points( - user_id=order.MerchantOrderDB.user_id, - points=order.MerchantOrderDB.gift_points, - description=f"团购券核销奖励", - order_id=order.MerchantOrderDB.order_id + user_id=order.user_id, + points=order.gift_points, + description=order.merchant_product.name, + order_id=order.order_id ) # 对商家进行结算 account_manager = AccountManager(db) - settlement_amount = float(order.MerchantProductDB.settlement_amount) * order.MerchantOrderDB.qty + settlement_amount = float(order.settlement_amount) * order.qty if settlement_amount > 0: account_manager.change_balance( - user_id=order.MerchantDB.user_id, + user_id=order.merchant.user_id, amount=settlement_amount, - description=f"团购券核销", - transaction_id=order.MerchantOrderDB.order_id + description=order.merchant_product.name, + transaction_id=order.order_id ) # 更新商品销量 - if order.MerchantProductDB: - order.MerchantProductDB.sold_total += order.MerchantOrderDB.qty + if order.merchant_product: + order.merchant_product.sold_total += order.qty db.commit() return success_response( message="核销成功", - data=MerchantOrderInfo.model_validate(order.MerchantOrderDB) + data=MerchantOrderInfo.model_validate(order) ) except Exception as e: db.rollback() diff --git a/app/api/endpoints/merchant_product.py b/app/api/endpoints/merchant_product.py index cc6bdc8..900f64f 100644 --- a/app/api/endpoints/merchant_product.py +++ b/app/api/endpoints/merchant_product.py @@ -8,7 +8,7 @@ from app.models.merchant_product import ( MerchantProductInfo ) from app.models.database import get_db -from app.api.deps import get_admin_user +from app.api.deps import get_admin_user, get_merchant_user from app.models.user import UserDB from app.core.response import success_response, error_response, ResponseModel from app.models.merchant import MerchantDB @@ -77,15 +77,42 @@ async def update_product( db.rollback() return error_response(code=500, message=f"更新失败: {str(e)}") +@router.get("/merchant_product_list", response_model=ResponseModel) +async def merchant_product_list( + db: Session = Depends(get_db), + current_user: UserDB = Depends(get_merchant_user) +): + """获取商家产品列表(商家端)""" + + merchant = db.query(MerchantDB).filter(MerchantDB.user_id == current_user.userid).first() + if not merchant: + return error_response(code=404, message="商家不存在") + + products = db.query(MerchantProductDB).filter(MerchantProductDB.merchant_id == merchant.id).all() + + product_list = [] + for product in products: + product_list.append( + { + **MerchantProductInfo.model_validate(product).model_dump(), + "merchant": MerchantInfo.model_validate(merchant).model_dump(), + "total_sales_amount": 0, + "total_profit_amount": 0 + } + ) + + return success_response(data=product_list) + + @router.get("/list", response_model=ResponseModel) async def list_merchant_products( user_id: Optional[int] = None, community_id: Optional[int] = None, skip: int = 0, limit: int = 20, - db: Session = Depends(get_db) + db: Session = Depends(get_db), ): - """获取商品列表""" + """获取商品列表(用户端)""" # 联表查询商家信息 query = db.query(MerchantProductDB).options( joinedload(MerchantProductDB.merchant) diff --git a/jobs.sqlite b/jobs.sqlite index 70eb5f2cb16aace87c4ed0a9240de61428a3c9a8..9902dba2428be45f7451274c7ab58c6985492600 100644 GIT binary patch delta 21 bcmZoTz}Rqrae@>Rr~E`2Cm^{oVL?0qN=F8O delta 21 bcmZoTz}Rqrae@>R$J>cAPC#;F!h(1JR}}~v From 0f00caf23a977a8033f56a0e31805fd8792a4589 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Sun, 30 Mar 2025 11:33:16 +0800 Subject: [PATCH 09/21] update --- app/api/endpoints/wecom.py | 267 +++++++++++++++++++++++++-- app/core/wecomclient.py | 297 ++++++++++++++++++++++++++++-- app/models/wecom_external_chat.py | 98 ++++++++++ jobs.sqlite | Bin 24576 -> 24576 bytes 4 files changed, 625 insertions(+), 37 deletions(-) create mode 100644 app/models/wecom_external_chat.py diff --git a/app/api/endpoints/wecom.py b/app/api/endpoints/wecom.py index 1d86d5a..516eb22 100644 --- a/app/api/endpoints/wecom.py +++ b/app/api/endpoints/wecom.py @@ -15,8 +15,9 @@ from app.models.user import UserDB from app.api.deps import get_current_user from fastapi import Depends from sqlalchemy.orm import Session -from app.core.response import error_response, success_response +from app.core.response import error_response, success_response, ResponseModel from pydantic import BaseModel +from app.models.wecom_external_chat import WecomExternalChatDB, WecomExternalChatInfo, WecomExternalChatMemberDB, WecomExternalChatMemberInfo router = APIRouter() @@ -109,43 +110,52 @@ async def wechat_corp_callback( # 解析解密后的XML msg_root = ET.fromstring(decrypted_msg) - print(f"企业微信回调消息:{decrypted_msg}") + logging.info(f"企业微信回调消息:{decrypted_msg}") # 解析基本信息 msg_type = msg_root.find('MsgType').text - print(f"msg_type: {msg_type}") + logging.info(f"msg_type: {msg_type}") # 处理事件消息 if msg_type == 'event': event = msg_root.find('Event').text - print(f"event: {event}") + logging.info(f"event: {event}") - # 处理进群事件 + # 处理外部群聊变更事件 if event == 'change_external_chat': chat_id = msg_root.find('ChatId').text change_type = msg_root.find('ChangeType').text update_detail = msg_root.find('UpdateDetail').text - - print(f"chat_id: {chat_id}") - print(f"change_type: {change_type}") - print(f"update_detail: {update_detail}") - # 处理进群事件 - - if update_detail == 'add_member': - print(f"发送欢迎消息") - # 发送欢迎消息 - # await wecom_client.send_welcome_message(chat_id) + join_user_id = None + if update_detail == 'add_member' and msg_root.find('JoinScene') is not None: + logging.info(f"有新成员加入群聊") + # 获取加入的成员ID + join_user_id_elem = msg_root.find('JoinUserID') + if join_user_id_elem is not None: + join_user_id = join_user_id_elem.text + logging.info(f"chat_id: {chat_id}, change_type: {change_type}, update_detail: {update_detail}, join_user_id: {join_user_id}") + + # 处理群聊变更事件 + await wecom_client.handle_chat_change_event( + chat_id=chat_id, + change_type=change_type, + update_detail=update_detail, + join_user_id=join_user_id + ) + + if update_detail == 'add_member' and join_user_id: + logging.info(f"发送欢迎消息到群聊:{chat_id}") + await wecom_client.send_welcome_message(chat_id) return Response(content="success", media_type="text/plain") except Exception as e: logging.exception("处理企业微信回调消息异常") - return Response(content="success", media_type="text/plain") - + return Response(content="success", media_type="text/plain") class UnionidToExternalUseridRequest(BaseModel): unionid: str @@ -158,4 +168,225 @@ async def unionid_to_external_userid( """根据unionid获取external_userid""" result = await wecom_client.unionid_to_external_userid(request.unionid, request.openid) print(f"根据unionid获取external_userid结果: {result}") - return success_response(message="获取external_userid成功", data=result) \ No newline at end of file + return success_response(message="获取external_userid成功", data=result) + +@router.get("/external-chats", response_model=ResponseModel) +async def get_external_chats( + db: Session = Depends(get_db), + current_user: UserDB = Depends(get_current_user) +): + """获取企业微信外部群聊列表""" + try: + # 检查是否为管理员 + if current_user.userid != settings.PLATFORM_USER_ID: + return error_response(code=403, message="权限不足") + + # 获取群聊列表 + chats = db.query(WecomExternalChatDB).filter( + WecomExternalChatDB.is_active == True + ).order_by(WecomExternalChatDB.update_time.desc()).all() + + # 转换为Pydantic模型 + chat_list = [WecomExternalChatInfo.model_validate(chat) for chat in chats] + + return success_response(message="获取群聊列表成功", data=chat_list) + except Exception as e: + logging.exception("获取群聊列表异常") + return error_response(code=500, message=f"获取群聊列表失败: {str(e)}") + +@router.get("/external-chats/{chat_id}/members", response_model=ResponseModel) +async def get_external_chat_members( + chat_id: str, + db: Session = Depends(get_db), + current_user: UserDB = Depends(get_current_user) +): + """获取企业微信外部群聊成员列表""" + try: + # 检查是否为管理员 + if current_user.userid != settings.PLATFORM_USER_ID: + return error_response(code=403, message="权限不足") + + # 检查群聊是否存在 + chat = db.query(WecomExternalChatDB).filter( + WecomExternalChatDB.chat_id == chat_id + ).first() + + if not chat: + return error_response(code=404, message="群聊不存在") + + # 获取成员列表 + members = db.query(WecomExternalChatMemberDB).filter( + WecomExternalChatMemberDB.chat_id == chat_id + ).order_by(WecomExternalChatMemberDB.join_time.desc()).all() + + # 转换为Pydantic模型 + member_list = [WecomExternalChatMemberInfo.model_validate(member) for member in members] + + return success_response(message="获取群聊成员列表成功", data=member_list) + except Exception as e: + logging.exception("获取群聊成员列表异常") + return error_response(code=500, message=f"获取群聊成员列表失败: {str(e)}") + +@router.post("/sync-chat/{chat_id}", response_model=ResponseModel) +async def sync_external_chat( + chat_id: str, + db: Session = Depends(get_db), + current_user: UserDB = Depends(get_current_user) +): + """同步企业微信外部群聊信息""" + try: + # 检查是否为管理员 + if current_user.userid != settings.PLATFORM_USER_ID: + return error_response(code=403, message="权限不足") + + # 获取群聊信息 + result = await wecom_client.handle_chat_change_event( + chat_id=chat_id, + change_type="create", + update_detail="" + ) + + if not result: + return error_response(code=500, message="同步群聊信息失败") + + return success_response(message="同步群聊信息成功") + except Exception as e: + logging.exception("同步群聊信息异常") + return error_response(code=500, message=f"同步群聊信息失败: {str(e)}") + +@router.get("/chat-dashboard") +async def chat_dashboard( + current_user: UserDB = Depends(get_current_user), + db: Session = Depends(get_db) +): + """显示企业微信外部群聊信息的HTML页面""" + # 检查是否为管理员 + if current_user.userid != settings.PLATFORM_USER_ID: + return Response(content="权限不足", media_type="text/html") + + # 获取群聊列表 + chats = db.query(WecomExternalChatDB).filter( + WecomExternalChatDB.is_active == True + ).order_by(WecomExternalChatDB.update_time.desc()).all() + + # 生成HTML + html = """ + + + + 企业微信外部群聊信息 + + + + + + +
+

企业微信外部群聊信息

+ """ + + if not chats: + html += "

暂无群聊信息

" + else: + for chat in chats: + html += f""" +
+

{chat.name or '未命名群聊'} ({chat.chat_id})

+

+ 创建时间: {chat.create_time.strftime('%Y-%m-%d %H:%M:%S')}
+ 更新时间: {chat.update_time.strftime('%Y-%m-%d %H:%M:%S') if chat.update_time else '无'}
+ 成员数量: {chat.member_count}
+ 群主: {chat.owner or '未知'}
+ 公告: {chat.notice or '无'}
+

+ + +
+
+ """ + + html += """ +
+ + + """ + + return Response(content=html, media_type="text/html") \ No newline at end of file diff --git a/app/core/wecomclient.py b/app/core/wecomclient.py index b5393b6..ee32c1c 100644 --- a/app/core/wecomclient.py +++ b/app/core/wecomclient.py @@ -112,8 +112,16 @@ class WecomClient: logging.error(f"unionid_to_external_userid异常: {str(e)}") return None - async def send_welcome_message(self, chat_id: str) -> bool: - """发送欢迎消息""" + async def send_welcome_message(self, chat_id: str, user_id: str = None) -> bool: + """发送欢迎消息 + + Args: + chat_id: 群聊ID + user_id: 用户ID,如果指定则发送私信,否则发送群消息 + + Returns: + bool: 是否发送成功 + """ try: # 1. 获取 access_token access_token = await self.get_access_token() @@ -121,24 +129,37 @@ class WecomClient: logging.error("获取access_token失败") return False + welcome_text = f"""🥳 欢迎您进群,在群内可以享受📦【代取快递】跑腿服务。 + +‼ 微信下单,快递到家 ‼ + +🎁 新人礼包 +𝟏 赠送𝟏𝟓张【𝟑元跑腿券】 +𝟐 赠送𝟔枚鲜鸡蛋【首次下单】 +━ ━ ━ ━ ━🎊━ ━ ━ ━ ━ +↓点击↓小程序领券下单 &""" + # 2. 发送欢迎消息 - url = f"https://qyapi.weixin.qq.com/cgi-bin/appchat/send?access_token={access_token}" - data = { - "chatid": chat_id, - "msgtype": "text", - "text": { - "content": f"""🥳 欢迎您进群,在群内可以享受📦【代取快递】跑腿服务。 - - ‼ 微信下单,快递到家 ‼ - - 🎁 新人礼包 - 𝟏 赠送𝟏𝟓张【𝟑元跑腿券】 - 𝟐 赠送𝟔枚鲜鸡蛋【首次下单】 - ━ ━ ━ ━ ━🎊━ ━ ━ ━ ━ - ↓点击↓小程序领券下单 &""" - }, - "safe": 0 - } + if user_id: + # 发送私信 + url = f"https://qyapi.weixin.qq.com/cgi-bin/externalcontact/send_welcome_msg?access_token={access_token}" + data = { + "welcome_code": user_id, # 这里使用user_id作为临时的欢迎码 + "text": { + "content": welcome_text + } + } + else: + # 发送群消息 + url = f"https://qyapi.weixin.qq.com/cgi-bin/appchat/send?access_token={access_token}" + data = { + "chatid": chat_id, + "msgtype": "text", + "text": { + "content": welcome_text + }, + "safe": 0 + } async with aiohttp.ClientSession() as session: async with session.post(url, json=data) as response: @@ -152,5 +173,243 @@ class WecomClient: except Exception as e: logging.error(f"发送欢迎消息异常: {str(e)}") return False + + async def get_external_chat_info(self, chat_id: str) -> Optional[Dict[str, Any]]: + """获取外部群聊信息 + + Args: + chat_id: 群聊ID + + Returns: + Dict: 群聊信息 + """ + try: + access_token = await self.get_access_token() + if not access_token: + logging.error("获取access_token失败") + return None + + url = f"https://qyapi.weixin.qq.com/cgi-bin/externalcontact/groupchat/get?access_token={access_token}" + data = { + "chat_id": chat_id + } + + async with aiohttp.ClientSession() as session: + async with session.post(url, json=data) as response: + result = await response.json() + if result.get("errcode") == 0: + return result.get("group_chat") + else: + logging.error(f"获取外部群聊信息失败: {result}") + return None + except Exception as e: + logging.error(f"获取外部群聊信息异常: {str(e)}") + return None + + async def get_external_contact_info(self, external_userid: str) -> Optional[Dict[str, Any]]: + """获取外部联系人信息 + + Args: + external_userid: 外部联系人ID + + Returns: + Dict: 外部联系人信息 + """ + try: + access_token = await self.get_access_token() + if not access_token: + logging.error("获取access_token失败") + return None + + url = f"https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get?access_token={access_token}" + params = { + "external_userid": external_userid + } + + async with aiohttp.ClientSession() as session: + async with session.get(url, params=params) as response: + result = await response.json() + if result.get("errcode") == 0: + return result.get("external_contact") + else: + logging.error(f"获取外部联系人信息失败: {result}") + return None + except Exception as e: + logging.error(f"获取外部联系人信息异常: {str(e)}") + return None + + async def handle_chat_change_event(self, chat_id: str, change_type: str, update_detail: str, join_user_id: str = None) -> bool: + """处理群聊变更事件 + + Args: + chat_id: 群聊ID + change_type: 变更类型 create/update/dismiss + update_detail: 变更详情 add_member/del_member/change_owner/change_name/change_notice + join_user_id: 加入的用户ID + + Returns: + bool: 处理是否成功 + """ + from app.models.wecom_external_chat import WecomExternalChatDB, WecomExternalChatMemberDB + from app.models.wecom_external_chat import WecomExternalChatCreate, WecomExternalChatMemberCreate + from app.models.database import SessionLocal + + try: + db = SessionLocal() + + # 群创建事件 + if change_type == "create": + # 获取群聊信息 + chat_info = await self.get_external_chat_info(chat_id) + if not chat_info: + return False + + # 保存群聊信息 + chat_db = db.query(WecomExternalChatDB).filter(WecomExternalChatDB.chat_id == chat_id).first() + if not chat_db: + chat_create = WecomExternalChatCreate( + chat_id=chat_id, + name=chat_info.get("name"), + owner=chat_info.get("owner"), + member_count=len(chat_info.get("member_list", [])), + notice=chat_info.get("notice") + ) + chat_db = WecomExternalChatDB(**chat_create.dict()) + db.add(chat_db) + db.commit() + + # 保存群成员信息 + for member in chat_info.get("member_list", []): + user_id = member.get("userid") + member_type = member.get("type") + + # 检查成员是否已存在 + member_db = db.query(WecomExternalChatMemberDB).filter( + WecomExternalChatMemberDB.chat_id == chat_id, + WecomExternalChatMemberDB.user_id == user_id + ).first() + + if not member_db: + # 获取外部联系人详情 + user_info = None + if member_type == "EXTERNAL": + user_info = await self.get_external_contact_info(user_id) + + member_create = WecomExternalChatMemberCreate( + chat_id=chat_id, + user_id=user_id, + type=member_type, + name=user_info.get("name") if user_info else None, + unionid=user_info.get("unionid") if user_info else None + ) + member_db = WecomExternalChatMemberDB(**member_create.dict()) + db.add(member_db) + + db.commit() + return True + + # 群更新事件 - 添加成员 + elif change_type == "update" and update_detail == "add_member": + if not join_user_id: + return False + + # 检查群聊是否存在 + chat_db = db.query(WecomExternalChatDB).filter(WecomExternalChatDB.chat_id == chat_id).first() + if not chat_db: + # 获取群聊信息并创建 + chat_info = await self.get_external_chat_info(chat_id) + if chat_info: + chat_create = WecomExternalChatCreate( + chat_id=chat_id, + name=chat_info.get("name"), + owner=chat_info.get("owner"), + member_count=len(chat_info.get("member_list", [])), + notice=chat_info.get("notice") + ) + chat_db = WecomExternalChatDB(**chat_create.dict()) + db.add(chat_db) + db.commit() + + # 检查成员是否已存在 + member_db = db.query(WecomExternalChatMemberDB).filter( + WecomExternalChatMemberDB.chat_id == chat_id, + WecomExternalChatMemberDB.user_id == join_user_id + ).first() + + if not member_db: + # 判断是内部成员还是外部联系人 + member_type = "EXTERNAL" # 默认为外部联系人 + + # 获取外部联系人详情 + user_info = await self.get_external_contact_info(join_user_id) + + member_create = WecomExternalChatMemberCreate( + chat_id=chat_id, + user_id=join_user_id, + type=member_type, + name=user_info.get("name") if user_info else None, + unionid=user_info.get("unionid") if user_info else None + ) + member_db = WecomExternalChatMemberDB(**member_create.dict()) + db.add(member_db) + db.commit() + + # 发送欢迎消息 + if member_type == "EXTERNAL": + await self.send_welcome_message(chat_id) + + # 更新发送欢迎消息状态 + member_db.welcome_sent = True + db.commit() + + # 更新群成员数量 + if chat_db: + chat_db.member_count = chat_db.member_count + 1 + db.commit() + + return True + + # 群更新事件 - 移除成员 + elif change_type == "update" and update_detail == "del_member": + if not join_user_id: + return False + + # 删除成员记录 + member_db = db.query(WecomExternalChatMemberDB).filter( + WecomExternalChatMemberDB.chat_id == chat_id, + WecomExternalChatMemberDB.user_id == join_user_id + ).first() + + if member_db: + db.delete(member_db) + db.commit() + + # 更新群成员数量 + chat_db = db.query(WecomExternalChatDB).filter(WecomExternalChatDB.chat_id == chat_id).first() + if chat_db: + chat_db.member_count = max(0, chat_db.member_count - 1) + db.commit() + + return True + + # 群解散事件 + elif change_type == "dismiss": + # 标记群聊为非活跃 + chat_db = db.query(WecomExternalChatDB).filter(WecomExternalChatDB.chat_id == chat_id).first() + if chat_db: + chat_db.is_active = False + db.commit() + + return True + + return False + except Exception as e: + logging.error(f"处理群聊变更事件异常: {str(e)}") + if 'db' in locals(): + db.close() + return False + finally: + if 'db' in locals(): + db.close() wecom_client = WecomClient() \ No newline at end of file diff --git a/app/models/wecom_external_chat.py b/app/models/wecom_external_chat.py new file mode 100644 index 0000000..bf30312 --- /dev/null +++ b/app/models/wecom_external_chat.py @@ -0,0 +1,98 @@ +from sqlalchemy import Column, String, Integer, DateTime, JSON, Boolean, ForeignKey, Text +from sqlalchemy.sql import func +from pydantic import BaseModel, Field +from typing import Optional, List, Dict, Any +from datetime import datetime +from .database import Base + +class WecomExternalChatDB(Base): + """企业微信外部群聊表""" + __tablename__ = "wecom_external_chats" + + id = Column(Integer, primary_key=True, autoincrement=True) + chat_id = Column(String(64), nullable=False, unique=True, index=True) # 群聊ID + name = Column(String(100), nullable=True) # 群名称 + owner = Column(String(64), nullable=True) # 群主ID + create_time = Column(DateTime(timezone=True), server_default=func.now()) + update_time = Column(DateTime(timezone=True), onupdate=func.now()) + member_count = Column(Integer, nullable=False, default=0) # 成员数量 + notice = Column(String(500), nullable=True) # 群公告 + is_active = Column(Boolean, nullable=False, default=True) # 是否活跃 + +class WecomExternalChatMemberDB(Base): + """企业微信外部群聊成员表""" + __tablename__ = "wecom_external_chat_members" + + id = Column(Integer, primary_key=True, autoincrement=True) + chat_id = Column(String(64), nullable=False, index=True) # 群聊ID + user_id = Column(String(64), nullable=False, index=True) # 用户ID + type = Column(String(32), nullable=False) # 成员类型: INTERNAL(内部成员)、EXTERNAL(外部联系人) + join_time = Column(DateTime(timezone=True), server_default=func.now()) # 加入时间 + unionid = Column(String(64), nullable=True) # 微信unionid + name = Column(String(100), nullable=True) # 成员名称 + mobile = Column(String(20), nullable=True) # 手机号 + welcome_sent = Column(Boolean, nullable=False, default=False) # 是否已发送欢迎消息 + + # 设置联合唯一索引 + __table_args__ = ( + {"mysql_charset": "utf8mb4"}, + ) + +# Pydantic 模型 +class WecomExternalChatCreate(BaseModel): + chat_id: str + name: Optional[str] = None + owner: Optional[str] = None + member_count: int = 0 + notice: Optional[str] = None + is_active: bool = True + +class WecomExternalChatUpdate(BaseModel): + name: Optional[str] = None + owner: Optional[str] = None + member_count: Optional[int] = None + notice: Optional[str] = None + is_active: Optional[bool] = None + +class WecomExternalChatInfo(BaseModel): + id: int + chat_id: str + name: Optional[str] = None + owner: Optional[str] = None + create_time: datetime + update_time: Optional[datetime] = None + member_count: int = 0 + notice: Optional[str] = None + is_active: bool = True + + class Config: + from_attributes = True + +class WecomExternalChatMemberCreate(BaseModel): + chat_id: str + user_id: str + type: str + unionid: Optional[str] = None + name: Optional[str] = None + mobile: Optional[str] = None + welcome_sent: bool = False + +class WecomExternalChatMemberUpdate(BaseModel): + unionid: Optional[str] = None + name: Optional[str] = None + mobile: Optional[str] = None + welcome_sent: Optional[bool] = None + +class WecomExternalChatMemberInfo(BaseModel): + id: int + chat_id: str + user_id: str + type: str + join_time: datetime + unionid: Optional[str] = None + name: Optional[str] = None + mobile: Optional[str] = None + welcome_sent: bool + + class Config: + from_attributes = True \ No newline at end of file diff --git a/jobs.sqlite b/jobs.sqlite index 9902dba2428be45f7451274c7ab58c6985492600..e96a0a0c033b988d6fbca04a61dec97409b71a3a 100644 GIT binary patch delta 82 zcmZoTz}Rqrae_3X Date: Sun, 30 Mar 2025 11:39:03 +0800 Subject: [PATCH 10/21] update --- app/api/endpoints/wecom.py | 2 ++ app/core/wecomclient.py | 3 +++ jobs.sqlite | Bin 24576 -> 24576 bytes 3 files changed, 5 insertions(+) diff --git a/app/api/endpoints/wecom.py b/app/api/endpoints/wecom.py index 516eb22..0057492 100644 --- a/app/api/endpoints/wecom.py +++ b/app/api/endpoints/wecom.py @@ -20,6 +20,8 @@ from pydantic import BaseModel from app.models.wecom_external_chat import WecomExternalChatDB, WecomExternalChatInfo, WecomExternalChatMemberDB, WecomExternalChatMemberInfo router = APIRouter() +logging.basicConfig(level=logging.INFO) + def decrypt_msg(msg_encrypt: str, signature: str, timestamp: str, nonce: str) -> typing.Optional[str]: """解密企业微信消息""" diff --git a/app/core/wecomclient.py b/app/core/wecomclient.py index ee32c1c..971c2a1 100644 --- a/app/core/wecomclient.py +++ b/app/core/wecomclient.py @@ -5,6 +5,9 @@ import logging from app.core.config import settings from typing import Dict, Any, Optional, List import aiohttp + +logging.basicConfig(level=logging.INFO) + class WecomClient: """企业微信客户端""" diff --git a/jobs.sqlite b/jobs.sqlite index e96a0a0c033b988d6fbca04a61dec97409b71a3a..3aa93de0c3be09dfbe73178f91c768e604570e22 100644 GIT binary patch delta 19 acmZoTz}Rqrae_3X?L--8M%#@E3*rGo`v$52 delta 19 acmZoTz}Rqrae_3X Date: Sun, 30 Mar 2025 11:42:19 +0800 Subject: [PATCH 11/21] update --- app/core/wecomclient.py | 2 +- jobs.sqlite | Bin 24576 -> 24576 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/wecomclient.py b/app/core/wecomclient.py index 971c2a1..adf9fa6 100644 --- a/app/core/wecomclient.py +++ b/app/core/wecomclient.py @@ -6,7 +6,7 @@ from app.core.config import settings from typing import Dict, Any, Optional, List import aiohttp -logging.basicConfig(level=logging.INFO) +logging.basicConfig(level=logging.DEBUG) class WecomClient: """企业微信客户端""" diff --git a/jobs.sqlite b/jobs.sqlite index 3aa93de0c3be09dfbe73178f91c768e604570e22..ff33cc153679fc83f13fb37b3d3924077c098acf 100644 GIT binary patch delta 19 acmZoTz}Rqrae_3X^F$eEM(2$Q3*rGpRR*>I delta 19 acmZoTz}Rqrae_3X?L--8M%#@E3*rGo`v$52 From f1871c74ea5a0e85e407ff4f968ff5aad2080d70 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Sun, 30 Mar 2025 11:42:30 +0800 Subject: [PATCH 12/21] update --- app/api/endpoints/wecom.py | 2 +- jobs.sqlite | Bin 24576 -> 24576 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/endpoints/wecom.py b/app/api/endpoints/wecom.py index 0057492..d7a18e2 100644 --- a/app/api/endpoints/wecom.py +++ b/app/api/endpoints/wecom.py @@ -20,7 +20,7 @@ from pydantic import BaseModel from app.models.wecom_external_chat import WecomExternalChatDB, WecomExternalChatInfo, WecomExternalChatMemberDB, WecomExternalChatMemberInfo router = APIRouter() -logging.basicConfig(level=logging.INFO) +logging.basicConfig(level=logging.DEBUG) def decrypt_msg(msg_encrypt: str, signature: str, timestamp: str, nonce: str) -> typing.Optional[str]: diff --git a/jobs.sqlite b/jobs.sqlite index ff33cc153679fc83f13fb37b3d3924077c098acf..62fc95503b73d98aad4aae5810e8c151cc0ef63e 100644 GIT binary patch delta 19 acmZoTz}Rqrae_3X`$QRMM)!>g3*rGpkp{j1 delta 19 acmZoTz}Rqrae_3X^F$eEM(2$Q3*rGpRR*>I From d7efef091ff331ddecda8e3d613014c865167d02 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Sun, 30 Mar 2025 11:47:11 +0800 Subject: [PATCH 13/21] update --- app/api/endpoints/wecom.py | 3 ++- app/core/wecomclient.py | 3 ++- jobs.sqlite | Bin 24576 -> 24576 bytes 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/api/endpoints/wecom.py b/app/api/endpoints/wecom.py index d7a18e2..33b9ccd 100644 --- a/app/api/endpoints/wecom.py +++ b/app/api/endpoints/wecom.py @@ -20,8 +20,9 @@ from pydantic import BaseModel from app.models.wecom_external_chat import WecomExternalChatDB, WecomExternalChatInfo, WecomExternalChatMemberDB, WecomExternalChatMemberInfo router = APIRouter() -logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) def decrypt_msg(msg_encrypt: str, signature: str, timestamp: str, nonce: str) -> typing.Optional[str]: """解密企业微信消息""" diff --git a/app/core/wecomclient.py b/app/core/wecomclient.py index adf9fa6..8ededca 100644 --- a/app/core/wecomclient.py +++ b/app/core/wecomclient.py @@ -6,7 +6,8 @@ from app.core.config import settings from typing import Dict, Any, Optional, List import aiohttp -logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) class WecomClient: """企业微信客户端""" diff --git a/jobs.sqlite b/jobs.sqlite index 62fc95503b73d98aad4aae5810e8c151cc0ef63e..9ff0b2bc00ed28c544fb6fa0c53cf8947602f7ab 100644 GIT binary patch delta 19 acmZoTz}Rqrae_3X_e2?IM(>RY3*rGp%?8E* delta 19 acmZoTz}Rqrae_3X`$QRMM)!>g3*rGpkp{j1 From f843a4ceb16528104d103d4a00652b5374262006 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Sun, 30 Mar 2025 11:51:56 +0800 Subject: [PATCH 14/21] update --- app/api/endpoints/wecom.py | 24 ++++++++++++------------ app/core/wecomclient.py | 30 +++++++++++++++--------------- jobs.sqlite | Bin 24576 -> 24576 bytes 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/app/api/endpoints/wecom.py b/app/api/endpoints/wecom.py index 33b9ccd..b0bd205 100644 --- a/app/api/endpoints/wecom.py +++ b/app/api/endpoints/wecom.py @@ -65,7 +65,7 @@ def decrypt_msg(msg_encrypt: str, signature: str, timestamp: str, nonce: str) -> return xml_content.decode() except Exception as e: - logging.exception("解密企业微信消息失败") + logger.exception("解密企业微信消息失败") return None @router.get("") @@ -85,7 +85,7 @@ async def verify_callback( return Response(content=decrypted_str, media_type="text/plain") except Exception as e: - logging.exception("验证回调配置失败") + logger.exception("验证回调配置失败") return Response(status_code=403) @router.post("") @@ -113,18 +113,18 @@ async def wechat_corp_callback( # 解析解密后的XML msg_root = ET.fromstring(decrypted_msg) - logging.info(f"企业微信回调消息:{decrypted_msg}") + logger.info(f"企业微信回调消息:{decrypted_msg}") # 解析基本信息 msg_type = msg_root.find('MsgType').text - logging.info(f"msg_type: {msg_type}") + logger.info(f"msg_type: {msg_type}") # 处理事件消息 if msg_type == 'event': event = msg_root.find('Event').text - logging.info(f"event: {event}") + logger.info(f"event: {event}") # 处理外部群聊变更事件 if event == 'change_external_chat': @@ -134,13 +134,13 @@ async def wechat_corp_callback( join_user_id = None if update_detail == 'add_member' and msg_root.find('JoinScene') is not None: - logging.info(f"有新成员加入群聊") + logger.info(f"有新成员加入群聊") # 获取加入的成员ID join_user_id_elem = msg_root.find('JoinUserID') if join_user_id_elem is not None: join_user_id = join_user_id_elem.text - logging.info(f"chat_id: {chat_id}, change_type: {change_type}, update_detail: {update_detail}, join_user_id: {join_user_id}") + logger.info(f"chat_id: {chat_id}, change_type: {change_type}, update_detail: {update_detail}, join_user_id: {join_user_id}") # 处理群聊变更事件 await wecom_client.handle_chat_change_event( @@ -151,13 +151,13 @@ async def wechat_corp_callback( ) if update_detail == 'add_member' and join_user_id: - logging.info(f"发送欢迎消息到群聊:{chat_id}") + logger.info(f"发送欢迎消息到群聊:{chat_id}") await wecom_client.send_welcome_message(chat_id) return Response(content="success", media_type="text/plain") except Exception as e: - logging.exception("处理企业微信回调消息异常") + logger.exception("处理企业微信回调消息异常") return Response(content="success", media_type="text/plain") class UnionidToExternalUseridRequest(BaseModel): @@ -194,7 +194,7 @@ async def get_external_chats( return success_response(message="获取群聊列表成功", data=chat_list) except Exception as e: - logging.exception("获取群聊列表异常") + logger.exception("获取群聊列表异常") return error_response(code=500, message=f"获取群聊列表失败: {str(e)}") @router.get("/external-chats/{chat_id}/members", response_model=ResponseModel) @@ -227,7 +227,7 @@ async def get_external_chat_members( return success_response(message="获取群聊成员列表成功", data=member_list) except Exception as e: - logging.exception("获取群聊成员列表异常") + logger.exception("获取群聊成员列表异常") return error_response(code=500, message=f"获取群聊成员列表失败: {str(e)}") @router.post("/sync-chat/{chat_id}", response_model=ResponseModel) @@ -254,7 +254,7 @@ async def sync_external_chat( return success_response(message="同步群聊信息成功") except Exception as e: - logging.exception("同步群聊信息异常") + logger.exception("同步群聊信息异常") return error_response(code=500, message=f"同步群聊信息失败: {str(e)}") @router.get("/chat-dashboard") diff --git a/app/core/wecomclient.py b/app/core/wecomclient.py index 8ededca..45b08ed 100644 --- a/app/core/wecomclient.py +++ b/app/core/wecomclient.py @@ -36,7 +36,7 @@ class WecomClient: else: raise Exception(f"获取access_token失败: {result}") except Exception as e: - logging.error(f"获取access_token异常: {str(e)}") + logger.error(f"获取access_token异常: {str(e)}") return None async def code2session(self, js_code: str) -> Optional[Dict[str, Any]]: @@ -68,11 +68,11 @@ class WecomClient: "session_key": result.get("session_key") } else: - logging.error(f"code2session失败: {result}") + logger.error(f"code2session失败: {result}") return None except Exception as e: - logging.error(f"code2session异常: {str(e)}") + logger.error(f"code2session异常: {str(e)}") return None async def get_unionid_from_userid(self, userid: str) -> Optional[str]: @@ -94,7 +94,7 @@ class WecomClient: return result.get("unionid") except Exception as e: - logging.error(f"get_unionid_from_userid异常: {str(e)}") + logger.error(f"get_unionid_from_userid异常: {str(e)}") return None async def unionid_to_external_userid(self, unionid: str, openid: str) -> Optional[str]: @@ -113,7 +113,7 @@ class WecomClient: return result except Exception as e: - logging.error(f"unionid_to_external_userid异常: {str(e)}") + logger.error(f"unionid_to_external_userid异常: {str(e)}") return None async def send_welcome_message(self, chat_id: str, user_id: str = None) -> bool: @@ -130,7 +130,7 @@ class WecomClient: # 1. 获取 access_token access_token = await self.get_access_token() if not access_token: - logging.error("获取access_token失败") + logger.error("获取access_token失败") return False welcome_text = f"""🥳 欢迎您进群,在群内可以享受📦【代取快递】跑腿服务。 @@ -171,11 +171,11 @@ class WecomClient: if result.get("errcode") == 0: return True else: - logging.error(f"发送欢迎消息失败: {result}") + logger.error(f"发送欢迎消息失败: {result}") return False except Exception as e: - logging.error(f"发送欢迎消息异常: {str(e)}") + logger.error(f"发送欢迎消息异常: {str(e)}") return False async def get_external_chat_info(self, chat_id: str) -> Optional[Dict[str, Any]]: @@ -190,7 +190,7 @@ class WecomClient: try: access_token = await self.get_access_token() if not access_token: - logging.error("获取access_token失败") + logger.error("获取access_token失败") return None url = f"https://qyapi.weixin.qq.com/cgi-bin/externalcontact/groupchat/get?access_token={access_token}" @@ -204,10 +204,10 @@ class WecomClient: if result.get("errcode") == 0: return result.get("group_chat") else: - logging.error(f"获取外部群聊信息失败: {result}") + logger.error(f"获取外部群聊信息失败: {result}") return None except Exception as e: - logging.error(f"获取外部群聊信息异常: {str(e)}") + logger.error(f"获取外部群聊信息异常: {str(e)}") return None async def get_external_contact_info(self, external_userid: str) -> Optional[Dict[str, Any]]: @@ -222,7 +222,7 @@ class WecomClient: try: access_token = await self.get_access_token() if not access_token: - logging.error("获取access_token失败") + logger.error("获取access_token失败") return None url = f"https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get?access_token={access_token}" @@ -236,10 +236,10 @@ class WecomClient: if result.get("errcode") == 0: return result.get("external_contact") else: - logging.error(f"获取外部联系人信息失败: {result}") + logger.error(f"获取外部联系人信息失败: {result}") return None except Exception as e: - logging.error(f"获取外部联系人信息异常: {str(e)}") + logger.error(f"获取外部联系人信息异常: {str(e)}") return None async def handle_chat_change_event(self, chat_id: str, change_type: str, update_detail: str, join_user_id: str = None) -> bool: @@ -408,7 +408,7 @@ class WecomClient: return False except Exception as e: - logging.error(f"处理群聊变更事件异常: {str(e)}") + logger.error(f"处理群聊变更事件异常: {str(e)}") if 'db' in locals(): db.close() return False diff --git a/jobs.sqlite b/jobs.sqlite index 9ff0b2bc00ed28c544fb6fa0c53cf8947602f7ab..2fa3dbfe0c14e0b896b625c9b6859a7b344eedab 100644 GIT binary patch delta 19 acmZoTz}Rqrae_2s;6xc`#=wmU3*rGqCkE00 delta 19 acmZoTz}Rqrae_3X_e2?IM(>RY3*rGp%?8E* From c907b1518c204dda3dd673fe57bc72935d6c2591 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Sun, 30 Mar 2025 12:08:07 +0800 Subject: [PATCH 15/21] update --- app/api/endpoints/wecom.py | 182 +++++++++++++++++++++++++++++++++---- app/core/wecomclient.py | 19 +++- jobs.sqlite | Bin 24576 -> 24576 bytes 3 files changed, 178 insertions(+), 23 deletions(-) diff --git a/app/api/endpoints/wecom.py b/app/api/endpoints/wecom.py index b0bd205..814ca74 100644 --- a/app/api/endpoints/wecom.py +++ b/app/api/endpoints/wecom.py @@ -18,6 +18,7 @@ from sqlalchemy.orm import Session from app.core.response import error_response, success_response, ResponseModel from pydantic import BaseModel from app.models.wecom_external_chat import WecomExternalChatDB, WecomExternalChatInfo, WecomExternalChatMemberDB, WecomExternalChatMemberInfo +from datetime import datetime router = APIRouter() @@ -133,26 +134,80 @@ async def wechat_corp_callback( update_detail = msg_root.find('UpdateDetail').text join_user_id = None - if update_detail == 'add_member' and msg_root.find('JoinScene') is not None: + quit_user_ids = [] + + # 处理成员加入事件 + if update_detail == 'add_member': logger.info(f"有新成员加入群聊") - # 获取加入的成员ID - join_user_id_elem = msg_root.find('JoinUserID') - if join_user_id_elem is not None: - join_user_id = join_user_id_elem.text + join_scene_elem = msg_root.find('JoinScene') + join_scene = int(join_scene_elem.text) if join_scene_elem is not None else 0 + logger.info(f"加入场景: {join_scene}") + + # 获取加入成员列表 + mem_change_list = msg_root.find('MemChangeList') + if mem_change_list is not None: + for item in mem_change_list.findall('Item'): + join_user_id = item.text + if join_user_id: + logger.info(f"从MemChangeList中获取到新加入成员: {join_user_id}") + # 处理群聊变更事件 - 添加单个成员 + await wecom_client.handle_chat_change_event( + chat_id=chat_id, + change_type=change_type, + update_detail=update_detail, + join_user_id=join_user_id + ) + logger.info(f"发送欢迎消息到群聊:{chat_id}") + await wecom_client.send_welcome_message(chat_id) + + # 兼容旧格式,如果找不到MemChangeList,尝试JoinUserID + elif msg_root.find('JoinUserID') is not None: + join_user_id = msg_root.find('JoinUserID').text + if join_user_id: + logger.info(f"从JoinUserID中获取到新加入成员: {join_user_id}") + # 处理群聊变更事件 - 添加单个成员 + await wecom_client.handle_chat_change_event( + chat_id=chat_id, + change_type=change_type, + update_detail=update_detail, + join_user_id=join_user_id + ) + logger.info(f"发送欢迎消息到群聊:{chat_id}") + await wecom_client.send_welcome_message(chat_id) + + # 处理成员离开事件 + elif update_detail == 'del_member': + logger.info(f"有成员离开群聊") + quit_scene_elem = msg_root.find('QuitScene') + quit_scene = int(quit_scene_elem.text) if quit_scene_elem is not None else 0 + + # 获取离开成员列表 + mem_change_list = msg_root.find('MemChangeList') + if mem_change_list is not None: + for item in mem_change_list.findall('Item'): + quit_user_id = item.text + if quit_user_id: + quit_user_ids.append(quit_user_id) + # 处理成员离开 + await wecom_client.handle_chat_change_event( + chat_id=chat_id, + change_type=change_type, + update_detail=update_detail, + join_user_id=quit_user_id # 复用这个参数名来表示离开的用户ID + ) + + logger.info(f"离开场景: {quit_scene}, 离开成员: {quit_user_ids}") - logger.info(f"chat_id: {chat_id}, change_type: {change_type}, update_detail: {update_detail}, join_user_id: {join_user_id}") + logger.info(f"chat_id: {chat_id}, change_type: {change_type}, update_detail: {update_detail}, join_user_id: {join_user_id}, quit_user_ids: {quit_user_ids}") - # 处理群聊变更事件 - await wecom_client.handle_chat_change_event( - chat_id=chat_id, - change_type=change_type, - update_detail=update_detail, - join_user_id=join_user_id - ) - - if update_detail == 'add_member' and join_user_id: - logger.info(f"发送欢迎消息到群聊:{chat_id}") - await wecom_client.send_welcome_message(chat_id) + # 处理其他群聊变更事件(不是add_member和del_member) + if update_detail != 'add_member' and update_detail != 'del_member': + # 其他群聊变更事件 + await wecom_client.handle_chat_change_event( + chat_id=chat_id, + change_type=change_type, + update_detail=update_detail + ) return Response(content="success", media_type="text/plain") @@ -307,6 +362,23 @@ async def chat_dashboard( text-decoration: none; } .btn:hover { background-color: #286090; } + .stats { + background-color: #f5f5f5; + border: 1px solid #ddd; + border-radius: 4px; + padding: 15px; + margin-bottom: 20px; + } + .stat-item { + display: inline-block; + margin-right: 30px; + font-size: 16px; + } + .stat-number { + font-size: 24px; + font-weight: bold; + color: #337ab7; + }

企业微信外部群聊信息

+ +
+
+
总群聊数
+
-
+
+
+
总成员数
+
-
+
+
+
活跃群聊数
+
-
+
+
""" if not chats: @@ -392,4 +502,40 @@ async def chat_dashboard( """ - return Response(content=html, media_type="text/html") \ No newline at end of file + return Response(content=html, media_type="text/html") + +@router.get("/chat-stats", response_model=ResponseModel) +async def get_chat_stats( + db: Session = Depends(get_db), + current_user: UserDB = Depends(get_current_user) +): + """获取企业微信外部群聊统计数据""" + try: + # 检查是否为管理员 + if current_user.userid != settings.PLATFORM_USER_ID: + return error_response(code=403, message="权限不足") + + # 计算统计数据 + total_chats = db.query(WecomExternalChatDB).count() + active_chats = db.query(WecomExternalChatDB).filter(WecomExternalChatDB.is_active == True).count() + total_members = db.query(WecomExternalChatMemberDB).count() + + # 获取今日新增成员数 + today = datetime.now().date() + today_start = datetime.combine(today, datetime.min.time()) + today_members = db.query(WecomExternalChatMemberDB).filter( + WecomExternalChatMemberDB.join_time >= today_start + ).count() + + # 返回统计数据 + stats = { + "total_chats": total_chats, + "active_chats": active_chats, + "total_members": total_members, + "today_members": today_members + } + + return success_response(message="获取统计数据成功", data=stats) + except Exception as e: + logger.exception("获取统计数据异常") + return error_response(code=500, message=f"获取统计数据失败: {str(e)}") \ No newline at end of file diff --git a/app/core/wecomclient.py b/app/core/wecomclient.py index 45b08ed..e1ca932 100644 --- a/app/core/wecomclient.py +++ b/app/core/wecomclient.py @@ -249,7 +249,7 @@ class WecomClient: chat_id: 群聊ID change_type: 变更类型 create/update/dismiss update_detail: 变更详情 add_member/del_member/change_owner/change_name/change_notice - join_user_id: 加入的用户ID + join_user_id: 加入/离开的用户ID Returns: bool: 处理是否成功 @@ -278,7 +278,7 @@ class WecomClient: member_count=len(chat_info.get("member_list", [])), notice=chat_info.get("notice") ) - chat_db = WecomExternalChatDB(**chat_create.dict()) + chat_db = WecomExternalChatDB(**chat_create.model_dump()) db.add(chat_db) db.commit() @@ -306,7 +306,7 @@ class WecomClient: name=user_info.get("name") if user_info else None, unionid=user_info.get("unionid") if user_info else None ) - member_db = WecomExternalChatMemberDB(**member_create.dict()) + member_db = WecomExternalChatMemberDB(**member_create.model_dump()) db.add(member_db) db.commit() @@ -330,7 +330,7 @@ class WecomClient: member_count=len(chat_info.get("member_list", [])), notice=chat_info.get("notice") ) - chat_db = WecomExternalChatDB(**chat_create.dict()) + chat_db = WecomExternalChatDB(**chat_create.model_dump()) db.add(chat_db) db.commit() @@ -354,7 +354,7 @@ class WecomClient: name=user_info.get("name") if user_info else None, unionid=user_info.get("unionid") if user_info else None ) - member_db = WecomExternalChatMemberDB(**member_create.dict()) + member_db = WecomExternalChatMemberDB(**member_create.model_dump()) db.add(member_db) db.commit() @@ -378,6 +378,8 @@ class WecomClient: if not join_user_id: return False + logger.info(f"处理成员离开群聊事件: 群ID={chat_id}, 成员ID={join_user_id}") + # 删除成员记录 member_db = db.query(WecomExternalChatMemberDB).filter( WecomExternalChatMemberDB.chat_id == chat_id, @@ -385,14 +387,20 @@ class WecomClient: ).first() if member_db: + logger.info(f"从数据库中删除成员记录: {member_db.user_id}, 姓名: {member_db.name}") db.delete(member_db) db.commit() + else: + logger.warning(f"未找到要删除的成员记录: chat_id={chat_id}, user_id={join_user_id}") # 更新群成员数量 chat_db = db.query(WecomExternalChatDB).filter(WecomExternalChatDB.chat_id == chat_id).first() if chat_db: chat_db.member_count = max(0, chat_db.member_count - 1) + logger.info(f"更新群成员数量: {chat_db.member_count}") db.commit() + else: + logger.warning(f"未找到群聊记录: chat_id={chat_id}") return True @@ -403,6 +411,7 @@ class WecomClient: if chat_db: chat_db.is_active = False db.commit() + logger.info(f"群聊解散: {chat_id}") return True diff --git a/jobs.sqlite b/jobs.sqlite index 2fa3dbfe0c14e0b896b625c9b6859a7b344eedab..2ce557e297f0ede658a54057c5f45aae1dae12d0 100644 GIT binary patch delta 19 acmZoTz}Rqrae_2s;zSu|#>9;Y3*rGr7Y6bG delta 19 acmZoTz}Rqrae_2s;6xc`#=wmU3*rGqCkE00 From 1b34452c1f6d943542eae16cbf54ae700049e724 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Sun, 30 Mar 2025 13:24:29 +0800 Subject: [PATCH 16/21] update --- app/core/wecomclient.py | 1 + jobs.sqlite | Bin 24576 -> 24576 bytes 2 files changed, 1 insertion(+) diff --git a/app/core/wecomclient.py b/app/core/wecomclient.py index e1ca932..f922f05 100644 --- a/app/core/wecomclient.py +++ b/app/core/wecomclient.py @@ -234,6 +234,7 @@ class WecomClient: async with session.get(url, params=params) as response: result = await response.json() if result.get("errcode") == 0: + logger.info(f"获取外部联系人信息成功: {result}") return result.get("external_contact") else: logger.error(f"获取外部联系人信息失败: {result}") diff --git a/jobs.sqlite b/jobs.sqlite index 2ce557e297f0ede658a54057c5f45aae1dae12d0..f91b1d3efa793f246e08f9fc93c61a237c45cbfc 100644 GIT binary patch delta 19 acmZoTz}Rqrae_2s@9;Y3*rGr7Y6bG From bfff5ada51a19a1350fcd22ab07201a615184f24 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Sun, 30 Mar 2025 13:41:18 +0800 Subject: [PATCH 17/21] update --- app/api/endpoints/wecom.py | 4 --- app/core/wecomclient.py | 70 ------------------------------------- jobs.sqlite | Bin 24576 -> 24576 bytes 3 files changed, 74 deletions(-) diff --git a/app/api/endpoints/wecom.py b/app/api/endpoints/wecom.py index 814ca74..1e657c1 100644 --- a/app/api/endpoints/wecom.py +++ b/app/api/endpoints/wecom.py @@ -157,8 +157,6 @@ async def wechat_corp_callback( update_detail=update_detail, join_user_id=join_user_id ) - logger.info(f"发送欢迎消息到群聊:{chat_id}") - await wecom_client.send_welcome_message(chat_id) # 兼容旧格式,如果找不到MemChangeList,尝试JoinUserID elif msg_root.find('JoinUserID') is not None: @@ -172,8 +170,6 @@ async def wechat_corp_callback( update_detail=update_detail, join_user_id=join_user_id ) - logger.info(f"发送欢迎消息到群聊:{chat_id}") - await wecom_client.send_welcome_message(chat_id) # 处理成员离开事件 elif update_detail == 'del_member': diff --git a/app/core/wecomclient.py b/app/core/wecomclient.py index f922f05..d1bcaad 100644 --- a/app/core/wecomclient.py +++ b/app/core/wecomclient.py @@ -116,68 +116,6 @@ class WecomClient: logger.error(f"unionid_to_external_userid异常: {str(e)}") return None - async def send_welcome_message(self, chat_id: str, user_id: str = None) -> bool: - """发送欢迎消息 - - Args: - chat_id: 群聊ID - user_id: 用户ID,如果指定则发送私信,否则发送群消息 - - Returns: - bool: 是否发送成功 - """ - try: - # 1. 获取 access_token - access_token = await self.get_access_token() - if not access_token: - logger.error("获取access_token失败") - return False - - welcome_text = f"""🥳 欢迎您进群,在群内可以享受📦【代取快递】跑腿服务。 - -‼ 微信下单,快递到家 ‼ - -🎁 新人礼包 -𝟏 赠送𝟏𝟓张【𝟑元跑腿券】 -𝟐 赠送𝟔枚鲜鸡蛋【首次下单】 -━ ━ ━ ━ ━🎊━ ━ ━ ━ ━ -↓点击↓小程序领券下单 &""" - - # 2. 发送欢迎消息 - if user_id: - # 发送私信 - url = f"https://qyapi.weixin.qq.com/cgi-bin/externalcontact/send_welcome_msg?access_token={access_token}" - data = { - "welcome_code": user_id, # 这里使用user_id作为临时的欢迎码 - "text": { - "content": welcome_text - } - } - else: - # 发送群消息 - url = f"https://qyapi.weixin.qq.com/cgi-bin/appchat/send?access_token={access_token}" - data = { - "chatid": chat_id, - "msgtype": "text", - "text": { - "content": welcome_text - }, - "safe": 0 - } - - async with aiohttp.ClientSession() as session: - async with session.post(url, json=data) as response: - result = await response.json() - if result.get("errcode") == 0: - return True - else: - logger.error(f"发送欢迎消息失败: {result}") - return False - - except Exception as e: - logger.error(f"发送欢迎消息异常: {str(e)}") - return False - async def get_external_chat_info(self, chat_id: str) -> Optional[Dict[str, Any]]: """获取外部群聊信息 @@ -358,14 +296,6 @@ class WecomClient: member_db = WecomExternalChatMemberDB(**member_create.model_dump()) db.add(member_db) db.commit() - - # 发送欢迎消息 - if member_type == "EXTERNAL": - await self.send_welcome_message(chat_id) - - # 更新发送欢迎消息状态 - member_db.welcome_sent = True - db.commit() # 更新群成员数量 if chat_db: diff --git a/jobs.sqlite b/jobs.sqlite index f91b1d3efa793f246e08f9fc93c61a237c45cbfc..f5886f979eb6427739244f7cced44b1c757945a4 100644 GIT binary patch delta 19 acmZoTz}Rqrae_2s_Cy(H#_Wv=3*rGrtp^4G delta 19 acmZoTz}Rqrae_2s@ Date: Sun, 30 Mar 2025 13:48:18 +0800 Subject: [PATCH 18/21] update --- app/core/wecomclient.py | 44 ++++++++++++++++++++++++++++++++++------ jobs.sqlite | Bin 24576 -> 24576 bytes 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/app/core/wecomclient.py b/app/core/wecomclient.py index d1bcaad..9823b9c 100644 --- a/app/core/wecomclient.py +++ b/app/core/wecomclient.py @@ -205,8 +205,11 @@ class WecomClient: # 获取群聊信息 chat_info = await self.get_external_chat_info(chat_id) if not chat_info: + logger.warning(f"无法获取群聊信息: {chat_id}") return False + logger.info(f"获取到群聊信息: {chat_info}") + # 保存群聊信息 chat_db = db.query(WecomExternalChatDB).filter(WecomExternalChatDB.chat_id == chat_id).first() if not chat_db: @@ -236,13 +239,16 @@ class WecomClient: # 获取外部联系人详情 user_info = None if member_type == "EXTERNAL": - user_info = await self.get_external_contact_info(user_id) + try: + user_info = await self.get_external_contact_info(user_id) + except Exception as e: + logger.warning(f"获取外部联系人信息失败: {user_id}, 错误: {str(e)}") member_create = WecomExternalChatMemberCreate( chat_id=chat_id, user_id=user_id, type=member_type, - name=user_info.get("name") if user_info else None, + name=user_info.get("name") if user_info else member.get("name", "未知"), unionid=user_info.get("unionid") if user_info else None ) member_db = WecomExternalChatMemberDB(**member_create.model_dump()) @@ -280,22 +286,46 @@ class WecomClient: ).first() if not member_db: - # 判断是内部成员还是外部联系人 + # 尝试获取群聊成员信息来确定成员类型 member_type = "EXTERNAL" # 默认为外部联系人 + member_name = None + user_info = None - # 获取外部联系人详情 - user_info = await self.get_external_contact_info(join_user_id) + # 获取群聊详情,查找成员类型 + try: + chat_info = await self.get_external_chat_info(chat_id) + if chat_info and "member_list" in chat_info: + for member in chat_info["member_list"]: + if member.get("userid") == join_user_id: + member_type = member.get("type", "EXTERNAL") + member_name = member.get("name") + logger.info(f"从群聊详情中确定成员类型: {member_type}, 成员: {join_user_id}") + break + except Exception as e: + logger.warning(f"获取群聊详情失败: {str(e)}") + + # 如果确定是外部联系人,尝试获取详情 + if member_type == "EXTERNAL": + try: + user_info = await self.get_external_contact_info(join_user_id) + if user_info: + member_name = user_info.get("name") + logger.info(f"获取到外部联系人详情: {member_name}") + except Exception as e: + logger.warning(f"获取外部联系人信息失败,可能是内部成员: {str(e)}") + # 如果获取失败但在群聊详情中找到了,则使用群聊详情中的信息 member_create = WecomExternalChatMemberCreate( chat_id=chat_id, user_id=join_user_id, type=member_type, - name=user_info.get("name") if user_info else None, + name=member_name, unionid=user_info.get("unionid") if user_info else None ) member_db = WecomExternalChatMemberDB(**member_create.model_dump()) db.add(member_db) db.commit() + # 更新群成员数量 if chat_db: @@ -349,6 +379,8 @@ class WecomClient: return False except Exception as e: logger.error(f"处理群聊变更事件异常: {str(e)}") + import traceback + logger.error(traceback.format_exc()) if 'db' in locals(): db.close() return False diff --git a/jobs.sqlite b/jobs.sqlite index f5886f979eb6427739244f7cced44b1c757945a4..628cfce1762edbf6f267fb9a64e10a79765d67ff 100644 GIT binary patch delta 19 acmZoTz}Rqrae_2s?L--8#@dYu3*rGsy9X)& delta 19 acmZoTz}Rqrae_2s_Cy(H#_Wv=3*rGrtp^4G From 8ac6961a4d6c0400c7448cd9f087c714c53d037c Mon Sep 17 00:00:00 2001 From: aaron <> Date: Sun, 30 Mar 2025 14:00:37 +0800 Subject: [PATCH 19/21] update --- app/core/wecomclient.py | 107 ++++++++++++++++++++++++++++++++++++---- jobs.sqlite | Bin 24576 -> 24576 bytes 2 files changed, 97 insertions(+), 10 deletions(-) diff --git a/app/core/wecomclient.py b/app/core/wecomclient.py index 9823b9c..39dd1e4 100644 --- a/app/core/wecomclient.py +++ b/app/core/wecomclient.py @@ -181,6 +181,36 @@ class WecomClient: logger.error(f"获取外部联系人信息异常: {str(e)}") return None + async def get_internal_user_info(self, user_id: str) -> Optional[Dict[str, Any]]: + """获取企业内部成员信息 + + Args: + user_id: 企业成员userid + + Returns: + Dict: 企业成员信息 + """ + try: + access_token = await self.get_access_token() + if not access_token: + logger.error("获取access_token失败") + return None + + url = f"https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token={access_token}&userid={user_id}" + + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + result = await response.json() + if result.get("errcode") == 0: + logger.info(f"获取内部成员信息成功: {result}") + return result + else: + logger.error(f"获取内部成员信息失败: {result}") + return None + except Exception as e: + logger.error(f"获取内部成员信息异常: {str(e)}") + return None + async def handle_chat_change_event(self, chat_id: str, change_type: str, update_detail: str, join_user_id: str = None) -> bool: """处理群聊变更事件 @@ -227,7 +257,16 @@ class WecomClient: # 保存群成员信息 for member in chat_info.get("member_list", []): user_id = member.get("userid") - member_type = member.get("type") + # 处理数字类型: 1=企业成员, 2=外部联系人 + member_type_num = member.get("type") + if member_type_num == 1: + member_type = "INTERNAL" + elif member_type_num == 2: + member_type = "EXTERNAL" + else: + member_type = "EXTERNAL" # 默认为外部联系人 + + logger.info(f"成员类型: {member_type}(原始值:{member_type_num}), 成员ID: {user_id}") # 检查成员是否已存在 member_db = db.query(WecomExternalChatMemberDB).filter( @@ -236,20 +275,44 @@ class WecomClient: ).first() if not member_db: - # 获取外部联系人详情 + # 获取成员详情 - 根据成员类型调用不同API user_info = None + if member_type == "EXTERNAL": try: user_info = await self.get_external_contact_info(user_id) + logger.info(f"获取到外部联系人详情: {user_info}") except Exception as e: logger.warning(f"获取外部联系人信息失败: {user_id}, 错误: {str(e)}") + elif member_type == "INTERNAL": + try: + user_info = await self.get_internal_user_info(user_id) + logger.info(f"获取到内部成员详情: {user_info}") + except Exception as e: + logger.warning(f"获取内部成员信息失败: {user_id}, 错误: {str(e)}") + + # 获取名称和unionid (内部成员可能没有unionid) + member_name = None + member_unionid = None + + if user_info: + if member_type == "EXTERNAL": + member_name = user_info.get("name") + member_unionid = user_info.get("unionid") + elif member_type == "INTERNAL": + member_name = user_info.get("name") + # 注意: 内部成员可能没有unionid字段 + + # 如果API获取失败,则使用群聊详情中的信息 + if not member_name: + member_name = "未知成员" # 没有任何名称信息时的默认值 member_create = WecomExternalChatMemberCreate( chat_id=chat_id, user_id=user_id, type=member_type, - name=user_info.get("name") if user_info else member.get("name", "未知"), - unionid=user_info.get("unionid") if user_info else None + name=member_name, + unionid=member_unionid ) member_db = WecomExternalChatMemberDB(**member_create.model_dump()) db.add(member_db) @@ -297,35 +360,59 @@ class WecomClient: if chat_info and "member_list" in chat_info: for member in chat_info["member_list"]: if member.get("userid") == join_user_id: - member_type = member.get("type", "EXTERNAL") + # 处理数字类型: 1=企业成员, 2=外部联系人 + member_type_num = member.get("type") + if member_type_num == 1: + member_type = "INTERNAL" + elif member_type_num == 2: + member_type = "EXTERNAL" + else: + member_type = "EXTERNAL" # 默认为外部联系人 + member_name = member.get("name") - logger.info(f"从群聊详情中确定成员类型: {member_type}, 成员: {join_user_id}") + logger.info(f"从群聊详情中确定成员类型: {member_type}(原始值:{member_type_num}), 成员: {join_user_id}") break except Exception as e: logger.warning(f"获取群聊详情失败: {str(e)}") # 如果确定是外部联系人,尝试获取详情 + user_info = None + member_name = None + member_unionid = None + if member_type == "EXTERNAL": try: user_info = await self.get_external_contact_info(join_user_id) if user_info: member_name = user_info.get("name") - logger.info(f"获取到外部联系人详情: {member_name}") + member_unionid = user_info.get("unionid") + logger.info(f"获取到外部联系人详情: {member_name}, unionid: {member_unionid}") except Exception as e: logger.warning(f"获取外部联系人信息失败,可能是内部成员: {str(e)}") - # 如果获取失败但在群聊详情中找到了,则使用群聊详情中的信息 + elif member_type == "INTERNAL": + try: + user_info = await self.get_internal_user_info(join_user_id) + if user_info: + member_name = user_info.get("name") + # 内部成员没有unionid + logger.info(f"获取到内部成员详情: {member_name}") + except Exception as e: + logger.warning(f"获取内部成员信息失败: {str(e)}") + + # 如果API获取失败,则使用群聊详情中的信息 + if not member_name: + member_name = "未知成员" # 没有任何名称信息时的默认值 member_create = WecomExternalChatMemberCreate( chat_id=chat_id, user_id=join_user_id, type=member_type, name=member_name, - unionid=user_info.get("unionid") if user_info else None + unionid=member_unionid ) member_db = WecomExternalChatMemberDB(**member_create.model_dump()) db.add(member_db) db.commit() - # 更新群成员数量 if chat_db: diff --git a/jobs.sqlite b/jobs.sqlite index 628cfce1762edbf6f267fb9a64e10a79765d67ff..59ef3a3093a6b645a702e3e05f18a34c0e5f64f1 100644 GIT binary patch delta 19 bcmZoTz}Rqrae_4C)QK|Aj8iuzEQkjHNh1eZ delta 19 acmZoTz}Rqrae_2s?L--8#@dYu3*rGsy9X)& From 9376f9a785d18d6821b90fdfef242e022da6b260 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Sun, 30 Mar 2025 14:28:46 +0800 Subject: [PATCH 20/21] update --- app/core/wecomclient.py | 19 +++++++++---------- jobs.sqlite | Bin 24576 -> 24576 bytes 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/app/core/wecomclient.py b/app/core/wecomclient.py index 39dd1e4..b9f5a1d 100644 --- a/app/core/wecomclient.py +++ b/app/core/wecomclient.py @@ -375,7 +375,12 @@ class WecomClient: except Exception as e: logger.warning(f"获取群聊详情失败: {str(e)}") - # 如果确定是外部联系人,尝试获取详情 + # 如果是内部成员,不做任何处理 + if member_type == "INTERNAL": + logger.info(f"成员 {join_user_id} 是内部成员,不做处理") + return True + + # 如果是外部联系人,获取详细信息 user_info = None member_name = None member_unionid = None @@ -389,15 +394,9 @@ class WecomClient: logger.info(f"获取到外部联系人详情: {member_name}, unionid: {member_unionid}") except Exception as e: logger.warning(f"获取外部联系人信息失败,可能是内部成员: {str(e)}") - elif member_type == "INTERNAL": - try: - user_info = await self.get_internal_user_info(join_user_id) - if user_info: - member_name = user_info.get("name") - # 内部成员没有unionid - logger.info(f"获取到内部成员详情: {member_name}") - except Exception as e: - logger.warning(f"获取内部成员信息失败: {str(e)}") + if "84061" in str(e) or "not external contact" in str(e): + logger.info(f"用户 {join_user_id} 可能是内部成员,不做处理") + return True # 如果API获取失败,则使用群聊详情中的信息 if not member_name: diff --git a/jobs.sqlite b/jobs.sqlite index 59ef3a3093a6b645a702e3e05f18a34c0e5f64f1..407094322016965a408ec6ed070ec452be3e4db9 100644 GIT binary patch delta 19 bcmZoTz}Rqrae_4C@`*CejLSDBEQkjHN-+n1 delta 19 bcmZoTz}Rqrae_4C)QK|Aj8iuzEQkjHNh1eZ From 0ed2b1d7322b003d6ba182ed98b84c87bf6ef73a Mon Sep 17 00:00:00 2001 From: aaron <> Date: Sun, 30 Mar 2025 16:52:57 +0800 Subject: [PATCH 21/21] update --- app/api/endpoints/order.py | 18 ++++++++++-------- app/core/config.py | 13 +++++-------- jobs.sqlite | Bin 24576 -> 24576 bytes 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/app/api/endpoints/order.py b/app/api/endpoints/order.py index bd3921c..dc46d32 100644 --- a/app/api/endpoints/order.py +++ b/app/api/endpoints/order.py @@ -1261,21 +1261,23 @@ async def deliveryman_complete_order( db.commit() - # 如果当前订单是首单,如果有邀请人,给邀请人发放优惠券 - if order.is_first_order and order_user.referral_code: + # 有邀请人,给邀请人积分奖励 + if order_user.referral_code: # 查询邀请人 invite_user = db.query(UserDB).filter( UserDB.user_code == order_user.referral_code ).first() if invite_user: - expire_time = datetime.now() + timedelta(days=settings.FIRST_ORDER_REFERRAL_COUPON_EXPIRE_DAYS) - manager = CouponManager(db) - manager.add_coupon( + points = settings.FIRST_ORDER_REFERRAL_POINT if order.is_first_order else settings.COMMON_ORDER_REFERRAL_POINT + desc = f"蜜友首单奖励" if order.is_first_order else f"蜜友下单奖励" + # 邀请人赠送积分 + point_manager = PointManager(db) + point_manager.add_points( user_id=invite_user.userid, - coupon_id=settings.FIRST_ORDER_REFERRAL_COUPON_ID, - expire_time=expire_time, - count=settings.FIRST_ORDER_REFERRAL_COUPON_COUNT + points=points, + description=desc, + order_id=order.orderid ) # 发送企业微信消息 diff --git a/app/core/config.py b/app/core/config.py index 7d4704e..0a69037 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -29,10 +29,11 @@ class Settings(BaseSettings): ORDER_EXTRA_PACKAGE_PRICE: float = 0.5 # 额外包裹费用 ORDER_EXTRA_PACKAGE_THRESHOLD: int = 5 # 额外收费阈值 - # 邀请新人赠送优惠券ID - FIRST_ORDER_REFERRAL_COUPON_ID: int = 1 - FIRST_ORDER_REFERRAL_COUPON_COUNT: int = 1 - FIRST_ORDER_REFERRAL_COUPON_EXPIRE_DAYS: int = 7 + # 邀请新人赠送积分 + FIRST_ORDER_REFERRAL_POINT: int = 5 + COMMON_ORDER_REFERRAL_POINT: int = 1 + + # JWT 配置 SECRET_KEY: str = "s10GmiRMmplfYWXYZLSsE3X36Ld4gVZxHgAcdqFGC20v3llv7UdOeWLBEEP3e40p" @@ -193,10 +194,6 @@ class ProdSettings(Settings): REDIS_PASSWORD: str = "redis_tjcZif" VERIFICATION_CODE_EXPIRE_SECONDS: int = 300 - FIRST_ORDER_REFERRAL_COUPON_ID: int = 2 - FIRST_ORDER_REFERRAL_COUPON_COUNT: int = 1 - FIRST_ORDER_REFERRAL_COUPON_EXPIRE_DAYS: int = 3 - class Config: env_file = ".env.prod" diff --git a/jobs.sqlite b/jobs.sqlite index 407094322016965a408ec6ed070ec452be3e4db9..b6c79a221646cec04e8c522bba5803f1558485fb 100644 GIT binary patch delta 19 bcmZoTz}Rqrae_4C-ib2KjC(gGEQkjHOM3^Z delta 19 bcmZoTz}Rqrae_4C@`*CejLSDBEQkjHN-+n1