Files

89 lines
3.5 KiB
Python
Raw Permalink Normal View History

"""客户画像中期记忆:MySQL 事实源 + Redis Cache-Aside。"""
from __future__ import annotations
import json
from decimal import Decimal
from typing import Any
from config.database.redis import client as redis_client
from repositories.fin_customer_profile import FinCustomerProfileRepo
class CustomerProfileMemory:
"""提供客户画像读取、缓存失效和刷新能力。"""
CACHE_TTL = 7 * 24 * 60 * 60
def __init__(self, *, redis=None, repository_factory=FinCustomerProfileRepo):
self.redis = redis or redis_client()
self.repository_factory = repository_factory
@staticmethod
def cache_key(customer_id: int) -> str:
"""生成客户画像缓存 Key。"""
return f"profile:{customer_id}"
async def get(self, db, customer_id: int) -> tuple[dict[str, Any] | None, list[str]]:
"""优先读取缓存,未命中后回源 MySQL,并返回 warnings。"""
warnings: list[str] = []
key = self.cache_key(customer_id)
try:
cached = await self.redis.get(key)
if cached:
return json.loads(cached), warnings
except Exception as exc:
warnings.append(f"profile_cache_read_failed:{type(exc).__name__}")
try:
profile = await self.repository_factory(db).get_by_customer_id(customer_id)
except Exception as exc:
warnings.append(f"profile_mysql_read_failed:{type(exc).__name__}")
return None, warnings
if profile is None:
return None, warnings
payload = self._to_dict(profile)
try:
await self.redis.set(key, json.dumps(payload, ensure_ascii=False), ex=self.CACHE_TTL)
except Exception as exc:
warnings.append(f"profile_cache_write_failed:{type(exc).__name__}")
return payload, warnings
async def invalidate(self, customer_id: int) -> list[str]:
"""删除客户画像缓存,确保更新后不会长期读取旧值。"""
try:
await self.redis.delete(self.cache_key(customer_id))
return []
except Exception as exc:
return [f"profile_cache_invalidate_failed:{type(exc).__name__}"]
async def refresh(self, db, customer_id: int) -> tuple[dict[str, Any] | None, list[str]]:
"""先删除缓存,再从 MySQL 读取并重新缓存画像。"""
warnings = await self.invalidate(customer_id)
profile, read_warnings = await self.get(db, customer_id)
return profile, warnings + read_warnings
@staticmethod
def _to_dict(profile) -> dict[str, Any]:
"""将 ORM 画像转换为可安全写入 Redis 的字典。"""
data = {
"customer_id": profile.customer_id,
"risk_level": profile.risk_level,
"risk_score": profile.risk_score,
"investment_experience": profile.investment_experience,
"annual_income_range": profile.annual_income_range,
"total_assets": profile.total_assets,
"asset_allocation": profile.asset_allocation,
"product_preference": profile.product_preference,
"customer_level": profile.customer_level,
"confidence_score": profile.confidence_score,
"profile_version": profile.profile_version,
"update_time": profile.update_time.isoformat() if profile.update_time else None,
}
return json.loads(json.dumps(data, default=lambda value: str(value), ensure_ascii=False))
__all__ = ["CustomerProfileMemory"]