完成数据库等相关。
This commit is contained in:
parent
3d7a4cec7f
commit
41e61b364d
1
.gitignore
vendored
1
.gitignore
vendored
@ -24,6 +24,7 @@ var/
|
||||
venv/
|
||||
ENV/
|
||||
.env
|
||||
.venv/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
|
||||
56
README.md
56
README.md
@ -12,9 +12,13 @@ meida-api/
|
||||
│ ├── core/ # 核心配置
|
||||
│ ├── db/ # 数据库相关
|
||||
│ ├── models/ # 数据模型
|
||||
│ │ └── users.py # 用户模型
|
||||
│ ├── schemas/ # 数据验证模式
|
||||
│ │ └── user.py # 用户数据模式
|
||||
│ └── services/ # 业务服务层
|
||||
│ └── user.py # 用户服务
|
||||
├── main.py # 应用入口
|
||||
├── init_db.py # 数据库初始化脚本
|
||||
└── requirements.txt # 项目依赖
|
||||
```
|
||||
|
||||
@ -33,7 +37,12 @@ venv\Scripts\activate # Windows
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. 运行服务
|
||||
3. 初始化数据库 (如果需要)
|
||||
```bash
|
||||
python init_db.py
|
||||
```
|
||||
|
||||
4. 运行服务
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
@ -42,13 +51,54 @@ python main.py
|
||||
uvicorn main:app --reload
|
||||
```
|
||||
|
||||
4. 访问API文档
|
||||
5. 访问API文档
|
||||
- Swagger文档: http://localhost:8000/docs
|
||||
- ReDoc文档: http://localhost:8000/redoc
|
||||
|
||||
## API端点
|
||||
|
||||
### 基础端点
|
||||
- `/health` - 健康检查
|
||||
|
||||
### V1 API
|
||||
- `/api/v1/` - API基本信息
|
||||
- `/api/v1/products` - 获取产品列表
|
||||
- `/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 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.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设置
|
||||
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:
|
||||
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 app.core.config import settings
|
||||
from app.api.v1.api import api_router
|
||||
from app.db.init_db import init_db
|
||||
|
||||
app = FastAPI(
|
||||
title=settings.PROJECT_NAME,
|
||||
@ -29,6 +30,11 @@ async def root():
|
||||
async def health_check():
|
||||
return {"status": "healthy"}
|
||||
|
||||
# 应用启动事件
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
await init_db()
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
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-settings==2.0.3
|
||||
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