Sync DATA_MODEL_CLIENT.md field-level Chinese annotations to Django models across 11 client tables (Client, ClientContact, ClientRequirement, ClientSchoolPreference, ClientFavoriteFolder, ClientFolderItem, ClientFollowLog, ClientFollowLogAttachment, ClientViewing, ClientPropertyMatch, ClientStatusLog). Pre-existing docstrings retained on ClientFollowLog (partitioned parent treated as unmanaged) and ClientStatusLog (immutable audit log).
282 lines
8.6 KiB
Python
282 lines
8.6 KiB
Python
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"),
|
||
]
|