from django.contrib.postgres.fields import ArrayField from django.db import models from core.enums import ( ClientPropertyMatchGroup, ClientPropertyMatchSource, ClientPropertyMatchStatus, ClientStatusLogChangeType, ClientViewingIntent, ClientViewingType, ) from core.models.base import UUIDPrimaryKeyModel class ClientViewing(UUIDPrimaryKeyModel): client = models.ForeignKey( "fonrey_client.Client", on_delete=models.RESTRICT, related_name="viewings", verbose_name="所属客源", help_text="带看记录仅软删除,不随客源删除", ) property = models.ForeignKey( "fonrey_property.Property", on_delete=models.RESTRICT, related_name="client_viewings", verbose_name="带看房源", help_text="房源删除时保留带看记录", ) viewing_type = models.CharField( max_length=20, choices=ClientViewingType.choices, default=ClientViewingType.VIEWING, verbose_name="带看类型", help_text="appointment=预约 / viewing=带看 / revisit=复看 / empty=空看", ) agent = models.ForeignKey( "org.Staff", null=True, blank=True, on_delete=models.SET_NULL, related_name="led_viewings", verbose_name="主带看经纪人", ) companion_ids = ArrayField( models.UUIDField(), blank=True, default=list, verbose_name="陪看人员", help_text="员工 ID 数组(最多5人)", ) cooperator_ids = ArrayField( models.UUIDField(), blank=True, default=list, verbose_name="合作带看人", help_text="员工 ID 数组(最多5人)", ) scheduled_at = models.DateTimeField( null=True, blank=True, verbose_name="预约时间", ) viewing_start_at = models.DateTimeField( null=True, blank=True, verbose_name="实际带看开始时间", ) viewing_end_at = models.DateTimeField( null=True, blank=True, verbose_name="带看结束时间", ) situation = models.TextField( blank=True, default="", verbose_name="带看情况", help_text="必填,≥6字", ) client_intent = models.CharField( max_length=20, choices=ClientViewingIntent.choices, blank=True, default="", verbose_name="客户意向", help_text="interested=感兴趣 / not_interested=不感兴趣 / negotiating=谈判中 / cancelled=取消", ) viewing_progress = models.SmallIntegerField( null=True, blank=True, verbose_name="带看进度", help_text="1=一看,2=二看…,冗余字段,触发器维护", ) created_at = models.DateTimeField( auto_now_add=True, verbose_name="创建时间", ) deleted_at = models.DateTimeField( null=True, blank=True, verbose_name="删除时间", help_text="软删除时间戳;带看记录可软删除", ) created_by = models.ForeignKey( "org.Staff", null=True, blank=True, on_delete=models.SET_NULL, related_name="created_client_viewings", verbose_name="创建人", ) class Meta: db_table = "client_viewings" verbose_name = "带看记录" verbose_name_plural = "带看记录" indexes = [ models.Index( fields=["client", "-viewing_start_at"], name="idx_cv_client_time" ), models.Index(fields=["property"], name="idx_cv_property"), models.Index(fields=["agent"], name="idx_cv_agent"), ] class ClientPropertyMatch(UUIDPrimaryKeyModel): client = models.ForeignKey( "fonrey_client.Client", on_delete=models.CASCADE, related_name="property_matches", verbose_name="所属客源", ) property = models.ForeignKey( "fonrey_property.Property", on_delete=models.CASCADE, related_name="client_matches", verbose_name="匹配房源", ) match_source = models.CharField( max_length=20, choices=ClientPropertyMatchSource.choices, default=ClientPropertyMatchSource.RECORDED, verbose_name="匹配来源", help_text="recorded=录客配房(基于录入需求) / system=系统配房(算法推荐)", ) match_group = models.CharField( max_length=30, choices=ClientPropertyMatchGroup.choices, blank=True, default="", verbose_name="匹配分组", help_text="quality_layout=优质户型 / price_reduced=降价 / hot=热门 / newly_listed=新上", ) match_score = models.DecimalField( max_digits=5, decimal_places=2, null=True, blank=True, verbose_name="匹配度评分", help_text="0-100", ) match_reasons = models.JSONField( null=True, blank=True, verbose_name="匹配原因详情", help_text='格式:[{"key": "budget", "match": true}, ...]', ) status = models.CharField( max_length=20, choices=ClientPropertyMatchStatus.choices, default=ClientPropertyMatchStatus.SUGGESTED, verbose_name="状态", help_text="suggested=待推送 / shared=已分享 / rejected=已反馈不合适 / viewed=客户已查看", ) shared_at = models.DateTimeField( null=True, blank=True, verbose_name="分享时间", ) feedback = models.CharField( max_length=50, blank=True, default="", verbose_name="反馈原因", help_text="lookup_items 维护", ) calculated_at = models.DateTimeField( auto_now_add=True, verbose_name="配房计算时间", ) created_by = models.ForeignKey( "org.Staff", null=True, blank=True, on_delete=models.SET_NULL, related_name="created_matches", verbose_name="创建人", help_text="触发配房操作的员工(录客配房时记录,系统配房可为NULL)", ) class Meta: db_table = "client_property_matches" verbose_name = "智能配房" verbose_name_plural = "智能配房" constraints = [ models.UniqueConstraint( fields=["client", "property"], name="uq_client_match_pair" ), ] indexes = [ models.Index( fields=["client", "match_source", "match_group"], name="idx_cpm_client_grp", ), models.Index(fields=["client", "status"], name="idx_cpm_status"), ] class ClientStatusLog(models.Model): """Audit log; record-level immutable (no deleted_at).""" id = models.UUIDField( primary_key=True, verbose_name="主键", ) client = models.ForeignKey( "fonrey_client.Client", on_delete=models.RESTRICT, related_name="status_logs", verbose_name="所属客源", help_text="状态日志永久保留,RESTRICT 防止删除客源", ) change_type = models.CharField( max_length=30, choices=ClientStatusLogChangeType.choices, verbose_name="变更类型", help_text="status_change=改状态 / grade_change=改等级 / to_public=转公客 / to_transacted=转成交 / to_invalid=转无效 / owner_change=改归属人 / source_change=改来源", ) old_value = models.JSONField( null=True, blank=True, verbose_name="变更前快照", help_text='格式:{"status": "buying", "label": "求购"}', ) new_value = models.JSONField( null=True, blank=True, verbose_name="变更后快照", ) reason = models.TextField( blank=True, default="", verbose_name="变更理由", help_text="改状态必填,最多200字", ) operator = models.ForeignKey( "org.Staff", on_delete=models.RESTRICT, related_name="client_status_changes", verbose_name="操作人", help_text="必填,状态变更审计用", ) operated_at = models.DateTimeField( auto_now_add=True, verbose_name="操作时间", ) class Meta: db_table = "client_status_logs" verbose_name = "客源状态变更日志" verbose_name_plural = "客源状态变更日志" indexes = [ models.Index(fields=["client", "-operated_at"], name="idx_csl_client"), models.Index(fields=["change_type", "-operated_at"], name="idx_csl_type"), ]