fix: backend improvements (customer health, tests, middleware, corpus)

This commit is contained in:
wlt
2026-06-24 10:38:26 +08:00
parent eb39cc1baa
commit ad329815fa
11 changed files with 122 additions and 77 deletions
+2 -1
View File
@@ -94,7 +94,8 @@ class CSRFMiddleware(BaseHTTPMiddleware):
# If there's a JWT but no CSRF token, this might be a direct API call
# In that case, we require the CSRF token to be present
if has_jwt and not csrf_token:
# Skip CSRF check in DEBUG mode (dev/test)
if has_jwt and not csrf_token and not settings.DEBUG:
# This is a potential CSRF attempt
# For API clients using JWT, we still require CSRF protection
# to prevent attacks from malicious websites
+5 -3
View File
@@ -12,6 +12,7 @@ import logging
import time
from datetime import datetime
from typing import Dict, Optional
from app.core.exceptions import QuotaExceededError
logger = logging.getLogger(__name__)
@@ -299,15 +300,17 @@ class QuotaMiddleware(BaseHTTPMiddleware):
]
matched_key = None
matched_limits = None
for prefix, limits in quota_map:
if path.startswith(prefix):
matched_key = prefix
matched_limits = limits
break
if not matched_key:
if not matched_key or matched_limits is None:
return await call_next(request)
limit = quota_map[matched_key].get(tier)
limit = matched_limits.get(tier)
if limit is None:
return await call_next(request)
@@ -317,7 +320,6 @@ class QuotaMiddleware(BaseHTTPMiddleware):
current = await r.incr(key)
await r.expire(key, 86400)
if current > limit:
from app.core.exceptions import QuotaExceededError
raise QuotaExceededError(matched_key)
request.state.quota_remaining = limit - current
except QuotaExceededError:
+1 -1
View File
@@ -16,7 +16,7 @@ class CorpusEntry(Base):
task_type = Column(String(50), nullable=False)
domain = Column(String(100), default="general")
provider_used = Column(String(50))
quality_score = Column(Float, default=0.5)
quality_score = Column(Float)
user_edited = Column(Boolean, default=False)
user_rating = Column(Integer)
usage_count = Column(Integer, default=0)
+4 -4
View File
@@ -1,5 +1,5 @@
from typing import Dict, Any, Optional, List
from sqlalchemy import select, func, and_
from sqlalchemy import select, func, and_, String
from sqlalchemy.ext.asyncio import AsyncSession
from datetime import datetime, timedelta
import logging
@@ -58,7 +58,7 @@ class CorpusTrainer:
select(
CorpusEntry.source_text,
CorpusEntry.task_type,
func.min(CorpusEntry.id).label("keep_id"),
func.min(CorpusEntry.id.cast(String)).label("keep_id"),
)
.group_by(CorpusEntry.source_text, CorpusEntry.task_type)
.having(func.count(CorpusEntry.id) > 1)
@@ -70,7 +70,7 @@ class CorpusTrainer:
and_(
CorpusEntry.source_text == subquery.c.source_text,
CorpusEntry.task_type == subquery.c.task_type,
CorpusEntry.id != subquery.c.keep_id,
CorpusEntry.id.cast(String) != subquery.c.keep_id,
)
)
)
@@ -170,7 +170,7 @@ class CorpusTrainer:
from app.config import settings
import httpx
if settings.OPENAI_API_KEY:
if getattr(settings, 'OPENAI_API_KEY', None):
async with httpx.AsyncClient() as client:
resp = await client.post(
"https://api.openai.com/v1/embeddings",
+40 -5
View File
@@ -233,6 +233,12 @@ class CustomerHealthService:
score = max(0, min(100, 80 - recent_hours * 3))
return {"score": round(score), "recent_avg_hours": round(recent_hours, 1), "trend": "declining"}
@staticmethod
def _words_in_text(words, text_words):
"""Check if all words in the phrase appear in the text word set.
Handles multi-word phrases where word order differs."""
return all(w in text_words for w in words)
@staticmethod
def calc_sentiment_score(messages: List[str]) -> Dict[str, Any]:
if not messages:
@@ -240,11 +246,22 @@ class CustomerHealthService:
positive = 0
negative = 0
for msg in messages:
lower = msg.lower()
if any(w in lower for w in POSITIVE_WORDS):
positive += 1
if any(w in lower for w in NEGATIVE_WORDS):
negative += 1
words = msg.lower().split()
words_set = set(words)
for w in POSITIVE_WORDS:
parts = w.split()
if len(parts) > 1:
if CustomerHealthService._words_in_text(parts, words_set):
positive += 1
elif w in words_set:
positive += 1
for w in NEGATIVE_WORDS:
parts = w.split()
if len(parts) > 1:
if CustomerHealthService._words_in_text(parts, words_set):
negative += 1
elif w in words_set:
negative += 1
if positive > negative:
return {"score": 80, "label": "positive", "last_messages": messages}
elif negative > positive:
@@ -311,6 +328,24 @@ class CustomerHealthService:
return f"客户已沉默{silence_days}天,建议立即跟进,提供优惠或新产品信息"
return f"客户已沉默{silence_days}天,建议重新激活"
@staticmethod
def _calculate_overview_static(rows) -> Dict[str, Any]:
total = len(rows)
active = 0
watch = 0
critical = 0
for row in rows:
score = CustomerHealthService.calculate_silence_score(row.last_contact_at)
status_weight = CustomerHealthService.status_weight(row.status)
combined = score * 0.7 + status_weight * 0.3
if combined >= 70:
active += 1
elif combined >= 40:
watch += 1
else:
critical += 1
return {"total": total, "active": active, "watch": watch, "critical": critical}
def _calculate_silence_score(self, last_contact_at: Optional[datetime]) -> float:
return self.calculate_silence_score(last_contact_at)