This commit is contained in:
2026-06-06 23:54:11 +08:00
commit 33639129b1
58 changed files with 10309 additions and 0 deletions
+35
View File
@@ -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
+84
View File
@@ -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"}
+32
View File
@@ -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
+161
View File
@@ -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"}