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).
This commit is contained in:
2026-04-30 09:15:43 +08:00
parent 79c3cf2924
commit 3638fc0302
4 changed files with 1328 additions and 243 deletions

View File

@@ -22,23 +22,39 @@ from core.models.base import SoftDeleteModel, TimeStampedModel, UUIDPrimaryKeyMo
class Property(SoftDeleteModel):
property_type = models.CharField(max_length=30, choices=PropertyType.choices)
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 字",
)
private_reason = models.TextField(blank=True, default="")
complex = models.ForeignKey(
"fonrey_complex.Complex",
on_delete=models.RESTRICT,
related_name="properties",
verbose_name="所属楼盘",
help_text="房源必须挂在楼盘下,禁止级联删除",
)
building = models.ForeignKey(
"fonrey_complex.Building",
@@ -46,75 +62,285 @@ class Property(SoftDeleteModel):
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="正整数",
)
block_no = models.CharField(max_length=30, blank=True, default="")
unit_no = models.CharField(max_length=30, blank=True, default="")
room_no = models.CharField(max_length=30, blank=True, default="")
floor = models.SmallIntegerField()
total_floors = models.SmallIntegerField()
bedroom_count = models.SmallIntegerField(default=0)
living_room_count = models.SmallIntegerField(default=0)
bathroom_count = models.SmallIntegerField(default=0)
kitchen_count = models.SmallIntegerField(default=0)
balcony_count = models.SmallIntegerField(default=0)
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)
inner_area = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True)
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)
sale_bottom_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
sale_record_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
rent_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
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
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
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可空老房源无记录影响营销发房",
)
has_elevator = models.BooleanField(null=True, blank=True)
built_year = models.SmallIntegerField(null=True, blank=True)
usage_type = models.CharField(max_length=30, blank=True, default="")
usage_subtype = models.CharField(max_length=30, blank=True, default="")
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)
shop_depth = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=True)
shop_height = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=True)
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
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
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
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)
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="")
ownership_years_detail = models.CharField(max_length=20, blank=True, default="")
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
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=未确认;影响交易税费计算",
)
is_only_house = models.BooleanField(null=True, blank=True)
payment_method = models.CharField(
max_length=15, blank=True, default="", choices=PropertyPaymentMethod.choices
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
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 字;如'置换'",
)
has_mortgage = models.BooleanField(null=True, blank=True)
has_loan = models.BooleanField(null=True, blank=True)
has_seal = models.BooleanField(null=True, blank=True)
has_restriction = models.BooleanField(null=True, blank=True)
original_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
sale_reason = models.TextField(blank=True, default="")
remarks = models.TextField(blank=True, default="")
remarks = models.TextField(
blank=True,
default="",
verbose_name="房源备注",
help_text="经纪人内部备注,最多 500 字,不对外展示",
)
first_recorder = models.ForeignKey(
"org.Staff",
@@ -122,6 +348,8 @@ class Property(SoftDeleteModel):
blank=True,
on_delete=models.SET_NULL,
related_name="first_recorded_properties",
verbose_name="首录方",
help_text="最初录入该房源的经纪人;人员离职后置 NULL",
)
number_holder = models.ForeignKey(
"org.Staff",
@@ -129,6 +357,8 @@ class Property(SoftDeleteModel):
blank=True,
on_delete=models.SET_NULL,
related_name="held_properties",
verbose_name="号码方",
help_text="持有业主联系号码的经纪人;变更需走审批流",
)
seller_agent = models.ForeignKey(
"org.Staff",
@@ -136,6 +366,8 @@ class Property(SoftDeleteModel):
blank=True,
on_delete=models.SET_NULL,
related_name="selling_properties",
verbose_name="出售方",
help_text="负责出售跟进的经纪人",
)
buyer_agent = models.ForeignKey(
"org.Staff",
@@ -143,14 +375,36 @@ class Property(SoftDeleteModel):
blank=True,
on_delete=models.SET_NULL,
related_name="buying_properties",
verbose_name="实买方",
help_text="促成成交的买方经纪人",
)
source = models.CharField(max_length=50, blank=True, default="")
source = models.CharField(
max_length=50,
blank=True,
default="",
verbose_name="房源来源渠道",
help_text="枚举值由 lookup_items 维护,如:门店拓客/转介绍/网络等",
)
completeness_score = models.SmallIntegerField(default=0)
completeness_score = models.SmallIntegerField(
default=0,
verbose_name="维护完成度评分",
help_text="0-100由 Celery 异步计算,非实时;前端列表页展示徽章",
)
listed_at = models.DateTimeField(null=True, blank=True)
last_followed_at = models.DateTimeField(null=True, blank=True)
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",
@@ -158,6 +412,7 @@ class Property(SoftDeleteModel):
blank=True,
on_delete=models.SET_NULL,
related_name="created_properties",
verbose_name="创建人",
)
updated_by = models.ForeignKey(
"org.Staff",
@@ -165,10 +420,20 @@ class Property(SoftDeleteModel):
blank=True,
on_delete=models.SET_NULL,
related_name="updated_properties",
verbose_name="最后修改人",
)
search_vector = SearchVectorField(null=True, blank=True)
version = models.IntegerField(default=1)
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"
@@ -206,37 +471,100 @@ class Property(SoftDeleteModel):
class PropertyContact(SoftDeleteModel):
property = models.ForeignKey(
Property, on_delete=models.CASCADE, related_name="contacts"
Property,
on_delete=models.CASCADE,
related_name="contacts",
verbose_name="所属房源",
help_text="房源删除时联级删除",
)
name = models.CharField(
max_length=50,
verbose_name="联系人姓名",
help_text="'张先生';业主或其代理人的真实姓名",
)
name = models.CharField(max_length=50)
gender = models.CharField(
max_length=10, choices=PropertyContactGender.choices, default=PropertyContactGender.MALE
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()
phone_hash = models.CharField(max_length=64)
phone2_enc = models.BinaryField(null=True, blank=True)
phone2_hash = models.CharField(max_length=64, blank=True, default="")
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="")
qq = models.CharField(max_length=20, blank=True, default="")
remarks = models.TextField(blank=True, default="")
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)
number_holder_approved_at = models.DateTimeField(null=True, blank=True)
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)
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",
@@ -244,6 +572,7 @@ class PropertyContact(SoftDeleteModel):
blank=True,
on_delete=models.SET_NULL,
related_name="updated_property_contacts",
verbose_name="最后修改人",
)
class Meta:
@@ -259,20 +588,62 @@ class PropertyContact(SoftDeleteModel):
class PropertyMarketing(UUIDPrimaryKeyModel):
property = models.OneToOneField(
Property, on_delete=models.CASCADE, related_name="marketing"
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 字;楼盘/小区周边配套描述",
)
marketing_title = models.CharField(max_length=30, blank=True, default="")
core_selling_points = models.TextField(blank=True, default="")
owner_attitude = models.TextField(blank=True, default="")
layout_description = models.TextField(blank=True, default="")
complex_description = models.TextField(blank=True, default="")
ai_generated_points = models.BooleanField(default=False)
ai_generated_attitude = models.BooleanField(default=False)
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)
updated_at = models.DateTimeField(auto_now=True, verbose_name="最后更新时间")
updated_by = 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="最后修改人",
)
class Meta:
@@ -283,22 +654,83 @@ class PropertyMarketing(UUIDPrimaryKeyModel):
class PropertyCertificate(UUIDPrimaryKeyModel):
property = models.OneToOneField(
Property, on_delete=models.CASCADE, related_name="certificate"
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 字",
)
owner_name = models.CharField(max_length=100, blank=True, default="")
owner_id_number = models.CharField(max_length=50, blank=True, default="")
owner_cert_type = models.CharField(max_length=20, blank=True, default="")
property_location = models.CharField(max_length=500, blank=True, default="")
cert_status = models.CharField(max_length=30, blank=True, default="")
cert_no = models.CharField(max_length=100, blank=True, default="")
first_registered_at = models.DateField(null=True, blank=True)
ownership_nature = models.CharField(max_length=30, blank=True, default="")
land_nature = models.CharField(max_length=30, blank=True, default="")
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)
updated_at = models.DateTimeField(auto_now=True, verbose_name="最后更新时间")
updated_by = 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="最后修改人",
)
class Meta:
@@ -309,21 +741,73 @@ class PropertyCertificate(UUIDPrimaryKeyModel):
class PropertyCompleteness(UUIDPrimaryKeyModel):
property = models.OneToOneField(
Property, on_delete=models.CASCADE, related_name="completeness"
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 冗余",
)
score_core_info = models.SmallIntegerField(default=0)
score_attachment = models.SmallIntegerField(default=0)
score_survey = models.SmallIntegerField(default=0)
score_vr = models.SmallIntegerField(default=0)
score_key = models.SmallIntegerField(default=0)
score_commission = models.SmallIntegerField(default=0)
score_verification = models.SmallIntegerField(default=0)
score_follow_up = models.SmallIntegerField(default=0)
score_viewing = models.SmallIntegerField(default=0)
score_other = models.SmallIntegerField(default=0)
total_score = models.SmallIntegerField(default=0)
calculated_at = models.DateTimeField(auto_now=True)
calculated_at = models.DateTimeField(
auto_now=True,
verbose_name="最近计算时间",
help_text="最近一次 Celery 任务异步计算完成时间",
)
class Meta:
db_table = "property_completeness"
@@ -333,16 +817,44 @@ class PropertyCompleteness(UUIDPrimaryKeyModel):
class PropertyProtection(UUIDPrimaryKeyModel):
property = models.OneToOneField(
Property, on_delete=models.CASCADE, related_name="protection"
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=长期保护",
)
is_protected = models.BooleanField(default=False)
reason = models.TextField(blank=True, default="")
start_at = models.DateTimeField(null=True, blank=True)
end_at = models.DateTimeField(null=True, blank=True)
set_by = 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="设置人",
help_text="人员离职后置 NULL",
)
created_at = models.DateTimeField(auto_now_add=True)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
class Meta:
db_table = "property_protections"

