feat: 双Token认证(Access 2h/Refresh 7d) + 通知系统(转交/驳回自动推送)
This commit is contained in:
@ -1,20 +1,32 @@
|
||||
"""认证 API — 对接 MOM sys_user"""
|
||||
"""认证 API — 对接 MOM sys_user + 双 Token 刷新"""
|
||||
from fastapi import APIRouter, Depends
|
||||
from app.schemas.user import LoginRequest, LoginResponse, UserResponse
|
||||
from app.services.auth_service import login, get_current_user
|
||||
from app.schemas.user import (
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
RefreshRequest,
|
||||
RefreshResponse,
|
||||
UserResponse,
|
||||
)
|
||||
from app.services.auth_service import login, refresh_access_token, get_current_user
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["认证"])
|
||||
|
||||
|
||||
@router.post("/login", response_model=LoginResponse)
|
||||
def login_endpoint(data: LoginRequest):
|
||||
"""登录 — 验证 MOM sys_user 表,返回 JWT"""
|
||||
"""登录 — 验证 MOM sys_user 表,返回 Access + Refresh 双 Token"""
|
||||
return login(data.username, data.password)
|
||||
|
||||
|
||||
@router.post("/refresh", response_model=RefreshResponse)
|
||||
def refresh_endpoint(data: RefreshRequest):
|
||||
"""刷新 Access Token — 使用 Refresh Token 换取新的 Access Token"""
|
||||
return refresh_access_token(data.refresh_token)
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
def get_me(current_user: dict = Depends(get_current_user)):
|
||||
"""获取当前用户信息(从 JWT 解析)"""
|
||||
"""获取当前用户信息(从 Access Token 解析)"""
|
||||
return UserResponse(
|
||||
id=current_user["sub"],
|
||||
username=current_user.get("username", ""),
|
||||
|
||||
79
backend/app/api/v1/endpoints/notifications.py
Normal file
79
backend/app/api/v1/endpoints/notifications.py
Normal file
@ -0,0 +1,79 @@
|
||||
"""通知 API 端点 — 获取列表、标记已读"""
|
||||
from __future__ import annotations
|
||||
import uuid
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.models.notification import Notification
|
||||
from app.schemas.notification import NotificationResponse, NotificationListResponse
|
||||
|
||||
router = APIRouter(prefix="/notifications", tags=["消息通知"])
|
||||
|
||||
|
||||
@router.get("/", response_model=NotificationListResponse)
|
||||
async def list_notifications(
|
||||
user_id: str = Query(..., description="当前用户ID"),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取当前用户的通知列表(按时间倒序)"""
|
||||
# 总数
|
||||
count_stmt = select(func.count()).select_from(Notification).where(
|
||||
Notification.user_id == user_id
|
||||
)
|
||||
total_result = await db.execute(count_stmt)
|
||||
total = total_result.scalar() or 0
|
||||
|
||||
# 未读数
|
||||
unread_stmt = select(func.count()).select_from(Notification).where(
|
||||
Notification.user_id == user_id,
|
||||
Notification.is_read.is_(False),
|
||||
)
|
||||
unread_result = await db.execute(unread_stmt)
|
||||
unread_count = unread_result.scalar() or 0
|
||||
|
||||
# 列表
|
||||
stmt = (
|
||||
select(Notification)
|
||||
.where(Notification.user_id == user_id)
|
||||
.order_by(Notification.created_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
notifications = result.scalars().all()
|
||||
|
||||
return NotificationListResponse(
|
||||
notifications=[
|
||||
NotificationResponse.model_validate(n) for n in notifications
|
||||
],
|
||||
total=total,
|
||||
unread_count=unread_count,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{notification_id}/read", response_model=NotificationResponse)
|
||||
async def mark_notification_read(
|
||||
notification_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""标记单条通知为已读"""
|
||||
nid = uuid.UUID(notification_id)
|
||||
result = await db.execute(
|
||||
select(Notification).where(Notification.id == nid)
|
||||
)
|
||||
notification = result.scalar_one_or_none()
|
||||
if not notification:
|
||||
from fastapi import HTTPException, status
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"通知不存在: {notification_id}",
|
||||
)
|
||||
|
||||
notification.is_read = True
|
||||
await db.commit()
|
||||
await db.refresh(notification)
|
||||
return NotificationResponse.model_validate(notification)
|
||||
@ -62,11 +62,15 @@ async def scan_product(
|
||||
@router.get("/", response_model=list[ProductResponse])
|
||||
async def list_products(
|
||||
skip: int = Query(0, ge=0, description="跳过条数"),
|
||||
limit: int = Query(50, ge=1, le=200, description="返回条数"),
|
||||
limit: int = Query(50, ge=1, le=1000, description="返回条数"),
|
||||
keyword: str | None = Query(None, description="多维搜索: 产品身份证/订单号/规格型号"),
|
||||
status: str | None = Query(None, description="产品状态筛选: PENDING/WIP/COMPLETED/ARCHIVED"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取产品列表"""
|
||||
return await product_service.get_all_products(db, skip=skip, limit=limit)
|
||||
"""获取产品列表 — 支持 keyword 搜索 + 状态筛选"""
|
||||
return await product_service.get_all_products(
|
||||
db, skip=skip, limit=limit, keyword=keyword, status_filter=status,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{product_id}", response_model=ProductResponse)
|
||||
|
||||
@ -10,6 +10,7 @@ from app.api.v1.endpoints.materials import router as materials_router
|
||||
from app.api.v1.endpoints.users import router as users_router
|
||||
from app.api.v1.endpoints.upload import router as upload_router
|
||||
from app.api.v1.endpoints.records import router as records_router
|
||||
from app.api.v1.endpoints.notifications import router as notifications_router
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
@ -23,3 +24,4 @@ api_router.include_router(materials_router)
|
||||
api_router.include_router(users_router)
|
||||
api_router.include_router(upload_router)
|
||||
api_router.include_router(records_router)
|
||||
api_router.include_router(notifications_router)
|
||||
|
||||
Reference in New Issue
Block a user