From 381a95654a20b6fc3b6d046a2ba43a451ce0e144 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Thu, 27 Feb 2025 15:24:56 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8C=BA=E5=88=86=E7=8E=AF=E5=A2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 0 .env.dev | 0 .env.prd | 0 .gitignore | 1 - app/api/endpoints/wechat.py | 28 ++--- app/core/config.py | 49 ++++++++- app/core/wecomclient.py | 199 +++++++++++++++++++++--------------- app/main.py | 8 ++ 8 files changed, 188 insertions(+), 97 deletions(-) create mode 100644 .env create mode 100644 .env.dev create mode 100644 .env.prd diff --git a/.env b/.env new file mode 100644 index 0000000..e69de29 diff --git a/.env.dev b/.env.dev new file mode 100644 index 0000000..e69de29 diff --git a/.env.prd b/.env.prd new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore index 1d6019e..50e2816 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,5 @@ ENV/ *.log # Local development -.env .env.local *.db \ No newline at end of file diff --git a/app/api/endpoints/wechat.py b/app/api/endpoints/wechat.py index 905915b..d2b78b4 100644 --- a/app/api/endpoints/wechat.py +++ b/app/api/endpoints/wechat.py @@ -44,7 +44,7 @@ async def wechat_phone_login( ): """通过微信手机号登录/注册""" try: - # 初始化微信客户端 + # 初始化微信客户端 wechat = WeChatClient() # 获取用户 openid @@ -66,17 +66,17 @@ async def wechat_phone_login( if not phone: return error_response(code=400, message="手机号为空") - # # 获取企业微信的 userid - wecom_client = WecomClient() - wecom_info = await wecom_client.miniprogram_to_userid(openid=openid) - print(f"获取到的企业微信用户信息: {wecom_info}") + # # # 获取企业微信的 userid + # wecom_client = WecomClient() + # wecom_info = await wecom_client.miniprogram_to_userid(openid=openid) + # print(f"获取到的企业微信用户信息: {wecom_info}") - wecom_userid = None - wecom_pending_id = None + # wecom_userid = None + # wecom_pending_id = None - if wecom_info: - wecom_userid = wecom_info.get("userid") - wecom_pending_id = wecom_info.get("pending_id") + # if wecom_info: + # wecom_userid = wecom_info.get("userid") + # wecom_pending_id = wecom_info.get("pending_id") # 查找或创建用户 @@ -93,8 +93,8 @@ async def wechat_phone_login( password=get_password_hash("123456"), openid=openid, # 保存 openid unionid=unionid, # 保存 unionid - wecom_userid=wecom_userid, - wecom_pending_id=wecom_pending_id + # wecom_userid=wecom_userid, + # wecom_pending_id=wecom_pending_id ) db.add(user) db.flush() @@ -106,8 +106,8 @@ async def wechat_phone_login( # 更新现有用户的 openid 和 unionid user.openid = openid user.unionid = unionid - user.wecom_userid = wecom_userid - user.wecom_pending_id = wecom_pending_id + # user.wecom_userid = wecom_userid + # user.wecom_pending_id = wecom_pending_id db.commit() # 创建访问令牌 diff --git a/app/core/config.py b/app/core/config.py index 0e61c2c..7e9edd2 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,5 +1,7 @@ from typing import Optional from pydantic_settings import BaseSettings +import os +from functools import lru_cache class Settings(BaseSettings): DEBUG: bool = True # 开发模式标志 @@ -108,4 +110,49 @@ class Settings(BaseSettings): env_file = ".env" extra = "allow" # 允许额外的环境变量 -settings = Settings() \ No newline at end of file +class DevSettings(Settings): + DEBUG: bool = True # 开发模式标志 + API_V1_STR: str = "/api/v1" + PROJECT_NAME: str = "FastAPI 项目 (开发环境)" + + MYSQL_HOST: str = "gz-cynosdbmysql-grp-2j1cnopr.sql.tencentcdb.com" + MYSQL_PORT: int = 27469 + MYSQL_USER: str = "root" + MYSQL_PASSWORD: str = "Aa#223388" + MYSQL_DB: str = "beefast" + + class Config: + env_file = ".env.dev" + +class ProdSettings(Settings): + DEBUG: bool = False # 生产模式标志 + API_V1_STR: str = "/api/v1" + PROJECT_NAME: str = "FastAPI 项目 (生产环境)" + + MYSQL_HOST: str = "cd-cynosdbmysql-grp-7kdd8qe4.sql.tencentcdb.com" + MYSQL_PORT: int = 26558 + MYSQL_USER: str = "root" + MYSQL_PASSWORD: str = "Ss@123!@#" + MYSQL_DB: str = "beefast" + + class Config: + env_file = ".env.prod" + + +@lru_cache() +def get_settings() -> BaseSettings: + """ + 获取配置实例 + 使用环境变量 APP_ENV 来决定使用哪个配置 + """ + env = os.getenv("APP_ENV", "dev").lower() + config_map = { + "dev": DevSettings, + "prod": ProdSettings + } + + config_class = config_map.get(env, DevSettings) + return config_class() + +# 导出配置实例 +settings = get_settings() \ No newline at end of file diff --git a/app/core/wecomclient.py b/app/core/wecomclient.py index 40317e8..19b139a 100644 --- a/app/core/wecomclient.py +++ b/app/core/wecomclient.py @@ -1,93 +1,130 @@ -import aiohttp -from app.core.config import settings +import requests +import json +import time import logging -from typing import Optional, Dict +from app.core.config import settings +from typing import Dict, Any, Optional, List class WecomClient: + """企业微信客户端""" + def __init__(self): - self.corpid = settings.WECHAT_CORP_ID - self.secret = settings.WECHAT_CORP_SECRET # 注意:这里使用普通的 secret,不是 provider_secret + self.corp_id = settings.WECHAT_CORP_ID + self.corp_secret = settings.WECHAT_CORP_SECRET self.access_token = None - + self.token_expires_at = 0 + async def get_access_token(self) -> str: - """获取企业微信普通access_token""" + """获取访问令牌""" + # 检查令牌是否过期 + if self.access_token and time.time() < self.token_expires_at: + return self.access_token + + # 获取新令牌 + url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={self.corp_id}&corpsecret={self.corp_secret}" + try: - url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken" - params = { - "corpid": self.corpid, - "corpsecret": self.secret - } + response = requests.get(url) + result = response.json() - async with aiohttp.ClientSession() as session: - async with session.get(url, params=params) as response: - result = await response.json() - - if result.get("errcode") == 0: - self.access_token = result["access_token"] - print(f"企业微信access_token: {self.access_token}") - return self.access_token - else: - logging.error(f"获取access_token返回: {result}") - raise Exception(f"获取access_token失败: {result}") - + if result.get("errcode") == 0: + self.access_token = result.get("access_token") + self.token_expires_at = time.time() + result.get("expires_in") - 200 # 提前200秒过期 + return self.access_token + else: + logging.error(f"获取企业微信访问令牌失败: {result}") + return None + except Exception as e: - logging.error(f"获取access_token失败: {str(e)}") - raise - - async def miniprogram_to_userid(self, openid: str) -> Optional[Dict]: - """ - 转换小程序身份到企业微信 - 可以使用code或openid - """ - try: - # 构建请求参数 - url = "https://qyapi.weixin.qq.com/cgi-bin/miniprogram/jscode2session" - params = { - "access_token": await self.get_access_token(), - "appid": settings.WECHAT_APPID, - } - - # 根据传入参数类型选择 - params["openid"] = openid - - async with aiohttp.ClientSession() as session: - async with session.get(url, params=params) as response: - result = await response.json() - logging.info(f"转换结果: {result}") - - if result.get("errcode") == 0: - return { - "userid": result.get("userid"), # 如果是企业成员,返回userid - "pending_id": result.get("pending_id"), # 如果是外部联系人,返回pending_id - "openid": result.get("openid"), - "session_key": result.get("session_key") - } - else: - logging.error(f"身份转换失败: {result}") - return None - except Exception as e: - logging.error(f"身份转换失败: {str(e)}") + logging.exception(f"获取企业微信访问令牌异常: {str(e)}") return None - - async def get_user_info(self, userid: str) -> Optional[Dict]: - """获取企业成员信息""" - try: - url = "https://qyapi.weixin.qq.com/cgi-bin/user/get" - params = { - "access_token": await self.get_access_token(), - "userid": userid - } + + async def miniprogram_to_userid(self, openid: str) -> Optional[Dict[str, Any]]: + """ + 小程序openid转换为企业微信userid + + Args: + openid: 小程序用户的openid - 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 - else: - logging.error(f"获取用户信息失败: {result}") - return None - + Returns: + Dict: 包含userid或pending_id的字典 + """ + token = await self.get_access_token() + if not token: + return None + + url = f"https://qyapi.weixin.qq.com/cgi-bin/miniprogram/jscode2session?access_token={token}" + + data = { + "code": openid, + "grant_type": "authorization_code" + } + + try: + response = requests.post(url, json=data) + result = response.json() + + if result.get("errcode") == 0: + return { + "userid": result.get("userid"), + "pending_id": result.get("pending_id") + } + else: + logging.error(f"小程序openid转换失败: {result}") + return None + except Exception as e: - logging.error(f"获取用户信息失败: {str(e)}") - return None \ No newline at end of file + logging.exception(f"小程序openid转换异常: {str(e)}") + return None + + async def get_user_in_group_chat(self, userid: str) -> List[str]: + """ + 获取用户所在的群聊列表 + + Args: + userid: 企业微信用户ID + + Returns: + List[str]: 群聊ID列表 + """ + token = await self.get_access_token() + if not token: + return [] + + url = f"https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list_group_chat?access_token={token}" + + # 先获取所有群聊 + try: + response = requests.post(url, json={}) + result = response.json() + + if result.get("errcode") != 0: + logging.error(f"获取群聊列表失败: {result}") + return [] + + chat_ids = [] + for chat in result.get("group_chat_list", []): + chat_id = chat.get("chat_id") + + # 获取群聊详情 + detail_url = f"https://qyapi.weixin.qq.com/cgi-bin/externalcontact/groupchat/get?access_token={token}" + detail_response = requests.post(detail_url, json={"chat_id": chat_id}) + detail_result = detail_response.json() + + if detail_result.get("errcode") == 0: + group_chat = detail_result.get("group_chat", {}) + members = group_chat.get("member_list", []) + + # 检查用户是否在群内 + for member in members: + if member.get("userid") == userid: + chat_ids.append(chat_id) + break + + return chat_ids + + except Exception as e: + logging.exception(f"获取用户群聊异常: {str(e)}") + return [] + +wecom_client = WecomClient() \ No newline at end of file diff --git a/app/main.py b/app/main.py index ba5b1f4..61473a7 100644 --- a/app/main.py +++ b/app/main.py @@ -35,6 +35,14 @@ app.add_middleware( # 添加请求日志中间件 app.add_middleware(RequestLoggerMiddleware) +@app.get("/api/info") +async def get_info(): + """获取当前环境信息""" + return { + "project": settings.PROJECT_NAME, + "debug": settings.DEBUG + } + # 添加用户路由 app.include_router(dashboard.router, prefix="/api/dashboard", tags=["仪表盘"]) app.include_router(wechat.router,prefix="/api/wechat",tags=["微信"])