from django.db import models from core.enums import ( PropertyFollowAiTag, PropertyFollowAttachmentFileType, PropertyFollowLogType, PropertyKeyType, ) from core.models.base import UUIDPrimaryKeyModel class FollowLog(models.Model): """Partitioned table (PARTITION BY RANGE created_at). Managed via RunSQL; Django ORM treats parent as unmanaged. """ id = models.UUIDField(primary_key=True, verbose_name="主键") created_at = models.DateTimeField( verbose_name="创建时间", help_text="分区键,必须在最前声明;系统自动", ) property = models.ForeignKey( "fonrey_property.Property", on_delete=models.CASCADE, related_name="follow_logs", verbose_name="所属房源", ) log_type = models.CharField( max_length=30, choices=PropertyFollowLogType.choices, verbose_name="跟进日志类型", help_text="written=经纪人主动写入/modified=字段变更自动生成/sensitive_op=敏感操作跟进/sensitive_view=敏感信息查看(不可删)/other=其他/system=系统日志", ) purpose = models.CharField( max_length=50, blank=True, default="", verbose_name="跟进目的", help_text="枚举值由 lookup_items 维护,如:电话/业主跟进/议价/带看;仅 written 类型使用", ) content = models.TextField( blank=True, default="", verbose_name="跟进内容", help_text="最少 6 字,最多 500 字;仅 written 类型必填", ) ai_tag = models.CharField( max_length=20, blank=True, default="", choices=PropertyFollowAiTag.choices, verbose_name="AI 辅助标签", help_text="ai_for_sale=AI判断业主在售/ai_not_for_sale=AI判断业主不售;由系统智能分析后打标", ) change_detail = models.JSONField( null=True, blank=True, verbose_name="字段变更明细", help_text='格式:{"field": "sale_price", "old": 850, "new": 800, "label": "售价"};modified 类型使用', ) log_tag = models.CharField( max_length=50, blank=True, default="", verbose_name="前端展示标签", help_text="如:查看号码/图片下载/改状态/改价格/改等级/修改相关方;对应跟进时间线显示的方括号标签", ) is_public = models.BooleanField( default=True, verbose_name="是否公开", help_text="true=全员可见/false=仅本人及上级可见", ) operator = models.ForeignKey( "org.Staff", null=True, blank=True, on_delete=models.SET_NULL, verbose_name="操作人", help_text="人员离职后置 NULL,但 snapshot 保留", ) operator_snapshot = models.JSONField( null=True, blank=True, verbose_name="操作人快照", help_text="{name, role, org_unit_name, store_group};防止人员离职后丢失显示信息", ) is_deletable = models.BooleanField( default=True, verbose_name="是否可软删除", help_text="false=敏感信息查看类型,合规要求不可删除", ) deleted_at = models.DateTimeField( null=True, blank=True, verbose_name="软删除时间戳", help_text="仅 is_deletable=TRUE 时可软删;NULL=未删除", ) class Meta: db_table = "follow_logs" verbose_name = "房源跟进日志" verbose_name_plural = "房源跟进日志" managed = False unique_together = (("id", "created_at"),) class FollowLogAttachment(UUIDPrimaryKeyModel): follow_log_id = models.UUIDField( verbose_name="所属跟进日志ID", help_text="跨分区外键,未通过 Django FK 强约束;日志删除时联级删除", ) file_key = models.TextField( verbose_name="图片存储路径", help_text="Cloudflare R2 对象路径", ) file_name = models.CharField( max_length=255, verbose_name="原始文件名", help_text="用户上传时的文件名", ) file_size = models.IntegerField( verbose_name="文件大小", help_text="bytes;最大 20MB = 20971520", ) file_type = models.CharField( max_length=10, blank=True, default="", choices=PropertyFollowAttachmentFileType.choices, verbose_name="文件格式", help_text="bmp/jpg/png/svg/gif(PRD 限定格式)", ) sort_order = models.SmallIntegerField( default=0, verbose_name="排序权重", help_text="控制同一跟进附件的显示顺序", ) created_at = models.DateTimeField(auto_now_add=True, verbose_name="上传时间") class Meta: db_table = "follow_log_attachments" verbose_name = "跟进附件" verbose_name_plural = "跟进附件" indexes = [models.Index(fields=["follow_log_id"], name="idx_fla_log")] class FollowLogRecording(UUIDPrimaryKeyModel): follow_log_id = models.UUIDField( verbose_name="所属跟进日志ID", help_text="跨分区外键,未通过 Django FK 强约束;日志删除时联级删除", ) file_key = models.TextField( verbose_name="录音文件存储路径", help_text="Cloudflare R2 对象路径", ) duration_seconds = models.IntegerField( null=True, blank=True, verbose_name="录音时长", help_text="秒;可空,上传时若能解析则填写", ) created_at = models.DateTimeField(auto_now_add=True, verbose_name="上传时间") class Meta: db_table = "follow_log_recordings" verbose_name = "跟进录音" verbose_name_plural = "跟进录音" indexes = [models.Index(fields=["follow_log_id"], name="idx_flr_log")] class PropertyKey(UUIDPrimaryKeyModel): property = models.ForeignKey( "fonrey_property.Property", on_delete=models.CASCADE, related_name="keys", verbose_name="所属房源", ) key_type = models.CharField( max_length=20, choices=PropertyKeyType.choices, verbose_name="钥匙类型", help_text="mechanical=机械钥匙/password=密码(如密码门锁)", ) holder = models.ForeignKey( "org.Staff", null=True, blank=True, on_delete=models.SET_NULL, related_name="held_keys", verbose_name="持有人", help_text="人员离职后置 NULL", ) holder_snapshot = models.JSONField( null=True, blank=True, verbose_name="持有人快照", help_text="{name, store_group};防止人员离职后丢失显示信息", ) storage_unit = models.ForeignKey( "org.OrgUnit", null=True, blank=True, on_delete=models.SET_NULL, related_name="stored_keys", verbose_name="保管部门", help_text="钥匙存放在哪个部门", ) is_other_agency = models.BooleanField( default=False, verbose_name="是否他司钥匙", help_text="true=是他中介公司的钥匙/false=本司钥匙", ) other_agency_info = models.CharField( max_length=30, blank=True, default="", verbose_name="他司中介信息", help_text='最多 30 字;is_other_agency=true 时填写,如"链家"', ) remarks = models.TextField( blank=True, default="", verbose_name="备注", help_text="最多 200 字;如密码内容等补充说明", ) is_active = models.BooleanField( default=True, verbose_name="是否有效", help_text="true=在管中/false=已归还或失效", ) created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") updated_at = models.DateTimeField(auto_now=True, verbose_name="最后更新时间") created_by = models.ForeignKey( "org.Staff", null=True, blank=True, on_delete=models.SET_NULL, related_name="created_property_keys", verbose_name="创建人", ) class Meta: db_table = "property_keys" verbose_name = "房源钥匙" verbose_name_plural = "房源钥匙" indexes = [models.Index(fields=["property"], name="idx_pk_property")] class KeyAttachment(UUIDPrimaryKeyModel): key = models.ForeignKey( PropertyKey, on_delete=models.CASCADE, related_name="attachments", verbose_name="所属钥匙记录", help_text="钥匙删除时联级删除", ) file_key = models.TextField( verbose_name="附件存储路径", help_text="Cloudflare R2 对象路径", ) file_name = models.CharField(max_length=255, verbose_name="原始文件名") created_at = models.DateTimeField(auto_now_add=True, verbose_name="上传时间") class Meta: db_table = "key_attachments" verbose_name = "钥匙附件" verbose_name_plural = "钥匙附件" indexes = [models.Index(fields=["key"], name="idx_ka_key")]