upload project source code

This commit is contained in:
2026-04-30 18:49:43 +08:00
commit 9b394ba682
2277 changed files with 660945 additions and 0 deletions

View File

@@ -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 -*-

View File

@@ -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="登录成功")

View File

@@ -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})

View File

@@ -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="最后登录时间")

View File

@@ -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="用户信息")

View File

@@ -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())
}