Files
fonrey/apps/complex/migrations/0001_initial.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

333 lines
20 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.
# 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'),
),
]