This commit is contained in:
aaron 2025-02-18 22:04:04 +08:00
parent db6a9af7c0
commit 2b0f4aacef
6 changed files with 210 additions and 6 deletions

83
app/api/endpoints/mp.py Normal file
View File

@ -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")

View File

@ -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()
# 创建访问令牌

View File

@ -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"

109
app/core/mpclient.py Normal file
View File

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

View File

@ -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=["提现"])

View File

@ -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