feat(client,setting): complete Phase 2 with partitioned client_follow_logs
- apps/client (11 models): Client, ClientContact, ClientRequirement, ClientSchoolPreference, ClientFollowLog (partitioned), ClientFollowLogAttachment, ClientViewing, ClientPropertyMatch, ClientStatusLog, ClientFavoriteFolder, ClientFolderItem - apps/client/0002 RunSQL: PARTITION BY RANGE(created_at) for client_follow_logs + monthly partitions + default; triggers update_client_last_follow + update_client_viewing_progress; partial unique index on client_no WHERE deleted_at IS NULL - apps/setting (4 models): LookupGroup, LookupItem, TenantSetting, FieldRequirementRule (tenant schema only per spec) manage.py check green; all 9 Phase 2 apps complete.
This commit is contained in:
351
apps/client/migrations/0001_initial.py
Normal file
351
apps/client/migrations/0001_initial.py
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
# Generated by Django 4.2.16 on 2026-04-29 09:31
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('org', '0001_initial'),
|
||||||
|
('fonrey_property', '0002_partitions_and_triggers'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ClientFollowLog',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(primary_key=True, serialize=False)),
|
||||||
|
('created_at', models.DateTimeField()),
|
||||||
|
('log_type', models.CharField(choices=[('written', '写入跟进'), ('modified', '修改跟进'), ('sensitive_view', '敏感查看'), ('other', '其他'), ('system', '系统')], max_length=30)),
|
||||||
|
('purpose', models.CharField(blank=True, default='', max_length=50)),
|
||||||
|
('content', models.TextField(blank=True, default='')),
|
||||||
|
('log_tag', models.CharField(blank=True, default='', max_length=50)),
|
||||||
|
('change_detail', models.JSONField(blank=True, null=True)),
|
||||||
|
('is_public', models.BooleanField(default=True)),
|
||||||
|
('is_deletable', models.BooleanField(default=True)),
|
||||||
|
('operator_snapshot', models.JSONField(blank=True, null=True)),
|
||||||
|
('deleted_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'client_follow_logs',
|
||||||
|
'managed': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Client',
|
||||||
|
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)),
|
||||||
|
('client_no', models.CharField(max_length=30, unique=True)),
|
||||||
|
('client_type', models.CharField(choices=[('private', '私客'), ('public', '公客'), ('transacted', '成交客')], default='private', max_length=20)),
|
||||||
|
('status', models.CharField(choices=[('buying', '求购'), ('renting', '求租'), ('buy_or_rent', '租购'), ('suspended', '暂缓'), ('bought', '已购'), ('rented_done', '已租'), ('public', '公客'), ('invalid', '无效')], default='buying', max_length=20)),
|
||||||
|
('grade', models.CharField(choices=[('A', 'A(急迫)'), ('B', 'B(较强)'), ('C', 'C(一般)'), ('D', 'D(较弱)'), ('E', 'E(暂不关注)')], default='C', max_length=5)),
|
||||||
|
('property_usage', models.CharField(choices=[('residential', '住宅'), ('villa', '别墅'), ('commercial_residential', '商住'), ('shop', '商铺'), ('office', '写字楼'), ('other', '其他')], default='residential', max_length=30)),
|
||||||
|
('buying_purpose', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('rigid', '刚需'), ('investment', '投资'), ('school_district', '学区'), ('upgrade', '改善'), ('commercial', '商用'), ('other', '其他')], max_length=20), blank=True, default=list, size=None)),
|
||||||
|
('payment_method', models.CharField(blank=True, choices=[('full', '全额'), ('mortgage', '商业贷款'), ('mortgage_fund', '商贷+公积金'), ('fund', '公积金')], default='', max_length=30)),
|
||||||
|
('properties_owned', models.CharField(blank=True, choices=[('none', '无'), ('local_none', '本地无/外地有'), ('local_has', '本地有')], default='', max_length=20)),
|
||||||
|
('has_loan_record', models.BooleanField(blank=True, null=True)),
|
||||||
|
('id_type', models.CharField(blank=True, choices=[('id_card', '身份证'), ('passport', '护照'), ('hk_macao', '港澳通行证'), ('other', '其他')], default='', max_length=20)),
|
||||||
|
('id_number_enc', models.BinaryField(blank=True, null=True)),
|
||||||
|
('source', models.CharField(blank=True, default='', max_length=50)),
|
||||||
|
('remarks', models.TextField(blank=True, default='')),
|
||||||
|
('is_starred', models.BooleanField(default=False)),
|
||||||
|
('is_pinned', models.BooleanField(default=False)),
|
||||||
|
('is_big_value', models.BooleanField(default=False)),
|
||||||
|
('is_protected', models.BooleanField(default=False)),
|
||||||
|
('prefers_new_house', models.BooleanField(blank=True, null=True)),
|
||||||
|
('transfer_to_public_type', models.CharField(blank=True, choices=[('manual', '手动转公'), ('auto', '自动转公'), ('marketing_jump', '营销客跳公'), ('resource_public', '资料客素公')], default='', max_length=20)),
|
||||||
|
('transferred_public_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('invalid_reason', models.CharField(blank=True, choices=[('invalid_phone', '号码无效'), ('peer_agent', '同行'), ('ad', '广告推销'), ('no_intent', '无意向'), ('other', '其他')], default='', max_length=30)),
|
||||||
|
('invalidated_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('transacted_at', models.DateField(blank=True, null=True)),
|
||||||
|
('transacted_price', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)),
|
||||||
|
('transacted_type', models.CharField(blank=True, choices=[('bought', '我购'), ('rented', '我租')], default='', max_length=20)),
|
||||||
|
('transacted_property_type', models.CharField(blank=True, choices=[('second_hand', '二手'), ('new_house', '新房')], default='', max_length=20)),
|
||||||
|
('activity_level', models.CharField(blank=True, choices=[('new_matched', '新配对'), ('active_7d', '7日活跃'), ('active_30d', '30日活跃'), ('active_90d', '90日活跃'), ('expiring', '即将过期'), ('frozen', '暂缓中'), ('invalid', '无效')], default='', max_length=20)),
|
||||||
|
('last_active_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('last_follow_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('commission_date', models.DateField(blank=True, null=True)),
|
||||||
|
('entrust_count', models.SmallIntegerField(default=1)),
|
||||||
|
('version', models.IntegerField(default=1)),
|
||||||
|
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_created', to='org.staff')),
|
||||||
|
('first_recorder', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='first_recorded_clients', to='org.staff')),
|
||||||
|
('org_unit', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='clients', to='org.orgunit')),
|
||||||
|
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owned_clients', to='org.staff')),
|
||||||
|
('transacted_property', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transacted_clients', to='fonrey_property.property')),
|
||||||
|
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated', to='org.staff')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'clients',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ClientFavoriteFolder',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=10)),
|
||||||
|
('is_default', models.BooleanField(default=False)),
|
||||||
|
('sort_order', models.IntegerField(default=0)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('deleted_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('staff', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorite_folders', to='org.staff')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'client_favorite_folders',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ClientRequirement',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('requirement_type', models.CharField(choices=[('second_hand', '二手'), ('new_house', '新房'), ('rental', '租房')], max_length=20)),
|
||||||
|
('is_primary', models.BooleanField(default=True)),
|
||||||
|
('budget_min', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)),
|
||||||
|
('budget_max', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)),
|
||||||
|
('area_min', models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True)),
|
||||||
|
('area_max', models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True)),
|
||||||
|
('bedroom_counts', django.contrib.postgres.fields.ArrayField(base_field=models.SmallIntegerField(), blank=True, default=list, size=None)),
|
||||||
|
('floor_preferences', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('no_first', '不要一楼'), ('low', '低楼层'), ('mid', '中楼层'), ('high', '高楼层'), ('no_top', '不要顶楼')], max_length=20), blank=True, default=list, size=None)),
|
||||||
|
('orientations', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('east', '东'), ('south', '南'), ('west', '西'), ('north', '北')], max_length=10), blank=True, default=list, size=None)),
|
||||||
|
('decorations', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('rough', '毛坯'), ('plain', '清水'), ('simple', '简装'), ('medium', '中装'), ('fine', '精装'), ('luxury', '豪装')], max_length=10), blank=True, default=list, size=None)),
|
||||||
|
('building_age_ranges', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('within_5y', '5年内'), ('5_10y', '5-10年'), ('10_15y', '10-15年'), ('15_20y', '15-20年'), ('over_20y', '20年以上')], max_length=20), blank=True, default=list, size=None)),
|
||||||
|
('intent_district_ids', django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(), blank=True, default=list, size=None)),
|
||||||
|
('intent_business_area_ids', django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(), blank=True, default=list, size=None)),
|
||||||
|
('intent_complex_names', models.TextField(blank=True, default='')),
|
||||||
|
('transportation', models.CharField(blank=True, default='', max_length=50)),
|
||||||
|
('intent_school_names', models.TextField(blank=True, default='')),
|
||||||
|
('school_enrollment_date', models.DateField(blank=True, null=True)),
|
||||||
|
('traffic_preference', models.TextField(blank=True, default='')),
|
||||||
|
('requirement_notes', models.CharField(blank=True, default='', max_length=200)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requirements', to='fonrey_client.client')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'client_requirements',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ClientPropertyMatch',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('match_source', models.CharField(choices=[('recorded', '录客配房'), ('system', '系统配房')], default='recorded', max_length=20)),
|
||||||
|
('match_group', models.CharField(blank=True, choices=[('quality_layout', '优质户型'), ('price_reduced', '降价'), ('hot', '热门'), ('newly_listed', '新上')], default='', max_length=30)),
|
||||||
|
('match_score', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True)),
|
||||||
|
('match_reasons', models.JSONField(blank=True, null=True)),
|
||||||
|
('status', models.CharField(choices=[('suggested', '待推送'), ('shared', '已分享'), ('rejected', '已反馈不合适'), ('viewed', '客户已查看')], default='suggested', max_length=20)),
|
||||||
|
('shared_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('feedback', models.CharField(blank=True, default='', max_length=50)),
|
||||||
|
('calculated_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='property_matches', to='fonrey_client.client')),
|
||||||
|
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_matches', to='org.staff')),
|
||||||
|
('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='client_matches', to='fonrey_property.property')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'client_property_matches',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ClientFollowLogAttachment',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('follow_log_id', models.UUIDField()),
|
||||||
|
('file_key', models.TextField()),
|
||||||
|
('file_name', models.CharField(max_length=255)),
|
||||||
|
('file_size', models.IntegerField()),
|
||||||
|
('file_type', models.CharField(blank=True, default='', max_length=10)),
|
||||||
|
('has_location', models.BooleanField(default=False)),
|
||||||
|
('sort_order', models.SmallIntegerField(default=0)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'client_follow_log_attachments',
|
||||||
|
'indexes': [models.Index(fields=['follow_log_id'], name='idx_cfla_log')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ClientFolderItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('added_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='folder_items', to='fonrey_client.client')),
|
||||||
|
('folder', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='fonrey_client.clientfavoritefolder')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'client_folder_items',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ClientContact',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('sort_order', models.SmallIntegerField(default=0)),
|
||||||
|
('name', models.CharField(max_length=50)),
|
||||||
|
('gender', models.CharField(choices=[('male', '先生'), ('female', '女士')], default='male', max_length=10)),
|
||||||
|
('phone_enc', models.BinaryField()),
|
||||||
|
('phone_hash', models.CharField(max_length=64)),
|
||||||
|
('phone_country_code', models.CharField(default='+86', max_length=10)),
|
||||||
|
('phone_is_invalid', models.BooleanField(default=False)),
|
||||||
|
('phone2_enc', models.BinaryField(blank=True, null=True)),
|
||||||
|
('phone2_hash', models.CharField(blank=True, default='', max_length=64)),
|
||||||
|
('wechat', models.CharField(blank=True, default='', max_length=100)),
|
||||||
|
('qq', models.CharField(blank=True, default='', max_length=20)),
|
||||||
|
('remarks', models.CharField(blank=True, default='', max_length=200)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('deleted_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contacts', to='fonrey_client.client')),
|
||||||
|
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_client_contacts', to='org.staff')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'client_contacts',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ClientViewing',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('viewing_type', models.CharField(choices=[('appointment', '预约'), ('viewing', '带看'), ('revisit', '复看'), ('empty', '空看')], default='viewing', max_length=20)),
|
||||||
|
('companion_ids', django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(), blank=True, default=list, size=None)),
|
||||||
|
('cooperator_ids', django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(), blank=True, default=list, size=None)),
|
||||||
|
('scheduled_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('viewing_start_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('viewing_end_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('situation', models.TextField(blank=True, default='')),
|
||||||
|
('client_intent', models.CharField(blank=True, choices=[('interested', '感兴趣'), ('not_interested', '不感兴趣'), ('negotiating', '谈判中'), ('cancelled', '取消')], default='', max_length=20)),
|
||||||
|
('viewing_progress', models.SmallIntegerField(blank=True, null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('deleted_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('agent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='led_viewings', to='org.staff')),
|
||||||
|
('client', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='viewings', to='fonrey_client.client')),
|
||||||
|
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_client_viewings', to='org.staff')),
|
||||||
|
('property', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='client_viewings', to='fonrey_property.property')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'client_viewings',
|
||||||
|
'indexes': [models.Index(fields=['client', '-viewing_start_at'], name='idx_cv_client_time'), models.Index(fields=['property'], name='idx_cv_property'), models.Index(fields=['agent'], name='idx_cv_agent')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ClientStatusLog',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(primary_key=True, serialize=False)),
|
||||||
|
('change_type', models.CharField(choices=[('status_change', '改状态'), ('grade_change', '改等级'), ('to_public', '转公客'), ('to_transacted', '转成交'), ('to_invalid', '转无效'), ('owner_change', '改归属人'), ('source_change', '改来源'), ('merge', '合并客源')], max_length=30)),
|
||||||
|
('old_value', models.JSONField(blank=True, null=True)),
|
||||||
|
('new_value', models.JSONField(blank=True, null=True)),
|
||||||
|
('reason', models.TextField(blank=True, default='')),
|
||||||
|
('operated_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('client', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='status_logs', to='fonrey_client.client')),
|
||||||
|
('operator', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='client_status_changes', to='org.staff')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'client_status_logs',
|
||||||
|
'indexes': [models.Index(fields=['client', '-operated_at'], name='idx_csl_client'), models.Index(fields=['change_type', '-operated_at'], name='idx_csl_type')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ClientSchoolPreference',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('school_id', models.UUIDField(blank=True, null=True)),
|
||||||
|
('school_name', models.CharField(max_length=100)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('requirement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='school_preferences', to='fonrey_client.clientrequirement')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'client_school_preferences',
|
||||||
|
'indexes': [models.Index(fields=['requirement'], name='idx_csp_requirement')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='clientrequirement',
|
||||||
|
index=models.Index(fields=['client'], name='idx_creq_client'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='clientrequirement',
|
||||||
|
index=models.Index(fields=['requirement_type', 'client'], name='idx_creq_type'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='clientrequirement',
|
||||||
|
index=models.Index(fields=['budget_min', 'budget_max'], name='idx_creq_budget'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='clientrequirement',
|
||||||
|
index=models.Index(fields=['area_min', 'area_max'], name='idx_creq_area'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='clientpropertymatch',
|
||||||
|
index=models.Index(fields=['client', 'match_source', 'match_group'], name='idx_cpm_client_grp'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='clientpropertymatch',
|
||||||
|
index=models.Index(fields=['client', 'status'], name='idx_cpm_status'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='clientpropertymatch',
|
||||||
|
constraint=models.UniqueConstraint(fields=('client', 'property'), name='uq_client_match_pair'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='clientfolderitem',
|
||||||
|
index=models.Index(fields=['client'], name='idx_cfi_client'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='clientfolderitem',
|
||||||
|
constraint=models.UniqueConstraint(fields=('folder', 'client'), name='uq_cfi_folder_client'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='clientfavoritefolder',
|
||||||
|
index=models.Index(fields=['staff'], name='idx_cff_staff'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='clientfavoritefolder',
|
||||||
|
constraint=models.UniqueConstraint(condition=models.Q(('deleted_at__isnull', True), ('is_default', True)), fields=('staff',), name='uq_cff_default_per_staff'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='clientcontact',
|
||||||
|
index=models.Index(fields=['phone_hash'], name='idx_cc_phone_hash'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='clientcontact',
|
||||||
|
index=models.Index(fields=['phone2_hash'], name='idx_cc_phone2_hash'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='clientcontact',
|
||||||
|
index=models.Index(fields=['client'], name='idx_cc_client'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='client',
|
||||||
|
index=models.Index(fields=['client_type', 'status'], name='idx_clients_type_stat'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='client',
|
||||||
|
index=models.Index(fields=['owner'], name='idx_clients_owner'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='client',
|
||||||
|
index=models.Index(fields=['org_unit'], name='idx_clients_org_unit'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='client',
|
||||||
|
index=models.Index(fields=['activity_level', '-last_active_at'], name='idx_clients_activity'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='client',
|
||||||
|
index=models.Index(fields=['grade'], name='idx_clients_grade'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='client',
|
||||||
|
index=models.Index(fields=['-transferred_public_at'], name='idx_clients_transferred'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='client',
|
||||||
|
index=models.Index(fields=['-last_follow_at'], name='idx_clients_last_follow'),
|
||||||
|
),
|
||||||
|
]
|
||||||
99
apps/client/migrations/0002_partitions_and_triggers.py
Normal file
99
apps/client/migrations/0002_partitions_and_triggers.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
CREATE_CLIENT_FOLLOW_LOGS = """
|
||||||
|
CREATE TABLE client_follow_logs (
|
||||||
|
id UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||||
|
log_type VARCHAR(30) NOT NULL
|
||||||
|
CHECK (log_type IN ('written','modified','sensitive_view',
|
||||||
|
'other','system')),
|
||||||
|
purpose VARCHAR(50),
|
||||||
|
content TEXT,
|
||||||
|
log_tag VARCHAR(50),
|
||||||
|
change_detail JSONB,
|
||||||
|
is_public BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
is_deletable BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
operator_id UUID REFERENCES staff(id) ON DELETE SET NULL,
|
||||||
|
operator_snapshot JSONB,
|
||||||
|
deleted_at TIMESTAMPTZ,
|
||||||
|
PRIMARY KEY (id, created_at)
|
||||||
|
) PARTITION BY RANGE (created_at);
|
||||||
|
|
||||||
|
CREATE TABLE client_follow_logs_2026_04 PARTITION OF client_follow_logs
|
||||||
|
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
|
||||||
|
CREATE TABLE client_follow_logs_2026_05 PARTITION OF client_follow_logs
|
||||||
|
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
|
||||||
|
CREATE TABLE client_follow_logs_default PARTITION OF client_follow_logs DEFAULT;
|
||||||
|
|
||||||
|
CREATE INDEX idx_cfl_client_time ON client_follow_logs(client_id, created_at DESC)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_cfl_type ON client_follow_logs(client_id, log_type, created_at DESC)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_cfl_operator ON client_follow_logs(operator_id, created_at DESC)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_cfl_sensitive ON client_follow_logs(client_id, created_at DESC)
|
||||||
|
WHERE log_type = 'sensitive_view';
|
||||||
|
"""
|
||||||
|
|
||||||
|
DROP_CLIENT_FOLLOW_LOGS = "DROP TABLE IF EXISTS client_follow_logs CASCADE;"
|
||||||
|
|
||||||
|
CREATE_TRIGGERS = """
|
||||||
|
CREATE OR REPLACE FUNCTION update_client_last_follow()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.log_type = 'written' THEN
|
||||||
|
UPDATE clients
|
||||||
|
SET last_follow_at = NEW.created_at,
|
||||||
|
last_active_at = NEW.created_at,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = NEW.client_id;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_client_last_follow
|
||||||
|
AFTER INSERT ON client_follow_logs
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_client_last_follow();
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION update_client_viewing_progress()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
UPDATE clients
|
||||||
|
SET updated_at = NOW()
|
||||||
|
WHERE id = NEW.client_id;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_client_viewing_progress
|
||||||
|
AFTER INSERT ON client_viewings
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_client_viewing_progress();
|
||||||
|
"""
|
||||||
|
|
||||||
|
DROP_TRIGGERS = """
|
||||||
|
DROP TRIGGER IF EXISTS trg_client_viewing_progress ON client_viewings;
|
||||||
|
DROP FUNCTION IF EXISTS update_client_viewing_progress();
|
||||||
|
DROP TRIGGER IF EXISTS trg_client_last_follow ON client_follow_logs;
|
||||||
|
DROP FUNCTION IF EXISTS update_client_last_follow();
|
||||||
|
"""
|
||||||
|
|
||||||
|
CREATE_UNIQUE_CLIENT_NO = """
|
||||||
|
CREATE UNIQUE INDEX idx_clients_client_no_active ON clients(client_no)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
"""
|
||||||
|
|
||||||
|
DROP_UNIQUE_CLIENT_NO = "DROP INDEX IF EXISTS idx_clients_client_no_active;"
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("fonrey_client", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunSQL(CREATE_CLIENT_FOLLOW_LOGS, reverse_sql=DROP_CLIENT_FOLLOW_LOGS),
|
||||||
|
migrations.RunSQL(CREATE_TRIGGERS, reverse_sql=DROP_TRIGGERS),
|
||||||
|
migrations.RunSQL(CREATE_UNIQUE_CLIENT_NO, reverse_sql=DROP_UNIQUE_CLIENT_NO),
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from .contacts import ClientContact, ClientRequirement, ClientSchoolPreference
|
||||||
|
from .core import Client
|
||||||
|
from .folders import ClientFavoriteFolder, ClientFolderItem
|
||||||
|
from .follow import ClientFollowLog, ClientFollowLogAttachment
|
||||||
|
from .viewing_match import (
|
||||||
|
ClientPropertyMatch,
|
||||||
|
ClientStatusLog,
|
||||||
|
ClientViewing,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Client",
|
||||||
|
"ClientContact",
|
||||||
|
"ClientRequirement",
|
||||||
|
"ClientSchoolPreference",
|
||||||
|
"ClientFollowLog",
|
||||||
|
"ClientFollowLogAttachment",
|
||||||
|
"ClientViewing",
|
||||||
|
"ClientPropertyMatch",
|
||||||
|
"ClientStatusLog",
|
||||||
|
"ClientFavoriteFolder",
|
||||||
|
"ClientFolderItem",
|
||||||
|
]
|
||||||
|
|||||||
143
apps/client/models/contacts.py
Normal file
143
apps/client/models/contacts.py
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from core.enums import (
|
||||||
|
ClientBuildingAgeRange,
|
||||||
|
ClientContactGender,
|
||||||
|
ClientDecoration,
|
||||||
|
ClientFloorPreference,
|
||||||
|
ClientOrientation,
|
||||||
|
ClientRequirementType,
|
||||||
|
)
|
||||||
|
from core.models.base import UUIDPrimaryKeyModel
|
||||||
|
|
||||||
|
|
||||||
|
class ClientContact(UUIDPrimaryKeyModel):
|
||||||
|
client = models.ForeignKey(
|
||||||
|
"fonrey_client.Client", on_delete=models.CASCADE, related_name="contacts"
|
||||||
|
)
|
||||||
|
sort_order = models.SmallIntegerField(default=0)
|
||||||
|
name = models.CharField(max_length=50)
|
||||||
|
gender = models.CharField(
|
||||||
|
max_length=10, choices=ClientContactGender.choices, default=ClientContactGender.MALE
|
||||||
|
)
|
||||||
|
|
||||||
|
phone_enc = models.BinaryField()
|
||||||
|
phone_hash = models.CharField(max_length=64)
|
||||||
|
phone_country_code = models.CharField(max_length=10, default="+86")
|
||||||
|
phone_is_invalid = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
phone2_enc = models.BinaryField(null=True, blank=True)
|
||||||
|
phone2_hash = models.CharField(max_length=64, blank=True, default="")
|
||||||
|
|
||||||
|
wechat = models.CharField(max_length=100, blank=True, default="")
|
||||||
|
qq = models.CharField(max_length=20, blank=True, default="")
|
||||||
|
remarks = models.CharField(max_length=200, blank=True, default="")
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
created_by = models.ForeignKey(
|
||||||
|
"org.Staff",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="created_client_contacts",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "client_contacts"
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["phone_hash"], name="idx_cc_phone_hash"),
|
||||||
|
models.Index(fields=["phone2_hash"], name="idx_cc_phone2_hash"),
|
||||||
|
models.Index(fields=["client"], name="idx_cc_client"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ClientRequirement(UUIDPrimaryKeyModel):
|
||||||
|
client = models.ForeignKey(
|
||||||
|
"fonrey_client.Client", on_delete=models.CASCADE, related_name="requirements"
|
||||||
|
)
|
||||||
|
requirement_type = models.CharField(
|
||||||
|
max_length=20, choices=ClientRequirementType.choices
|
||||||
|
)
|
||||||
|
is_primary = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
budget_min = models.DecimalField(
|
||||||
|
max_digits=12, decimal_places=2, null=True, blank=True
|
||||||
|
)
|
||||||
|
budget_max = models.DecimalField(
|
||||||
|
max_digits=12, decimal_places=2, null=True, blank=True
|
||||||
|
)
|
||||||
|
area_min = models.DecimalField(
|
||||||
|
max_digits=8, decimal_places=2, null=True, blank=True
|
||||||
|
)
|
||||||
|
area_max = models.DecimalField(
|
||||||
|
max_digits=8, decimal_places=2, null=True, blank=True
|
||||||
|
)
|
||||||
|
|
||||||
|
bedroom_counts = ArrayField(
|
||||||
|
models.SmallIntegerField(), blank=True, default=list
|
||||||
|
)
|
||||||
|
floor_preferences = ArrayField(
|
||||||
|
models.CharField(max_length=20, choices=ClientFloorPreference.choices),
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
)
|
||||||
|
orientations = ArrayField(
|
||||||
|
models.CharField(max_length=10, choices=ClientOrientation.choices),
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
)
|
||||||
|
decorations = ArrayField(
|
||||||
|
models.CharField(max_length=10, choices=ClientDecoration.choices),
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
)
|
||||||
|
building_age_ranges = ArrayField(
|
||||||
|
models.CharField(max_length=20, choices=ClientBuildingAgeRange.choices),
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
)
|
||||||
|
|
||||||
|
intent_district_ids = ArrayField(
|
||||||
|
models.UUIDField(), blank=True, default=list
|
||||||
|
)
|
||||||
|
intent_business_area_ids = ArrayField(
|
||||||
|
models.UUIDField(), blank=True, default=list
|
||||||
|
)
|
||||||
|
intent_complex_names = models.TextField(blank=True, default="")
|
||||||
|
transportation = models.CharField(max_length=50, blank=True, default="")
|
||||||
|
intent_school_names = models.TextField(blank=True, default="")
|
||||||
|
school_enrollment_date = models.DateField(null=True, blank=True)
|
||||||
|
traffic_preference = models.TextField(blank=True, default="")
|
||||||
|
requirement_notes = models.CharField(max_length=200, blank=True, default="")
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "client_requirements"
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["client"], name="idx_creq_client"),
|
||||||
|
models.Index(fields=["requirement_type", "client"], name="idx_creq_type"),
|
||||||
|
models.Index(fields=["budget_min", "budget_max"], name="idx_creq_budget"),
|
||||||
|
models.Index(fields=["area_min", "area_max"], name="idx_creq_area"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ClientSchoolPreference(UUIDPrimaryKeyModel):
|
||||||
|
requirement = models.ForeignKey(
|
||||||
|
ClientRequirement,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="school_preferences",
|
||||||
|
)
|
||||||
|
school_id = models.UUIDField(null=True, blank=True)
|
||||||
|
school_name = models.CharField(max_length=100)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "client_school_preferences"
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["requirement"], name="idx_csp_requirement"),
|
||||||
|
]
|
||||||
147
apps/client/models/core.py
Normal file
147
apps/client/models/core.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from core.enums import (
|
||||||
|
ClientActivityLevel,
|
||||||
|
ClientBuyingPurpose,
|
||||||
|
ClientGrade,
|
||||||
|
ClientIdType,
|
||||||
|
ClientInvalidReason,
|
||||||
|
ClientPaymentMethod,
|
||||||
|
ClientPropertiesOwned,
|
||||||
|
ClientPropertyUsage,
|
||||||
|
ClientStatus,
|
||||||
|
ClientTransactedPropertyType,
|
||||||
|
ClientTransactedType,
|
||||||
|
ClientTransferToPublicType,
|
||||||
|
ClientType,
|
||||||
|
)
|
||||||
|
from core.models.base import AuditedModel
|
||||||
|
|
||||||
|
|
||||||
|
class Client(AuditedModel):
|
||||||
|
client_no = models.CharField(max_length=30, unique=True)
|
||||||
|
client_type = models.CharField(
|
||||||
|
max_length=20, choices=ClientType.choices, default=ClientType.PRIVATE
|
||||||
|
)
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=20, choices=ClientStatus.choices, default=ClientStatus.BUYING
|
||||||
|
)
|
||||||
|
grade = models.CharField(
|
||||||
|
max_length=5, choices=ClientGrade.choices, default=ClientGrade.C
|
||||||
|
)
|
||||||
|
property_usage = models.CharField(
|
||||||
|
max_length=30,
|
||||||
|
choices=ClientPropertyUsage.choices,
|
||||||
|
default=ClientPropertyUsage.RESIDENTIAL,
|
||||||
|
)
|
||||||
|
buying_purpose = ArrayField(
|
||||||
|
models.CharField(max_length=20, choices=ClientBuyingPurpose.choices),
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
)
|
||||||
|
payment_method = models.CharField(
|
||||||
|
max_length=30, choices=ClientPaymentMethod.choices, blank=True, default=""
|
||||||
|
)
|
||||||
|
properties_owned = models.CharField(
|
||||||
|
max_length=20, choices=ClientPropertiesOwned.choices, blank=True, default=""
|
||||||
|
)
|
||||||
|
has_loan_record = models.BooleanField(null=True, blank=True)
|
||||||
|
|
||||||
|
id_type = models.CharField(
|
||||||
|
max_length=20, choices=ClientIdType.choices, blank=True, default=""
|
||||||
|
)
|
||||||
|
id_number_enc = models.BinaryField(null=True, blank=True)
|
||||||
|
|
||||||
|
source = models.CharField(max_length=50, blank=True, default="")
|
||||||
|
remarks = models.TextField(blank=True, default="")
|
||||||
|
|
||||||
|
is_starred = models.BooleanField(default=False)
|
||||||
|
is_pinned = models.BooleanField(default=False)
|
||||||
|
is_big_value = models.BooleanField(default=False)
|
||||||
|
is_protected = models.BooleanField(default=False)
|
||||||
|
prefers_new_house = models.BooleanField(null=True, blank=True)
|
||||||
|
|
||||||
|
transfer_to_public_type = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
choices=ClientTransferToPublicType.choices,
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
transferred_public_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
invalid_reason = models.CharField(
|
||||||
|
max_length=30, choices=ClientInvalidReason.choices, blank=True, default=""
|
||||||
|
)
|
||||||
|
invalidated_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
transacted_at = models.DateField(null=True, blank=True)
|
||||||
|
transacted_property = models.ForeignKey(
|
||||||
|
"fonrey_property.Property",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="transacted_clients",
|
||||||
|
)
|
||||||
|
transacted_price = models.DecimalField(
|
||||||
|
max_digits=12, decimal_places=2, null=True, blank=True
|
||||||
|
)
|
||||||
|
transacted_type = models.CharField(
|
||||||
|
max_length=20, choices=ClientTransactedType.choices, blank=True, default=""
|
||||||
|
)
|
||||||
|
transacted_property_type = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
choices=ClientTransactedPropertyType.choices,
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
|
||||||
|
first_recorder = models.ForeignKey(
|
||||||
|
"org.Staff",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="first_recorded_clients",
|
||||||
|
)
|
||||||
|
owner = models.ForeignKey(
|
||||||
|
"org.Staff",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="owned_clients",
|
||||||
|
)
|
||||||
|
org_unit = models.ForeignKey(
|
||||||
|
"org.OrgUnit",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="clients",
|
||||||
|
)
|
||||||
|
|
||||||
|
activity_level = models.CharField(
|
||||||
|
max_length=20, choices=ClientActivityLevel.choices, blank=True, default=""
|
||||||
|
)
|
||||||
|
last_active_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
last_follow_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
commission_date = models.DateField(null=True, blank=True)
|
||||||
|
entrust_count = models.SmallIntegerField(default=1)
|
||||||
|
|
||||||
|
version = models.IntegerField(default=1)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "clients"
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["client_type", "status"], name="idx_clients_type_stat"),
|
||||||
|
models.Index(fields=["owner"], name="idx_clients_owner"),
|
||||||
|
models.Index(fields=["org_unit"], name="idx_clients_org_unit"),
|
||||||
|
models.Index(
|
||||||
|
fields=["activity_level", "-last_active_at"],
|
||||||
|
name="idx_clients_activity",
|
||||||
|
),
|
||||||
|
models.Index(fields=["grade"], name="idx_clients_grade"),
|
||||||
|
models.Index(
|
||||||
|
fields=["-transferred_public_at"], name="idx_clients_transferred"
|
||||||
|
),
|
||||||
|
models.Index(fields=["-last_follow_at"], name="idx_clients_last_follow"),
|
||||||
|
]
|
||||||
48
apps/client/models/folders.py
Normal file
48
apps/client/models/folders.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from core.models.base import UUIDPrimaryKeyModel
|
||||||
|
|
||||||
|
|
||||||
|
class ClientFavoriteFolder(UUIDPrimaryKeyModel):
|
||||||
|
staff = models.ForeignKey(
|
||||||
|
"org.Staff", on_delete=models.CASCADE, related_name="favorite_folders"
|
||||||
|
)
|
||||||
|
name = models.CharField(max_length=10)
|
||||||
|
is_default = models.BooleanField(default=False)
|
||||||
|
sort_order = models.IntegerField(default=0)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "client_favorite_folders"
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["staff"], name="idx_cff_staff"),
|
||||||
|
]
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["staff"],
|
||||||
|
condition=models.Q(is_default=True, deleted_at__isnull=True),
|
||||||
|
name="uq_cff_default_per_staff",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ClientFolderItem(models.Model):
|
||||||
|
folder = models.ForeignKey(
|
||||||
|
ClientFavoriteFolder, on_delete=models.CASCADE, related_name="items"
|
||||||
|
)
|
||||||
|
client = models.ForeignKey(
|
||||||
|
"fonrey_client.Client", on_delete=models.CASCADE, related_name="folder_items"
|
||||||
|
)
|
||||||
|
added_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "client_folder_items"
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["folder", "client"], name="uq_cfi_folder_client"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["client"], name="idx_cfi_client"),
|
||||||
|
]
|
||||||
55
apps/client/models/follow.py
Normal file
55
apps/client/models/follow.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from core.enums import ClientFollowLogType
|
||||||
|
from core.models.base import UUIDPrimaryKeyModel
|
||||||
|
|
||||||
|
|
||||||
|
class ClientFollowLog(models.Model):
|
||||||
|
"""Partitioned table (PARTITION BY RANGE created_at).
|
||||||
|
|
||||||
|
Managed via RunSQL; Django ORM treats parent as unmanaged.
|
||||||
|
"""
|
||||||
|
|
||||||
|
id = models.UUIDField(primary_key=True)
|
||||||
|
created_at = models.DateTimeField()
|
||||||
|
client = models.ForeignKey(
|
||||||
|
"fonrey_client.Client",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="follow_logs",
|
||||||
|
)
|
||||||
|
|
||||||
|
log_type = models.CharField(max_length=30, choices=ClientFollowLogType.choices)
|
||||||
|
purpose = models.CharField(max_length=50, blank=True, default="")
|
||||||
|
content = models.TextField(blank=True, default="")
|
||||||
|
log_tag = models.CharField(max_length=50, blank=True, default="")
|
||||||
|
change_detail = models.JSONField(null=True, blank=True)
|
||||||
|
|
||||||
|
is_public = models.BooleanField(default=True)
|
||||||
|
is_deletable = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
operator = models.ForeignKey(
|
||||||
|
"org.Staff", null=True, blank=True, on_delete=models.SET_NULL
|
||||||
|
)
|
||||||
|
operator_snapshot = models.JSONField(null=True, blank=True)
|
||||||
|
|
||||||
|
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "client_follow_logs"
|
||||||
|
managed = False
|
||||||
|
unique_together = (("id", "created_at"),)
|
||||||
|
|
||||||
|
|
||||||
|
class ClientFollowLogAttachment(UUIDPrimaryKeyModel):
|
||||||
|
follow_log_id = models.UUIDField() # cross-partitioned FK; not enforced via Django FK
|
||||||
|
file_key = models.TextField()
|
||||||
|
file_name = models.CharField(max_length=255)
|
||||||
|
file_size = models.IntegerField()
|
||||||
|
file_type = models.CharField(max_length=10, blank=True, default="")
|
||||||
|
has_location = models.BooleanField(default=False)
|
||||||
|
sort_order = models.SmallIntegerField(default=0)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "client_follow_log_attachments"
|
||||||
|
indexes = [models.Index(fields=["follow_log_id"], name="idx_cfla_log")]
|
||||||
147
apps/client/models/viewing_match.py
Normal file
147
apps/client/models/viewing_match.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from core.enums import (
|
||||||
|
ClientPropertyMatchGroup,
|
||||||
|
ClientPropertyMatchSource,
|
||||||
|
ClientPropertyMatchStatus,
|
||||||
|
ClientStatusLogChangeType,
|
||||||
|
ClientViewingIntent,
|
||||||
|
ClientViewingType,
|
||||||
|
)
|
||||||
|
from core.models.base import UUIDPrimaryKeyModel
|
||||||
|
|
||||||
|
|
||||||
|
class ClientViewing(UUIDPrimaryKeyModel):
|
||||||
|
client = models.ForeignKey(
|
||||||
|
"fonrey_client.Client", on_delete=models.RESTRICT, related_name="viewings"
|
||||||
|
)
|
||||||
|
property = models.ForeignKey(
|
||||||
|
"fonrey_property.Property",
|
||||||
|
on_delete=models.RESTRICT,
|
||||||
|
related_name="client_viewings",
|
||||||
|
)
|
||||||
|
viewing_type = models.CharField(
|
||||||
|
max_length=20, choices=ClientViewingType.choices, default=ClientViewingType.VIEWING
|
||||||
|
)
|
||||||
|
|
||||||
|
agent = models.ForeignKey(
|
||||||
|
"org.Staff",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="led_viewings",
|
||||||
|
)
|
||||||
|
companion_ids = ArrayField(models.UUIDField(), blank=True, default=list)
|
||||||
|
cooperator_ids = ArrayField(models.UUIDField(), blank=True, default=list)
|
||||||
|
|
||||||
|
scheduled_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
viewing_start_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
viewing_end_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
situation = models.TextField(blank=True, default="")
|
||||||
|
client_intent = models.CharField(
|
||||||
|
max_length=20, choices=ClientViewingIntent.choices, blank=True, default=""
|
||||||
|
)
|
||||||
|
viewing_progress = models.SmallIntegerField(null=True, blank=True)
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
created_by = models.ForeignKey(
|
||||||
|
"org.Staff",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="created_client_viewings",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "client_viewings"
|
||||||
|
indexes = [
|
||||||
|
models.Index(
|
||||||
|
fields=["client", "-viewing_start_at"], name="idx_cv_client_time"
|
||||||
|
),
|
||||||
|
models.Index(fields=["property"], name="idx_cv_property"),
|
||||||
|
models.Index(fields=["agent"], name="idx_cv_agent"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ClientPropertyMatch(UUIDPrimaryKeyModel):
|
||||||
|
client = models.ForeignKey(
|
||||||
|
"fonrey_client.Client", on_delete=models.CASCADE, related_name="property_matches"
|
||||||
|
)
|
||||||
|
property = models.ForeignKey(
|
||||||
|
"fonrey_property.Property",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="client_matches",
|
||||||
|
)
|
||||||
|
|
||||||
|
match_source = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
choices=ClientPropertyMatchSource.choices,
|
||||||
|
default=ClientPropertyMatchSource.RECORDED,
|
||||||
|
)
|
||||||
|
match_group = models.CharField(
|
||||||
|
max_length=30, choices=ClientPropertyMatchGroup.choices, blank=True, default=""
|
||||||
|
)
|
||||||
|
match_score = models.DecimalField(
|
||||||
|
max_digits=5, decimal_places=2, null=True, blank=True
|
||||||
|
)
|
||||||
|
match_reasons = models.JSONField(null=True, blank=True)
|
||||||
|
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
choices=ClientPropertyMatchStatus.choices,
|
||||||
|
default=ClientPropertyMatchStatus.SUGGESTED,
|
||||||
|
)
|
||||||
|
shared_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
feedback = models.CharField(max_length=50, blank=True, default="")
|
||||||
|
calculated_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_matches",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "client_property_matches"
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["client", "property"], name="uq_client_match_pair"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
indexes = [
|
||||||
|
models.Index(
|
||||||
|
fields=["client", "match_source", "match_group"],
|
||||||
|
name="idx_cpm_client_grp",
|
||||||
|
),
|
||||||
|
models.Index(fields=["client", "status"], name="idx_cpm_status"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ClientStatusLog(models.Model):
|
||||||
|
"""Audit log; record-level immutable (no deleted_at)."""
|
||||||
|
|
||||||
|
id = models.UUIDField(primary_key=True)
|
||||||
|
client = models.ForeignKey(
|
||||||
|
"fonrey_client.Client", on_delete=models.RESTRICT, related_name="status_logs"
|
||||||
|
)
|
||||||
|
change_type = models.CharField(
|
||||||
|
max_length=30, choices=ClientStatusLogChangeType.choices
|
||||||
|
)
|
||||||
|
old_value = models.JSONField(null=True, blank=True)
|
||||||
|
new_value = models.JSONField(null=True, blank=True)
|
||||||
|
reason = models.TextField(blank=True, default="")
|
||||||
|
operator = models.ForeignKey(
|
||||||
|
"org.Staff", on_delete=models.RESTRICT, related_name="client_status_changes"
|
||||||
|
)
|
||||||
|
operated_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "client_status_logs"
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["client", "-operated_at"], name="idx_csl_client"),
|
||||||
|
models.Index(fields=["change_type", "-operated_at"], name="idx_csl_type"),
|
||||||
|
]
|
||||||
114
apps/setting/migrations/0001_initial.py
Normal file
114
apps/setting/migrations/0001_initial.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# Generated by Django 4.2.16 on 2026-04-29 09:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('org', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='FieldRequirementRule',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('module', models.CharField(choices=[('property', '房源'), ('client', '客源')], max_length=20)),
|
||||||
|
('entity_type', models.CharField(choices=[('residential', '住宅'), ('villa', '别墅'), ('commercial_residential', '商住'), ('shop', '商铺'), ('office', '写字楼'), ('other', '其他')], max_length=50)),
|
||||||
|
('trade_status', models.CharField(choices=[('sale', '出售'), ('rent', '出租'), ('sale_rent', '租售'), ('*', '全部')], max_length=20)),
|
||||||
|
('field_key', models.CharField(max_length=50)),
|
||||||
|
('requirement', models.CharField(choices=[('required', '必填'), ('optional', '选填'), ('hidden', '隐藏')], max_length=10)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'field_requirement_rules',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='LookupGroup',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('module', models.CharField(max_length=50)),
|
||||||
|
('key', models.CharField(max_length=100)),
|
||||||
|
('label_zh', models.CharField(max_length=50)),
|
||||||
|
('description', models.TextField(blank=True, default='')),
|
||||||
|
('sort_order', models.SmallIntegerField(default=0)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'lookup_groups',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='TenantSetting',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('category', models.CharField(max_length=50)),
|
||||||
|
('key', models.CharField(max_length=100)),
|
||||||
|
('value', models.JSONField()),
|
||||||
|
('value_type', models.CharField(choices=[('bool', '布尔'), ('int', '整数'), ('string', '字符串'), ('enum', '枚举')], max_length=20)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tenant_settings', to='org.staff')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'tenant_settings',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='LookupItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('value', models.CharField(max_length=100)),
|
||||||
|
('label_zh', models.CharField(max_length=50)),
|
||||||
|
('is_system', models.BooleanField(default=False)),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('sort_order', models.SmallIntegerField(default=0)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_lookup_items', to='org.staff')),
|
||||||
|
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='setting.lookupgroup')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'lookup_items',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='lookupgroup',
|
||||||
|
constraint=models.UniqueConstraint(fields=('module', 'key'), name='uq_lookup_groups_module_key'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='fieldrequirementrule',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_field_rules', to='org.staff'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='tenantsetting',
|
||||||
|
index=models.Index(fields=['category'], name='idx_tenant_settings_cat'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='tenantsetting',
|
||||||
|
constraint=models.UniqueConstraint(fields=('category', 'key'), name='uq_tenant_settings_cat_key'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='lookupitem',
|
||||||
|
index=models.Index(fields=['group', 'is_active', 'sort_order'], name='idx_lookup_items_active'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='lookupitem',
|
||||||
|
constraint=models.UniqueConstraint(fields=('group', 'value'), name='uq_lookup_items_group_value'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='fieldrequirementrule',
|
||||||
|
index=models.Index(fields=['module', 'entity_type', 'trade_status'], name='idx_field_req_lookup'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='fieldrequirementrule',
|
||||||
|
constraint=models.UniqueConstraint(fields=('module', 'entity_type', 'trade_status', 'field_key'), name='uq_field_req_quad'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from .lookup import LookupGroup, LookupItem
|
||||||
|
from .setting import FieldRequirementRule, TenantSetting
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"LookupGroup",
|
||||||
|
"LookupItem",
|
||||||
|
"TenantSetting",
|
||||||
|
"FieldRequirementRule",
|
||||||
|
]
|
||||||
|
|||||||
55
apps/setting/models/lookup.py
Normal file
55
apps/setting/models/lookup.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from core.models.base import UUIDPrimaryKeyModel
|
||||||
|
|
||||||
|
|
||||||
|
class LookupGroup(UUIDPrimaryKeyModel):
|
||||||
|
module = models.CharField(max_length=50)
|
||||||
|
key = models.CharField(max_length=100)
|
||||||
|
label_zh = models.CharField(max_length=50)
|
||||||
|
description = models.TextField(blank=True, default="")
|
||||||
|
sort_order = models.SmallIntegerField(default=0)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "lookup_groups"
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["module", "key"], name="uq_lookup_groups_module_key"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class LookupItem(UUIDPrimaryKeyModel):
|
||||||
|
group = models.ForeignKey(
|
||||||
|
LookupGroup, on_delete=models.CASCADE, related_name="items"
|
||||||
|
)
|
||||||
|
value = models.CharField(max_length=100)
|
||||||
|
label_zh = models.CharField(max_length=50)
|
||||||
|
is_system = models.BooleanField(default=False)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
sort_order = models.SmallIntegerField(default=0)
|
||||||
|
created_by = models.ForeignKey(
|
||||||
|
"org.Staff",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="created_lookup_items",
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "lookup_items"
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["group", "value"], name="uq_lookup_items_group_value"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
indexes = [
|
||||||
|
models.Index(
|
||||||
|
fields=["group", "is_active", "sort_order"],
|
||||||
|
name="idx_lookup_items_active",
|
||||||
|
),
|
||||||
|
]
|
||||||
75
apps/setting/models/setting.py
Normal file
75
apps/setting/models/setting.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from core.enums import (
|
||||||
|
FieldRuleEntityType,
|
||||||
|
FieldRuleModule,
|
||||||
|
FieldRuleRequirement,
|
||||||
|
SettingValueType,
|
||||||
|
)
|
||||||
|
from core.models.base import UUIDPrimaryKeyModel
|
||||||
|
|
||||||
|
TRADE_STATUS_CHOICES = (
|
||||||
|
("sale", "出售"),
|
||||||
|
("rent", "出租"),
|
||||||
|
("sale_rent", "租售"),
|
||||||
|
("*", "全部"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TenantSetting(UUIDPrimaryKeyModel):
|
||||||
|
category = models.CharField(max_length=50)
|
||||||
|
key = models.CharField(max_length=100)
|
||||||
|
value = models.JSONField()
|
||||||
|
value_type = models.CharField(max_length=20, choices=SettingValueType.choices)
|
||||||
|
updated_by = models.ForeignKey(
|
||||||
|
"org.Staff",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="updated_tenant_settings",
|
||||||
|
)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "tenant_settings"
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["category", "key"], name="uq_tenant_settings_cat_key"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["category"], name="idx_tenant_settings_cat"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class FieldRequirementRule(UUIDPrimaryKeyModel):
|
||||||
|
module = models.CharField(max_length=20, choices=FieldRuleModule.choices)
|
||||||
|
entity_type = models.CharField(max_length=50, choices=FieldRuleEntityType.choices)
|
||||||
|
trade_status = models.CharField(max_length=20, choices=TRADE_STATUS_CHOICES)
|
||||||
|
field_key = models.CharField(max_length=50)
|
||||||
|
requirement = models.CharField(
|
||||||
|
max_length=10, choices=FieldRuleRequirement.choices
|
||||||
|
)
|
||||||
|
updated_by = models.ForeignKey(
|
||||||
|
"org.Staff",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="updated_field_rules",
|
||||||
|
)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "field_requirement_rules"
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["module", "entity_type", "trade_status", "field_key"],
|
||||||
|
name="uq_field_req_quad",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
indexes = [
|
||||||
|
models.Index(
|
||||||
|
fields=["module", "entity_type", "trade_status"],
|
||||||
|
name="idx_field_req_lookup",
|
||||||
|
),
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user