Files
----/后端源码/yifan.action-ai.cn/app/api/v1/module_application/miniapp/service.py

154 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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())
}