154 lines
5.1 KiB
Python
154 lines
5.1 KiB
Python
# -*- 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())
|
||
}
|