From 3638fc030203d2de6e0c150021477c5fd793386d Mon Sep 17 00:00:00 2001 From: ishenwei Date: Thu, 30 Apr 2026 09:15:43 +0800 Subject: [PATCH] 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). --- apps/property/models/core.py | 722 ++++++++++++++++++++++++---- apps/property/models/follow_keys.py | 216 +++++++-- apps/property/models/listings.py | 337 +++++++++++-- apps/property/models/media.py | 296 ++++++++++-- 4 files changed, 1328 insertions(+), 243 deletions(-) diff --git a/apps/property/models/core.py b/apps/property/models/core.py index 8ebdaf6..dc7df4b 100644 --- a/apps/property/models/core.py +++ b/apps/property/models/core.py @@ -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_floors(CheckConstraint 校验)", + ) + 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="建筑面积(m²)", + help_text="含公摊;录入必填", + ) + inner_area = models.DecimalField( + max_digits=8, + decimal_places=2, + null=True, + blank=True, + verbose_name="套内面积(m²)", + 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-256;phone2_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="满分 8;VR/全景照片上传情况", + ) + 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" diff --git a/apps/property/models/follow_keys.py b/apps/property/models/follow_keys.py index 541c61b..7231bc2 100644 --- a/apps/property/models/follow_keys.py +++ b/apps/property/models/follow_keys.py @@ -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/gif(PRD 限定格式)", + ) + 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" diff --git a/apps/property/models/listings.py b/apps/property/models/listings.py index d59593e..68f3590 100644 --- a/apps/property/models/listings.py +++ b/apps/property/models/listings.py @@ -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" diff --git a/apps/property/models/media.py b/apps/property/models/media.py index 754aa5b..8504167 100644 --- a/apps/property/models/media.py +++ b/apps/property/models/media.py @@ -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"