Files
fonrey/apps/complex/models/complex.py
ishenwei c57462f6d1 feat(complex): add apps.complex with 10 models and full-text search
- 10 models: Complex, ComplexAlias, ComplexBusinessArea, ComplexSchool, ComplexMetroStation, Building, RoomUnit, ComplexPhoto, ComplexAttachment, ComplexPriceTrend
- RunSQL migration: pg_trgm extension, gin_trgm_ops indexes, tsvector triggers for complex search_vector (from name/alias/address)
- Optimistic locking via version field on Complex
- 4 lock flags (lock_building/room/info/standard_room) per spec
- Adds ComplexPropertyUsageType + ComplexBuildingStructure enums
- manage.py check passes

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-29 17:19:01 +08:00

458 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django.contrib.postgres.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"]