完成数据库等相关。

This commit is contained in:
aaron 2025-04-09 10:49:02 +08:00
parent 3d7a4cec7f
commit 41e61b364d
25 changed files with 481 additions and 7 deletions

1
.gitignore vendored
View File

@ -24,6 +24,7 @@ var/
venv/ venv/
ENV/ ENV/
.env .env
.venv/
# IDE # IDE
.idea/ .idea/

View File

@ -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
View File

@ -0,0 +1 @@
# API相关初始化文件

35
app/api/deps.py Normal file
View 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
View File

@ -0,0 +1 @@
# API v1 版本初始化文件

View File

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

View File

@ -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
View 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
View File

@ -0,0 +1 @@
# 数据库相关初始化文件

35
app/db/database.py Normal file
View 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
View 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
View File

@ -0,0 +1,2 @@
# 导入所有模型确保它们被SQLAlchemy注册
from app.models.users import User

16
app/models/users.py Normal file
View 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
View File

@ -0,0 +1 @@
# 数据模式初始化文件

16
app/schemas/auth.py Normal file
View 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
View 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
View File

@ -0,0 +1 @@
# 服务层初始化文件

55
app/services/user.py Normal file
View 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
View 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
View File

@ -0,0 +1,6 @@
from app.db.init_db import init_db_sync
if __name__ == "__main__":
print("初始化数据库...")
init_db_sync()
print("数据库初始化完成。")

View File

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

View File

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

1
start.sh Normal file
View File

@ -0,0 +1 @@
python3 -m uvicorn main:app --reload