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>
This commit is contained in:
332
apps/complex/migrations/0001_initial.py
Normal file
332
apps/complex/migrations/0001_initial.py
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
# Generated by Django 4.2.16 on 2026-04-29 09:12
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
import django.contrib.postgres.indexes
|
||||||
|
import django.contrib.postgres.search
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('region', '0001_initial'),
|
||||||
|
('org', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Complex',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('deleted_at', models.DateTimeField(blank=True, db_index=True, null=True)),
|
||||||
|
('name', models.CharField(help_text='标准楼盘名称,不可在编辑页直接修改', max_length=200)),
|
||||||
|
('address', models.CharField(blank=True, default='', max_length=500)),
|
||||||
|
('address_summary', models.CharField(blank=True, default='', max_length=100)),
|
||||||
|
('latitude', models.DecimalField(blank=True, decimal_places=7, max_digits=10, null=True)),
|
||||||
|
('longitude', models.DecimalField(blank=True, decimal_places=7, max_digits=10, null=True)),
|
||||||
|
('property_usage_types', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('residential', '住宅'), ('villa', '别墅'), ('commercial_residential', '商住'), ('commercial', '商业'), ('office', '写字楼'), ('other', '其他')], max_length=30), blank=True, default=list, size=None)),
|
||||||
|
('building_structure', models.CharField(blank=True, choices=[('unit_room', '单元-房号'), ('other', '其他')], default='', max_length=30)),
|
||||||
|
('building_type', models.CharField(blank=True, choices=[('slab', '板楼'), ('tower', '塔楼'), ('slab_tower', '板塔结合')], default='', max_length=20)),
|
||||||
|
('land_use_years', models.CharField(blank=True, default='', max_length=30)),
|
||||||
|
('built_year', models.SmallIntegerField(blank=True, null=True)),
|
||||||
|
('built_years', django.contrib.postgres.fields.ArrayField(base_field=models.SmallIntegerField(), blank=True, default=list, size=None)),
|
||||||
|
('ownership_category', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=30), blank=True, default=list, size=None)),
|
||||||
|
('total_units', models.IntegerField(blank=True, null=True)),
|
||||||
|
('total_households', models.IntegerField(blank=True, null=True)),
|
||||||
|
('total_floor_area', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)),
|
||||||
|
('plot_area', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)),
|
||||||
|
('plot_ratio', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True)),
|
||||||
|
('green_rate', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True)),
|
||||||
|
('developer', models.CharField(blank=True, default='', max_length=200)),
|
||||||
|
('property_company', models.CharField(blank=True, default='', max_length=200)),
|
||||||
|
('property_fee', models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True)),
|
||||||
|
('property_phone', models.CharField(blank=True, default='', max_length=30)),
|
||||||
|
('parking_total', models.IntegerField(blank=True, null=True)),
|
||||||
|
('parking_underground', models.IntegerField(blank=True, null=True)),
|
||||||
|
('parking_ratio', models.CharField(blank=True, default='', max_length=20)),
|
||||||
|
('water_type', models.CharField(blank=True, choices=[('civil', '民水'), ('commercial', '商水')], default='', max_length=10)),
|
||||||
|
('electricity_type', models.CharField(blank=True, choices=[('civil', '民电'), ('commercial', '商电')], default='', max_length=10)),
|
||||||
|
('has_central_heating', models.BooleanField(blank=True, null=True)),
|
||||||
|
('has_gas', models.BooleanField(blank=True, null=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', django.contrib.postgres.search.SearchVectorField(blank=True, null=True)),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('version', models.IntegerField(default=1, help_text='乐观锁版本号;UPDATE 时 +1')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'complexes',
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ComplexSchool',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('zone_type', models.CharField(blank=True, choices=[('guaranteed', '对口'), ('reference', '参考'), ('lottery', '摇号')], default='', max_length=30)),
|
||||||
|
('complex', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='complex_schools', to='fonrey_complex.complex')),
|
||||||
|
('school', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='complex_links', to='region.school')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'complex_schools',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ComplexPriceTrend',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('record_month', models.DateField(help_text='月份(统一存为该月1日)')),
|
||||||
|
('avg_sale_price', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)),
|
||||||
|
('avg_unit_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
|
||||||
|
('transaction_count', models.IntegerField(blank=True, null=True)),
|
||||||
|
('listing_count', models.IntegerField(blank=True, null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('complex', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='price_trends', to='fonrey_complex.complex')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'complex_price_trends',
|
||||||
|
'ordering': ['complex_id', '-record_month'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ComplexPhoto',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('category', models.CharField(choices=[('complex', '楼盘图'), ('layout', '户型图'), ('vr', 'VR图'), ('other', '其他')], max_length=20)),
|
||||||
|
('file_key', models.TextField()),
|
||||||
|
('thumbnail_key', models.TextField(blank=True, default='')),
|
||||||
|
('file_name', models.CharField(blank=True, default='', max_length=255)),
|
||||||
|
('file_size', models.IntegerField(blank=True, help_text='bytes', null=True)),
|
||||||
|
('width', models.IntegerField(blank=True, null=True)),
|
||||||
|
('height', models.IntegerField(blank=True, null=True)),
|
||||||
|
('is_cover', models.BooleanField(default=False)),
|
||||||
|
('sort_order', models.SmallIntegerField(default=0)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('complex', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='photos', to='fonrey_complex.complex')),
|
||||||
|
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_complex_photos', to='org.staff')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'complex_photos',
|
||||||
|
'ordering': ['complex_id', 'sort_order'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ComplexMetroStation',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('distance_meters', models.IntegerField(blank=True, null=True)),
|
||||||
|
('complex', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='complex_metro_stations', to='fonrey_complex.complex')),
|
||||||
|
('station', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='complex_links', to='region.metrostation')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'complex_metro_stations',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ComplexBusinessArea',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('is_primary', models.BooleanField(default=False)),
|
||||||
|
('business_area', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='complex_links', to='region.businessarea')),
|
||||||
|
('complex', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='complex_business_areas', to='fonrey_complex.complex')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'complex_business_areas',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ComplexAttachment',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('file_key', models.TextField()),
|
||||||
|
('file_name', models.CharField(max_length=255)),
|
||||||
|
('file_size', models.IntegerField(blank=True, null=True)),
|
||||||
|
('file_type', models.CharField(blank=True, default='', help_text='MIME type', max_length=50)),
|
||||||
|
('sort_order', models.SmallIntegerField(default=0)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('complex', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='fonrey_complex.complex')),
|
||||||
|
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_complex_attachments', to='org.staff')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'complex_attachments',
|
||||||
|
'ordering': ['complex_id', 'sort_order'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ComplexAlias',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('alias', models.CharField(max_length=200)),
|
||||||
|
('is_system', models.BooleanField(default=False, help_text='TRUE=系统/标准别名(只读)')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('complex', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='aliases', to='fonrey_complex.complex')),
|
||||||
|
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_complex_aliases', to='org.staff')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'complex_aliases',
|
||||||
|
'ordering': ['complex_id', 'alias'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='complex',
|
||||||
|
name='business_areas',
|
||||||
|
field=models.ManyToManyField(related_name='complexes', through='fonrey_complex.ComplexBusinessArea', to='region.businessarea'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='complex',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_complexes', to='org.staff'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='complex',
|
||||||
|
name='district',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='complexes', to='region.district'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='complex',
|
||||||
|
name='metro_stations',
|
||||||
|
field=models.ManyToManyField(related_name='complexes', through='fonrey_complex.ComplexMetroStation', to='region.metrostation'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='complex',
|
||||||
|
name='schools',
|
||||||
|
field=models.ManyToManyField(related_name='complexes', through='fonrey_complex.ComplexSchool', to='region.school'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='complex',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_complexes', to='org.staff'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Building',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('name', models.CharField(help_text='楼栋名,如「1号楼」「A栋2单元」', max_length=50)),
|
||||||
|
('is_standard', models.BooleanField(default=False, help_text='TRUE=标准结构(经运营核准)')),
|
||||||
|
('property_usage_type', models.CharField(blank=True, choices=[('residential', '住宅'), ('villa', '别墅'), ('commercial_residential', '商住'), ('commercial', '商业'), ('office', '写字楼'), ('other', '其他')], default='', max_length=30)),
|
||||||
|
('built_year', models.SmallIntegerField(blank=True, null=True)),
|
||||||
|
('total_floors', models.SmallIntegerField(blank=True, null=True)),
|
||||||
|
('land_use_years', models.CharField(blank=True, default='', max_length=30)),
|
||||||
|
('has_elevator', models.BooleanField(blank=True, null=True)),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('complex', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='buildings', to='fonrey_complex.complex')),
|
||||||
|
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_buildings', to='org.staff')),
|
||||||
|
('school', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='buildings', to='region.school')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'buildings',
|
||||||
|
'ordering': ['complex_id', 'name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RoomUnit',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('floor', models.SmallIntegerField(help_text='楼层(实际层数,地下为负数)')),
|
||||||
|
('floor_name', models.CharField(blank=True, default='', max_length=20)),
|
||||||
|
('room_no', models.CharField(max_length=30)),
|
||||||
|
('display_no', models.CharField(blank=True, default='', max_length=50)),
|
||||||
|
('is_standard', models.BooleanField(default=False)),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('building', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='room_units', to='fonrey_complex.building')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'room_units',
|
||||||
|
'ordering': ['building_id', '-floor', 'room_no'],
|
||||||
|
'indexes': [models.Index(condition=models.Q(('is_active', True)), fields=['building'], name='idx_room_units_building')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='roomunit',
|
||||||
|
constraint=models.UniqueConstraint(condition=models.Q(('is_active', True)), fields=('building', 'floor', 'room_no'), name='uq_room_units_unique'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='complexschool',
|
||||||
|
index=models.Index(fields=['school'], name='idx_complex_schools_school'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='complexschool',
|
||||||
|
constraint=models.UniqueConstraint(fields=('complex', 'school'), name='pk_complex_schools'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='complexpricetrend',
|
||||||
|
index=models.Index(fields=['complex', '-record_month'], name='idx_cpx_price_trend_complex'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='complexpricetrend',
|
||||||
|
constraint=models.UniqueConstraint(fields=('complex', 'record_month'), name='uq_complex_price_trend_month'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='complexphoto',
|
||||||
|
index=models.Index(fields=['complex'], name='idx_complex_photos_complex'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='complexphoto',
|
||||||
|
index=models.Index(fields=['complex', 'category'], name='idx_complex_photos_category'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='complexphoto',
|
||||||
|
constraint=models.UniqueConstraint(condition=models.Q(('is_cover', True)), fields=('complex',), name='uq_complex_photos_cover'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='complexmetrostation',
|
||||||
|
index=models.Index(fields=['complex'], name='idx_complex_metro_complex'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='complexmetrostation',
|
||||||
|
index=models.Index(fields=['station'], name='idx_complex_metro_station'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='complexmetrostation',
|
||||||
|
constraint=models.UniqueConstraint(fields=('complex', 'station'), name='pk_complex_metro_stations'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='complexbusinessarea',
|
||||||
|
constraint=models.UniqueConstraint(fields=('complex', 'business_area'), name='pk_complex_business_areas'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='complexbusinessarea',
|
||||||
|
constraint=models.UniqueConstraint(condition=models.Q(('is_primary', True)), fields=('complex',), name='uq_complex_biz_area_primary'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='complexalias',
|
||||||
|
index=models.Index(fields=['complex'], name='idx_complex_aliases_complex'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='complex',
|
||||||
|
index=models.Index(condition=models.Q(('deleted_at__isnull', True)), fields=['district'], name='idx_complexes_district'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='complex',
|
||||||
|
index=django.contrib.postgres.indexes.GinIndex(fields=['search_vector'], name='idx_complexes_search'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='complex',
|
||||||
|
index=models.Index(condition=models.Q(('deleted_at__isnull', True), ('latitude__isnull', False)), fields=['latitude', 'longitude'], name='idx_complexes_geo'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='complex',
|
||||||
|
index=models.Index(condition=models.Q(('deleted_at__isnull', True)), fields=['is_active'], name='idx_complexes_active'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='building',
|
||||||
|
index=models.Index(condition=models.Q(('is_active', True)), fields=['complex'], name='idx_buildings_complex'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='building',
|
||||||
|
constraint=models.UniqueConstraint(condition=models.Q(('is_active', True)), fields=('complex', 'name'), name='uq_buildings_complex_name'),
|
||||||
|
),
|
||||||
|
]
|
||||||
76
apps/complex/migrations/0002_pg_trgm_and_search_vector.py
Normal file
76
apps/complex/migrations/0002_pg_trgm_and_search_vector.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
SQL_FORWARDS = r"""
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_complexes_name_trgm
|
||||||
|
ON complexes USING gin (name gin_trgm_ops);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_complex_aliases_alias_trgm
|
||||||
|
ON complex_aliases USING gin (alias gin_trgm_ops);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_schools_name_trgm
|
||||||
|
ON schools USING gin (name gin_trgm_ops);
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION update_complex_search_vector()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.search_vector :=
|
||||||
|
setweight(to_tsvector('simple', COALESCE(NEW.name, '')), 'A') ||
|
||||||
|
setweight(to_tsvector('simple', COALESCE(NEW.address_summary, '')), 'B') ||
|
||||||
|
setweight(to_tsvector('simple', COALESCE(NEW.address, '')), 'C');
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS trg_complex_search_vector ON complexes;
|
||||||
|
CREATE TRIGGER trg_complex_search_vector
|
||||||
|
BEFORE INSERT OR UPDATE OF name, address_summary, address
|
||||||
|
ON complexes
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_complex_search_vector();
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION update_complex_search_on_alias()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
UPDATE complexes
|
||||||
|
SET search_vector = (
|
||||||
|
setweight(to_tsvector('simple', COALESCE(name, '')), 'A') ||
|
||||||
|
setweight(to_tsvector('simple',
|
||||||
|
COALESCE((SELECT string_agg(alias, ' ') FROM complex_aliases WHERE complex_id = complexes.id), '')), 'B') ||
|
||||||
|
setweight(to_tsvector('simple', COALESCE(address_summary, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('simple', COALESCE(address, '')), 'D')
|
||||||
|
),
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = COALESCE(NEW.complex_id, OLD.complex_id);
|
||||||
|
RETURN COALESCE(NEW, OLD);
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS trg_complex_alias_search ON complex_aliases;
|
||||||
|
CREATE TRIGGER trg_complex_alias_search
|
||||||
|
AFTER INSERT OR UPDATE OR DELETE ON complex_aliases
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_complex_search_on_alias();
|
||||||
|
"""
|
||||||
|
|
||||||
|
SQL_REVERSE = r"""
|
||||||
|
DROP TRIGGER IF EXISTS trg_complex_alias_search ON complex_aliases;
|
||||||
|
DROP TRIGGER IF EXISTS trg_complex_search_vector ON complexes;
|
||||||
|
DROP FUNCTION IF EXISTS update_complex_search_on_alias();
|
||||||
|
DROP FUNCTION IF EXISTS update_complex_search_vector();
|
||||||
|
DROP INDEX IF EXISTS idx_schools_name_trgm;
|
||||||
|
DROP INDEX IF EXISTS idx_complex_aliases_alias_trgm;
|
||||||
|
DROP INDEX IF EXISTS idx_complexes_name_trgm;
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("fonrey_complex", "0001_initial"),
|
||||||
|
("region", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunSQL(sql=SQL_FORWARDS, reverse_sql=SQL_REVERSE),
|
||||||
|
]
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from apps.complex.models.complex import (
|
||||||
|
Building,
|
||||||
|
Complex,
|
||||||
|
ComplexAlias,
|
||||||
|
ComplexAttachment,
|
||||||
|
ComplexBusinessArea,
|
||||||
|
ComplexMetroStation,
|
||||||
|
ComplexPhoto,
|
||||||
|
ComplexPriceTrend,
|
||||||
|
ComplexSchool,
|
||||||
|
RoomUnit,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Building",
|
||||||
|
"Complex",
|
||||||
|
"ComplexAlias",
|
||||||
|
"ComplexAttachment",
|
||||||
|
"ComplexBusinessArea",
|
||||||
|
"ComplexMetroStation",
|
||||||
|
"ComplexPhoto",
|
||||||
|
"ComplexPriceTrend",
|
||||||
|
"ComplexSchool",
|
||||||
|
"RoomUnit",
|
||||||
|
]
|
||||||
|
|||||||
457
apps/complex/models/complex.py
Normal file
457
apps/complex/models/complex.py
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
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"]
|
||||||
0
apps/complex/serializers.py
Normal file
0
apps/complex/serializers.py
Normal file
0
apps/complex/services/__init__.py
Normal file
0
apps/complex/services/__init__.py
Normal file
0
apps/complex/tasks.py
Normal file
0
apps/complex/tasks.py
Normal file
0
apps/complex/templates/complex/.gitkeep
Normal file
0
apps/complex/templates/complex/.gitkeep
Normal file
0
apps/complex/tests/__init__.py
Normal file
0
apps/complex/tests/__init__.py
Normal file
5
apps/complex/urls.py
Normal file
5
apps/complex/urls.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
app_name = "complex"
|
||||||
|
|
||||||
|
urlpatterns: list = []
|
||||||
0
apps/complex/views.py
Normal file
0
apps/complex/views.py
Normal file
@@ -352,6 +352,20 @@ class ComplexPhotoCategory(models.TextChoices):
|
|||||||
OTHER = "other", "其他"
|
OTHER = "other", "其他"
|
||||||
|
|
||||||
|
|
||||||
|
class ComplexPropertyUsageType(models.TextChoices):
|
||||||
|
RESIDENTIAL = "residential", "住宅"
|
||||||
|
VILLA = "villa", "别墅"
|
||||||
|
COMMERCIAL_RESIDENTIAL = "commercial_residential", "商住"
|
||||||
|
COMMERCIAL = "commercial", "商业"
|
||||||
|
OFFICE = "office", "写字楼"
|
||||||
|
OTHER = "other", "其他"
|
||||||
|
|
||||||
|
|
||||||
|
class ComplexBuildingStructure(models.TextChoices):
|
||||||
|
UNIT_ROOM = "unit_room", "单元-房号"
|
||||||
|
OTHER = "other", "其他"
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────
|
||||||
# 3.5 property
|
# 3.5 property
|
||||||
# ──────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user