"""客户画像中期记忆: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"]