完成数据库等相关。
This commit is contained in:
parent
3d7a4cec7f
commit
41e61b364d
1
.gitignore
vendored
1
.gitignore
vendored
@ -24,6 +24,7 @@ var/
|
|||||||
venv/
|
venv/
|
||||||
ENV/
|
ENV/
|
||||||
.env
|
.env
|
||||||
|
.venv/
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
56
README.md
56
README.md
@ -12,9 +12,13 @@ meida-api/
|
|||||||
│ ├── core/ # 核心配置
|
│ ├── core/ # 核心配置
|
||||||
│ ├── db/ # 数据库相关
|
│ ├── db/ # 数据库相关
|
||||||
│ ├── models/ # 数据模型
|
│ ├── models/ # 数据模型
|
||||||
|
│ │ └── users.py # 用户模型
|
||||||
│ ├── schemas/ # 数据验证模式
|
│ ├── schemas/ # 数据验证模式
|
||||||
|
│ │ └── user.py # 用户数据模式
|
||||||
│ └── services/ # 业务服务层
|
│ └── services/ # 业务服务层
|
||||||
|
│ └── user.py # 用户服务
|
||||||
├── main.py # 应用入口
|
├── main.py # 应用入口
|
||||||
|
├── init_db.py # 数据库初始化脚本
|
||||||
└── requirements.txt # 项目依赖
|
└── requirements.txt # 项目依赖
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -33,7 +37,12 @@ venv\Scripts\activate # Windows
|
|||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
3. 运行服务
|
3. 初始化数据库 (如果需要)
|
||||||
|
```bash
|
||||||
|
python init_db.py
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 运行服务
|
||||||
```bash
|
```bash
|
||||||
python main.py
|
python main.py
|
||||||
```
|
```
|
||||||
@ -42,13 +51,54 @@ python main.py
|
|||||||
uvicorn main:app --reload
|
uvicorn main:app --reload
|
||||||
```
|
```
|
||||||
|
|
||||||
4. 访问API文档
|
5. 访问API文档
|
||||||
- Swagger文档: http://localhost:8000/docs
|
- Swagger文档: http://localhost:8000/docs
|
||||||
- ReDoc文档: http://localhost:8000/redoc
|
- ReDoc文档: http://localhost:8000/redoc
|
||||||
|
|
||||||
## API端点
|
## API端点
|
||||||
|
|
||||||
|
### 基础端点
|
||||||
|
- `/health` - 健康检查
|
||||||
|
|
||||||
|
### V1 API
|
||||||
- `/api/v1/` - API基本信息
|
- `/api/v1/` - API基本信息
|
||||||
- `/api/v1/products` - 获取产品列表
|
- `/api/v1/products` - 获取产品列表
|
||||||
- `/api/v1/products/{product_id}` - 获取特定产品详情
|
- `/api/v1/products/{product_id}` - 获取特定产品详情
|
||||||
- `/health` - 健康检查
|
|
||||||
|
### 用户API
|
||||||
|
- `/api/v1/users/` - 获取所有用户 (GET) / 创建用户 (POST)
|
||||||
|
- `/api/v1/users/{user_id}` - 获取/更新/删除特定用户
|
||||||
|
- `/api/v1/users/openid/{openid}` - 通过openid获取用户
|
||||||
|
- `/api/v1/users/me` - 获取当前登录用户信息 (需要认证)
|
||||||
|
- `/api/v1/users/me` - 更新当前登录用户信息 (需要认证)
|
||||||
|
|
||||||
|
### 认证API
|
||||||
|
- `/api/v1/auth/login/wechat` - 微信登录/注册
|
||||||
|
|
||||||
|
## 认证
|
||||||
|
|
||||||
|
本API使用JWT令牌进行认证。认证流程如下:
|
||||||
|
|
||||||
|
1. 调用微信登录接口获取令牌:
|
||||||
|
```
|
||||||
|
POST /api/v1/auth/login/wechat
|
||||||
|
{
|
||||||
|
"code": "微信临时登录凭证"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 在需要认证的请求头中添加令牌:
|
||||||
|
```
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据模型
|
||||||
|
|
||||||
|
### 用户模型
|
||||||
|
|
||||||
|
- id: 自增长主键
|
||||||
|
- openid: 用户唯一标识
|
||||||
|
- unionid: 统一标识
|
||||||
|
- avatar: 用户头像
|
||||||
|
- nickname: 用户昵称
|
||||||
|
- create_time: 创建时间
|
||||||
1
app/api/__init__.py
Normal file
1
app/api/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# API相关初始化文件
|
||||||
35
app/api/deps.py
Normal file
35
app/api/deps.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from fastapi import Depends, HTTPException, status
|
||||||
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from jose import jwt
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.core.security import verify_token
|
||||||
|
from app.db.database import get_db
|
||||||
|
from app.services import user as user_service
|
||||||
|
|
||||||
|
# OAuth2密码Bearer - JWT token的位置
|
||||||
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/auth/login")
|
||||||
|
|
||||||
|
# 获取当前用户的依赖项
|
||||||
|
async def get_current_user(
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
token: str = Depends(oauth2_scheme)
|
||||||
|
):
|
||||||
|
credentials_exception = HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="无效的认证凭证",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# 验证token
|
||||||
|
openid = verify_token(token)
|
||||||
|
if not openid:
|
||||||
|
raise credentials_exception
|
||||||
|
|
||||||
|
# 根据openid获取用户
|
||||||
|
user = await user_service.get_user_by_openid(db, openid=openid)
|
||||||
|
if user is None:
|
||||||
|
raise credentials_exception
|
||||||
|
|
||||||
|
return user
|
||||||
1
app/api/v1/__init__.py
Normal file
1
app/api/v1/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# API v1 版本初始化文件
|
||||||
@ -1,5 +1,9 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from app.api.v1.endpoints import router as endpoints_router
|
from app.api.v1.endpoints import router as endpoints_router
|
||||||
|
from app.api.v1.users import router as users_router
|
||||||
|
from app.api.v1.auth import router as auth_router
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
api_router.include_router(endpoints_router, prefix="")
|
api_router.include_router(endpoints_router, prefix="")
|
||||||
|
api_router.include_router(auth_router, prefix="/auth")
|
||||||
|
api_router.include_router(users_router, prefix="/users")
|
||||||
|
|||||||
57
app/api/v1/auth.py
Normal file
57
app/api/v1/auth.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.schemas.auth import WechatLogin, LoginResponse
|
||||||
|
from app.schemas.user import UserCreate
|
||||||
|
from app.services import wechat as wechat_service
|
||||||
|
from app.services import user as user_service
|
||||||
|
from app.core.security import create_access_token
|
||||||
|
from app.db.database import get_db
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.post("/login/wechat", response_model=LoginResponse, tags=["auth"])
|
||||||
|
async def wechat_login(
|
||||||
|
login_data: WechatLogin,
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
微信登录接口
|
||||||
|
|
||||||
|
- 使用微信临时登录凭证(code)获取openid和unionid
|
||||||
|
- 如果用户不存在,则创建新用户
|
||||||
|
- 生成JWT令牌
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 调用微信API获取openid和unionid
|
||||||
|
openid, unionid = await wechat_service.code2session(login_data.code)
|
||||||
|
|
||||||
|
# 检查用户是否存在
|
||||||
|
existing_user = await user_service.get_user_by_openid(db, openid=openid)
|
||||||
|
is_new_user = existing_user is None
|
||||||
|
|
||||||
|
if is_new_user:
|
||||||
|
# 创建新用户
|
||||||
|
user_create = UserCreate(
|
||||||
|
openid=openid,
|
||||||
|
unionid=unionid
|
||||||
|
)
|
||||||
|
user = await user_service.create_user(db, user=user_create)
|
||||||
|
else:
|
||||||
|
user = existing_user
|
||||||
|
|
||||||
|
# 创建访问令牌 - 使用openid作为标识
|
||||||
|
access_token = create_access_token(subject=openid)
|
||||||
|
|
||||||
|
# 返回登录响应
|
||||||
|
return LoginResponse(
|
||||||
|
access_token=access_token,
|
||||||
|
is_new_user=is_new_user,
|
||||||
|
openid=openid
|
||||||
|
)
|
||||||
|
|
||||||
|
except wechat_service.WechatLoginError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=str(e)
|
||||||
|
)
|
||||||
36
app/api/v1/users.py
Normal file
36
app/api/v1/users.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from app.schemas.user import User, UserCreate, UserUpdate
|
||||||
|
from app.services import user as user_service
|
||||||
|
from app.db.database import get_db
|
||||||
|
from app.api.deps import get_current_user
|
||||||
|
from app.models.users import User as UserModel
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get("/me", response_model=User, tags=["users"])
|
||||||
|
async def read_user_me(
|
||||||
|
current_user: UserModel = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
获取当前登录用户信息
|
||||||
|
|
||||||
|
需要JWT令牌认证
|
||||||
|
"""
|
||||||
|
return current_user
|
||||||
|
|
||||||
|
@router.put("/me", response_model=User, tags=["users"])
|
||||||
|
async def update_user_me(
|
||||||
|
user_update: UserUpdate,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: UserModel = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
更新当前登录用户信息
|
||||||
|
|
||||||
|
需要JWT令牌认证
|
||||||
|
"""
|
||||||
|
user = await user_service.update_user(db, user_id=current_user.id, user_update=user_update)
|
||||||
|
return user
|
||||||
@ -14,8 +14,25 @@ class Settings(BaseSettings):
|
|||||||
# CORS设置
|
# CORS设置
|
||||||
BACKEND_CORS_ORIGINS: List[str] = ["*"]
|
BACKEND_CORS_ORIGINS: List[str] = ["*"]
|
||||||
|
|
||||||
# 数据库设置 (后续可根据需要配置)
|
# 数据库设置
|
||||||
# DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///./meida.db")
|
DB_HOST: str = os.getenv("DB_HOST", "localhost")
|
||||||
|
DB_PORT: str = os.getenv("DB_PORT", "3306")
|
||||||
|
DB_USER: str = os.getenv("DB_USER", "root")
|
||||||
|
DB_PASSWORD: str = os.getenv("DB_PASSWORD", "password")
|
||||||
|
DB_NAME: str = os.getenv("DB_NAME", "meida")
|
||||||
|
DB_ECHO: bool = os.getenv("DB_ECHO", "False").lower() == "true"
|
||||||
|
|
||||||
|
# DashScope API密钥
|
||||||
|
DASHSCOPE_API_KEY: str = os.getenv("DASHSCOPE_API_KEY", "sk-caa199589f1c451aaac471fad2986e28")
|
||||||
|
|
||||||
|
# 安全设置
|
||||||
|
SECRET_KEY: str = os.getenv("SECRET_KEY", "your-secret-key-for-jwt-please-change-in-production")
|
||||||
|
ALGORITHM: str = "HS256"
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7天
|
||||||
|
|
||||||
|
# 微信设置
|
||||||
|
WECHAT_APP_ID: str = os.getenv("WECHAT_APP_ID", "")
|
||||||
|
WECHAT_APP_SECRET: str = os.getenv("WECHAT_APP_SECRET", "")
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
env_file = ".env"
|
env_file = ".env"
|
||||||
|
|||||||
33
app/core/security.py
Normal file
33
app/core/security.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Any, Union, Optional
|
||||||
|
|
||||||
|
from jose import jwt
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
# 密码上下文
|
||||||
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
|
||||||
|
# 创建访问令牌
|
||||||
|
def create_access_token(
|
||||||
|
subject: Union[str, Any], expires_delta: Optional[timedelta] = None
|
||||||
|
) -> str:
|
||||||
|
if expires_delta:
|
||||||
|
expire = datetime.now() + expires_delta
|
||||||
|
else:
|
||||||
|
expire = datetime.now() + timedelta(
|
||||||
|
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
|
||||||
|
)
|
||||||
|
to_encode = {"exp": expire, "sub": str(subject)}
|
||||||
|
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||||
|
return encoded_jwt
|
||||||
|
|
||||||
|
# 验证令牌
|
||||||
|
def verify_token(token: str) -> Optional[str]:
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(
|
||||||
|
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
|
||||||
|
)
|
||||||
|
return payload.get("sub")
|
||||||
|
except jwt.JWTError:
|
||||||
|
return None
|
||||||
1
app/db/__init__.py
Normal file
1
app/db/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# 数据库相关初始化文件
|
||||||
35
app/db/database.py
Normal file
35
app/db/database.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from app.core.config import settings
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
# 创建异步数据库URL - 使用异步驱动
|
||||||
|
SQLALCHEMY_DATABASE_URL = f"mysql+aiomysql://{settings.DB_USER}:{settings.DB_PASSWORD}@{settings.DB_HOST}:{settings.DB_PORT}/{settings.DB_NAME}"
|
||||||
|
|
||||||
|
# 创建异步数据库引擎
|
||||||
|
engine = create_async_engine(
|
||||||
|
SQLALCHEMY_DATABASE_URL,
|
||||||
|
echo=settings.DB_ECHO,
|
||||||
|
pool_pre_ping=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建异步会话工厂
|
||||||
|
AsyncSessionLocal = sessionmaker(
|
||||||
|
engine,
|
||||||
|
class_=AsyncSession,
|
||||||
|
expire_on_commit=False,
|
||||||
|
autocommit=False,
|
||||||
|
autoflush=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建基本模型类
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
# 异步数据库会话依赖项
|
||||||
|
async def get_db():
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
try:
|
||||||
|
yield session
|
||||||
|
finally:
|
||||||
|
await session.close()
|
||||||
18
app/db/init_db.py
Normal file
18
app/db/init_db.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import asyncio
|
||||||
|
from app.db.database import Base, engine
|
||||||
|
from app.models.users import User
|
||||||
|
|
||||||
|
# 创建所有表格
|
||||||
|
async def init_db():
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
# 在SQLAlchemy 2.0中可以直接使用异步创建表
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
|
# 同步入口点
|
||||||
|
def init_db_sync():
|
||||||
|
asyncio.run(init_db())
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("创建数据库表...")
|
||||||
|
init_db_sync()
|
||||||
|
print("数据库表创建完成。")
|
||||||
2
app/models/__init__.py
Normal file
2
app/models/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# 导入所有模型,确保它们被SQLAlchemy注册
|
||||||
|
from app.models.users import User
|
||||||
16
app/models/users.py
Normal file
16
app/models/users.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, DateTime
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.db.database import Base
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||||
|
openid = Column(String(50), unique=True, index=True)
|
||||||
|
unionid = Column(String(50), nullable=True, index=True)
|
||||||
|
avatar = Column(String(255), nullable=True, comment="头像")
|
||||||
|
nickname = Column(String(50), nullable=True, comment="昵称")
|
||||||
|
create_time = Column(DateTime, default=func.now(), comment="创建时间")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<User(id={self.id}, nickname={self.nickname})>"
|
||||||
1
app/schemas/__init__.py
Normal file
1
app/schemas/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# 数据模式初始化文件
|
||||||
16
app/schemas/auth.py
Normal file
16
app/schemas/auth.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
class WechatLogin(BaseModel):
|
||||||
|
"""微信登录请求"""
|
||||||
|
code: str
|
||||||
|
|
||||||
|
class Token(BaseModel):
|
||||||
|
"""令牌响应"""
|
||||||
|
access_token: str
|
||||||
|
token_type: str = "bearer"
|
||||||
|
|
||||||
|
class LoginResponse(Token):
|
||||||
|
"""登录响应"""
|
||||||
|
is_new_user: bool # 是否为新用户
|
||||||
|
openid: str
|
||||||
26
app/schemas/user.py
Normal file
26
app/schemas/user.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
class UserBase(BaseModel):
|
||||||
|
openid: str
|
||||||
|
unionid: Optional[str] = None
|
||||||
|
avatar: Optional[str] = None
|
||||||
|
nickname: Optional[str] = None
|
||||||
|
|
||||||
|
class UserCreate(UserBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class UserUpdate(BaseModel):
|
||||||
|
avatar: Optional[str] = None
|
||||||
|
nickname: Optional[str] = None
|
||||||
|
|
||||||
|
class UserInDB(UserBase):
|
||||||
|
id: int
|
||||||
|
create_time: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
class User(UserInDB):
|
||||||
|
pass
|
||||||
1
app/services/__init__.py
Normal file
1
app/services/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# 服务层初始化文件
|
||||||
55
app/services/user.py
Normal file
55
app/services/user.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select, update, delete
|
||||||
|
from sqlalchemy.future import select
|
||||||
|
from app.models.users import User
|
||||||
|
from app.schemas.user import UserCreate, UserUpdate
|
||||||
|
|
||||||
|
async def get_user(db: AsyncSession, user_id: int):
|
||||||
|
result = await db.execute(select(User).filter(User.id == user_id))
|
||||||
|
return result.scalars().first()
|
||||||
|
|
||||||
|
async def get_user_by_openid(db: AsyncSession, openid: str):
|
||||||
|
result = await db.execute(select(User).filter(User.openid == openid))
|
||||||
|
return result.scalars().first()
|
||||||
|
|
||||||
|
async def get_users(db: AsyncSession, skip: int = 0, limit: int = 100):
|
||||||
|
result = await db.execute(select(User).offset(skip).limit(limit))
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
async def create_user(db: AsyncSession, user: UserCreate):
|
||||||
|
db_user = User(
|
||||||
|
openid=user.openid,
|
||||||
|
unionid=user.unionid,
|
||||||
|
avatar=user.avatar,
|
||||||
|
nickname=user.nickname
|
||||||
|
)
|
||||||
|
db.add(db_user)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(db_user)
|
||||||
|
return db_user
|
||||||
|
|
||||||
|
async def update_user(db: AsyncSession, user_id: int, user_update: UserUpdate):
|
||||||
|
update_data = user_update.model_dump(exclude_unset=True)
|
||||||
|
if not update_data:
|
||||||
|
# 没有要更新的数据
|
||||||
|
return await get_user(db, user_id)
|
||||||
|
|
||||||
|
# 先获取用户
|
||||||
|
db_user = await get_user(db, user_id)
|
||||||
|
if not db_user:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 更新用户数据
|
||||||
|
for key, value in update_data.items():
|
||||||
|
setattr(db_user, key, value)
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(db_user)
|
||||||
|
return db_user
|
||||||
|
|
||||||
|
async def delete_user(db: AsyncSession, user_id: int):
|
||||||
|
db_user = await get_user(db, user_id)
|
||||||
|
if db_user:
|
||||||
|
await db.delete(db_user)
|
||||||
|
await db.commit()
|
||||||
|
return db_user
|
||||||
49
app/services/wechat.py
Normal file
49
app/services/wechat.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import httpx
|
||||||
|
from typing import Optional, Dict, Any, Tuple
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
class WechatLoginError(Exception):
|
||||||
|
"""微信登录错误"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def code2session(code: str) -> Tuple[str, Optional[str]]:
|
||||||
|
"""
|
||||||
|
使用微信登录code获取openid和unionid
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code: 微信临时登录凭证
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[str, Optional[str]]: (openid, unionid)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
WechatLoginError: 当微信API调用失败时
|
||||||
|
"""
|
||||||
|
url = "https://api.weixin.qq.com/sns/jscode2session"
|
||||||
|
params = {
|
||||||
|
"appid": settings.WECHAT_APP_ID,
|
||||||
|
"secret": settings.WECHAT_APP_SECRET,
|
||||||
|
"js_code": code,
|
||||||
|
"grant_type": "authorization_code"
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get(url, params=params)
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
if "errcode" in result and result["errcode"] != 0:
|
||||||
|
raise WechatLoginError(f"微信登录失败: {result.get('errmsg', '未知错误')}")
|
||||||
|
|
||||||
|
openid = result.get("openid")
|
||||||
|
unionid = result.get("unionid") # 可能为None
|
||||||
|
|
||||||
|
if not openid:
|
||||||
|
raise WechatLoginError("无法获取openid")
|
||||||
|
|
||||||
|
return openid, unionid
|
||||||
|
|
||||||
|
except httpx.RequestError as e:
|
||||||
|
raise WechatLoginError(f"网络请求失败: {str(e)}")
|
||||||
|
except Exception as e:
|
||||||
|
raise WechatLoginError(f"未知错误: {str(e)}")
|
||||||
6
init_db.py
Normal file
6
init_db.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from app.db.init_db import init_db_sync
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("初始化数据库...")
|
||||||
|
init_db_sync()
|
||||||
|
print("数据库初始化完成。")
|
||||||
6
main.py
6
main.py
@ -2,6 +2,7 @@ from fastapi import FastAPI
|
|||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.api.v1.api import api_router
|
from app.api.v1.api import api_router
|
||||||
|
from app.db.init_db import init_db
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title=settings.PROJECT_NAME,
|
title=settings.PROJECT_NAME,
|
||||||
@ -29,6 +30,11 @@ async def root():
|
|||||||
async def health_check():
|
async def health_check():
|
||||||
return {"status": "healthy"}
|
return {"status": "healthy"}
|
||||||
|
|
||||||
|
# 应用启动事件
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup_event():
|
||||||
|
await init_db()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
||||||
@ -3,3 +3,9 @@ uvicorn==0.24.0
|
|||||||
pydantic==2.4.2
|
pydantic==2.4.2
|
||||||
pydantic-settings==2.0.3
|
pydantic-settings==2.0.3
|
||||||
python-dotenv==1.0.0
|
python-dotenv==1.0.0
|
||||||
|
sqlalchemy==2.0.21
|
||||||
|
aiomysql==0.2.0
|
||||||
|
greenlet==2.0.2
|
||||||
|
python-jose[cryptography]==3.3.0
|
||||||
|
passlib==1.7.4
|
||||||
|
httpx==0.24.1
|
||||||
Loading…
Reference in New Issue
Block a user