# -*- coding: utf-8 -*- import uuid import json import httpx from datetime import datetime, timedelta from redis.asyncio.client import Redis from app.core.exceptions import CustomException from app.core.logger import log from app.core.security import create_access_token from app.core.redis_crud import RedisCURD from app.common.enums import RedisInitKeyConfig from app.config.setting import settings from app.api.v1.module_system.auth.schema import AuthSchema, JWTPayloadSchema from .crud import MiniappUserCRUD from .schema import ( MiniappLoginSchema, MiniappUserCreateSchema, MiniappUserOutSchema, MiniappLoginOutSchema, ) class MiniappService: """小程序服务层""" # 微信登录接口 WX_LOGIN_URL = "https://api.weixin.qq.com/sns/jscode2session" @classmethod async def login_service(cls, auth: AuthSchema, redis: Redis, data: MiniappLoginSchema) -> dict: """ 小程序登录 流程: 1. 用微信code换取openid和session_key 2. 查找或创建用户 3. 生成JWT token """ # 1. 调用微信接口获取openid wx_result = await cls._get_wx_session(code=data.code) openid = wx_result.get("openid") session_key = wx_result.get("session_key") unionid = wx_result.get("unionid") if not openid: raise CustomException(msg="微信登录失败,无法获取openid") # 2. 查找或创建用户 user = await MiniappUserCRUD(auth).get_by_openid(openid=openid) if user: # 更新session_key和登录时间 await MiniappUserCRUD(auth).update_session_key(id=user.id, session_key=session_key) await MiniappUserCRUD(auth).update_last_login(id=user.id) log.info(f"小程序用户登录: {openid}") else: # 创建新用户 user_data = MiniappUserCreateSchema( openid=openid, unionid=unionid, session_key=session_key, ) user = await MiniappUserCRUD(auth).create(data=user_data) log.info(f"小程序新用户注册: {openid}") # 3. 生成token token_data = await cls._create_miniapp_token(redis=redis, user_id=user.id, openid=openid) return MiniappLoginOutSchema( access_token=token_data["access_token"], token_type="Bearer", expires_in=token_data["expires_in"], user=MiniappUserOutSchema.model_validate(user) ).model_dump() @classmethod async def get_user_info_service(cls, auth: AuthSchema, user_id: int) -> dict: """获取用户信息""" user = await MiniappUserCRUD(auth).get_by_id_crud(id=user_id) if not user: raise CustomException(msg="用户不存在") return MiniappUserOutSchema.model_validate(user).model_dump() @classmethod async def _get_wx_session(cls, code: str) -> dict: """ 调用微信接口获取session信息 注意: 需要在配置中设置 MINIAPP_APPID 和 MINIAPP_SECRET """ appid = getattr(settings, "MINIAPP_APPID", None) secret = getattr(settings, "MINIAPP_SECRET", None) if not appid or not secret: # 开发环境模拟返回 log.warning("未配置小程序appid和secret,使用模拟数据") return { "openid": f"mock_openid_{code[:8]}", "session_key": "mock_session_key", "unionid": None } params = { "appid": appid, "secret": secret, "js_code": code, "grant_type": "authorization_code" } async with httpx.AsyncClient() as client: response = await client.get(cls.WX_LOGIN_URL, params=params) result = response.json() if "errcode" in result and result["errcode"] != 0: log.error(f"微信登录失败: {result}") raise CustomException(msg=f"微信登录失败: {result.get('errmsg', '未知错误')}") return result @classmethod async def _create_miniapp_token(cls, redis: Redis, user_id: int, openid: str) -> dict: """创建小程序用户token""" session_id = str(uuid.uuid4()) access_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) now = datetime.now() session_info = json.dumps({ "session_id": session_id, "user_id": user_id, "openid": openid, "login_type": "miniapp" }) access_token = create_access_token(payload=JWTPayloadSchema( sub=session_info, is_refresh=False, exp=now + access_expires, )) # 存储到Redis await RedisCURD(redis).set( key=f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:miniapp:{session_id}', value=access_token, expire=int(access_expires.total_seconds()) ) return { "access_token": access_token, "expires_in": int(access_expires.total_seconds()) }