View File

@@ -15,32 +15,93 @@ class FollowLog(models.Model):
Managed via RunSQL; Django ORM treats parent as unmanaged.
"""
id = models.UUIDField(primary_key=True)
created_at = models.DateTimeField()
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"
"fonrey_property.Property",
on_delete=models.CASCADE,
related_name="follow_logs",
verbose_name="所属房源",
)
log_type = models.CharField(max_length=30, choices=PropertyFollowLogType.choices)
purpose = models.CharField(max_length=50, blank=True, default="")
content = models.TextField(blank=True, default="")
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
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)
log_tag = models.CharField(max_length=50, blank=True, default="")
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)
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
"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};防止人员离职后丢失显示信息",
)
operator_snapshot = models.JSONField(null=True, blank=True)
is_deletable = models.BooleanField(default=True)
deleted_at = models.DateTimeField(null=True, blank=True)
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"
@@ -51,15 +112,37 @@ class FollowLog(models.Model):
class FollowLogAttachment(UUIDPrimaryKeyModel):
follow_log_id = models.UUIDField() # cross-partitioned FK; not enforced via Django FK
file_key = models.TextField()
file_name = models.CharField(max_length=255)
file_size = models.IntegerField()
file_type = models.CharField(
max_length=10, blank=True, default="", choices=PropertyFollowAttachmentFileType.choices
follow_log_id = models.UUIDField(
verbose_name="所属跟进日志ID",
help_text="跨分区外键,未通过 Django FK 强约束;日志删除时联级删除",
)
sort_order = models.SmallIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
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/gifPRD 限定格式)",
)
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"
@@ -69,10 +152,21 @@ class FollowLogAttachment(UUIDPrimaryKeyModel):
class FollowLogRecording(UUIDPrimaryKeyModel):
follow_log_id = models.UUIDField()
file_key = models.TextField()
duration_seconds = models.IntegerField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
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"
@@ -83,9 +177,17 @@ class FollowLogRecording(UUIDPrimaryKeyModel):
class PropertyKey(UUIDPrimaryKeyModel):
property = models.ForeignKey(
"fonrey_property.Property", on_delete=models.CASCADE, related_name="keys"
"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=密码(如密码门锁)",
)
key_type = models.CharField(max_length=20, choices=PropertyKeyType.choices)
holder = models.ForeignKey(
"org.Staff",
@@ -93,29 +195,58 @@ class PropertyKey(UUIDPrimaryKeyModel):
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};防止人员离职后丢失显示信息",
)
holder_snapshot = models.JSONField(null=True, blank=True)
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)
other_agency_info = models.CharField(max_length=30, blank=True, default="")
remarks = models.TextField(blank=True, default="")
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)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
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:
@@ -127,11 +258,18 @@ class PropertyKey(UUIDPrimaryKeyModel):
class KeyAttachment(UUIDPrimaryKeyModel):
key = models.ForeignKey(
PropertyKey, on_delete=models.CASCADE, related_name="attachments"
PropertyKey,
on_delete=models.CASCADE,
related_name="attachments",
verbose_name="所属钥匙记录",
help_text="钥匙删除时联级删除",
)
file_key = models.TextField()
file_name = models.CharField(max_length=255)
created_at = models.DateTimeField(auto_now_add=True)
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"

View File

@@ -16,31 +16,101 @@ class ListingHistory(UUIDPrimaryKeyModel):
"fonrey_property.Property",
on_delete=models.RESTRICT,
related_name="listing_histories",
verbose_name="所属房源",
help_text="禁止级联删除,保留历史",
)
listing_type = models.CharField(
max_length=20,
choices=PropertyListingType.choices,
verbose_name="挂牌类型",
help_text="for_sale=出售挂牌/for_rent=出租挂牌",
)
listing_type = models.CharField(max_length=20, choices=PropertyListingType.choices)
status = models.CharField(
max_length=10,
choices=PropertyListingHistoryStatus.choices,
default=PropertyListingHistoryStatus.ACTIVE,
verbose_name="挂牌状态",
help_text="active=挂牌中/ended=已结束",
)
sale_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
rent_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
sale_unit_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
sale_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="元/月;出租挂牌时记录",
)
sale_unit_price = models.DecimalField(
max_digits=10,
decimal_places=2,
null=True,
blank=True,
verbose_name="本次挂牌售价单价",
help_text="元/m²由 sale_price ÷ area 计算后存储",
)
ownership_years = models.CharField(max_length=30, blank=True, default="")
is_only_house = models.BooleanField(null=True, blank=True)
tax_included = models.CharField(max_length=15, blank=True, default="")
sale_reason = models.TextField(blank=True, default="")
ownership_years = models.CharField(
max_length=30,
blank=True,
default="",
verbose_name="房本年限快照",
help_text='本次挂牌时的房本年限,如"满2年"',
)
is_only_house = models.BooleanField(
null=True,
blank=True,
verbose_name="唯一住房状态快照",
help_text="本次挂牌时的唯一住房状态",
)
tax_included = models.CharField(
max_length=15,
blank=True,
default="",
verbose_name="包税费方式快照",
help_text="each_party=各付/net=到手/inclusive=包税",
)
sale_reason = models.TextField(
blank=True,
default="",
verbose_name="售房原因快照",
help_text="本次挂牌时的售房原因",
)
seller_agent = 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="出售经纪人",
help_text="本次挂牌的出售经纪人;人员离职后置 NULL但 snapshot 保留",
)
seller_agent_snapshot = models.JSONField(
null=True,
blank=True,
verbose_name="出售经纪人快照",
help_text="{name, store_group, org_unit_name};防止人员变动后数据丢失",
)
seller_agent_snapshot = models.JSONField(null=True, blank=True)
started_at = models.DateTimeField(auto_now_add=False)
ended_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
started_at = models.DateTimeField(
auto_now_add=False,
verbose_name="本次挂牌开始时间",
)
ended_at = models.DateTimeField(
null=True,
blank=True,
verbose_name="本次挂牌结束时间",
help_text="NULL=当前仍在挂牌中",
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
class Meta:
db_table = "listing_histories"
@@ -54,20 +124,88 @@ class ListingHistory(UUIDPrimaryKeyModel):
class PriceChange(UUIDPrimaryKeyModel):
property = models.ForeignKey(
"fonrey_property.Property", on_delete=models.RESTRICT, related_name="price_changes"
"fonrey_property.Property",
on_delete=models.RESTRICT,
related_name="price_changes",
verbose_name="所属房源",
help_text="禁止级联删除,保留调价历史",
)
old_sale_price = models.DecimalField(
max_digits=12,
decimal_places=2,
null=True,
blank=True,
verbose_name="调价前挂牌售价",
help_text="万元NULL=首次定价",
)
new_sale_price = models.DecimalField(
max_digits=12,
decimal_places=2,
null=True,
blank=True,
verbose_name="调价后挂牌售价",
help_text="万元",
)
old_bottom_price = models.DecimalField(
max_digits=12,
decimal_places=2,
null=True,
blank=True,
verbose_name="调价前售底价",
help_text="万元NULL=未设置",
)
new_bottom_price = models.DecimalField(
max_digits=12,
decimal_places=2,
null=True,
blank=True,
verbose_name="调价后售底价",
help_text="万元NULL=本次不变更底价",
)
old_record_price = models.DecimalField(
max_digits=12,
decimal_places=2,
null=True,
blank=True,
verbose_name="调价前备案/核验价",
help_text="万元NULL=未设置",
)
new_record_price = models.DecimalField(
max_digits=12,
decimal_places=2,
null=True,
blank=True,
verbose_name="调价后备案/核验价",
help_text="万元NULL=本次不变更",
)
old_rent_price = models.DecimalField(
max_digits=10,
decimal_places=2,
null=True,
blank=True,
verbose_name="调价前挂牌租价",
help_text="元/月NULL=非出租类或未设置",
)
new_rent_price = models.DecimalField(
max_digits=10,
decimal_places=2,
null=True,
blank=True,
verbose_name="调价后挂牌租价",
help_text="元/月",
)
old_sale_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
new_sale_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
old_bottom_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
new_bottom_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
old_record_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
new_record_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
old_rent_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
new_rent_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
change_reason = models.TextField()
changed_at = models.DateTimeField(auto_now_add=True)
changed_by = models.ForeignKey("org.Staff", on_delete=models.RESTRICT)
change_reason = models.TextField(
verbose_name="调价原因",
help_text='必填,最多 200 字;如"业主主动降价"',
)
changed_at = models.DateTimeField(auto_now_add=True, verbose_name="调价操作时间")
changed_by = models.ForeignKey(
"org.Staff",
on_delete=models.RESTRICT,
verbose_name="操作人",
help_text="禁止置 NULL保留审计追溯",
)
class Meta:
db_table = "price_changes"
@@ -81,12 +219,28 @@ class PriceChange(UUIDPrimaryKeyModel):
class Commission(TimeStampedModel):
property = models.ForeignKey(
"fonrey_property.Property", on_delete=models.CASCADE, related_name="commissions"
"fonrey_property.Property",
on_delete=models.CASCADE,
related_name="commissions",
verbose_name="所属房源",
)
commission_type = models.CharField(
max_length=50,
verbose_name="委托类型",
help_text="独家委托/非独家委托;由 lookup_items 维护",
)
period_start = models.DateField(verbose_name="委托开始日期")
period_end = models.DateField(
null=True,
blank=True,
verbose_name="委托结束日期",
help_text="is_open_ended=true 时为 NULL",
)
is_open_ended = models.BooleanField(
default=False,
verbose_name="是否无固定结束日期",
help_text="true=长期委托/false=有截止日期",
)
commission_type = models.CharField(max_length=50)
period_start = models.DateField()
period_end = models.DateField(null=True, blank=True)
is_open_ended = models.BooleanField(default=False)
agent = models.ForeignKey(
"org.Staff",
@@ -94,15 +248,30 @@ class Commission(TimeStampedModel):
blank=True,
on_delete=models.SET_NULL,
related_name="commissions_as_agent",
verbose_name="委托经纪人",
help_text="人员离职后置 NULL",
)
agent_snapshot = models.JSONField(
null=True,
blank=True,
verbose_name="经纪人快照",
help_text="{name, store_group};防止人员变动后数据丢失",
)
agent_snapshot = models.JSONField(null=True, blank=True)
signing_method = models.CharField(max_length=50, blank=True, default="")
signing_method = models.CharField(
max_length=50,
blank=True,
default="",
verbose_name="签约方式",
help_text="选择后动态展示委托书模板",
)
owner_type = models.CharField(
max_length=20,
choices=PropertyCommissionOwnerType.choices,
default=PropertyCommissionOwnerType.OWNER,
verbose_name="委托人类型",
help_text="owner=产权人本人/authorized_third=被授权第三方",
)
property_owner_contact = models.ForeignKey(
"fonrey_property.PropertyContact",
@@ -110,18 +279,49 @@ class Commission(TimeStampedModel):
blank=True,
on_delete=models.SET_NULL,
related_name="commissions",
verbose_name="关联联系人",
help_text="若委托人已录入联系人则关联,否则填写下方姓名/证件",
)
owner_name = models.CharField(
max_length=50,
blank=True,
default="",
verbose_name="委托人姓名",
)
owner_id_type = models.CharField(
max_length=20,
blank=True,
default="",
verbose_name="委托人证件类型",
help_text="如:身份证/护照",
)
owner_id_number = models.CharField(
max_length=50,
blank=True,
default="",
verbose_name="委托人证件号明文",
help_text="仅供参考;加密版本见 owner_id_number_enc",
)
owner_id_number_enc = models.BinaryField(
null=True,
blank=True,
verbose_name="委托人证件号密文",
help_text="AES-256-GCM 加密",
)
owner_name = models.CharField(max_length=50, blank=True, default="")
owner_id_type = models.CharField(max_length=20, blank=True, default="")
owner_id_number = models.CharField(max_length=50, blank=True, default="")
owner_id_number_enc = models.BinaryField(null=True, blank=True)
remarks = models.TextField(blank=True, default="")
remarks = models.TextField(
blank=True,
default="",
verbose_name="备注",
help_text="最多 200 字",
)
status = models.CharField(
max_length=20,
choices=PropertyCommissionStatus.choices,
default=PropertyCommissionStatus.ACTIVE,
verbose_name="委托状态",
help_text="active=有效/expired=已过期/cancelled=已取消",
)
created_by = models.ForeignKey(
@@ -130,6 +330,7 @@ class Commission(TimeStampedModel):
blank=True,
on_delete=models.SET_NULL,
related_name="created_commissions",
verbose_name="创建人",
)
class Meta:
@@ -144,16 +345,35 @@ class Commission(TimeStampedModel):
class CommissionAttachment(UUIDPrimaryKeyModel):
commission = models.ForeignKey(
Commission, on_delete=models.CASCADE, related_name="attachments"
Commission,
on_delete=models.CASCADE,
related_name="attachments",
verbose_name="所属委托",
help_text="委托删除时联级删除",
)
category = models.CharField(
max_length=20, choices=PropertyCommissionAttachmentCategory.choices
max_length=20,
choices=PropertyCommissionAttachmentCategory.choices,
verbose_name="附件分类",
help_text="id_card=身份证/property_cert=产权证书/commission_letter=委托书/other=其他材料",
)
file_key = models.TextField()
file_name = models.CharField(max_length=255)
file_size = models.IntegerField(null=True, blank=True)
sort_order = models.SmallIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
file_key = models.TextField(
verbose_name="附件存储路径",
help_text="Cloudflare R2 对象路径",
)
file_name = models.CharField(max_length=255, verbose_name="原始文件名")
file_size = models.IntegerField(
null=True,
blank=True,
verbose_name="文件大小",
help_text="bytes",
)
sort_order = models.SmallIntegerField(
default=0,
verbose_name="排序权重",
help_text="数值越小越靠前",
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="上传时间")
class Meta:
db_table = "commission_attachments"
@@ -167,15 +387,22 @@ class NumberHolderApproval(UUIDPrimaryKeyModel):
"fonrey_property.Property",
on_delete=models.CASCADE,
related_name="number_holder_approvals",
verbose_name="所属房源",
)
contact = models.ForeignKey(
"fonrey_property.PropertyContact",
on_delete=models.CASCADE,
related_name="number_holder_approvals",
verbose_name="申请变更的联系方",
help_text="即号码方候选联系人",
)
applicant = models.ForeignKey(
"org.Staff", on_delete=models.RESTRICT, related_name="nh_applications"
"org.Staff",
on_delete=models.RESTRICT,
related_name="nh_applications",
verbose_name="申请人",
help_text="提交号码方变更申请的经纪人;禁止置 NULL 保留审计",
)
approver = models.ForeignKey(
"org.Staff",
@@ -183,16 +410,30 @@ class NumberHolderApproval(UUIDPrimaryKeyModel):
blank=True,
on_delete=models.SET_NULL,
related_name="nh_approvals",
verbose_name="审批人",
help_text="上级审批人;审批前为 NULL",
)
status = models.CharField(
max_length=20,
choices=PropertyNumberHolderApprovalStatus.choices,
default=PropertyNumberHolderApprovalStatus.PENDING,
verbose_name="审批状态",
help_text="pending=待审批/approved=已通过/rejected=已驳回",
)
remarks = models.TextField(
blank=True,
default="",
verbose_name="审批备注",
help_text="审批人填写的意见或驳回原因",
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="申请提交时间")
decided_at = models.DateTimeField(
null=True,
blank=True,
verbose_name="审批决定时间",
help_text="NULL=尚未审批",
)
remarks = models.TextField(blank=True, default="")
created_at = models.DateTimeField(auto_now_add=True)
decided_at = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = "number_holder_approvals"

View File

@@ -11,24 +11,65 @@ from core.models.base import UUIDPrimaryKeyModel
class FieldSurvey(UUIDPrimaryKeyModel):
property = models.ForeignKey(
"fonrey_property.Property", on_delete=models.CASCADE, related_name="field_surveys"
"fonrey_property.Property",
on_delete=models.CASCADE,
related_name="field_surveys",
verbose_name="所属房源",
)
status = models.CharField(
max_length=10,
choices=PropertyFieldSurveyStatus.choices,
default=PropertyFieldSurveyStatus.DRAFT,
verbose_name="实勘状态",
help_text="draft=草稿(未提交)/submitted=已提交(已完成)",
)
gps_latitude = models.DecimalField(max_digits=10, decimal_places=7, null=True, blank=True)
gps_longitude = models.DecimalField(max_digits=10, decimal_places=7, null=True, blank=True)
gps_accuracy = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=True)
gps_latitude = models.DecimalField(
max_digits=10,
decimal_places=7,
null=True,
blank=True,
verbose_name="GPS 纬度",
help_text="实勘打卡位置;精度 7 位小数",
)
gps_longitude = models.DecimalField(
max_digits=10,
decimal_places=7,
null=True,
blank=True,
verbose_name="GPS 经度",
help_text="实勘打卡位置;精度 7 位小数",
)
gps_accuracy = models.DecimalField(
max_digits=6,
decimal_places=2,
null=True,
blank=True,
verbose_name="GPS 精度",
help_text="米;标注定位误差",
)
description = models.TextField(blank=True, default="")
description = models.TextField(
blank=True,
default="",
verbose_name="实勘说明",
help_text="最多 200 字;经纪人现场情况描述",
)
submitted_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey("org.Staff", on_delete=models.RESTRICT)
submitted_at = models.DateTimeField(
null=True,
blank=True,
verbose_name="提交时间",
help_text="status 变为 submitted 时记录NULL=尚未提交",
)
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",
on_delete=models.RESTRICT,
verbose_name="实勘人",
help_text="禁止置 NULL 保留审计",
)
class Meta:
db_table = "field_surveys"
@@ -41,16 +82,58 @@ class FieldSurvey(UUIDPrimaryKeyModel):
class SurveyPhoto(UUIDPrimaryKeyModel):
survey = models.ForeignKey(FieldSurvey, on_delete=models.CASCADE, related_name="photos")
category = models.CharField(max_length=20, choices=PropertySurveyPhotoCategory.choices)
file_key = models.TextField()
thumbnail_key = models.TextField(blank=True, default="")
file_size = models.IntegerField(null=True, blank=True)
width = models.IntegerField(null=True, blank=True)
height = models.IntegerField(null=True, blank=True)
sort_order = models.SmallIntegerField(default=0)
is_vr_screenshot = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
survey = models.ForeignKey(
FieldSurvey,
on_delete=models.CASCADE,
related_name="photos",
verbose_name="所属实勘",
help_text="实勘删除时联级删除",
)
category = models.CharField(
max_length=20,
choices=PropertySurveyPhotoCategory.choices,
verbose_name="照片空间分类",
help_text="layout=户型图/living_room=客厅/dining_room=餐厅/bedroom=卧室/bathroom=卫生间/kitchen=厨房/entrance=门厅/balcony=阳台/study=书房/indoor_other=室内其他/outdoor=外景",
)
file_key = models.TextField(
verbose_name="原图存储路径",
help_text="Cloudflare R2 对象路径",
)
thumbnail_key = models.TextField(
blank=True,
default="",
verbose_name="缩略图路径",
help_text="Cloudflare Images 自动生成",
)
file_size = models.IntegerField(
null=True,
blank=True,
verbose_name="文件大小",
help_text="bytes",
)
width = models.IntegerField(
null=True,
blank=True,
verbose_name="图片宽度",
help_text="像素;上传时解析",
)
height = models.IntegerField(
null=True,
blank=True,
verbose_name="图片高度",
help_text="像素;上传时解析",
)
sort_order = models.SmallIntegerField(
default=0,
verbose_name="排序权重",
help_text="同一空间分类内,数值越小越靠前",
)
is_vr_screenshot = models.BooleanField(
default=False,
verbose_name="是否为VR截图",
help_text="true=全景/VR截图区别于普通实拍照片",
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="上传时间")
class Meta:
db_table = "survey_photos"
@@ -68,26 +151,77 @@ class PropertyPhoto(models.Model):
Managed via RunSQL; Django ORM treats parent as unmanaged.
"""
id = models.UUIDField(primary_key=True)
created_at = models.DateTimeField()
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="photos"
"fonrey_property.Property",
on_delete=models.CASCADE,
related_name="photos",
verbose_name="所属房源",
)
category = models.CharField(max_length=20, choices=PropertyPhotoCategory.choices)
file_key = models.TextField()
thumbnail_key = models.TextField(blank=True, default="")
file_name = models.CharField(max_length=255, blank=True, default="")
file_size = models.IntegerField(null=True, blank=True)
width = models.IntegerField(null=True, blank=True)
height = models.IntegerField(null=True, blank=True)
category = models.CharField(
max_length=20,
choices=PropertyPhotoCategory.choices,
verbose_name="照片分类",
help_text="cover=封面/entrance=门厅/living_room=客厅/dining_room=餐厅/bedroom=卧室/bathroom=卫生间/kitchen=厨房/balcony=阳台/study=书房/indoor_other=室内其他/outdoor=外景/panorama=全景",
)
file_key = models.TextField(
verbose_name="原图存储路径",
help_text="Cloudflare R2/S3 对象路径",
)
thumbnail_key = models.TextField(
blank=True,
default="",
verbose_name="缩略图路径",
help_text="Cloudflare Images 自动生成",
)
file_name = models.CharField(
max_length=255,
blank=True,
default="",
verbose_name="原始文件名",
)
file_size = models.IntegerField(
null=True,
blank=True,
verbose_name="文件大小",
help_text="bytes",
)
width = models.IntegerField(
null=True,
blank=True,
verbose_name="图片宽度",
help_text="像素;上传时解析",
)
height = models.IntegerField(
null=True,
blank=True,
verbose_name="图片高度",
help_text="像素;上传时解析",
)
is_cover = models.BooleanField(default=False)
sort_order = models.SmallIntegerField(default=0)
is_cover = models.BooleanField(
default=False,
verbose_name="是否为封面图",
help_text="true=封面;每套房源只能有一张封面(唯一约束保证)",
)
sort_order = models.SmallIntegerField(
default=0,
verbose_name="排序权重",
help_text="同一房源内,数值越小越靠前",
)
updated_at = models.DateTimeField(auto_now=True)
updated_at = models.DateTimeField(auto_now=True, verbose_name="最后更新时间")
created_by = 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="上传人",
)
class Meta:
@@ -100,21 +234,46 @@ class PropertyPhoto(models.Model):
class PropertyAttachment(UUIDPrimaryKeyModel):
property = models.ForeignKey(
"fonrey_property.Property", on_delete=models.CASCADE, related_name="attachments"
"fonrey_property.Property",
on_delete=models.CASCADE,
related_name="attachments",
verbose_name="所属房源",
)
category = models.CharField(
max_length=20,
choices=PropertyAttachmentCategory.choices,
default=PropertyAttachmentCategory.OTHER,
verbose_name="附件分类",
help_text="id_card=身份证/property_cert=产权证书/commission_letter=委托书/other=其他材料",
)
file_key = models.TextField()
file_name = models.CharField(max_length=255)
file_size = models.IntegerField()
file_type = models.CharField(max_length=50, blank=True, default="")
sort_order = models.SmallIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
file_key = models.TextField(
verbose_name="附件存储路径",
help_text="Cloudflare R2 对象路径",
)
file_name = models.CharField(max_length=255, verbose_name="原始文件名")
file_size = models.IntegerField(
verbose_name="文件大小",
help_text="bytes",
)
file_type = models.CharField(
max_length=50,
blank=True,
default="",
verbose_name="MIME 类型",
help_text="如 application/pdf、image/jpeg",
)
sort_order = models.SmallIntegerField(
default=0,
verbose_name="排序权重",
help_text="控制同一房源附件的显示顺序",
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="上传时间")
created_by = 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="上传人",
)
class Meta:
@@ -128,11 +287,33 @@ class PropertyAttachment(UUIDPrimaryKeyModel):
class PropertyTag(UUIDPrimaryKeyModel):
name = models.CharField(max_length=50)
color = models.CharField(max_length=7, blank=True, default="")
is_system = models.BooleanField(default=False)
sort_order = models.IntegerField(default=0)
is_active = models.BooleanField(default=True)
name = models.CharField(
max_length=50,
verbose_name="标签名称",
help_text="最多 50 字;如:学区/地铁口/满五唯一",
)
color = models.CharField(
max_length=7,
blank=True,
default="",
verbose_name="显示颜色",
help_text="HEX 色值,如 #FF5733前端标签徽章颜色",
)
is_system = models.BooleanField(
default=False,
verbose_name="是否系统预置",
help_text="true=系统内置标签不可删除false=运营自定义标签可删",
)
sort_order = models.IntegerField(
default=0,
verbose_name="排序权重",
help_text="数值越小越靠前",
)
is_active = models.BooleanField(
default=True,
verbose_name="是否启用",
help_text="false=已停用不再展示",
)
class Meta:
db_table = "property_tags"
@@ -142,10 +323,16 @@ class PropertyTag(UUIDPrimaryKeyModel):
class PropertyTagRelation(models.Model):
property = models.ForeignKey(
"fonrey_property.Property", on_delete=models.CASCADE, related_name="tag_relations"
"fonrey_property.Property",
on_delete=models.CASCADE,
related_name="tag_relations",
verbose_name="所属房源",
)
tag = models.ForeignKey(
PropertyTag, on_delete=models.CASCADE, related_name="property_relations"
PropertyTag,
on_delete=models.CASCADE,
related_name="property_relations",
verbose_name="所属标签",
)
class Meta:
@@ -163,12 +350,19 @@ class PropertyTagRelation(models.Model):
class PropertyFavorite(models.Model):
staff = models.ForeignKey(
"org.Staff", on_delete=models.CASCADE, related_name="favorite_properties"
"org.Staff",
on_delete=models.CASCADE,
related_name="favorite_properties",
verbose_name="收藏人",
help_text="员工注销时删除收藏记录",
)
property = models.ForeignKey(
"fonrey_property.Property", on_delete=models.CASCADE, related_name="favorited_by"
"fonrey_property.Property",
on_delete=models.CASCADE,
related_name="favorited_by",
verbose_name="收藏的房源",
)
created_at = models.DateTimeField(auto_now_add=True)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="收藏时间")
class Meta:
db_table = "property_favorites"