Files
fonrey/apps/complex/models/complex.py
ishenwei a3800bf09d feat(complex): add Chinese verbose_name and help_text to all complex fields (Phase 4.1 part 3/9)
Sync DATA_MODEL_COMPLEX.md field-level Chinese annotations to Django
models across 10 complex tables (Complex, ComplexAlias,
ComplexBusinessArea, ComplexSchool, ComplexMetroStation, Building,
RoomUnit, ComplexPhoto, ComplexAttachment, ComplexPriceTrend).
2026-04-30 09:22:08 +08:00

842 lines
24 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,
verbose_name="楼盘名称",
help_text="标准楼盘名称,不可在编辑页直接修改(需走合并/申请流程)",
)
district = models.ForeignKey(
"region.District",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="complexes",
verbose_name="所属城区",
)
address = models.CharField(
max_length=500,
blank=True,
default="",
verbose_name="详细地址",
help_text="不可在编辑页修改,需走纠错流程",
)
address_summary = models.CharField(
max_length=100,
blank=True,
default="",
verbose_name="概要地址",
help_text='如「海波路1000弄」可编辑',
)
latitude = models.DecimalField(
max_digits=10,
decimal_places=7,
null=True,
blank=True,
verbose_name="纬度",
help_text="WGS84完整度目标 ≥ 90%",
)
longitude = models.DecimalField(
max_digits=10,
decimal_places=7,
null=True,
blank=True,
verbose_name="经度",
help_text="WGS84",
)
property_usage_types = ArrayField(
models.CharField(max_length=30, choices=ComplexPropertyUsageType.choices),
default=list,
blank=True,
verbose_name="物业类型",
help_text="多选residential / villa / commercial_residential / commercial / office / other",
)
building_structure = models.CharField(
max_length=30,
blank=True,
default="",
choices=ComplexBuildingStructure.choices,
verbose_name="楼栋结构",
help_text="unit_room=单元-房号 / other=其他",
)
building_type = models.CharField(
max_length=20,
blank=True,
default="",
choices=ComplexBuildingType.choices,
verbose_name="建筑类型",
help_text="slab=板楼 / tower=塔楼 / slab_tower=板塔结合",
)
land_use_years = models.CharField(
max_length=30,
blank=True,
default="",
verbose_name="土地使用年限",
help_text='如「70年」',
)
built_year = models.SmallIntegerField(
null=True,
blank=True,
verbose_name="竣工年份",
help_text="可多选时存最早竣工年",
)
built_years = ArrayField(
models.SmallIntegerField(),
default=list,
blank=True,
verbose_name="竣工年份多值",
help_text="楼盘分期竣工",
)
ownership_category = ArrayField(
models.CharField(max_length=30),
default=list,
blank=True,
verbose_name="权属类别",
help_text="多选(运营维护枚举)",
)
total_units = models.IntegerField(
null=True,
blank=True,
verbose_name="单元总数",
)
total_households = models.IntegerField(
null=True,
blank=True,
verbose_name="总户数",
)
total_floor_area = models.DecimalField(
max_digits=12,
decimal_places=2,
null=True,
blank=True,
verbose_name="小区总建筑面积",
help_text="单位",
)
plot_area = models.DecimalField(
max_digits=12,
decimal_places=2,
null=True,
blank=True,
verbose_name="小区占地面积",
help_text="单位",
)
plot_ratio = models.DecimalField(
max_digits=5,
decimal_places=2,
null=True,
blank=True,
verbose_name="容积率",
)
green_rate = models.DecimalField(
max_digits=5,
decimal_places=2,
null=True,
blank=True,
verbose_name="绿化率",
help_text="单位:%",
)
developer = models.CharField(
max_length=200,
blank=True,
default="",
verbose_name="开发商",
)
property_company = models.CharField(
max_length=200,
blank=True,
default="",
verbose_name="物业公司",
)
property_fee = models.DecimalField(
max_digits=8,
decimal_places=2,
null=True,
blank=True,
verbose_name="物业费",
help_text="单位:元/m²/月",
)
property_phone = models.CharField(
max_length=30,
blank=True,
default="",
verbose_name="物业电话",
)
parking_total = models.IntegerField(
null=True,
blank=True,
verbose_name="车位总数",
)
parking_underground = models.IntegerField(
null=True,
blank=True,
verbose_name="地下车位数",
)
parking_ratio = models.CharField(
max_length=20,
blank=True,
default="",
verbose_name="车位配比",
help_text='如「100:63」',
)
water_type = models.CharField(
max_length=10,
blank=True,
default="",
choices=ComplexWaterType.choices,
verbose_name="水费类型",
help_text="civil=民水 / commercial=商水",
)
electricity_type = models.CharField(
max_length=10,
blank=True,
default="",
choices=ComplexElectricityType.choices,
verbose_name="电费类型",
help_text="civil=民电 / commercial=商电",
)
has_central_heating = models.BooleanField(
null=True,
blank=True,
verbose_name="是否统一供暖",
)
has_gas = models.BooleanField(
null=True,
blank=True,
verbose_name="是否有燃气",
)
remarks = models.TextField(
blank=True,
default="",
verbose_name="备注",
)
lock_building = models.BooleanField(
default=False,
verbose_name="楼栋锁",
help_text="锁定后不可增删楼栋",
)
lock_room = models.BooleanField(
default=False,
verbose_name="房号锁",
)
lock_info = models.BooleanField(
default=False,
verbose_name="信息锁",
help_text="锁定后基本信息只读",
)
lock_standard_room = models.BooleanField(
default=False,
verbose_name="标准房号锁",
)
search_vector = SearchVectorField(
null=True,
blank=True,
verbose_name="全文检索向量",
help_text="由触发器自动维护name + alias + address",
)
is_active = models.BooleanField(
default=True,
verbose_name="是否启用",
help_text="FALSE=已停用楼盘",
)
created_by = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="created_complexes",
verbose_name="创建人",
)
updated_by = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="updated_complexes",
verbose_name="最后更新人",
)
version = models.IntegerField(
default=1,
verbose_name="版本号",
help_text="乐观锁UPDATE 时 +1应用层检测 0 行受影响时抛 ConflictError",
)
business_areas = models.ManyToManyField(
"region.BusinessArea",
through="fonrey_complex.ComplexBusinessArea",
related_name="complexes",
verbose_name="关联商圈",
)
schools = models.ManyToManyField(
"region.School",
through="fonrey_complex.ComplexSchool",
related_name="complexes",
verbose_name="对口学校",
)
metro_stations = models.ManyToManyField(
"region.MetroStation",
through="fonrey_complex.ComplexMetroStation",
related_name="complexes",
verbose_name="周边地铁站",
)
class Meta:
db_table = "complexes"
verbose_name = "楼盘"
verbose_name_plural = "楼盘"
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",
verbose_name="所属楼盘",
help_text="别名随楼盘级联删除",
)
alias = models.CharField(
max_length=200,
verbose_name="别名",
help_text="最多20字/条,多别名多行存储",
)
is_system = models.BooleanField(
default=False,
verbose_name="是否系统别名",
help_text="TRUE=系统/标准别名只读FALSE=用户自定义",
)
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,
related_name="created_complex_aliases",
verbose_name="创建人",
)
class Meta:
db_table = "complex_aliases"
verbose_name = "楼盘别名"
verbose_name_plural = "楼盘别名"
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",
verbose_name="所属楼盘",
)
business_area = models.ForeignKey(
"region.BusinessArea",
on_delete=models.CASCADE,
related_name="complex_links",
verbose_name="关联商圈",
)
is_primary = models.BooleanField(
default=False,
verbose_name="是否主商圈",
help_text="主商圈唯一,用于列表显示",
)
class Meta:
db_table = "complex_business_areas"
verbose_name = "楼盘商圈关联"
verbose_name_plural = "楼盘商圈关联"
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",
verbose_name="所属楼盘",
)
school = models.ForeignKey(
"region.School",
on_delete=models.CASCADE,
related_name="complex_links",
verbose_name="对口学校",
)
zone_type = models.CharField(
max_length=30,
blank=True,
default="",
choices=SchoolZoneType.choices,
verbose_name="学区类型",
help_text="guaranteed=对口(直升) / reference=参考(可能入读) / lottery=摇号",
)
class Meta:
db_table = "complex_schools"
verbose_name = "楼盘学校关联"
verbose_name_plural = "楼盘学校关联"
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",
verbose_name="所属楼盘",
)
station = models.ForeignKey(
"region.MetroStation",
on_delete=models.CASCADE,
related_name="complex_links",
verbose_name="关联地铁站",
)
distance_meters = models.IntegerField(
null=True,
blank=True,
verbose_name="步行距离",
help_text="单位:米",
)
class Meta:
db_table = "complex_metro_stations"
verbose_name = "楼盘地铁站关联"
verbose_name_plural = "楼盘地铁站关联"
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",
verbose_name="所属楼盘",
)
name = models.CharField(
max_length=50,
verbose_name="楼栋名称",
help_text='如「1号楼」「A栋2单元」',
)
is_standard = models.BooleanField(
default=False,
verbose_name="是否标准结构",
help_text="TRUE=已经运营核准",
)
property_usage_type = models.CharField(
max_length=30,
blank=True,
default="",
choices=ComplexPropertyUsageType.choices,
verbose_name="物业类型",
help_text="可与楼盘不同,如商住楼盘内有纯商铺楼栋",
)
built_year = models.SmallIntegerField(
null=True,
blank=True,
verbose_name="竣工年份",
)
total_floors = models.SmallIntegerField(
null=True,
blank=True,
verbose_name="总层数",
)
land_use_years = models.CharField(
max_length=30,
blank=True,
default="",
verbose_name="土地使用年限",
)
has_elevator = models.BooleanField(
null=True,
blank=True,
verbose_name="是否有电梯",
)
school = models.ForeignKey(
"region.School",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="buildings",
verbose_name="对口学校",
help_text="楼栋级别的学区差异",
)
is_active = models.BooleanField(
default=True,
verbose_name="是否启用",
help_text="FALSE=已停用(楼栋被删除或合并)",
)
created_by = models.ForeignKey(
"org.Staff",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="created_buildings",
verbose_name="创建人",
)
class Meta:
db_table = "buildings"
verbose_name = "楼栋"
verbose_name_plural = "楼栋"
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",
verbose_name="所属楼栋",
)
floor = models.SmallIntegerField(
verbose_name="楼层",
help_text="实际层数,地下为负数",
)
floor_name = models.CharField(
max_length=20,
blank=True,
default="",
verbose_name="楼层名称",
help_text='如「1层」「B1层」',
)
room_no = models.CharField(
max_length=30,
verbose_name="房号",
help_text='如「01」「101」',
)
display_no = models.CharField(
max_length=50,
blank=True,
default="",
verbose_name="展示房号",
help_text='展示用完整房号如「3-1-101」',
)
is_standard = models.BooleanField(
default=False,
verbose_name="是否标准化",
help_text="TRUE=已归一化为标准结构",
)
is_active = models.BooleanField(
default=True,
verbose_name="是否启用",
help_text="FALSE=已拆除/不存在",
)
class Meta:
db_table = "room_units"
verbose_name = "房号单元"
verbose_name_plural = "房号单元"
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",
verbose_name="所属楼盘",
)
category = models.CharField(
max_length=20,
choices=ComplexPhotoCategory.choices,
verbose_name="照片类别",
help_text="complex=楼盘图 / layout=户型图 / vr=VR全景 / other=其他",
)
file_key = models.TextField(
verbose_name="文件存储路径",
help_text="R2/S3 路径",
)
thumbnail_key = models.TextField(
blank=True,
default="",
verbose_name="缩略图路径",
)
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="单位px",
)
height = models.IntegerField(
null=True,
blank=True,
verbose_name="图片高度",
help_text="单位px",
)
is_cover = models.BooleanField(
default=False,
verbose_name="是否封面图",
help_text="楼盘封面图(每楼盘唯一)",
)
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,
related_name="created_complex_photos",
verbose_name="上传人",
)
class Meta:
db_table = "complex_photos"
verbose_name = "楼盘照片"
verbose_name_plural = "楼盘照片"
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",
verbose_name="所属楼盘",
)
file_key = models.TextField(
verbose_name="文件存储路径",
help_text="R2/S3 存储路径",
)
file_name = models.CharField(
max_length=255,
verbose_name="原始文件名",
)
file_size = models.IntegerField(
null=True,
blank=True,
verbose_name="文件大小",
help_text="单位bytes",
)
file_type = models.CharField(
max_length=50,
blank=True,
default="",
verbose_name="文件类型",
help_text="MIME type",
)
sort_order = models.SmallIntegerField(
default=0,
verbose_name="排序顺序",
)
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,
related_name="created_complex_attachments",
verbose_name="上传人",
)
class Meta:
db_table = "complex_attachments"
verbose_name = "楼盘附件"
verbose_name_plural = "楼盘附件"
ordering = ["complex_id", "sort_order"]
class ComplexPriceTrend(UUIDPrimaryKeyModel):
complex = models.ForeignKey(
"fonrey_complex.Complex",
on_delete=models.CASCADE,
related_name="price_trends",
verbose_name="所属楼盘",
)
record_month = models.DateField(
verbose_name="月份",
help_text="统一存为该月1日如 2026-04-01",
)
avg_sale_price = models.DecimalField(
max_digits=12,
decimal_places=2,
null=True,
blank=True,
verbose_name="月均售价",
help_text="单位:万元/套",
)
avg_unit_price = models.DecimalField(
max_digits=10,
decimal_places=2,
null=True,
blank=True,
verbose_name="月均单价",
help_text="单位:元/m²",
)
transaction_count = models.IntegerField(
null=True,
blank=True,
verbose_name="成交套数",
)
listing_count = models.IntegerField(
null=True,
blank=True,
verbose_name="当月挂牌套数",
)
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name="创建时间",
)
class Meta:
db_table = "complex_price_trends"
verbose_name = "楼盘价格走势"
verbose_name_plural = "楼盘价格走势"
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"]