2026-06-01 21:10:37 +08:00
|
|
|
"""User feedback API service functions."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
from typing import Any, Dict
|
|
|
|
|
|
|
|
|
|
from fastapi import HTTPException, Request
|
|
|
|
|
|
|
|
|
|
from src.database.db_manager import DBManager
|
|
|
|
|
from web.core import UserFeedbackRequest
|
|
|
|
|
import web.routes as legacy_routes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_ALLOWED_FEEDBACK_CATEGORIES = {
|
|
|
|
|
"bug",
|
|
|
|
|
"data",
|
|
|
|
|
"idea",
|
|
|
|
|
"payment",
|
|
|
|
|
"account",
|
|
|
|
|
"other",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_category(value: str) -> str:
|
|
|
|
|
normalized = str(value or "").strip().lower()
|
|
|
|
|
return normalized if normalized in _ALLOWED_FEEDBACK_CATEGORIES else "other"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _bounded_context(context: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
|
|
if not isinstance(context, dict):
|
|
|
|
|
return {}
|
|
|
|
|
try:
|
|
|
|
|
encoded = json.dumps(context, ensure_ascii=False, default=str)
|
|
|
|
|
except Exception:
|
|
|
|
|
return {"_error": "context_not_serializable"}
|
|
|
|
|
if len(encoded) <= 20_000:
|
|
|
|
|
return json.loads(encoded)
|
|
|
|
|
return {
|
|
|
|
|
"_truncated": True,
|
|
|
|
|
"raw_preview": encoded[:20_000],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def submit_user_feedback(request: Request, body: UserFeedbackRequest) -> Dict[str, Any]:
|
|
|
|
|
legacy_routes._bind_optional_supabase_identity(request)
|
|
|
|
|
|
|
|
|
|
message = str(body.message or "").strip()
|
|
|
|
|
if len(message) < 3:
|
|
|
|
|
raise HTTPException(status_code=400, detail="message is required")
|
|
|
|
|
|
|
|
|
|
user_id = str(getattr(request.state, "auth_user_id", "") or "").strip()
|
|
|
|
|
user_email = str(getattr(request.state, "auth_email", "") or "").strip()
|
|
|
|
|
contact = str(body.contact or "").strip() or user_email
|
|
|
|
|
|
|
|
|
|
feedback = DBManager().append_user_feedback(
|
|
|
|
|
category=_normalize_category(body.category),
|
|
|
|
|
message=message,
|
|
|
|
|
source=str(body.source or "terminal").strip().lower() or "terminal",
|
|
|
|
|
contact=contact,
|
|
|
|
|
user_id=user_id,
|
|
|
|
|
user_email=user_email,
|
|
|
|
|
context=_bounded_context(body.context),
|
|
|
|
|
)
|
|
|
|
|
return {"ok": True, "feedback": feedback}
|
2026-06-06 22:55:15 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def list_current_user_feedback(request: Request, *, limit: int = 20) -> Dict[str, Any]:
|
|
|
|
|
legacy_routes._bind_optional_supabase_identity(request)
|
|
|
|
|
|
|
|
|
|
user_id = str(getattr(request.state, "auth_user_id", "") or "").strip()
|
|
|
|
|
user_email = str(getattr(request.state, "auth_email", "") or "").strip()
|
|
|
|
|
if not user_id and not user_email:
|
|
|
|
|
raise HTTPException(status_code=401, detail="Supabase user required")
|
|
|
|
|
|
|
|
|
|
rows = DBManager().list_user_feedback(
|
|
|
|
|
limit=limit,
|
|
|
|
|
user_id=user_id,
|
|
|
|
|
user_email=user_email,
|
|
|
|
|
)
|
|
|
|
|
status_counts: Dict[str, int] = {}
|
|
|
|
|
for row in rows:
|
|
|
|
|
key = str(row.get("status") or "unknown")
|
|
|
|
|
status_counts[key] = status_counts.get(key, 0) + 1
|
|
|
|
|
return {
|
|
|
|
|
"feedback": rows,
|
|
|
|
|
"total": len(rows),
|
|
|
|
|
"status_counts": status_counts,
|
|
|
|
|
}
|