Files
fonrey/apps/property/models/core.py
ishenwei 3638fc0302 feat(property): add Chinese verbose_name and help_text to all property fields (Phase 4.1)
Sync DATA_MODEL_PROPERTY.md field-level Chinese annotations to Django
models across 23 property tables. Adds verbose_name= and help_text= to
every field in core.py, follow_keys.py, listings.py, media.py.

Pre-existing partitioned-table docstrings on FollowLog/PropertyPhoto
retained (signal Django ORM treats parent as unmanaged, RunSQL managed).
2026-04-30 09:15:43 +08:00

863 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django.contrib.postgres.indexes import GinIndex
from django.contrib.postgres.search import SearchVectorField
from django.db import models
from core.enums import (
PropertyAttribute,
PropertyContactGender,
PropertyContactIdentity,
PropertyDecoration,
PropertyGrade,
PropertyHouseStatus,
PropertyOrientation,
PropertyOwnershipNature,
PropertyPaymentMethod,
PropertyShopLocation,
PropertyStatus,
PropertyTaxIncluded,
PropertyType,
PropertyViewingTime,
)
from core.models.base import SoftDeleteModel, TimeStampedModel, UUIDPrimaryKeyModel
class Property(SoftDeleteModel):
property_type = models.CharField(
max_length=30,
choices=PropertyType.choices,
verbose_name="房源类型",
help_text="residential=住宅/villa=别墅/commercial_residential=商住/shop=商铺/office=写字楼/other=其他(详见 ENUMS",
)
status = models.CharField(
max_length=20,
choices=PropertyStatus.choices,
default=PropertyStatus.FOR_SALE,
verbose_name="交易状态",
help_text="for_sale=出售/for_rent=出租/for_sale_rent=租售/suspended=暂缓/sold_elsewhere=他售/rented_elsewhere=他租/sold=成交/unlisted=未挂牌(详见 ENUMS",
)
attribute = models.CharField(
max_length=10,
choices=PropertyAttribute.choices,
default=PropertyAttribute.PUBLIC,
verbose_name="流通属性",
help_text="public=公盘/private=私盘/special=特盘/sealed=封盘;控制可见范围",
)
private_reason = models.TextField(
blank=True,
default="",
verbose_name="私盘/封盘原因",
help_text="attribute 为 private/sealed 时必填,最多 200 字",
)
complex = models.ForeignKey(
"fonrey_complex.Complex",
on_delete=models.RESTRICT,
related_name="properties",
verbose_name="所属楼盘",
help_text="房源必须挂在楼盘下,禁止级联删除",
)
building = models.ForeignKey(
"fonrey_complex.Building",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="properties",
verbose_name="所属楼栋",
help_text="楼栋被删除时置 NULL",
)
block_no = models.CharField(
max_length=30,
blank=True,
default="",
verbose_name="栋/幢/弄号",
help_text="'3栋''A幢'",
)
unit_no = models.CharField(
max_length=30,
blank=True,
default="",
verbose_name="单元号",
help_text="'1单元''055'",
)
room_no = models.CharField(
max_length=30,
blank=True,
default="",
verbose_name="房号/门牌号",
help_text="'0301''1502'",
)
floor = models.SmallIntegerField(
verbose_name="所在楼层",
help_text="正整数;不超过 total_floorsCheckConstraint 校验)",
)
total_floors = models.SmallIntegerField(
verbose_name="楼栋总层数",
help_text="正整数",
)
bedroom_count = models.SmallIntegerField(default=0, verbose_name="卧室数(室)")
living_room_count = models.SmallIntegerField(default=0, verbose_name="客厅/餐厅数(厅)")
bathroom_count = models.SmallIntegerField(default=0, verbose_name="卫生间数(卫)")
kitchen_count = models.SmallIntegerField(default=0, verbose_name="厨房数(厨)")
balcony_count = models.SmallIntegerField(default=0, verbose_name="阳台数", help_text="0=无阳台")
area = models.DecimalField(
max_digits=8,
decimal_places=2,
verbose_name="建筑面积",
help_text="含公摊;录入必填",
)
inner_area = models.DecimalField(
max_digits=8,
decimal_places=2,
null=True,
blank=True,
verbose_name="套内面积",
help_text="不含公摊;选填,编辑页专属字段",
)
sale_price = models.DecimalField(
max_digits=12,
decimal_places=2,
null=True,
blank=True,
verbose_name="挂牌售价(万元)",
help_text="出售类房源必填,出租类可为 NULL",
)
sale_bottom_price = models.DecimalField(
max_digits=12,
decimal_places=2,
null=True,
blank=True,
verbose_name="售底价(万元)",
help_text="业主心理底价,仅内部可见,不对外展示",
)
sale_record_price = models.DecimalField(
max_digits=12,
decimal_places=2,
null=True,
blank=True,
verbose_name="备案/核验价(万元)",
help_text="填写后同步至营销库",
)
rent_price = models.DecimalField(
max_digits=10,
decimal_places=2,
null=True,
blank=True,
verbose_name="挂牌租价(元/月)",
help_text="出租类房源使用",
)
orientation = models.CharField(
max_length=15,
blank=True,
default="",
choices=PropertyOrientation.choices,
verbose_name="朝向",
help_text="east=东/south=南/west=西/north=北/southeast=东南/northeast=东北/east_west=东西/south_north=南北/northwest=西北/southwest=西南",
)
decoration = models.CharField(
max_length=10,
blank=True,
default="",
choices=PropertyDecoration.choices,
verbose_name="装修情况",
help_text="rough=毛坯/plain=清水/simple=简装/medium=中装/fine=精装/luxury=豪装",
)
has_elevator = models.BooleanField(
null=True,
blank=True,
verbose_name="是否有电梯",
help_text="true=有/false=无/NULL=未确认",
)
built_year = models.SmallIntegerField(
null=True,
blank=True,
verbose_name="建成年份",
help_text="如 2018可空老房源无记录影响营销发房",
)
usage_type = models.CharField(
max_length=30,
blank=True,
default="",
verbose_name="房屋用途大类",
help_text="如:住宅/商住/商业;对应更改用途浮窗第一级下拉",
)
usage_subtype = models.CharField(
max_length=30,
blank=True,
default="",
verbose_name="房屋用途细分小类",
help_text="如:普通住宅/花园洋房;对应更改用途浮窗第二级下拉",
)
shop_frontage = models.DecimalField(
max_digits=6,
decimal_places=2,
null=True,
blank=True,
verbose_name="开间(米)",
help_text="商铺专属,住宅类为 NULL",
)
shop_depth = models.DecimalField(
max_digits=6,
decimal_places=2,
null=True,
blank=True,
verbose_name="进深(米)",
help_text="商铺专属",
)
shop_height = models.DecimalField(
max_digits=6,
decimal_places=2,
null=True,
blank=True,
verbose_name="层高(米)",
help_text="商铺专属",
)
shop_location = models.CharField(
max_length=20,
blank=True,
default="",
choices=PropertyShopLocation.choices,
verbose_name="商铺位置类型",
help_text="street=沿街/mall=商场内/residential=住宅底商/ground_floor=楼栋底层/complex=综合体(商铺专属)",
)
house_status = models.CharField(
max_length=20,
blank=True,
default="",
choices=PropertyHouseStatus.choices,
verbose_name="房屋现状",
help_text="owner_occupied=业主自住/vacant=空置/tenant_occupied=租客租住/unknown=未知;影响带看安排",
)
viewing_time = models.CharField(
max_length=20,
blank=True,
default="",
choices=PropertyViewingTime.choices,
verbose_name="看房时间安排",
help_text="anytime=随时可看/by_appointment=提前预约/inconvenient=不方便看",
)
grade = models.CharField(
max_length=2,
blank=True,
default="",
choices=PropertyGrade.choices,
verbose_name="房源等级",
help_text="A=急迫/B=较强/C=一般/D=较弱(业主出售意向)",
)
ownership_years = models.CharField(
max_length=30,
blank=True,
default="",
verbose_name="房本年限",
help_text="不满2年/满2年/满5年 等(影响交易税费)",
)
ownership_years_detail = models.CharField(
max_length=20,
blank=True,
default="",
verbose_name="房本年限辅助说明",
help_text="满五/不满五(与 ownership_years 组合使用)",
)
ownership_nature = models.CharField(
max_length=20,
blank=True,
default="",
choices=PropertyOwnershipNature.choices,
verbose_name="产权性质",
help_text="commercial=商品房/reform_housing=房改房/collective=集资房/economic=经济活用房",
)
is_only_house = models.BooleanField(
null=True,
blank=True,
verbose_name="是否唯一住房",
help_text="true=唯一/false=非唯一/NULL=未确认;影响交易税费计算",
)
payment_method = models.CharField(
max_length=15,
blank=True,
default="",
choices=PropertyPaymentMethod.choices,
verbose_name="购房付款方式",
help_text="full=一次付清/mortgage=按揭付款/installment=分批次付款/advance=垫资解按",
)
tax_included = models.CharField(
max_length=15,
blank=True,
default="",
choices=PropertyTaxIncluded.choices,
verbose_name="包税费方式",
help_text="each_party=各付/net=到手/inclusive=包税",
)
has_mortgage = models.BooleanField(
null=True,
blank=True,
verbose_name="是否有抵押",
help_text="true=有/false=无/NULL=未确认",
)
has_loan = models.BooleanField(
null=True,
blank=True,
verbose_name="是否有贷款(未还清)",
help_text="true=有/false=无/NULL=未确认",
)
has_seal = models.BooleanField(
null=True,
blank=True,
verbose_name="是否被查封",
help_text="true=有/false=无/NULL=未确认",
)
has_restriction = models.BooleanField(
null=True,
blank=True,
verbose_name="是否有其他限制",
help_text="true=有/false=无/NULL=未确认",
)
original_price = models.DecimalField(
max_digits=12,
decimal_places=2,
null=True,
blank=True,
verbose_name="原购价(万元)",
help_text="业主当年购入价,用于计算增值",
)
sale_reason = models.TextField(
blank=True,
default="",
verbose_name="售房原因",
help_text="业主出售理由,最多 200 字;如'置换'",
)
remarks = models.TextField(
blank=True,
default="",
verbose_name="房源备注",
help_text="经纪人内部备注,最多 500 字,不对外展示",
)
first_recorder = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="first_recorded_properties",
verbose_name="首录方",
help_text="最初录入该房源的经纪人;人员离职后置 NULL",
)
number_holder = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="held_properties",
verbose_name="号码方",
help_text="持有业主联系号码的经纪人;变更需走审批流",
)
seller_agent = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="selling_properties",
verbose_name="出售方",
help_text="负责出售跟进的经纪人",
)
buyer_agent = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="buying_properties",
verbose_name="实买方",
help_text="促成成交的买方经纪人",
)
source = models.CharField(
max_length=50,
blank=True,
default="",
verbose_name="房源来源渠道",
help_text="枚举值由 lookup_items 维护,如:门店拓客/转介绍/网络等",
)
completeness_score = models.SmallIntegerField(
default=0,
verbose_name="维护完成度评分",
help_text="0-100由 Celery 异步计算,非实时;前端列表页展示徽章",
)
listed_at = models.DateTimeField(
null=True,
blank=True,
verbose_name="最近一次挂牌时间",
help_text="每次重新挂牌时更新",
)
last_followed_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_properties",
verbose_name="创建人",
)
updated_by = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="updated_properties",
verbose_name="最后修改人",
)
search_vector = SearchVectorField(
null=True,
blank=True,
verbose_name="全文检索向量",
help_text="由触发器自动维护,覆盖栋号/单元/房号/备注",
)
version = models.IntegerField(
default=1,
verbose_name="乐观锁版本号",
help_text="每次 UPDATE 必须 +1应用层检测 0 行受影响时抛 ConflictError",
)
class Meta:
db_table = "properties"
verbose_name = "房源"
verbose_name_plural = "房源"
constraints = [
models.CheckConstraint(
check=models.Q(floor__gt=0) & models.Q(floor__lte=models.F("total_floors")),
name="chk_property_floor",
),
]
indexes = [
GinIndex(fields=["search_vector"], name="idx_properties_search"),
models.Index(fields=["complex"], name="idx_properties_complex"),
models.Index(fields=["status"], name="idx_properties_status"),
models.Index(fields=["sale_price"], name="idx_properties_sale_price"),
models.Index(fields=["area"], name="idx_properties_area"),
models.Index(fields=["listed_at"], name="idx_properties_listed_at"),
models.Index(fields=["last_followed_at"], name="idx_properties_last_followed"),
models.Index(fields=["bedroom_count"], name="idx_properties_bedroom"),
models.Index(fields=["grade"], name="idx_properties_grade"),
models.Index(fields=["completeness_score"], name="idx_properties_completeness"),
models.Index(fields=["seller_agent"], name="idx_properties_seller_agent"),
models.Index(fields=["number_holder"], name="idx_properties_number_holder"),
models.Index(
fields=["status", "attribute", "complex", "sale_price"],
name="idx_properties_list_composite",
),
models.Index(
fields=["seller_agent", "status", "listed_at"],
name="idx_properties_my_properties",
),
]
class PropertyContact(SoftDeleteModel):
property = models.ForeignKey(
Property,
on_delete=models.CASCADE,
related_name="contacts",
verbose_name="所属房源",
help_text="房源删除时联级删除",
)
name = models.CharField(
max_length=50,
verbose_name="联系人姓名",
help_text="'张先生';业主或其代理人的真实姓名",
)
gender = models.CharField(
max_length=10,
choices=PropertyContactGender.choices,
default=PropertyContactGender.MALE,
verbose_name="性别",
help_text="male=先生/female=女士",
)
identity = models.CharField(
max_length=20,
choices=PropertyContactIdentity.choices,
default=PropertyContactIdentity.CONTACT,
verbose_name="联系人身份",
help_text="owner=业主/contact=联系人/subletter=二房东/tenant=租客/agent=代理人/corporate=企业法人",
)
phone_enc = models.BinaryField(
verbose_name="手机号1密文",
help_text="AES-256-GCM 加密,不可直接查询",
)
phone_hash = models.CharField(
max_length=64,
verbose_name="手机号1哈希",
help_text="SHA-256用于重复房源检测和精确查询",
)
phone2_enc = models.BinaryField(
null=True,
blank=True,
verbose_name="手机号2密文",
help_text="AES-256-GCM 加密;选填",
)
phone2_hash = models.CharField(
max_length=64,
blank=True,
default="",
verbose_name="手机号2哈希",
help_text="SHA-256phone2_enc 存在时必填",
)
wechat = models.CharField(
max_length=100,
blank=True,
default="",
verbose_name="微信号",
help_text="选填;无数据时前端展示'-'",
)
qq = models.CharField(
max_length=20,
blank=True,
default="",
verbose_name="QQ号",
help_text="选填;无数据时前端展示'-'",
)
remarks = models.TextField(
blank=True,
default="",
verbose_name="备注",
help_text="最多 200 字;补充说明联系人情况",
)
is_number_holder = models.BooleanField(
default=False,
verbose_name="是否为号码方",
help_text="true=是号码方(审批通过)/false=否;号码方变更须走审批流",
)
number_holder_approved_at = models.DateTimeField(
null=True,
blank=True,
verbose_name="号码方审批通过时间",
help_text="NULL=尚未成为号码方",
)
sort_order = models.IntegerField(
default=0,
verbose_name="排序权重",
help_text="数值越小越靠前;控制联系人在面板中的显示顺序",
)
created_by = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="created_property_contacts",
verbose_name="创建人",
)
updated_by = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="updated_property_contacts",
verbose_name="最后修改人",
)
class Meta:
db_table = "property_contacts"
verbose_name = "房源联系人"
verbose_name_plural = "房源联系人"
indexes = [
models.Index(fields=["property"], name="idx_pc_property"),
models.Index(fields=["phone_hash"], name="idx_pc_phone_hash"),
models.Index(fields=["phone2_hash"], name="idx_pc_phone2_hash"),
]
class PropertyMarketing(UUIDPrimaryKeyModel):
property = models.OneToOneField(
Property,
on_delete=models.CASCADE,
related_name="marketing",
verbose_name="所属房源",
help_text="1:1 关联 properties 表",
)
marketing_title = models.CharField(
max_length=30,
blank=True,
default="",
verbose_name="营销标题",
help_text="0-30 字;前端发房时展示给买家的吸睛标题",
)
core_selling_points = models.TextField(
blank=True,
default="",
verbose_name="核心卖点",
help_text="最多 200 字;展示给买家的重点卖点说明",
)
owner_attitude = models.TextField(
blank=True,
default="",
verbose_name="业主心态",
help_text="最多 200 字;仅内部可见,描述业主议价空间和心理状态",
)
layout_description = models.TextField(
blank=True,
default="",
verbose_name="户型介绍",
help_text="最多 200 字;房源户型特点描述,面向买家展示",
)
complex_description = models.TextField(
blank=True,
default="",
verbose_name="小区介绍",
help_text="最多 200 字;楼盘/小区周边配套描述",
)
ai_generated_points = models.BooleanField(
default=False,
verbose_name="核心卖点AI生成",
help_text="true=AI辅助生成经纪人确认后使用",
)
ai_generated_attitude = models.BooleanField(
default=False,
verbose_name="业主心态AI生成",
help_text="true=AI辅助生成",
)
updated_at = models.DateTimeField(auto_now=True, verbose_name="最后更新时间")
updated_by = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
verbose_name="最后修改人",
)
class Meta:
db_table = "property_marketing"
verbose_name = "房源营销信息"
verbose_name_plural = "房源营销信息"
class PropertyCertificate(UUIDPrimaryKeyModel):
property = models.OneToOneField(
Property,
on_delete=models.CASCADE,
related_name="certificate",
verbose_name="所属房源",
help_text="1:1 关联 properties 表",
)
owner_name = models.CharField(
max_length=100,
blank=True,
default="",
verbose_name="产权人姓名",
help_text="产权证书上登记的所有权人",
)
owner_id_number = models.CharField(
max_length=50,
blank=True,
default="",
verbose_name="产权人证件号码",
help_text="身份证号/统一社会信用代码等",
)
owner_cert_type = models.CharField(
max_length=20,
blank=True,
default="",
verbose_name="产权人证件类型",
help_text="如:身份证/护照/营业执照",
)
property_location = models.CharField(
max_length=500,
blank=True,
default="",
verbose_name="房屋坐落",
help_text="产权证书上的完整地址,最多 500 字",
)
cert_status = models.CharField(
max_length=30,
blank=True,
default="",
verbose_name="产证状态",
help_text="如:已过户/抵押中/查封/正常",
)
cert_no = models.CharField(
max_length=100,
blank=True,
default="",
verbose_name="产权证号",
help_text="不动产权证书编号",
)
first_registered_at = models.DateField(
null=True,
blank=True,
verbose_name="首次登记时间",
help_text="产权证上的初始登记日期",
)
ownership_nature = models.CharField(
max_length=30,
blank=True,
default="",
verbose_name="权属性质",
help_text="如:商品房/经济适用房/回迁房",
)
land_nature = models.CharField(
max_length=30,
blank=True,
default="",
verbose_name="土地性质",
help_text="如:国有/集体/划拨/出让",
)
updated_at = models.DateTimeField(auto_now=True, verbose_name="最后更新时间")
updated_by = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
verbose_name="最后修改人",
)
class Meta:
db_table = "property_certificates"
verbose_name = "房源产证"
verbose_name_plural = "房源产证"
class PropertyCompleteness(UUIDPrimaryKeyModel):
property = models.OneToOneField(
Property,
on_delete=models.CASCADE,
related_name="completeness",
verbose_name="所属房源",
help_text="1:1 关联 properties 表",
)
score_core_info = models.SmallIntegerField(
default=0,
verbose_name="重点信息得分",
help_text="满分 8包含房源核心字段完整度",
)
score_attachment = models.SmallIntegerField(
default=0,
verbose_name="附件得分",
help_text="满分 8身份证/产权证/委托书等材料上传情况",
)
score_survey = models.SmallIntegerField(
default=0,
verbose_name="实勘得分",
help_text="满分 16实勘照片和报告完整度",
)
score_vr = models.SmallIntegerField(
default=0,
verbose_name="VR得分",
help_text="满分 8VR/全景照片上传情况",
)
score_key = models.SmallIntegerField(
default=0,
verbose_name="钥匙得分",
help_text="满分 10钥匙托管情况",
)
score_commission = models.SmallIntegerField(
default=0,
verbose_name="委托得分",
help_text="满分 10独家/普通委托书情况",
)
score_verification = models.SmallIntegerField(
default=0,
verbose_name="验证得分",
help_text="满分 7房源信息核实情况",
)
score_follow_up = models.SmallIntegerField(
default=0,
verbose_name="跟进得分",
help_text="满分 8近期跟进记录情况",
)
score_viewing = models.SmallIntegerField(
default=0,
verbose_name="带看得分",
help_text="满分 8带看记录完整度",
)
score_other = models.SmallIntegerField(
default=0,
verbose_name="其他得分",
help_text="满分 7其他加分项",
)
total_score = models.SmallIntegerField(
default=0,
verbose_name="维护完成度总分",
help_text="0-100供列表排序用与 properties.completeness_score 冗余",
)
calculated_at = models.DateTimeField(
auto_now=True,
verbose_name="最近计算时间",
help_text="最近一次 Celery 任务异步计算完成时间",
)
class Meta:
db_table = "property_completeness"
verbose_name = "房源完整度"
verbose_name_plural = "房源完整度"
class PropertyProtection(UUIDPrimaryKeyModel):
property = models.OneToOneField(
Property,
on_delete=models.CASCADE,
related_name="protection",
verbose_name="所属房源",
help_text="1:1 关联 properties 表",
)
is_protected = models.BooleanField(
default=False,
verbose_name="是否处于保护状态",
help_text="true=受保护(防止被他人抢单/公盘化)/false=未保护",
)
reason = models.TextField(
blank=True,
default="",
verbose_name="保护原因",
help_text="说明为何启用保护",
)
start_at = models.DateTimeField(
null=True,
blank=True,
verbose_name="保护开始时间",
help_text="NULL=尚未生效",
)
end_at = models.DateTimeField(
null=True,
blank=True,
verbose_name="保护到期时间",
help_text="NULL=长期保护",
)
set_by = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
verbose_name="设置人",
help_text="人员离职后置 NULL",
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
class Meta:
db_table = "property_protections"
verbose_name = "房源保护期"
verbose_name_plural = "房源保护期"