From 2b0f4aacef94dae1284c76bb1826246cc61cee18 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Tue, 18 Feb 2025 22:04:04 +0800 Subject: [PATCH] update --- app/api/endpoints/mp.py | 83 +++++++++++++++++++++++++++ app/api/endpoints/wechat.py | 11 ++-- app/core/config.py | 5 ++ app/core/mpclient.py | 109 ++++++++++++++++++++++++++++++++++++ app/main.py | 4 +- app/models/user.py | 4 ++ 6 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 app/api/endpoints/mp.py create mode 100644 app/core/mpclient.py diff --git a/app/api/endpoints/mp.py b/app/api/endpoints/mp.py new file mode 100644 index 0000000..72ddfcb --- /dev/null +++ b/app/api/endpoints/mp.py @@ -0,0 +1,83 @@ +from fastapi import APIRouter, Request, Response +from app.core.mpclient import mp_client +from app.core.response import success_response, error_response +from app.models.database import get_db +from app.models.user import UserDB +import xml.etree.ElementTree as ET +from datetime import datetime +import hashlib +import time +from app.core.config import settings +import logging + +router = APIRouter() + +def check_signature(signature: str, timestamp: str, nonce: str) -> bool: + """验证微信服务器签名""" + token = settings.MP_TOKEN # 在配置文件中添加 MP_TOKEN + # 按字典序排序 + temp_list = [token, timestamp, nonce] + temp_list.sort() + # 拼接字符串 + temp_str = "".join(temp_list) + # SHA1加密 + sign = hashlib.sha1(temp_str.encode('utf-8')).hexdigest() + # 与微信发送的签名对比 + return sign == signature + +@router.get("") +async def verify_server( + signature: str, + timestamp: str, + nonce: str, + echostr: str +): + """验证服务器地址的有效性""" + if check_signature(signature, timestamp, nonce): + return Response(content=echostr, media_type="text/plain") + return Response(status_code=403) + +@router.post("") +async def handle_server(request: Request): + """处理微信服务器推送的消息和事件""" + try: + # 读取原始XML数据 + body = await request.body() + root = ET.fromstring(body) + + logging.info(f"微信公众号消息:{root}") + + # 解析基本信息 + msg_type = root.find('MsgType').text + from_user = root.find('FromUserName').text + to_user = root.find('ToUserName').text + create_time = int(root.find('CreateTime').text) + + # 获取数据库会话 + db = next(get_db()) + + # 处理不同类型的消息和事件 + if msg_type == 'event': + event = root.find('Event').text.lower() + + if event == 'subscribe': # 关注事件 + # 获取用户信息 + user_info = await mp_client.get_user_info(from_user) + if user_info: + # 查找或创建用户 + user = db.query(UserDB).filter( + UserDB.unionid == user_info.get('unionid') + ).first() + + if user: + # 更新用户信息 + user.mp_openid = from_user + db.commit() + + + return Response(content="", media_type="text/plain") + + except Exception as e: + logging.exception("处理微信消息异常") + # 返回空字符串表示接收成功 + return Response(content="", media_type="text/plain") \ No newline at end of file diff --git a/app/api/endpoints/wechat.py b/app/api/endpoints/wechat.py index 5c542b3..590b1a4 100644 --- a/app/api/endpoints/wechat.py +++ b/app/api/endpoints/wechat.py @@ -48,6 +48,7 @@ async def wechat_phone_login( # 获取用户 openid session_info = await wechat.code2session(request.login_code) openid = session_info["openid"] + unionid = session_info.get("unionid") # 获取用户手机号 phone_info = await wechat.get_phone_number(request.phone_code) @@ -73,20 +74,22 @@ async def wechat_phone_login( phone=phone, user_code=user_code, referral_code=request.referral_code, - openid=openid # 保存 openid + openid=openid, # 保存 openid + unionid=unionid # 保存 unionid ) db.add(user) db.flush() # 发放优惠券 - from app.api.endpoints.user import issue_register_coupons - issue_register_coupons(db, user.userid) + # from app.api.endpoints.user import issue_register_coupons + # issue_register_coupons(db, user.userid) db.commit() db.refresh(user) else: - # 更新现有用户的 openid + # 更新现有用户的 openid 和 unionid user.openid = openid + user.unionid = unionid db.commit() # 创建访问令牌 diff --git a/app/core/config.py b/app/core/config.py index 042743a..213e0ea 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -69,6 +69,11 @@ class Settings(BaseSettings): WECHAT_API_V3_KEY: str = "OAhAqXqebeT4ZC9VTYFkSWU0CENEahx5" # API v3密钥 WECHAT_PLATFORM_CERT_PATH: str = "app/cert/platform_key.pem" # 平台证书路径 + MP_APPID: str = "wxa9db2cc7868dfefd" + MP_SECRET: str = "3eed9a717654d6460ba9afda3b0f6be2" + MP_TOKEN: str = "yORAT7RL9I3sux7uc4PbMEEHT1xowc6H" # 用于验证服务器配置 + MP_AES_KEY: str = "XDc2mG1tWNikTithcSy66oD3fP5XXFasSeRk6ulicye" # 用于解密消息 + # 微信模板消息ID #配送订单创建成功 WECHAT_DELIVERY_ORDER_CREATED_TEMPLATE_ID: str = "-aFOuC2dv1E6Opn9auB39bSiU4p0DbKOrUtOFgS-AaA" diff --git a/app/core/mpclient.py b/app/core/mpclient.py new file mode 100644 index 0000000..d6c741a --- /dev/null +++ b/app/core/mpclient.py @@ -0,0 +1,109 @@ +import aiohttp +import json +from app.core.config import settings +import redis +import time +from typing import Dict, Any, Optional + +class MPClient: + """微信公众号客户端""" + + def __init__(self): + self.appid = settings.MP_APPID + self.secret = settings.MP_SECRET + + async def get_access_token(self) -> str: + """获取访问令牌""" + async with aiohttp.ClientSession() as session: + url = f"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={self.appid}&secret={self.secret}" + async with session.get(url) as response: + result = await response.json() + + if "access_token" not in result: + raise Exception(f"获取access_token失败: {result}") + + access_token = result["access_token"] + + return access_token + + async def send_template_message( + self, + openid: str, + template_id: str, + data: Dict[str, Any], + url: Optional[str] = None, + miniprogram: Optional[Dict[str, str]] = None + ) -> bool: + """ + 发送模板消息 + :param openid: 用户openid + :param template_id: 模板ID + :param data: 模板数据 + :param url: 点击跳转的链接(可选) + :param miniprogram: 跳转小程序信息(可选) + :return: 发送是否成功 + """ + try: + access_token = await self.get_access_token() + api_url = f"https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={access_token}" + + message = { + "touser": openid, + "template_id": template_id, + "data": { + key: { + "value": value + } for key, value in data.items() + } + } + + # 添加跳转链接 + if url: + message["url"] = url + + # 添加小程序跳转信息 + if miniprogram: + message["miniprogram"] = miniprogram + + async with aiohttp.ClientSession() as session: + async with session.post(api_url, json=message) as response: + result = await response.json() + + if result.get("errcode") == 0: + return True + + print(f"发送模板消息失败: {result}") + return False + + except Exception as e: + print(f"发送模板消息异常: {str(e)}") + return False + + + # 根据unionid获取用户信息 + async def get_user_info(self, unionid: str) -> Optional[Dict]: + """ + 获取用户基本信息 + :param unionid: 用户unionid + :return: 用户信息字典 + """ + try: + access_token = await self.get_access_token() + url = f"https://api.weixin.qq.com/cgi-bin/user/info?access_token={access_token}&unionid={unionid}&lang=zh_CN" + + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + result = await response.json() + + if "errcode" in result: + print(f"获取用户信息失败: {result}") + return None + + return result + + except Exception as e: + print(f"获取用户信息异常: {str(e)}") + return None + +# 创建全局实例 +mp_client = MPClient() \ No newline at end of file diff --git a/app/main.py b/app/main.py index b7036d1..13fcef8 100644 --- a/app/main.py +++ b/app/main.py @@ -1,6 +1,6 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from app.api.endpoints import wechat,user, address, community, station, order, coupon, community_building, upload, merchant, merchant_product, merchant_order, point, config, merchant_category, log, account,merchant_pay_order, message, bank_card, withdraw, subscribe +from app.api.endpoints import wechat,user, address, community, station, order, coupon, community_building, upload, merchant, merchant_product, merchant_order, point, config, merchant_category, log, account,merchant_pay_order, message, bank_card, withdraw, mp from app.models.database import Base, engine from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse @@ -34,7 +34,7 @@ app.add_middleware(RequestLoggerMiddleware) # 添加用户路由 app.include_router(wechat.router,prefix="/api/wechat",tags=["微信"]) -app.include_router(subscribe.router, prefix="/api/subscribe", tags=["小程序订阅消息"]) +app.include_router(mp.router, prefix="/api/mp", tags=["微信公众号"]) app.include_router(user.router, prefix="/api/user", tags=["用户"]) app.include_router(bank_card.router, prefix="/api/bank-cards", tags=["用户银行卡"]) app.include_router(withdraw.router, prefix="/api/withdraw", tags=["提现"]) diff --git a/app/models/user.py b/app/models/user.py index b72d577..bcfef03 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -26,6 +26,8 @@ class UserDB(Base): userid = Column(Integer, primary_key=True,autoincrement=True, index=True) openid = Column(String(64), unique=True, nullable=True) + unionid = Column(String(64), unique=True, nullable=True) + mp_openid = Column(String(64), unique=True, nullable=True) nickname = Column(String(50)) phone = Column(String(11), unique=True, index=True) user_code = Column(String(6), unique=True, nullable=False) @@ -53,6 +55,8 @@ class UserLogin(BaseModel): class UserInfo(BaseModel): userid: int openid: Optional[str] = None + unionid: Optional[str] = None + mp_openid: Optional[str] = None nickname: str phone: str user_code: str