feat(client): add Chinese verbose_name and help_text to all client fields (Phase 4.1 part 2/9)
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).
This commit is contained in:
@@ -14,35 +14,103 @@ from core.models.base import UUIDPrimaryKeyModel
|
|||||||
|
|
||||||
class ClientContact(UUIDPrimaryKeyModel):
|
class ClientContact(UUIDPrimaryKeyModel):
|
||||||
client = models.ForeignKey(
|
client = models.ForeignKey(
|
||||||
"fonrey_client.Client", on_delete=models.CASCADE, related_name="contacts"
|
"fonrey_client.Client",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="contacts",
|
||||||
|
verbose_name="所属客源",
|
||||||
|
help_text="联系人随客源级联删除",
|
||||||
|
)
|
||||||
|
sort_order = models.SmallIntegerField(
|
||||||
|
default=0,
|
||||||
|
verbose_name="排序顺序",
|
||||||
|
help_text="sort_order=0 为主联系人,姓名用于客源姓名显示",
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
verbose_name="联系人姓名",
|
||||||
)
|
)
|
||||||
sort_order = models.SmallIntegerField(default=0)
|
|
||||||
name = models.CharField(max_length=50)
|
|
||||||
gender = models.CharField(
|
gender = models.CharField(
|
||||||
max_length=10, choices=ClientContactGender.choices, default=ClientContactGender.MALE
|
max_length=10,
|
||||||
|
choices=ClientContactGender.choices,
|
||||||
|
default=ClientContactGender.MALE,
|
||||||
|
verbose_name="性别",
|
||||||
|
help_text="male=先生 / female=女士",
|
||||||
)
|
)
|
||||||
|
|
||||||
phone_enc = models.BinaryField()
|
phone_enc = models.BinaryField(
|
||||||
phone_hash = models.CharField(max_length=64)
|
verbose_name="手机号(加密)",
|
||||||
phone_country_code = models.CharField(max_length=10, default="+86")
|
help_text="AES-256-GCM 加密手机号(电话1)",
|
||||||
phone_is_invalid = models.BooleanField(default=False)
|
)
|
||||||
|
phone_hash = models.CharField(
|
||||||
|
max_length=64,
|
||||||
|
verbose_name="手机号哈希",
|
||||||
|
help_text="SHA-256 哈希(重复检测)",
|
||||||
|
)
|
||||||
|
phone_country_code = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
default="+86",
|
||||||
|
verbose_name="国际区号",
|
||||||
|
)
|
||||||
|
phone_is_invalid = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name="号码是否无效",
|
||||||
|
help_text="标记无效后该号码不再参与重复检测",
|
||||||
|
)
|
||||||
|
|
||||||
phone2_enc = models.BinaryField(null=True, blank=True)
|
phone2_enc = models.BinaryField(
|
||||||
phone2_hash = models.CharField(max_length=64, blank=True, default="")
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="备用电话2(加密)",
|
||||||
|
)
|
||||||
|
phone2_hash = models.CharField(
|
||||||
|
max_length=64,
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name="备用电话2哈希",
|
||||||
|
help_text="SHA-256,用于重复检测",
|
||||||
|
)
|
||||||
|
|
||||||
wechat = models.CharField(max_length=100, blank=True, default="")
|
wechat = models.CharField(
|
||||||
qq = models.CharField(max_length=20, blank=True, default="")
|
max_length=100,
|
||||||
remarks = models.CharField(max_length=200, blank=True, default="")
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name="微信号",
|
||||||
|
)
|
||||||
|
qq = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name="QQ号",
|
||||||
|
)
|
||||||
|
remarks = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name="联系人备注",
|
||||||
|
help_text="最多200字",
|
||||||
|
)
|
||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
auto_now_add=True,
|
||||||
deleted_at = models.DateTimeField(null=True, blank=True)
|
verbose_name="创建时间",
|
||||||
|
)
|
||||||
|
updated_at = models.DateTimeField(
|
||||||
|
auto_now=True,
|
||||||
|
verbose_name="最后更新时间",
|
||||||
|
)
|
||||||
|
deleted_at = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="删除时间",
|
||||||
|
help_text="软删除时间戳;NULL=未删除(不影响客源本身)",
|
||||||
|
)
|
||||||
created_by = models.ForeignKey(
|
created_by = models.ForeignKey(
|
||||||
"org.Staff",
|
"org.Staff",
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name="created_client_contacts",
|
related_name="created_client_contacts",
|
||||||
|
verbose_name="创建人",
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -58,65 +126,152 @@ class ClientContact(UUIDPrimaryKeyModel):
|
|||||||
|
|
||||||
class ClientRequirement(UUIDPrimaryKeyModel):
|
class ClientRequirement(UUIDPrimaryKeyModel):
|
||||||
client = models.ForeignKey(
|
client = models.ForeignKey(
|
||||||
"fonrey_client.Client", on_delete=models.CASCADE, related_name="requirements"
|
"fonrey_client.Client",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="requirements",
|
||||||
|
verbose_name="所属客源",
|
||||||
|
help_text="需求随客源级联删除",
|
||||||
)
|
)
|
||||||
requirement_type = models.CharField(
|
requirement_type = models.CharField(
|
||||||
max_length=20, choices=ClientRequirementType.choices
|
max_length=20,
|
||||||
|
choices=ClientRequirementType.choices,
|
||||||
|
verbose_name="需求类型",
|
||||||
|
help_text="second_hand=二手 / new_house=新房 / rental=租房",
|
||||||
|
)
|
||||||
|
is_primary = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
verbose_name="是否主需求",
|
||||||
|
help_text="用于列表展示",
|
||||||
)
|
)
|
||||||
is_primary = models.BooleanField(default=True)
|
|
||||||
|
|
||||||
budget_min = models.DecimalField(
|
budget_min = models.DecimalField(
|
||||||
max_digits=12, decimal_places=2, null=True, blank=True
|
max_digits=12,
|
||||||
|
decimal_places=2,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="最低预算",
|
||||||
|
help_text="单位:万元/元,依据需求类型",
|
||||||
)
|
)
|
||||||
budget_max = models.DecimalField(
|
budget_max = models.DecimalField(
|
||||||
max_digits=12, decimal_places=2, null=True, blank=True
|
max_digits=12,
|
||||||
|
decimal_places=2,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="最高预算",
|
||||||
)
|
)
|
||||||
area_min = models.DecimalField(
|
area_min = models.DecimalField(
|
||||||
max_digits=8, decimal_places=2, null=True, blank=True
|
max_digits=8,
|
||||||
|
decimal_places=2,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="最小面积",
|
||||||
|
help_text="单位:㎡",
|
||||||
)
|
)
|
||||||
area_max = models.DecimalField(
|
area_max = models.DecimalField(
|
||||||
max_digits=8, decimal_places=2, null=True, blank=True
|
max_digits=8,
|
||||||
|
decimal_places=2,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="最大面积",
|
||||||
|
help_text="单位:㎡",
|
||||||
)
|
)
|
||||||
|
|
||||||
bedroom_counts = ArrayField(
|
bedroom_counts = ArrayField(
|
||||||
models.SmallIntegerField(), blank=True, default=list
|
models.SmallIntegerField(),
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
verbose_name="可接受卧室数",
|
||||||
|
help_text="多选,如 [2,3]",
|
||||||
)
|
)
|
||||||
floor_preferences = ArrayField(
|
floor_preferences = ArrayField(
|
||||||
models.CharField(max_length=20, choices=ClientFloorPreference.choices),
|
models.CharField(max_length=20, choices=ClientFloorPreference.choices),
|
||||||
blank=True,
|
blank=True,
|
||||||
default=list,
|
default=list,
|
||||||
|
verbose_name="楼层偏好",
|
||||||
|
help_text="多选:no_first=不要一层 / low=低楼层 / mid=中楼层 / high=高楼层 / no_top=不要顶层",
|
||||||
)
|
)
|
||||||
orientations = ArrayField(
|
orientations = ArrayField(
|
||||||
models.CharField(max_length=10, choices=ClientOrientation.choices),
|
models.CharField(max_length=10, choices=ClientOrientation.choices),
|
||||||
blank=True,
|
blank=True,
|
||||||
default=list,
|
default=list,
|
||||||
|
verbose_name="朝向偏好",
|
||||||
|
help_text="多选:east=东 / south=南 / west=西 / north=北",
|
||||||
)
|
)
|
||||||
decorations = ArrayField(
|
decorations = ArrayField(
|
||||||
models.CharField(max_length=10, choices=ClientDecoration.choices),
|
models.CharField(max_length=10, choices=ClientDecoration.choices),
|
||||||
blank=True,
|
blank=True,
|
||||||
default=list,
|
default=list,
|
||||||
|
verbose_name="装修偏好",
|
||||||
|
help_text="多选(枚举同 properties.decoration)",
|
||||||
)
|
)
|
||||||
building_age_ranges = ArrayField(
|
building_age_ranges = ArrayField(
|
||||||
models.CharField(max_length=20, choices=ClientBuildingAgeRange.choices),
|
models.CharField(max_length=20, choices=ClientBuildingAgeRange.choices),
|
||||||
blank=True,
|
blank=True,
|
||||||
default=list,
|
default=list,
|
||||||
|
verbose_name="楼龄偏好",
|
||||||
|
help_text="多选:within_5y / 5_10y / 10_15y / 15_20y / over_20y",
|
||||||
)
|
)
|
||||||
|
|
||||||
intent_district_ids = ArrayField(
|
intent_district_ids = ArrayField(
|
||||||
models.UUIDField(), blank=True, default=list
|
models.UUIDField(),
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
verbose_name="意向行政区",
|
||||||
|
help_text="行政区 ID 数组",
|
||||||
)
|
)
|
||||||
intent_business_area_ids = ArrayField(
|
intent_business_area_ids = ArrayField(
|
||||||
models.UUIDField(), blank=True, default=list
|
models.UUIDField(),
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
verbose_name="意向商圈",
|
||||||
|
help_text="商圈 ID 数组",
|
||||||
|
)
|
||||||
|
intent_complex_names = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name="意向小区",
|
||||||
|
help_text="文本,逗号分隔,最多500字",
|
||||||
|
)
|
||||||
|
transportation = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name="交通要求",
|
||||||
|
help_text="最多50字",
|
||||||
|
)
|
||||||
|
intent_school_names = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name="意向学校",
|
||||||
|
help_text="文本,逗号分隔",
|
||||||
|
)
|
||||||
|
school_enrollment_date = models.DateField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="入学时间",
|
||||||
|
help_text="月份精度,取该月1日存储",
|
||||||
|
)
|
||||||
|
traffic_preference = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name="交通备注",
|
||||||
|
)
|
||||||
|
requirement_notes = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name="需求备注",
|
||||||
|
help_text="最多200字",
|
||||||
)
|
)
|
||||||
intent_complex_names = models.TextField(blank=True, default="")
|
|
||||||
transportation = models.CharField(max_length=50, blank=True, default="")
|
|
||||||
intent_school_names = models.TextField(blank=True, default="")
|
|
||||||
school_enrollment_date = models.DateField(null=True, blank=True)
|
|
||||||
traffic_preference = models.TextField(blank=True, default="")
|
|
||||||
requirement_notes = models.CharField(max_length=200, blank=True, default="")
|
|
||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
auto_now_add=True,
|
||||||
|
verbose_name="创建时间",
|
||||||
|
)
|
||||||
|
updated_at = models.DateTimeField(
|
||||||
|
auto_now=True,
|
||||||
|
verbose_name="最后更新时间",
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = "client_requirements"
|
db_table = "client_requirements"
|
||||||
@@ -135,10 +290,24 @@ class ClientSchoolPreference(UUIDPrimaryKeyModel):
|
|||||||
ClientRequirement,
|
ClientRequirement,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name="school_preferences",
|
related_name="school_preferences",
|
||||||
|
verbose_name="所属需求",
|
||||||
|
help_text="意向学校随需求级联删除",
|
||||||
|
)
|
||||||
|
school_id = models.UUIDField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="学校ID",
|
||||||
|
help_text="从学校表选择,允许为 NULL(自由输入)",
|
||||||
|
)
|
||||||
|
school_name = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
verbose_name="学校名称",
|
||||||
|
help_text="当 school_id 为 NULL 时为手动输入",
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(
|
||||||
|
auto_now_add=True,
|
||||||
|
verbose_name="创建时间",
|
||||||
)
|
)
|
||||||
school_id = models.UUIDField(null=True, blank=True)
|
|
||||||
school_name = models.CharField(max_length=100)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = "client_school_preferences"
|
db_table = "client_school_preferences"
|
||||||
|
|||||||
@@ -20,80 +20,190 @@ from core.models.base import AuditedModel
|
|||||||
|
|
||||||
|
|
||||||
class Client(AuditedModel):
|
class Client(AuditedModel):
|
||||||
client_no = models.CharField(max_length=30, unique=True)
|
client_no = models.CharField(
|
||||||
|
max_length=30,
|
||||||
|
unique=True,
|
||||||
|
verbose_name="客源编号",
|
||||||
|
help_text="系统生成的客源编号,格式由运营配置(如 KY20260424001)",
|
||||||
|
)
|
||||||
client_type = models.CharField(
|
client_type = models.CharField(
|
||||||
max_length=20, choices=ClientType.choices, default=ClientType.PRIVATE
|
max_length=20,
|
||||||
|
choices=ClientType.choices,
|
||||||
|
default=ClientType.PRIVATE,
|
||||||
|
verbose_name="客源分类",
|
||||||
|
help_text="private=私客 / public=公客 / transacted=成交客",
|
||||||
)
|
)
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
max_length=20, choices=ClientStatus.choices, default=ClientStatus.BUYING
|
max_length=20,
|
||||||
|
choices=ClientStatus.choices,
|
||||||
|
default=ClientStatus.BUYING,
|
||||||
|
verbose_name="客源状态",
|
||||||
|
help_text="buying=求购 / renting=求租 / buy_or_rent=租购 / suspended=暂缓 / bought=已购 / rented_done=已租 / public=公客 / invalid=无效(详见 ENUMS)",
|
||||||
)
|
)
|
||||||
grade = models.CharField(
|
grade = models.CharField(
|
||||||
max_length=5, choices=ClientGrade.choices, default=ClientGrade.C
|
max_length=5,
|
||||||
|
choices=ClientGrade.choices,
|
||||||
|
default=ClientGrade.C,
|
||||||
|
verbose_name="客源等级",
|
||||||
|
help_text="A=A急迫 / B=较强 / C=一般 / D=较弱 / E=暂不关注",
|
||||||
)
|
)
|
||||||
property_usage = models.CharField(
|
property_usage = models.CharField(
|
||||||
max_length=30,
|
max_length=30,
|
||||||
choices=ClientPropertyUsage.choices,
|
choices=ClientPropertyUsage.choices,
|
||||||
default=ClientPropertyUsage.RESIDENTIAL,
|
default=ClientPropertyUsage.RESIDENTIAL,
|
||||||
|
verbose_name="房屋用途",
|
||||||
|
help_text="residential=住宅 / villa=别墅 / commercial_residential=商住 / shop=商铺 / office=写字楼 / other=其他",
|
||||||
)
|
)
|
||||||
buying_purpose = ArrayField(
|
buying_purpose = ArrayField(
|
||||||
models.CharField(max_length=20, choices=ClientBuyingPurpose.choices),
|
models.CharField(max_length=20, choices=ClientBuyingPurpose.choices),
|
||||||
blank=True,
|
blank=True,
|
||||||
default=list,
|
default=list,
|
||||||
|
verbose_name="购房目的",
|
||||||
|
help_text="多选:rigid=刚需 / investment=投资 / school_district=学区 / upgrade=改善 / commercial=商用 / other=其他",
|
||||||
)
|
)
|
||||||
payment_method = models.CharField(
|
payment_method = models.CharField(
|
||||||
max_length=30, choices=ClientPaymentMethod.choices, blank=True, default=""
|
max_length=30,
|
||||||
|
choices=ClientPaymentMethod.choices,
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name="付款方式",
|
||||||
|
help_text="full=全额 / mortgage=商业贷款 / mortgage_fund=商贷+公积金 / fund=公积金",
|
||||||
)
|
)
|
||||||
properties_owned = models.CharField(
|
properties_owned = models.CharField(
|
||||||
max_length=20, choices=ClientPropertiesOwned.choices, blank=True, default=""
|
max_length=20,
|
||||||
|
choices=ClientPropertiesOwned.choices,
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name="名下房产",
|
||||||
|
help_text="none=无 / local_none=本地无外地有 / local_has=本地有",
|
||||||
|
)
|
||||||
|
has_loan_record = models.BooleanField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="有无贷款记录",
|
||||||
)
|
)
|
||||||
has_loan_record = models.BooleanField(null=True, blank=True)
|
|
||||||
|
|
||||||
id_type = models.CharField(
|
id_type = models.CharField(
|
||||||
max_length=20, choices=ClientIdType.choices, blank=True, default=""
|
max_length=20,
|
||||||
|
choices=ClientIdType.choices,
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name="证件类型",
|
||||||
|
help_text="id_card=身份证 / passport=护照 / hk_macao=港澳台 / other=其他",
|
||||||
|
)
|
||||||
|
id_number_enc = models.BinaryField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="证件号码(加密)",
|
||||||
|
help_text="AES 加密存储",
|
||||||
)
|
)
|
||||||
id_number_enc = models.BinaryField(null=True, blank=True)
|
|
||||||
|
|
||||||
source = models.CharField(max_length=50, blank=True, default="")
|
source = models.CharField(
|
||||||
remarks = models.TextField(blank=True, default="")
|
max_length=50,
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name="客户来源",
|
||||||
|
help_text="lookup_items 维护",
|
||||||
|
)
|
||||||
|
remarks = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name="备注",
|
||||||
|
help_text="最多200字",
|
||||||
|
)
|
||||||
|
|
||||||
is_starred = models.BooleanField(default=False)
|
is_starred = models.BooleanField(
|
||||||
is_pinned = models.BooleanField(default=False)
|
default=False,
|
||||||
is_big_value = models.BooleanField(default=False)
|
verbose_name="是否收藏",
|
||||||
is_protected = models.BooleanField(default=False)
|
help_text="快速标记,详细收藏夹用 client_folder_items",
|
||||||
prefers_new_house = models.BooleanField(null=True, blank=True)
|
)
|
||||||
|
is_pinned = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name="是否置顶",
|
||||||
|
help_text="列表顶部置顶",
|
||||||
|
)
|
||||||
|
is_big_value = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name="是否大价值客户",
|
||||||
|
help_text="影响筛选展示",
|
||||||
|
)
|
||||||
|
is_protected = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name="是否保护客",
|
||||||
|
help_text="影响转公逻辑",
|
||||||
|
)
|
||||||
|
prefers_new_house = models.BooleanField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="偏好新房",
|
||||||
|
help_text="用于筛选",
|
||||||
|
)
|
||||||
|
|
||||||
transfer_to_public_type = models.CharField(
|
transfer_to_public_type = models.CharField(
|
||||||
max_length=20,
|
max_length=20,
|
||||||
choices=ClientTransferToPublicType.choices,
|
choices=ClientTransferToPublicType.choices,
|
||||||
blank=True,
|
blank=True,
|
||||||
default="",
|
default="",
|
||||||
|
verbose_name="转公客方式",
|
||||||
|
help_text="manual=手动转公 / auto=自动转公(超时) / marketing_jump=营销客跳公 / resource_public=资料客素公",
|
||||||
|
)
|
||||||
|
transferred_public_at = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="进入公客池时间",
|
||||||
)
|
)
|
||||||
transferred_public_at = models.DateTimeField(null=True, blank=True)
|
|
||||||
|
|
||||||
invalid_reason = models.CharField(
|
invalid_reason = models.CharField(
|
||||||
max_length=30, choices=ClientInvalidReason.choices, blank=True, default=""
|
max_length=30,
|
||||||
|
choices=ClientInvalidReason.choices,
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name="无效原因",
|
||||||
|
help_text="invalid_phone=号码无效 / peer_agent=同行 / ad=广告推销 / no_intent=无意向 / other=其他",
|
||||||
|
)
|
||||||
|
invalidated_at = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="标记无效时间",
|
||||||
)
|
)
|
||||||
invalidated_at = models.DateTimeField(null=True, blank=True)
|
|
||||||
|
|
||||||
transacted_at = models.DateField(null=True, blank=True)
|
transacted_at = models.DateField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="成交日期",
|
||||||
|
)
|
||||||
transacted_property = models.ForeignKey(
|
transacted_property = models.ForeignKey(
|
||||||
"fonrey_property.Property",
|
"fonrey_property.Property",
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name="transacted_clients",
|
related_name="transacted_clients",
|
||||||
|
verbose_name="成交房源",
|
||||||
|
help_text="成交关联的房源",
|
||||||
)
|
)
|
||||||
transacted_price = models.DecimalField(
|
transacted_price = models.DecimalField(
|
||||||
max_digits=12, decimal_places=2, null=True, blank=True
|
max_digits=12,
|
||||||
|
decimal_places=2,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="成交价格",
|
||||||
|
help_text="单位:万元",
|
||||||
)
|
)
|
||||||
transacted_type = models.CharField(
|
transacted_type = models.CharField(
|
||||||
max_length=20, choices=ClientTransactedType.choices, blank=True, default=""
|
max_length=20,
|
||||||
|
choices=ClientTransactedType.choices,
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name="成交类型",
|
||||||
|
help_text="bought=我购 / rented=我租",
|
||||||
)
|
)
|
||||||
transacted_property_type = models.CharField(
|
transacted_property_type = models.CharField(
|
||||||
max_length=20,
|
max_length=20,
|
||||||
choices=ClientTransactedPropertyType.choices,
|
choices=ClientTransactedPropertyType.choices,
|
||||||
blank=True,
|
blank=True,
|
||||||
default="",
|
default="",
|
||||||
|
verbose_name="成交房源类型",
|
||||||
|
help_text="second_hand=二手 / new_house=新房",
|
||||||
)
|
)
|
||||||
|
|
||||||
first_recorder = models.ForeignKey(
|
first_recorder = models.ForeignKey(
|
||||||
@@ -102,6 +212,7 @@ class Client(AuditedModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name="first_recorded_clients",
|
related_name="first_recorded_clients",
|
||||||
|
verbose_name="首录人",
|
||||||
)
|
)
|
||||||
owner = models.ForeignKey(
|
owner = models.ForeignKey(
|
||||||
"org.Staff",
|
"org.Staff",
|
||||||
@@ -109,6 +220,8 @@ class Client(AuditedModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name="owned_clients",
|
related_name="owned_clients",
|
||||||
|
verbose_name="归属人",
|
||||||
|
help_text="私客独占跟进人",
|
||||||
)
|
)
|
||||||
org_unit = models.ForeignKey(
|
org_unit = models.ForeignKey(
|
||||||
"org.OrgUnit",
|
"org.OrgUnit",
|
||||||
@@ -116,18 +229,47 @@ class Client(AuditedModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name="clients",
|
related_name="clients",
|
||||||
|
verbose_name="归属部门",
|
||||||
|
help_text="冗余字段,加速筛选",
|
||||||
)
|
)
|
||||||
|
|
||||||
activity_level = models.CharField(
|
activity_level = models.CharField(
|
||||||
max_length=20, choices=ClientActivityLevel.choices, blank=True, default=""
|
max_length=20,
|
||||||
|
choices=ClientActivityLevel.choices,
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name="活跃度",
|
||||||
|
help_text="new_matched=新配偶 / active_7d / active_30d / active_90d / expiring / frozen / invalid(异步计算)",
|
||||||
|
)
|
||||||
|
last_active_at = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="最后有效跟进时间",
|
||||||
|
help_text="触发器维护",
|
||||||
|
)
|
||||||
|
last_follow_at = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="最后跟进时间",
|
||||||
|
help_text="冗余字段,列表排序用",
|
||||||
)
|
)
|
||||||
last_active_at = models.DateTimeField(null=True, blank=True)
|
|
||||||
last_follow_at = models.DateTimeField(null=True, blank=True)
|
|
||||||
|
|
||||||
commission_date = models.DateField(null=True, blank=True)
|
commission_date = models.DateField(
|
||||||
entrust_count = models.SmallIntegerField(default=1)
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="委托日期",
|
||||||
|
)
|
||||||
|
entrust_count = models.SmallIntegerField(
|
||||||
|
default=1,
|
||||||
|
verbose_name="委托次数",
|
||||||
|
help_text="成交后再委托则累加",
|
||||||
|
)
|
||||||
|
|
||||||
version = models.IntegerField(default=1)
|
version = models.IntegerField(
|
||||||
|
default=1,
|
||||||
|
verbose_name="版本号",
|
||||||
|
help_text="乐观锁;每次 UPDATE +1;应用层检测 0 行受影响时抛 ConflictError",
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = "clients"
|
db_table = "clients"
|
||||||
|
|||||||
@@ -5,13 +5,36 @@ from core.models.base import UUIDPrimaryKeyModel
|
|||||||
|
|
||||||
class ClientFavoriteFolder(UUIDPrimaryKeyModel):
|
class ClientFavoriteFolder(UUIDPrimaryKeyModel):
|
||||||
staff = models.ForeignKey(
|
staff = models.ForeignKey(
|
||||||
"org.Staff", on_delete=models.CASCADE, related_name="favorite_folders"
|
"org.Staff",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="favorite_folders",
|
||||||
|
verbose_name="所属经纪人",
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
verbose_name="收藏夹名称",
|
||||||
|
help_text="最多10字",
|
||||||
|
)
|
||||||
|
is_default = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name="是否默认",
|
||||||
|
help_text="系统默认收藏夹,每个经纪人只能有一个",
|
||||||
|
)
|
||||||
|
sort_order = models.IntegerField(
|
||||||
|
default=0,
|
||||||
|
verbose_name="显示顺序",
|
||||||
|
help_text="升序排列",
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(
|
||||||
|
auto_now_add=True,
|
||||||
|
verbose_name="创建时间",
|
||||||
|
)
|
||||||
|
deleted_at = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="删除时间",
|
||||||
|
help_text="软删除时间戳;NULL=未删除",
|
||||||
)
|
)
|
||||||
name = models.CharField(max_length=10)
|
|
||||||
is_default = models.BooleanField(default=False)
|
|
||||||
sort_order = models.IntegerField(default=0)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
deleted_at = models.DateTimeField(null=True, blank=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = "client_favorite_folders"
|
db_table = "client_favorite_folders"
|
||||||
@@ -31,12 +54,21 @@ class ClientFavoriteFolder(UUIDPrimaryKeyModel):
|
|||||||
|
|
||||||
class ClientFolderItem(models.Model):
|
class ClientFolderItem(models.Model):
|
||||||
folder = models.ForeignKey(
|
folder = models.ForeignKey(
|
||||||
ClientFavoriteFolder, on_delete=models.CASCADE, related_name="items"
|
ClientFavoriteFolder,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="items",
|
||||||
|
verbose_name="所属收藏夹",
|
||||||
)
|
)
|
||||||
client = models.ForeignKey(
|
client = models.ForeignKey(
|
||||||
"fonrey_client.Client", on_delete=models.CASCADE, related_name="folder_items"
|
"fonrey_client.Client",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="folder_items",
|
||||||
|
verbose_name="被收藏的客源",
|
||||||
|
)
|
||||||
|
added_at = models.DateTimeField(
|
||||||
|
auto_now_add=True,
|
||||||
|
verbose_name="加入收藏夹时间",
|
||||||
)
|
)
|
||||||
added_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = "client_folder_items"
|
db_table = "client_folder_items"
|
||||||
|
|||||||
@@ -10,29 +10,86 @@ class ClientFollowLog(models.Model):
|
|||||||
Managed via RunSQL; Django ORM treats parent as unmanaged.
|
Managed via RunSQL; Django ORM treats parent as unmanaged.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id = models.UUIDField(primary_key=True)
|
id = models.UUIDField(
|
||||||
created_at = models.DateTimeField()
|
primary_key=True,
|
||||||
|
verbose_name="主键",
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(
|
||||||
|
verbose_name="创建时间",
|
||||||
|
help_text="分区键",
|
||||||
|
)
|
||||||
client = models.ForeignKey(
|
client = models.ForeignKey(
|
||||||
"fonrey_client.Client",
|
"fonrey_client.Client",
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name="follow_logs",
|
related_name="follow_logs",
|
||||||
|
verbose_name="所属客源",
|
||||||
|
help_text="跟进日志随客源级联删除",
|
||||||
)
|
)
|
||||||
|
|
||||||
log_type = models.CharField(max_length=30, choices=ClientFollowLogType.choices)
|
log_type = models.CharField(
|
||||||
purpose = models.CharField(max_length=50, blank=True, default="")
|
max_length=30,
|
||||||
content = models.TextField(blank=True, default="")
|
choices=ClientFollowLogType.choices,
|
||||||
log_tag = models.CharField(max_length=50, blank=True, default="")
|
verbose_name="跟进类型",
|
||||||
change_detail = models.JSONField(null=True, blank=True)
|
help_text="written=写入跟进 / modified=修改跟进 / sensitive_view=敏感信息查看(不可删) / other=其他跟进 / system=系统日志",
|
||||||
|
)
|
||||||
|
purpose = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name="跟进目的",
|
||||||
|
help_text="lookup_items 维护,23项",
|
||||||
|
)
|
||||||
|
content = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name="跟进内容",
|
||||||
|
help_text="最少6字,最多500字",
|
||||||
|
)
|
||||||
|
log_tag = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name="跟进标签",
|
||||||
|
help_text="has_recording=有录音 / has_photo=有图片 / not_satisfied=对房源不满意 / still_considering=还在考虑 / ready_to_deposit=可交定金",
|
||||||
|
)
|
||||||
|
change_detail = models.JSONField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="字段变更明细",
|
||||||
|
help_text='修改跟进专用,格式:{"field": "grade", "old": "C", "new": "B", "label": "等级"}',
|
||||||
|
)
|
||||||
|
|
||||||
is_public = models.BooleanField(default=True)
|
is_public = models.BooleanField(
|
||||||
is_deletable = models.BooleanField(default=True)
|
default=True,
|
||||||
|
verbose_name="是否公开",
|
||||||
|
help_text="FALSE=仅本人及上级可见",
|
||||||
|
)
|
||||||
|
is_deletable = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
verbose_name="是否可删除",
|
||||||
|
help_text="敏感信息查看类型为 FALSE,不可删除",
|
||||||
|
)
|
||||||
|
|
||||||
operator = models.ForeignKey(
|
operator = models.ForeignKey(
|
||||||
"org.Staff", null=True, blank=True, on_delete=models.SET_NULL
|
"org.Staff",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
verbose_name="操作人",
|
||||||
|
)
|
||||||
|
operator_snapshot = models.JSONField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="操作人快照",
|
||||||
|
help_text="{name, store_group, role}(防止人员调动后显示异常)",
|
||||||
)
|
)
|
||||||
operator_snapshot = models.JSONField(null=True, blank=True)
|
|
||||||
|
|
||||||
deleted_at = models.DateTimeField(null=True, blank=True)
|
deleted_at = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="删除时间",
|
||||||
|
help_text="仅 is_deletable=TRUE 时可软删",
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = "client_follow_logs"
|
db_table = "client_follow_logs"
|
||||||
@@ -43,14 +100,43 @@ class ClientFollowLog(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class ClientFollowLogAttachment(UUIDPrimaryKeyModel):
|
class ClientFollowLogAttachment(UUIDPrimaryKeyModel):
|
||||||
follow_log_id = models.UUIDField() # cross-partitioned FK; not enforced via Django FK
|
follow_log_id = models.UUIDField(
|
||||||
file_key = models.TextField()
|
verbose_name="所属跟进日志ID",
|
||||||
file_name = models.CharField(max_length=255)
|
help_text="跨分区 FK;不通过 Django FK 强制约束",
|
||||||
file_size = models.IntegerField()
|
)
|
||||||
file_type = models.CharField(max_length=10, blank=True, default="")
|
file_key = models.TextField(
|
||||||
has_location = models.BooleanField(default=False)
|
verbose_name="文件存储路径",
|
||||||
sort_order = models.SmallIntegerField(default=0)
|
help_text="R2/S3 存储路径",
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
)
|
||||||
|
file_name = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
verbose_name="文件名",
|
||||||
|
help_text="原始文件名(用于展示和下载)",
|
||||||
|
)
|
||||||
|
file_size = models.IntegerField(
|
||||||
|
verbose_name="文件大小",
|
||||||
|
help_text="单位:bytes,最大 20MB",
|
||||||
|
)
|
||||||
|
file_type = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name="文件类型",
|
||||||
|
help_text="bmp / jpg / png / gif",
|
||||||
|
)
|
||||||
|
has_location = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name="是否含位置信息",
|
||||||
|
help_text="是否含 GPS 位置信息",
|
||||||
|
)
|
||||||
|
sort_order = models.SmallIntegerField(
|
||||||
|
default=0,
|
||||||
|
verbose_name="排序顺序",
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(
|
||||||
|
auto_now_add=True,
|
||||||
|
verbose_name="创建时间",
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = "client_follow_log_attachments"
|
db_table = "client_follow_log_attachments"
|
||||||
|
|||||||
@@ -14,15 +14,25 @@ from core.models.base import UUIDPrimaryKeyModel
|
|||||||
|
|
||||||
class ClientViewing(UUIDPrimaryKeyModel):
|
class ClientViewing(UUIDPrimaryKeyModel):
|
||||||
client = models.ForeignKey(
|
client = models.ForeignKey(
|
||||||
"fonrey_client.Client", on_delete=models.RESTRICT, related_name="viewings"
|
"fonrey_client.Client",
|
||||||
|
on_delete=models.RESTRICT,
|
||||||
|
related_name="viewings",
|
||||||
|
verbose_name="所属客源",
|
||||||
|
help_text="带看记录仅软删除,不随客源删除",
|
||||||
)
|
)
|
||||||
property = models.ForeignKey(
|
property = models.ForeignKey(
|
||||||
"fonrey_property.Property",
|
"fonrey_property.Property",
|
||||||
on_delete=models.RESTRICT,
|
on_delete=models.RESTRICT,
|
||||||
related_name="client_viewings",
|
related_name="client_viewings",
|
||||||
|
verbose_name="带看房源",
|
||||||
|
help_text="房源删除时保留带看记录",
|
||||||
)
|
)
|
||||||
viewing_type = models.CharField(
|
viewing_type = models.CharField(
|
||||||
max_length=20, choices=ClientViewingType.choices, default=ClientViewingType.VIEWING
|
max_length=20,
|
||||||
|
choices=ClientViewingType.choices,
|
||||||
|
default=ClientViewingType.VIEWING,
|
||||||
|
verbose_name="带看类型",
|
||||||
|
help_text="appointment=预约 / viewing=带看 / revisit=复看 / empty=空看",
|
||||||
)
|
)
|
||||||
|
|
||||||
agent = models.ForeignKey(
|
agent = models.ForeignKey(
|
||||||
@@ -31,28 +41,77 @@ class ClientViewing(UUIDPrimaryKeyModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name="led_viewings",
|
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人)",
|
||||||
)
|
)
|
||||||
companion_ids = ArrayField(models.UUIDField(), blank=True, default=list)
|
|
||||||
cooperator_ids = ArrayField(models.UUIDField(), blank=True, default=list)
|
|
||||||
|
|
||||||
scheduled_at = models.DateTimeField(null=True, blank=True)
|
scheduled_at = models.DateTimeField(
|
||||||
viewing_start_at = models.DateTimeField(null=True, blank=True)
|
null=True,
|
||||||
viewing_end_at = models.DateTimeField(null=True, blank=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="")
|
situation = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name="带看情况",
|
||||||
|
help_text="必填,≥6字",
|
||||||
|
)
|
||||||
client_intent = models.CharField(
|
client_intent = models.CharField(
|
||||||
max_length=20, choices=ClientViewingIntent.choices, blank=True, default=""
|
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=二看…,冗余字段,触发器维护",
|
||||||
)
|
)
|
||||||
viewing_progress = models.SmallIntegerField(null=True, blank=True)
|
|
||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(
|
||||||
deleted_at = models.DateTimeField(null=True, blank=True)
|
auto_now_add=True,
|
||||||
|
verbose_name="创建时间",
|
||||||
|
)
|
||||||
|
deleted_at = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="删除时间",
|
||||||
|
help_text="软删除时间戳;带看记录可软删除",
|
||||||
|
)
|
||||||
created_by = models.ForeignKey(
|
created_by = models.ForeignKey(
|
||||||
"org.Staff",
|
"org.Staff",
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name="created_client_viewings",
|
related_name="created_client_viewings",
|
||||||
|
verbose_name="创建人",
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -70,41 +129,79 @@ class ClientViewing(UUIDPrimaryKeyModel):
|
|||||||
|
|
||||||
class ClientPropertyMatch(UUIDPrimaryKeyModel):
|
class ClientPropertyMatch(UUIDPrimaryKeyModel):
|
||||||
client = models.ForeignKey(
|
client = models.ForeignKey(
|
||||||
"fonrey_client.Client", on_delete=models.CASCADE, related_name="property_matches"
|
"fonrey_client.Client",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="property_matches",
|
||||||
|
verbose_name="所属客源",
|
||||||
)
|
)
|
||||||
property = models.ForeignKey(
|
property = models.ForeignKey(
|
||||||
"fonrey_property.Property",
|
"fonrey_property.Property",
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name="client_matches",
|
related_name="client_matches",
|
||||||
|
verbose_name="匹配房源",
|
||||||
)
|
)
|
||||||
|
|
||||||
match_source = models.CharField(
|
match_source = models.CharField(
|
||||||
max_length=20,
|
max_length=20,
|
||||||
choices=ClientPropertyMatchSource.choices,
|
choices=ClientPropertyMatchSource.choices,
|
||||||
default=ClientPropertyMatchSource.RECORDED,
|
default=ClientPropertyMatchSource.RECORDED,
|
||||||
|
verbose_name="匹配来源",
|
||||||
|
help_text="recorded=录客配房(基于录入需求) / system=系统配房(算法推荐)",
|
||||||
)
|
)
|
||||||
match_group = models.CharField(
|
match_group = models.CharField(
|
||||||
max_length=30, choices=ClientPropertyMatchGroup.choices, blank=True, default=""
|
max_length=30,
|
||||||
|
choices=ClientPropertyMatchGroup.choices,
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name="匹配分组",
|
||||||
|
help_text="quality_layout=优质户型 / price_reduced=降价 / hot=热门 / newly_listed=新上",
|
||||||
)
|
)
|
||||||
match_score = models.DecimalField(
|
match_score = models.DecimalField(
|
||||||
max_digits=5, decimal_places=2, null=True, blank=True
|
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}, ...]',
|
||||||
)
|
)
|
||||||
match_reasons = models.JSONField(null=True, blank=True)
|
|
||||||
|
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
max_length=20,
|
max_length=20,
|
||||||
choices=ClientPropertyMatchStatus.choices,
|
choices=ClientPropertyMatchStatus.choices,
|
||||||
default=ClientPropertyMatchStatus.SUGGESTED,
|
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="配房计算时间",
|
||||||
)
|
)
|
||||||
shared_at = models.DateTimeField(null=True, blank=True)
|
|
||||||
feedback = models.CharField(max_length=50, blank=True, default="")
|
|
||||||
calculated_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
created_by = models.ForeignKey(
|
created_by = models.ForeignKey(
|
||||||
"org.Staff",
|
"org.Staff",
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name="created_matches",
|
related_name="created_matches",
|
||||||
|
verbose_name="创建人",
|
||||||
|
help_text="触发配房操作的员工(录客配房时记录,系统配房可为NULL)",
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -128,20 +225,51 @@ class ClientPropertyMatch(UUIDPrimaryKeyModel):
|
|||||||
class ClientStatusLog(models.Model):
|
class ClientStatusLog(models.Model):
|
||||||
"""Audit log; record-level immutable (no deleted_at)."""
|
"""Audit log; record-level immutable (no deleted_at)."""
|
||||||
|
|
||||||
id = models.UUIDField(primary_key=True)
|
id = models.UUIDField(
|
||||||
|
primary_key=True,
|
||||||
|
verbose_name="主键",
|
||||||
|
)
|
||||||
client = models.ForeignKey(
|
client = models.ForeignKey(
|
||||||
"fonrey_client.Client", on_delete=models.RESTRICT, related_name="status_logs"
|
"fonrey_client.Client",
|
||||||
|
on_delete=models.RESTRICT,
|
||||||
|
related_name="status_logs",
|
||||||
|
verbose_name="所属客源",
|
||||||
|
help_text="状态日志永久保留,RESTRICT 防止删除客源",
|
||||||
)
|
)
|
||||||
change_type = models.CharField(
|
change_type = models.CharField(
|
||||||
max_length=30, choices=ClientStatusLogChangeType.choices
|
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字",
|
||||||
)
|
)
|
||||||
old_value = models.JSONField(null=True, blank=True)
|
|
||||||
new_value = models.JSONField(null=True, blank=True)
|
|
||||||
reason = models.TextField(blank=True, default="")
|
|
||||||
operator = models.ForeignKey(
|
operator = models.ForeignKey(
|
||||||
"org.Staff", on_delete=models.RESTRICT, related_name="client_status_changes"
|
"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="操作时间",
|
||||||
)
|
)
|
||||||
operated_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = "client_status_logs"
|
db_table = "client_status_logs"
|
||||||
|
|||||||
Reference in New Issue
Block a user