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