init
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends, Header, HTTPException, status
|
||||
|
||||
from app.core.database import get_connection
|
||||
from app.core.schemas import UserProfile
|
||||
from app.services import repository
|
||||
|
||||
|
||||
def get_bearer_token(authorization: Annotated[str | None, Header()] = None) -> str:
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="missing_token")
|
||||
return authorization.replace("Bearer ", "", 1).strip()
|
||||
|
||||
|
||||
def get_current_user(token: Annotated[str, Depends(get_bearer_token)]) -> UserProfile:
|
||||
with get_connection() as connection:
|
||||
row = repository.get_user_by_token(connection, token)
|
||||
if row is None:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid_token")
|
||||
return repository.profile_from_row(row)
|
||||
|
||||
|
||||
def require_student(user: Annotated[UserProfile, Depends(get_current_user)]) -> UserProfile:
|
||||
if user.role != "student":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="student_only")
|
||||
return user
|
||||
|
||||
|
||||
def require_admin(user: Annotated[UserProfile, Depends(get_current_user)]) -> UserProfile:
|
||||
if user.role != "admin":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="admin_only")
|
||||
return user
|
||||
@@ -0,0 +1,84 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
|
||||
from app.api.deps import require_admin
|
||||
from app.core.database import get_connection
|
||||
from app.core.schemas import AdminOrderUpdateRequest, OrderDetailOut, OrderSummaryOut, UserProfile
|
||||
from app.services import repository
|
||||
|
||||
router = APIRouter(prefix="/api/admin", tags=["admin"])
|
||||
|
||||
|
||||
@router.get("/orders", response_model=list[OrderSummaryOut])
|
||||
def list_orders(
|
||||
_: Annotated[UserProfile, Depends(require_admin)],
|
||||
status_text: str | None = Query(default=None, alias="status"),
|
||||
category: str | None = Query(default=None),
|
||||
urgency: str | None = Query(default=None),
|
||||
) -> list[dict[str, str]]:
|
||||
with get_connection() as connection:
|
||||
return repository.list_orders(connection, status=status_text, category=category, urgency=urgency)
|
||||
|
||||
|
||||
@router.get("/orders/{order_id}", response_model=OrderDetailOut)
|
||||
def get_order(order_id: int, _: Annotated[UserProfile, Depends(require_admin)]) -> dict[str, str]:
|
||||
with get_connection() as connection:
|
||||
detail = repository.get_order_detail(connection, order_id)
|
||||
if detail is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="order_not_found")
|
||||
return detail
|
||||
|
||||
|
||||
@router.patch("/orders/{order_id}")
|
||||
def update_order(
|
||||
order_id: int,
|
||||
payload: AdminOrderUpdateRequest,
|
||||
admin: Annotated[UserProfile, Depends(require_admin)],
|
||||
) -> dict[str, str]:
|
||||
with get_connection() as connection:
|
||||
try:
|
||||
repository.update_order(connection, order_id, admin, payload)
|
||||
except KeyError as error:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(error)) from error
|
||||
except ValueError as error:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(error)) from error
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
def get_stats(_: Annotated[UserProfile, Depends(require_admin)]) -> dict[str, object]:
|
||||
with get_connection() as connection:
|
||||
return repository.get_stats(connection)
|
||||
|
||||
|
||||
@router.post("/orders/{order_id}/accept-rework")
|
||||
def accept_rework(
|
||||
order_id: int,
|
||||
admin: Annotated[UserProfile, Depends(require_admin)],
|
||||
) -> dict[str, str]:
|
||||
with get_connection() as connection:
|
||||
try:
|
||||
repository.accept_rework(connection, order_id, admin)
|
||||
except KeyError as error:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(error)) from error
|
||||
except ValueError as error:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(error)) from error
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.post("/orders/{order_id}/reject-rework")
|
||||
def reject_rework(
|
||||
order_id: int,
|
||||
admin: Annotated[UserProfile, Depends(require_admin)],
|
||||
) -> dict[str, str]:
|
||||
with get_connection() as connection:
|
||||
try:
|
||||
repository.reject_rework(connection, order_id, admin)
|
||||
except KeyError as error:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(error)) from error
|
||||
except ValueError as error:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(error)) from error
|
||||
return {"status": "ok"}
|
||||
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.core.database import get_connection
|
||||
from app.core.schemas import LoginRequest, LoginResponse, UserProfile
|
||||
from app.core.security import verify_password
|
||||
from app.services import repository
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
|
||||
|
||||
@router.post("/login", response_model=LoginResponse)
|
||||
def login(payload: LoginRequest) -> LoginResponse:
|
||||
with get_connection() as connection:
|
||||
row = connection.execute(
|
||||
"SELECT id, username, password_hash, role, display_name FROM users WHERE username = ?",
|
||||
(payload.username,),
|
||||
).fetchone()
|
||||
if row is None or not verify_password(payload.password, row["password_hash"]):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid_credentials")
|
||||
token = repository.create_session(connection, row["id"])
|
||||
user = repository.profile_from_row(row)
|
||||
return LoginResponse(token=token, user=user)
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserProfile)
|
||||
def me(user: Annotated[UserProfile, Depends(get_current_user)]) -> UserProfile:
|
||||
return user
|
||||
@@ -0,0 +1,161 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
||||
|
||||
from app.api.deps import require_student
|
||||
from app.core.database import get_connection
|
||||
from app.core.ratelimit import check_diagnosis_rate_limit
|
||||
from app.core.schemas import (
|
||||
DiagnosisAnswerRequest,
|
||||
DiagnosisResponse,
|
||||
DiagnosisStartRequest,
|
||||
FeedbackRequest,
|
||||
OrderCreateRequest,
|
||||
OrderDetailOut,
|
||||
OrderSummaryOut,
|
||||
ReworkRequest,
|
||||
SavedAddressOut,
|
||||
UserProfile,
|
||||
)
|
||||
from app.services import repository
|
||||
from app.services.diagnosis import get_diagnosis_provider
|
||||
|
||||
router = APIRouter(prefix="/api/student", tags=["student"])
|
||||
|
||||
|
||||
@router.post("/diagnosis/start", response_model=DiagnosisResponse)
|
||||
async def start_diagnosis(
|
||||
payload: DiagnosisStartRequest,
|
||||
_: Annotated[UserProfile, Depends(require_student)],
|
||||
_rl: None = Depends(check_diagnosis_rate_limit),
|
||||
) -> DiagnosisResponse:
|
||||
provider = get_diagnosis_provider()
|
||||
return await provider.start(payload.message)
|
||||
|
||||
|
||||
@router.post("/diagnosis/answer", response_model=DiagnosisResponse)
|
||||
async def answer_diagnosis(
|
||||
payload: DiagnosisAnswerRequest,
|
||||
_: Annotated[UserProfile, Depends(require_student)],
|
||||
_rl: None = Depends(check_diagnosis_rate_limit),
|
||||
) -> DiagnosisResponse:
|
||||
try:
|
||||
provider = get_diagnosis_provider()
|
||||
return await provider.answer(payload.session_id, payload.answers)
|
||||
except KeyError as error:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=error.args[0]) from error
|
||||
|
||||
|
||||
@router.get("/addresses", response_model=list[SavedAddressOut])
|
||||
def list_addresses(user: Annotated[UserProfile, Depends(require_student)]) -> list[dict[str, object]]:
|
||||
with get_connection() as connection:
|
||||
return repository.list_addresses(connection, user.id)
|
||||
|
||||
|
||||
@router.delete("/addresses/{address_id}")
|
||||
def delete_address(address_id: int, user: Annotated[UserProfile, Depends(require_student)]) -> dict[str, str]:
|
||||
with get_connection() as connection:
|
||||
if not repository.delete_address(connection, address_id, user.id):
|
||||
raise HTTPException(status_code=404, detail="地址不存在")
|
||||
return {"message": "已删除"}
|
||||
|
||||
|
||||
@router.post("/orders")
|
||||
def create_order(payload: OrderCreateRequest, user: Annotated[UserProfile, Depends(require_student)]) -> dict[str, int]:
|
||||
with get_connection() as connection:
|
||||
order_id = repository.create_order(connection, user, payload)
|
||||
return {"order_id": order_id}
|
||||
|
||||
|
||||
@router.post("/orders/{order_id}/attachments")
|
||||
def upload_attachments(
|
||||
order_id: int,
|
||||
user: Annotated[UserProfile, Depends(require_student)],
|
||||
files: Annotated[list[UploadFile], File(...)],
|
||||
) -> dict[str, str]:
|
||||
with get_connection() as connection:
|
||||
order = repository.get_order_detail(connection, order_id, student_id=user.id)
|
||||
if order is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="order_not_found")
|
||||
for file in files:
|
||||
try:
|
||||
repository.save_attachment(connection, order_id, file)
|
||||
except ValueError as error:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(error)) from error
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.post("/orders/{order_id}/cancel")
|
||||
def cancel_order(
|
||||
order_id: int,
|
||||
user: Annotated[UserProfile, Depends(require_student)],
|
||||
) -> dict[str, str]:
|
||||
with get_connection() as connection:
|
||||
try:
|
||||
repository.cancel_order(connection, order_id, user)
|
||||
except KeyError as error:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=error.args[0]) from error
|
||||
except ValueError as error:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error.args[0]) from error
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.get("/orders", response_model=list[OrderSummaryOut])
|
||||
def list_orders(user: Annotated[UserProfile, Depends(require_student)]) -> list[dict[str, str]]:
|
||||
with get_connection() as connection:
|
||||
return repository.list_orders(connection, student_id=user.id)
|
||||
|
||||
|
||||
@router.get("/orders/{order_id}", response_model=OrderDetailOut)
|
||||
def get_order(order_id: int, user: Annotated[UserProfile, Depends(require_student)]) -> dict[str, str]:
|
||||
with get_connection() as connection:
|
||||
detail = repository.get_order_detail(connection, order_id, student_id=user.id)
|
||||
if detail is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="order_not_found")
|
||||
return detail
|
||||
|
||||
|
||||
@router.post("/orders/{order_id}/confirm")
|
||||
def confirm_order(order_id: int, user: Annotated[UserProfile, Depends(require_student)]) -> dict[str, str]:
|
||||
with get_connection() as connection:
|
||||
try:
|
||||
repository.confirm_order(connection, order_id, user)
|
||||
except KeyError as error:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=error.args[0]) from error
|
||||
except ValueError as error:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error.args[0]) from error
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.post("/orders/{order_id}/feedback")
|
||||
def submit_feedback(
|
||||
order_id: int,
|
||||
payload: FeedbackRequest,
|
||||
user: Annotated[UserProfile, Depends(require_student)],
|
||||
) -> dict[str, str]:
|
||||
with get_connection() as connection:
|
||||
try:
|
||||
repository.add_feedback(connection, order_id, user, payload)
|
||||
except KeyError as error:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=error.args[0]) from error
|
||||
except ValueError as error:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error.args[0]) from error
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.post("/orders/{order_id}/rework")
|
||||
def submit_rework(
|
||||
order_id: int,
|
||||
payload: ReworkRequest,
|
||||
user: Annotated[UserProfile, Depends(require_student)],
|
||||
) -> dict[str, str]:
|
||||
with get_connection() as connection:
|
||||
try:
|
||||
repository.request_rework(connection, order_id, user, payload)
|
||||
except KeyError as error:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=error.args[0]) from error
|
||||
except ValueError as error:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error.args[0]) from error
|
||||
return {"status": "ok"}
|
||||
Reference in New Issue
Block a user