feat(agent): session deletion anonymization for iOS compliance

Replace soft-delete with anonymize + hard-delete to meet iOS App Store
data retention requirements. Non-PII fields are preserved in
anonymous_session_snapshots for analytics.

- Add anonymous_session_snapshots table and ORM model
- Implement anonymizer to extract non-PII fields before deletion
- Remove points_ledger.biz_id FK constraint (snapshot-style reference)
- Preserve transaction history while allowing session deletion
- Add 14 unit tests + 1 integration test
This commit is contained in:
qzl
2026-04-15 18:18:39 +08:00
parent a244eaa666
commit c2b726e7bd
10 changed files with 829 additions and 7 deletions
+2
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
from .agent_chat_message import AgentChatMessage
from .agent_chat_session import AgentChatSession
from .anonymous_session_snapshot import AnonymousSessionSnapshot
from .auth_user import AuthUser
from .invite_code import InviteCode
from .llm import Llm
@@ -18,6 +19,7 @@ from .user_points import UserPoints
__all__ = [
"AgentChatMessage",
"AgentChatSession",
"AnonymousSessionSnapshot",
"AuthUser",
"InviteCode",
"Llm",
@@ -0,0 +1,46 @@
from __future__ import annotations
from datetime import datetime
from decimal import Decimal
import uuid
from sqlalchemy import Boolean, DateTime, Integer, Numeric, String, Text
from sqlalchemy.dialects.postgresql import ARRAY, UUID
from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base
__all__ = ["AnonymousSessionSnapshot"]
class AnonymousSessionSnapshot(Base):
__tablename__: str = "anonymous_session_snapshots"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
anonymous_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False)
session_type: Mapped[str] = mapped_column(String(20), nullable=False)
message_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
status: Mapped[str | None] = mapped_column(String(20), nullable=True)
question_type: Mapped[str | None] = mapped_column(String(50), nullable=True)
tool_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
gua_name: Mapped[str | None] = mapped_column(String(50), nullable=True)
gua_name_hant: Mapped[str | None] = mapped_column(String(50), nullable=True)
target_gua_name: Mapped[str | None] = mapped_column(String(50), nullable=True)
has_changing_yao: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
sign_level: Mapped[str | None] = mapped_column(String(20), nullable=True)
keywords: Mapped[list[str] | None] = mapped_column(ARRAY(Text()), nullable=True)
model_code: Mapped[str | None] = mapped_column(String(50), nullable=True)
total_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True)
total_cost: Mapped[Decimal | None] = mapped_column(Numeric(12, 6), nullable=True)
total_latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False
)
last_activity_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
anonymized_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False
)