upload project source code
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
'''
|
||||
Author: caoziyuan ziyuan.cao@zhuying.com
|
||||
Date: 2025-12-22 17:25:15
|
||||
LastEditors: caoziyuan ziyuan.cao@zhuying.com
|
||||
LastEditTime: 2025-12-22 17:25:48
|
||||
FilePath: \backend\app\api\v1\module_application\miniapp\__init__.py
|
||||
Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from redis.asyncio.client import Redis
|
||||
|
||||
from app.common.response import SuccessResponse
|
||||
from app.core.logger import log
|
||||
from app.core.dependencies import db_getter, redis_getter
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
|
||||
from .service import MiniappService
|
||||
from .schema import MiniappLoginSchema, MiniappLoginOutSchema
|
||||
|
||||
|
||||
MiniappRouter = APIRouter(prefix="/miniapp", tags=["小程序"])
|
||||
|
||||
|
||||
@MiniappRouter.post("/login", summary="小程序登录", description="微信小程序用户登录", response_model=MiniappLoginOutSchema)
|
||||
async def miniapp_login_controller(
|
||||
data: MiniappLoginSchema,
|
||||
db: AsyncSession = Depends(db_getter),
|
||||
redis: Redis = Depends(redis_getter),
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
小程序登录接口
|
||||
|
||||
前端调用 wx.login() 获取 code,传给此接口换取 token
|
||||
|
||||
参数:
|
||||
- data (MiniappLoginSchema): 包含微信登录code
|
||||
|
||||
返回:
|
||||
- MiniappLoginOutSchema: 包含access_token和用户信息
|
||||
"""
|
||||
auth = AuthSchema(db=db)
|
||||
result = await MiniappService.login_service(auth=auth, redis=redis, data=data)
|
||||
log.info(f"小程序用户登录成功")
|
||||
return SuccessResponse(data=result, msg="登录成功")
|
||||
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Sequence
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .model import MiniappUserModel
|
||||
from .schema import MiniappUserCreateSchema, MiniappUserUpdateSchema
|
||||
|
||||
|
||||
class MiniappUserCRUD(CRUDBase[MiniappUserModel, MiniappUserCreateSchema, MiniappUserUpdateSchema]):
|
||||
"""小程序用户数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
super().__init__(model=MiniappUserModel, auth=auth)
|
||||
|
||||
async def get_by_openid(self, openid: str) -> MiniappUserModel | None:
|
||||
"""根据openid获取用户"""
|
||||
return await self.get(openid=openid)
|
||||
|
||||
async def get_by_id_crud(self, id: int) -> MiniappUserModel | None:
|
||||
"""根据ID获取用户"""
|
||||
return await self.get(id=id)
|
||||
|
||||
async def update_last_login(self, id: int) -> MiniappUserModel | None:
|
||||
"""更新最后登录时间"""
|
||||
return await self.update(id=id, data={"last_login": datetime.now()})
|
||||
|
||||
async def update_session_key(self, id: int, session_key: str) -> MiniappUserModel | None:
|
||||
"""更新session_key"""
|
||||
return await self.update(id=id, data={"session_key": session_key})
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime
|
||||
from sqlalchemy import String, DateTime, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin
|
||||
|
||||
|
||||
class MiniappUserModel(ModelMixin):
|
||||
"""
|
||||
小程序用户模型
|
||||
|
||||
存储微信小程序用户信息
|
||||
"""
|
||||
__tablename__: str = "miniapp_user"
|
||||
__table_args__: dict[str, str] = ({'comment': '小程序用户表'})
|
||||
|
||||
openid: Mapped[str] = mapped_column(String(64), nullable=False, unique=True, index=True, comment="微信openid")
|
||||
unionid: Mapped[str | None] = mapped_column(String(64), nullable=True, unique=True, comment="微信unionid")
|
||||
session_key: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="会话密钥")
|
||||
nickname: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="昵称")
|
||||
avatar: Mapped[str | None] = mapped_column(String(512), nullable=True, comment="头像URL")
|
||||
phone: Mapped[str | None] = mapped_column(String(20), nullable=True, comment="手机号")
|
||||
last_login: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, comment="最后登录时间")
|
||||
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from app.core.base_schema import BaseSchema
|
||||
|
||||
|
||||
class MiniappLoginSchema(BaseModel):
|
||||
"""小程序登录请求"""
|
||||
code: str = Field(..., min_length=1, description="微信登录code")
|
||||
|
||||
|
||||
class MiniappUserCreateSchema(BaseModel):
|
||||
"""小程序用户创建"""
|
||||
openid: str = Field(..., max_length=64, description="微信openid")
|
||||
unionid: str | None = Field(default=None, max_length=64, description="微信unionid")
|
||||
session_key: str | None = Field(default=None, max_length=64, description="会话密钥")
|
||||
nickname: str | None = Field(default=None, max_length=64, description="昵称")
|
||||
avatar: str | None = Field(default=None, max_length=512, description="头像URL")
|
||||
|
||||
|
||||
class MiniappUserUpdateSchema(MiniappUserCreateSchema):
|
||||
"""小程序用户更新"""
|
||||
phone: str | None = Field(default=None, max_length=20, description="手机号")
|
||||
|
||||
|
||||
class MiniappUserOutSchema(MiniappUserUpdateSchema, BaseSchema):
|
||||
"""小程序用户响应"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class MiniappLoginOutSchema(BaseModel):
|
||||
"""小程序登录响应"""
|
||||
access_token: str = Field(..., description="访问令牌")
|
||||
token_type: str = Field(default="Bearer", description="令牌类型")
|
||||
expires_in: int = Field(..., description="过期时间(秒)")
|
||||
user: MiniappUserOutSchema = Field(..., description="用户信息")
|
||||
@@ -0,0 +1,153 @@
|
||||
# -*- 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())
|
||||
}
|
||||
Reference in New Issue
Block a user