from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.indexes import GinIndex from django.contrib.postgres.search import SearchVectorField from django.db import models from core.enums import ( ComplexBuildingStructure, ComplexBuildingType, ComplexElectricityType, ComplexPhotoCategory, ComplexPropertyUsageType, ComplexWaterType, SchoolZoneType, ) from core.models.base import SoftDeleteModel, TimeStampedModel, UUIDPrimaryKeyModel class Complex(SoftDeleteModel): name = models.CharField(max_length=200, help_text="标准楼盘名称,不可在编辑页直接修改") district = models.ForeignKey( "region.District", null=True, blank=True, on_delete=models.SET_NULL, related_name="complexes", ) address = models.CharField(max_length=500, blank=True, default="") address_summary = models.CharField(max_length=100, blank=True, default="") latitude = models.DecimalField(max_digits=10, decimal_places=7, null=True, blank=True) longitude = models.DecimalField(max_digits=10, decimal_places=7, null=True, blank=True) property_usage_types = ArrayField( models.CharField(max_length=30, choices=ComplexPropertyUsageType.choices), default=list, blank=True, ) building_structure = models.CharField( max_length=30, blank=True, default="", choices=ComplexBuildingStructure.choices, ) building_type = models.CharField( max_length=20, blank=True, default="", choices=ComplexBuildingType.choices, ) land_use_years = models.CharField(max_length=30, blank=True, default="") built_year = models.SmallIntegerField(null=True, blank=True) built_years = ArrayField( models.SmallIntegerField(), default=list, blank=True, ) ownership_category = ArrayField( models.CharField(max_length=30), default=list, blank=True, ) total_units = models.IntegerField(null=True, blank=True) total_households = models.IntegerField(null=True, blank=True) total_floor_area = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) plot_area = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) plot_ratio = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True) green_rate = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True) developer = models.CharField(max_length=200, blank=True, default="") property_company = models.CharField(max_length=200, blank=True, default="") property_fee = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True) property_phone = models.CharField(max_length=30, blank=True, default="") parking_total = models.IntegerField(null=True, blank=True) parking_underground = models.IntegerField(null=True, blank=True) parking_ratio = models.CharField(max_length=20, blank=True, default="") water_type = models.CharField( max_length=10, blank=True, default="", choices=ComplexWaterType.choices, ) electricity_type = models.CharField( max_length=10, blank=True, default="", choices=ComplexElectricityType.choices, ) has_central_heating = models.BooleanField(null=True, blank=True) has_gas = models.BooleanField(null=True, blank=True) remarks = models.TextField(blank=True, default="") lock_building = models.BooleanField(default=False) lock_room = models.BooleanField(default=False) lock_info = models.BooleanField(default=False) lock_standard_room = models.BooleanField(default=False) search_vector = SearchVectorField(null=True, blank=True) is_active = models.BooleanField(default=True) created_by = models.ForeignKey( "org.Staff", null=True, blank=True, on_delete=models.SET_NULL, related_name="created_complexes", ) updated_by = models.ForeignKey( "org.Staff", null=True, blank=True, on_delete=models.SET_NULL, related_name="updated_complexes", ) version = models.IntegerField(default=1, help_text="乐观锁版本号;UPDATE 时 +1") business_areas = models.ManyToManyField( "region.BusinessArea", through="fonrey_complex.ComplexBusinessArea", related_name="complexes", ) schools = models.ManyToManyField( "region.School", through="fonrey_complex.ComplexSchool", related_name="complexes", ) metro_stations = models.ManyToManyField( "region.MetroStation", through="fonrey_complex.ComplexMetroStation", related_name="complexes", ) class Meta: db_table = "complexes" indexes = [ models.Index( fields=["district"], name="idx_complexes_district", condition=models.Q(deleted_at__isnull=True), ), GinIndex(fields=["search_vector"], name="idx_complexes_search"), models.Index( fields=["latitude", "longitude"], name="idx_complexes_geo", condition=models.Q(deleted_at__isnull=True, latitude__isnull=False), ), models.Index( fields=["is_active"], name="idx_complexes_active", condition=models.Q(deleted_at__isnull=True), ), ] ordering = ["name"] def __str__(self) -> str: return self.name class ComplexAlias(UUIDPrimaryKeyModel): complex = models.ForeignKey( "fonrey_complex.Complex", on_delete=models.CASCADE, related_name="aliases", ) alias = models.CharField(max_length=200) is_system = models.BooleanField(default=False, help_text="TRUE=系统/标准别名(只读)") created_at = models.DateTimeField(auto_now_add=True) created_by = models.ForeignKey( "org.Staff", null=True, blank=True, on_delete=models.SET_NULL, related_name="created_complex_aliases", ) class Meta: db_table = "complex_aliases" indexes = [ models.Index(fields=["complex"], name="idx_complex_aliases_complex"), ] ordering = ["complex_id", "alias"] def __str__(self) -> str: return self.alias class ComplexBusinessArea(models.Model): complex = models.ForeignKey( "fonrey_complex.Complex", on_delete=models.CASCADE, related_name="complex_business_areas", ) business_area = models.ForeignKey( "region.BusinessArea", on_delete=models.CASCADE, related_name="complex_links", ) is_primary = models.BooleanField(default=False) class Meta: db_table = "complex_business_areas" constraints = [ models.UniqueConstraint( fields=["complex", "business_area"], name="pk_complex_business_areas", ), models.UniqueConstraint( fields=["complex"], condition=models.Q(is_primary=True), name="uq_complex_biz_area_primary", ), ] class ComplexSchool(models.Model): complex = models.ForeignKey( "fonrey_complex.Complex", on_delete=models.CASCADE, related_name="complex_schools", ) school = models.ForeignKey( "region.School", on_delete=models.CASCADE, related_name="complex_links", ) zone_type = models.CharField( max_length=30, blank=True, default="", choices=SchoolZoneType.choices, ) class Meta: db_table = "complex_schools" constraints = [ models.UniqueConstraint( fields=["complex", "school"], name="pk_complex_schools", ), ] indexes = [ models.Index(fields=["school"], name="idx_complex_schools_school"), ] class ComplexMetroStation(models.Model): complex = models.ForeignKey( "fonrey_complex.Complex", on_delete=models.CASCADE, related_name="complex_metro_stations", ) station = models.ForeignKey( "region.MetroStation", on_delete=models.CASCADE, related_name="complex_links", ) distance_meters = models.IntegerField(null=True, blank=True) class Meta: db_table = "complex_metro_stations" constraints = [ models.UniqueConstraint( fields=["complex", "station"], name="pk_complex_metro_stations", ), ] indexes = [ models.Index(fields=["complex"], name="idx_complex_metro_complex"), models.Index(fields=["station"], name="idx_complex_metro_station"), ] class Building(TimeStampedModel): complex = models.ForeignKey( "fonrey_complex.Complex", on_delete=models.CASCADE, related_name="buildings", ) name = models.CharField(max_length=50, help_text="楼栋名,如「1号楼」「A栋2单元」") is_standard = models.BooleanField(default=False, help_text="TRUE=标准结构(经运营核准)") property_usage_type = models.CharField( max_length=30, blank=True, default="", choices=ComplexPropertyUsageType.choices, ) built_year = models.SmallIntegerField(null=True, blank=True) total_floors = models.SmallIntegerField(null=True, blank=True) land_use_years = models.CharField(max_length=30, blank=True, default="") has_elevator = models.BooleanField(null=True, blank=True) school = models.ForeignKey( "region.School", null=True, blank=True, on_delete=models.SET_NULL, related_name="buildings", ) is_active = models.BooleanField(default=True) created_by = models.ForeignKey( "org.Staff", null=True, blank=True, on_delete=models.SET_NULL, related_name="created_buildings", ) class Meta: db_table = "buildings" indexes = [ models.Index( fields=["complex"], name="idx_buildings_complex", condition=models.Q(is_active=True), ), ] constraints = [ models.UniqueConstraint( fields=["complex", "name"], condition=models.Q(is_active=True), name="uq_buildings_complex_name", ), ] ordering = ["complex_id", "name"] def __str__(self) -> str: return self.name class RoomUnit(TimeStampedModel): building = models.ForeignKey( "fonrey_complex.Building", on_delete=models.CASCADE, related_name="room_units", ) floor = models.SmallIntegerField(help_text="楼层(实际层数,地下为负数)") floor_name = models.CharField(max_length=20, blank=True, default="") room_no = models.CharField(max_length=30) display_no = models.CharField(max_length=50, blank=True, default="") is_standard = models.BooleanField(default=False) is_active = models.BooleanField(default=True) class Meta: db_table = "room_units" indexes = [ models.Index( fields=["building"], name="idx_room_units_building", condition=models.Q(is_active=True), ), ] constraints = [ models.UniqueConstraint( fields=["building", "floor", "room_no"], condition=models.Q(is_active=True), name="uq_room_units_unique", ), ] ordering = ["building_id", "-floor", "room_no"] def __str__(self) -> str: return self.display_no or f"{self.floor}/{self.room_no}" class ComplexPhoto(UUIDPrimaryKeyModel): complex = models.ForeignKey( "fonrey_complex.Complex", on_delete=models.CASCADE, related_name="photos", ) category = models.CharField(max_length=20, choices=ComplexPhotoCategory.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, help_text="bytes") width = models.IntegerField(null=True, blank=True) height = models.IntegerField(null=True, blank=True) is_cover = models.BooleanField(default=False) sort_order = models.SmallIntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True) created_by = models.ForeignKey( "org.Staff", null=True, blank=True, on_delete=models.SET_NULL, related_name="created_complex_photos", ) class Meta: db_table = "complex_photos" indexes = [ models.Index(fields=["complex"], name="idx_complex_photos_complex"), models.Index(fields=["complex", "category"], name="idx_complex_photos_category"), ] constraints = [ models.UniqueConstraint( fields=["complex"], condition=models.Q(is_cover=True), name="uq_complex_photos_cover", ), ] ordering = ["complex_id", "sort_order"] class ComplexAttachment(UUIDPrimaryKeyModel): complex = models.ForeignKey( "fonrey_complex.Complex", on_delete=models.CASCADE, related_name="attachments", ) file_key = models.TextField() file_name = models.CharField(max_length=255) file_size = models.IntegerField(null=True, blank=True) file_type = models.CharField(max_length=50, blank=True, default="", help_text="MIME type") sort_order = models.SmallIntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True) created_by = models.ForeignKey( "org.Staff", null=True, blank=True, on_delete=models.SET_NULL, related_name="created_complex_attachments", ) class Meta: db_table = "complex_attachments" ordering = ["complex_id", "sort_order"] class ComplexPriceTrend(UUIDPrimaryKeyModel): complex = models.ForeignKey( "fonrey_complex.Complex", on_delete=models.CASCADE, related_name="price_trends", ) record_month = models.DateField(help_text="月份(统一存为该月1日)") avg_sale_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) avg_unit_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) transaction_count = models.IntegerField(null=True, blank=True) listing_count = models.IntegerField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "complex_price_trends" constraints = [ models.UniqueConstraint( fields=["complex", "record_month"], name="uq_complex_price_trend_month", ), ] indexes = [ models.Index( fields=["complex", "-record_month"], name="idx_cpx_price_trend_complex", ), ] ordering = ["complex_id", "-record_month"]