diff --git a/.env.example b/.env.example deleted file mode 100644 index c58aeeb..0000000 --- a/.env.example +++ /dev/null @@ -1,38 +0,0 @@ -# ============================================================ -# Fonrey .env.example -# Copy to .env and fill real values. Never commit .env. -# Generate PHONE_ENCRYPTION_KEY: -# python -c 'import secrets,base64; print(base64.b64encode(secrets.token_bytes(32)).decode())' -# Generate SECRET_KEY: -# python -c 'import secrets; print(secrets.token_urlsafe(50))' -# ============================================================ - -# ----- Django ----- -SECRET_KEY= -DEBUG=True -DJANGO_SETTINGS_MODULE=config.settings.development -ALLOWED_HOSTS=localhost,127.0.0.1 - -# ----- Database (PostgreSQL 16) ----- -DB_NAME=fonrey -DB_USER=fonrey -DB_PASSWORD=fonrey -DB_HOST=db -DB_PORT=5432 - -# ----- Redis (Cache + Sessions + Celery) ----- -REDIS_URL=redis://redis:6379/0 -CELERY_BROKER_URL=redis://redis:6379/1 - -# ----- Cloudflare R2 (S3-compatible) ----- -R2_ENDPOINT_URL=https://.r2.cloudflarestorage.com -R2_ACCESS_KEY_ID= -R2_SECRET_ACCESS_KEY= -R2_BUCKET_NAME=media -R2_CUSTOM_DOMAIN= - -# ----- Sentry (production only) ----- -SENTRY_DSN= - -# ----- PII Encryption (AES-256-GCM, 32 bytes base64) ----- -PHONE_ENCRYPTION_KEY= diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 288ace4..0000000 --- a/.gitignore +++ /dev/null @@ -1,56 +0,0 @@ -# Python -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -*.egg-info/ -.eggs/ -build/ -dist/ - -# Environment -.env -.env.local -.env.*.local -.venv/ -venv/ - -# Sisyphus session state -.sisyphus/ - -# Django -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal -media/ -staticfiles/ - -# Tailwind build output -static/css/output.css - -# Frontend -node_modules/ - -# OS / Editor -.DS_Store -.idea/ -.vscode/ -*.swp -*.swo - -# Testing / coverage -.pytest_cache/ -.coverage -.coverage.* -htmlcov/ -coverage.xml -.tox/ - -# Tooling caches -.ruff_cache/ -.mypy_cache/ - -# Generated artifacts -openapi.json diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 7b67b3c..0000000 --- a/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM python:3.12-slim - -WORKDIR /app - -RUN apt-get update && apt-get install -y \ - libpq-dev gcc \ - && rm -rf /var/lib/apt/lists/* - -COPY requirements/base.txt requirements/base.txt - -RUN pip install --no-cache-dir \ - --proxy http://host.docker.internal:10808 \ - --timeout 120 \ - -r requirements/base.txt - -COPY . . - -EXPOSE 8000 - -CMD ["uvicorn", "config.asgi:application", "--host", "0.0.0.0", "--port", "8000"] diff --git a/Makefile b/Makefile deleted file mode 100644 index 28b2e43..0000000 --- a/Makefile +++ /dev/null @@ -1,23 +0,0 @@ -.PHONY: dev migrate shell createsuperuser test lint tailwind-build - -dev: - docker compose up - -migrate: - docker compose exec web python manage.py migrate_schemas --shared - docker compose exec web python manage.py migrate_schemas - -shell: - docker compose exec web python manage.py shell_plus - -test: - docker compose exec web pytest apps/ -v - -lint: - ruff check . && black --check . - -tailwind-build: - npm run build - -createsuperuser: - docker compose exec web python manage.py create_tenant_superuser diff --git a/apps/__init__.py b/apps/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/account/__init__.py b/apps/account/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/account/admin.py b/apps/account/admin.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/account/apps.py b/apps/account/apps.py deleted file mode 100644 index e2afed6..0000000 --- a/apps/account/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class AccountConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.account" - label = "account" diff --git a/apps/account/migrations/0001_initial.py b/apps/account/migrations/0001_initial.py deleted file mode 100644 index eb46cef..0000000 --- a/apps/account/migrations/0001_initial.py +++ /dev/null @@ -1,81 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-29 08:42 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='UserAccount', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('username', models.CharField(max_length=30)), - ('email', models.EmailField(blank=True, max_length=254, null=True)), - ('phone_enc', models.TextField(blank=True, help_text='AES-256-GCM ciphertext of phone (core.encryption.PhoneEncryption).', null=True)), - ('phone_hash', models.CharField(blank=True, max_length=64, null=True)), - ('is_tenant_admin', models.BooleanField(default=False)), - ('status', models.CharField(choices=[('active', '启用'), ('disabled', '停用'), ('locked', '锁定')], default='active', max_length=10)), - ('is_initial_password', models.BooleanField(default=True)), - ('locked_until', models.DateTimeField(blank=True, null=True)), - ('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_accounts', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'db_table': 'user_accounts', - }, - ), - migrations.CreateModel( - name='PasswordResetToken', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('token', models.CharField(max_length=86, unique=True)), - ('expires_at', models.DateTimeField()), - ('is_used', models.BooleanField(default=False)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reset_tokens', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'db_table': 'password_reset_tokens', - }, - ), - migrations.CreateModel( - name='PasswordHistory', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password_hash', models.CharField(max_length=128)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='password_histories', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'db_table': 'password_histories', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='LoginAttempt', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('username', models.CharField(max_length=30)), - ('ip_address', models.GenericIPAddressField()), - ('user_agent', models.TextField(blank=True, null=True)), - ('success', models.BooleanField()), - ('failure_reason', models.CharField(blank=True, choices=[('wrong_password', '用户名或密码错误'), ('wrong_captcha', '验证码错误'), ('account_locked', '账号锁定'), ('account_disabled', '账号停用'), ('tenant_not_found', '租户不存在')], max_length=30, null=True)), - ('attempted_at', models.DateTimeField(auto_now_add=True)), - ], - options={ - 'db_table': 'login_attempts', - 'indexes': [models.Index(fields=['username'], name='idx_login_attempts_username'), models.Index(fields=['ip_address'], name='idx_login_attempts_ip'), models.Index(fields=['-attempted_at'], name='idx_login_attempts_time'), models.Index(fields=['username', 'success', '-attempted_at'], name='idx_login_attempts_fail_check')], - }, - ), - ] diff --git a/apps/account/migrations/0002_initial.py b/apps/account/migrations/0002_initial.py deleted file mode 100644 index d3d3194..0000000 --- a/apps/account/migrations/0002_initial.py +++ /dev/null @@ -1,50 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-29 08:42 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('account', '0001_initial'), - ('org', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='useraccount', - name='staff', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='account', to='org.staff'), - ), - migrations.AddIndex( - model_name='passwordresettoken', - index=models.Index(fields=['user'], name='idx_pw_reset_tokens_user'), - ), - migrations.AddIndex( - model_name='passwordhistory', - index=models.Index(fields=['user', '-created_at'], name='idx_pw_histories_user'), - ), - migrations.AddIndex( - model_name='useraccount', - index=models.Index(fields=['status'], name='idx_user_accounts_status'), - ), - migrations.AddIndex( - model_name='useraccount', - index=models.Index(fields=['staff'], name='idx_user_accounts_staff'), - ), - migrations.AddConstraint( - model_name='useraccount', - constraint=models.UniqueConstraint(fields=('username',), name='uq_user_accounts_username'), - ), - migrations.AddConstraint( - model_name='useraccount', - constraint=models.UniqueConstraint(condition=models.Q(('email__isnull', False)), fields=('email',), name='uq_user_accounts_email'), - ), - migrations.AddConstraint( - model_name='useraccount', - constraint=models.UniqueConstraint(condition=models.Q(('phone_hash__isnull', False)), fields=('phone_hash',), name='uq_user_accounts_phone'), - ), - ] diff --git a/apps/account/migrations/0003_alter_loginattempt_options_and_more.py b/apps/account/migrations/0003_alter_loginattempt_options_and_more.py deleted file mode 100644 index ed8946d..0000000 --- a/apps/account/migrations/0003_alter_loginattempt_options_and_more.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-29 11:07 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('account', '0002_initial'), - ] - - operations = [ - migrations.AlterModelOptions( - name='loginattempt', - options={'verbose_name': '登录尝试记录', 'verbose_name_plural': '登录尝试记录'}, - ), - migrations.AlterModelOptions( - name='passwordhistory', - options={'ordering': ['-created_at'], 'verbose_name': '历史密码', 'verbose_name_plural': '历史密码'}, - ), - migrations.AlterModelOptions( - name='passwordresettoken', - options={'verbose_name': '密码重置令牌', 'verbose_name_plural': '密码重置令牌'}, - ), - migrations.AlterModelOptions( - name='useraccount', - options={'verbose_name': '用户账号', 'verbose_name_plural': '用户账号'}, - ), - ] diff --git a/apps/account/migrations/0004_alter_loginattempt_attempted_at_and_more.py b/apps/account/migrations/0004_alter_loginattempt_attempted_at_and_more.py deleted file mode 100644 index 065a694..0000000 --- a/apps/account/migrations/0004_alter_loginattempt_attempted_at_and_more.py +++ /dev/null @@ -1,146 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-30 01:46 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('org', '0003_alter_orgunit_address_city_and_more'), - ('account', '0003_alter_loginattempt_options_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='loginattempt', - name='attempted_at', - field=models.DateTimeField(auto_now_add=True, help_text='分区键,按月分区', verbose_name='尝试时间'), - ), - migrations.AlterField( - model_name='loginattempt', - name='failure_reason', - field=models.CharField(blank=True, choices=[('wrong_password', '用户名或密码错误'), ('wrong_captcha', '验证码错误'), ('account_locked', '账号锁定'), ('account_disabled', '账号停用'), ('tenant_not_found', '租户不存在')], help_text='wrong_password=密码错误 / wrong_captcha=验证码失败 / account_locked=账号锁定 / account_disabled=账号停用 / tenant_not_found=租户不存在', max_length=30, null=True, verbose_name='失败原因'), - ), - migrations.AlterField( - model_name='loginattempt', - name='ip_address', - field=models.GenericIPAddressField(help_text='支持 IPv4/IPv6', verbose_name='来源 IP'), - ), - migrations.AlterField( - model_name='loginattempt', - name='success', - field=models.BooleanField(verbose_name='是否登录成功'), - ), - migrations.AlterField( - model_name='loginattempt', - name='user_agent', - field=models.TextField(blank=True, help_text='Electron 版本信息', null=True, verbose_name='客户端 UA'), - ), - migrations.AlterField( - model_name='loginattempt', - name='username', - field=models.CharField(help_text='冗余存储,即使账号不存在也记录', max_length=30, verbose_name='登录用户名'), - ), - migrations.AlterField( - model_name='passwordhistory', - name='created_at', - field=models.DateTimeField(auto_now_add=True, help_text='密码修改时间', verbose_name='记录时间'), - ), - migrations.AlterField( - model_name='passwordhistory', - name='password_hash', - field=models.CharField(help_text='PBKDF2+SHA256 哈希值', max_length=128, verbose_name='密码哈希'), - ), - migrations.AlterField( - model_name='passwordhistory', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='password_histories', to=settings.AUTH_USER_MODEL, verbose_name='关联账号'), - ), - migrations.AlterField( - model_name='passwordresettoken', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'), - ), - migrations.AlterField( - model_name='passwordresettoken', - name='expires_at', - field=models.DateTimeField(help_text='created_at + 30 分钟', verbose_name='过期时间'), - ), - migrations.AlterField( - model_name='passwordresettoken', - name='is_used', - field=models.BooleanField(default=False, help_text='使用后立即置 True,防止重放攻击', verbose_name='是否已使用'), - ), - migrations.AlterField( - model_name='passwordresettoken', - name='token', - field=models.CharField(help_text='secrets.token_urlsafe(64) 生成(86 字符),全局唯一', max_length=86, unique=True, verbose_name='令牌'), - ), - migrations.AlterField( - model_name='passwordresettoken', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reset_tokens', to=settings.AUTH_USER_MODEL, verbose_name='关联账号'), - ), - migrations.AlterField( - model_name='useraccount', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'), - ), - migrations.AlterField( - model_name='useraccount', - name='created_by', - field=models.ForeignKey(blank=True, help_text='普通员工由 Tenant Admin 创建;Tenant Admin 由平台运营创建(可为 NULL)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_accounts', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), - ), - migrations.AlterField( - model_name='useraccount', - name='email', - field=models.EmailField(blank=True, help_text='用于找回密码/用户名;为空则无法自助找回;同租户唯一', max_length=254, null=True, verbose_name='绑定邮箱'), - ), - migrations.AlterField( - model_name='useraccount', - name='is_initial_password', - field=models.BooleanField(default=True, help_text='True 时登录成功后强制跳转修改密码页,不可跳过', verbose_name='是否初始密码'), - ), - migrations.AlterField( - model_name='useraccount', - name='is_tenant_admin', - field=models.BooleanField(default=False, help_text='每个租户最多 1 个(应用层约束)', verbose_name='是否租户超管'), - ), - migrations.AlterField( - model_name='useraccount', - name='locked_until', - field=models.DateTimeField(blank=True, help_text='到期后应用层将 status 恢复 active', null=True, verbose_name='锁定到期时间'), - ), - migrations.AlterField( - model_name='useraccount', - name='phone_enc', - field=models.TextField(blank=True, help_text='AES-256-GCM 加密密文;普通员工必填', null=True, verbose_name='手机号(加密)'), - ), - migrations.AlterField( - model_name='useraccount', - name='phone_hash', - field=models.CharField(blank=True, help_text='SHA-256 哈希;用于唯一性校验和查询;不可反推原文', max_length=64, null=True, verbose_name='手机号哈希'), - ), - migrations.AlterField( - model_name='useraccount', - name='staff', - field=models.OneToOneField(blank=True, help_text='员工档案绑定(1:1);普通员工必须有值;Tenant Admin 可为空', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='account', to='org.staff', verbose_name='员工档案'), - ), - migrations.AlterField( - model_name='useraccount', - name='status', - field=models.CharField(choices=[('active', '启用'), ('disabled', '停用'), ('locked', '锁定')], default='active', help_text='active=正常 / disabled=停用 / locked=锁定(30 分钟自动恢复)', max_length=10, verbose_name='账号状态'), - ), - migrations.AlterField( - model_name='useraccount', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='最后更新时间'), - ), - migrations.AlterField( - model_name='useraccount', - name='username', - field=models.CharField(help_text='普通员工=手机号(11位数字) / Tenant Admin=自定义(字母开头6~30位);创建后不可更改', max_length=30, verbose_name='登录名'), - ), - ] diff --git a/apps/account/migrations/__init__.py b/apps/account/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/account/models/__init__.py b/apps/account/models/__init__.py deleted file mode 100644 index 6691cba..0000000 --- a/apps/account/models/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -from apps.account.models.account import ( - LoginAttempt, - PasswordHistory, - PasswordResetToken, - UserAccount, - UserAccountManager, -) - -__all__ = [ - "LoginAttempt", - "PasswordHistory", - "PasswordResetToken", - "UserAccount", - "UserAccountManager", -] diff --git a/apps/account/models/account.py b/apps/account/models/account.py deleted file mode 100644 index 70b3f9a..0000000 --- a/apps/account/models/account.py +++ /dev/null @@ -1,245 +0,0 @@ -from django.contrib.auth.models import AbstractBaseUser, BaseUserManager -from django.db import models -from django.utils import timezone - -from core.enums import LoginFailureReason, UserAccountStatus - - -class UserAccountManager(BaseUserManager): - def create_user(self, username, password=None, **extra_fields): - if not username: - raise ValueError("username 不能为空") - user = self.model(username=username, **extra_fields) - if password: - user.set_password(password) - user.save(using=self._db) - return user - - -class UserAccount(AbstractBaseUser): - username = models.CharField( - max_length=30, - verbose_name="登录名", - help_text="普通员工=手机号(11位数字) / Tenant Admin=自定义(字母开头6~30位);创建后不可更改", - ) - email = models.EmailField( - null=True, - blank=True, - verbose_name="绑定邮箱", - help_text="用于找回密码/用户名;为空则无法自助找回;同租户唯一", - ) - phone_enc = models.TextField( - null=True, - blank=True, - verbose_name="手机号(加密)", - help_text="AES-256-GCM 加密密文;普通员工必填", - ) - phone_hash = models.CharField( - max_length=64, - null=True, - blank=True, - verbose_name="手机号哈希", - help_text="SHA-256 哈希;用于唯一性校验和查询;不可反推原文", - ) - staff = models.OneToOneField( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="account", - verbose_name="员工档案", - help_text="员工档案绑定(1:1);普通员工必须有值;Tenant Admin 可为空", - ) - is_tenant_admin = models.BooleanField( - default=False, - verbose_name="是否租户超管", - help_text="每个租户最多 1 个(应用层约束)", - ) - status = models.CharField( - max_length=10, - choices=UserAccountStatus.choices, - default=UserAccountStatus.ACTIVE, - verbose_name="账号状态", - help_text="active=正常 / disabled=停用 / locked=锁定(30 分钟自动恢复)", - ) - is_initial_password = models.BooleanField( - default=True, - verbose_name="是否初始密码", - help_text="True 时登录成功后强制跳转修改密码页,不可跳过", - ) - locked_until = models.DateTimeField( - null=True, - blank=True, - verbose_name="锁定到期时间", - help_text="到期后应用层将 status 恢复 active", - ) - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name="创建时间", - ) - updated_at = models.DateTimeField( - auto_now=True, - verbose_name="最后更新时间", - ) - created_by = models.ForeignKey( - "self", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="created_accounts", - verbose_name="创建人", - help_text="普通员工由 Tenant Admin 创建;Tenant Admin 由平台运营创建(可为 NULL)", - ) - - USERNAME_FIELD = "username" - REQUIRED_FIELDS: list = [] - - objects = UserAccountManager() - - class Meta: - db_table = "user_accounts" - verbose_name = "用户账号" - verbose_name_plural = "用户账号" - constraints = [ - models.UniqueConstraint(fields=["username"], name="uq_user_accounts_username"), - models.UniqueConstraint( - fields=["email"], - name="uq_user_accounts_email", - condition=models.Q(email__isnull=False), - ), - models.UniqueConstraint( - fields=["phone_hash"], - name="uq_user_accounts_phone", - condition=models.Q(phone_hash__isnull=False), - ), - ] - indexes = [ - models.Index(fields=["status"], name="idx_user_accounts_status"), - models.Index(fields=["staff"], name="idx_user_accounts_staff"), - ] - - def __str__(self) -> str: - kind = "admin" if self.is_tenant_admin else "staff" - return f"{self.username} ({kind})" - - def is_locked(self) -> bool: - if self.status != UserAccountStatus.LOCKED: - return False - if self.locked_until and timezone.now() >= self.locked_until: - return False - return True - - -class LoginAttempt(models.Model): - username = models.CharField( - max_length=30, - verbose_name="登录用户名", - help_text="冗余存储,即使账号不存在也记录", - ) - ip_address = models.GenericIPAddressField( - verbose_name="来源 IP", - help_text="支持 IPv4/IPv6", - ) - user_agent = models.TextField( - null=True, - blank=True, - verbose_name="客户端 UA", - help_text="Electron 版本信息", - ) - success = models.BooleanField( - verbose_name="是否登录成功", - ) - failure_reason = models.CharField( - max_length=30, - null=True, - blank=True, - choices=LoginFailureReason.choices, - verbose_name="失败原因", - help_text="wrong_password=密码错误 / wrong_captcha=验证码失败 / account_locked=账号锁定 / account_disabled=账号停用 / tenant_not_found=租户不存在", - ) - attempted_at = models.DateTimeField( - auto_now_add=True, - verbose_name="尝试时间", - help_text="分区键,按月分区", - ) - - class Meta: - db_table = "login_attempts" - verbose_name = "登录尝试记录" - verbose_name_plural = "登录尝试记录" - indexes = [ - models.Index(fields=["username"], name="idx_login_attempts_username"), - models.Index(fields=["ip_address"], name="idx_login_attempts_ip"), - models.Index(fields=["-attempted_at"], name="idx_login_attempts_time"), - models.Index( - fields=["username", "success", "-attempted_at"], - name="idx_login_attempts_fail_check", - ), - ] - - -class PasswordResetToken(models.Model): - user = models.ForeignKey( - "account.UserAccount", - on_delete=models.CASCADE, - related_name="reset_tokens", - verbose_name="关联账号", - ) - token = models.CharField( - max_length=86, - unique=True, - verbose_name="令牌", - help_text="secrets.token_urlsafe(64) 生成(86 字符),全局唯一", - ) - expires_at = models.DateTimeField( - verbose_name="过期时间", - help_text="created_at + 30 分钟", - ) - is_used = models.BooleanField( - default=False, - verbose_name="是否已使用", - help_text="使用后立即置 True,防止重放攻击", - ) - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name="创建时间", - ) - - class Meta: - db_table = "password_reset_tokens" - verbose_name = "密码重置令牌" - verbose_name_plural = "密码重置令牌" - indexes = [ - models.Index(fields=["user"], name="idx_pw_reset_tokens_user"), - ] - - def is_valid(self) -> bool: - return not self.is_used and timezone.now() < self.expires_at - - -class PasswordHistory(models.Model): - user = models.ForeignKey( - "account.UserAccount", - on_delete=models.CASCADE, - related_name="password_histories", - verbose_name="关联账号", - ) - password_hash = models.CharField( - max_length=128, - verbose_name="密码哈希", - help_text="PBKDF2+SHA256 哈希值", - ) - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name="记录时间", - help_text="密码修改时间", - ) - - class Meta: - db_table = "password_histories" - verbose_name = "历史密码" - verbose_name_plural = "历史密码" - ordering = ["-created_at"] - indexes = [ - models.Index(fields=["user", "-created_at"], name="idx_pw_histories_user"), - ] diff --git a/apps/account/serializers.py b/apps/account/serializers.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/account/services/__init__.py b/apps/account/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/account/tasks.py b/apps/account/tasks.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/account/templates/account/.gitkeep b/apps/account/templates/account/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/apps/account/tests/__init__.py b/apps/account/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/account/urls.py b/apps/account/urls.py deleted file mode 100644 index 485e8cf..0000000 --- a/apps/account/urls.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.urls import path - -app_name = "account" - -urlpatterns: list = [] diff --git a/apps/account/views.py b/apps/account/views.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/client/__init__.py b/apps/client/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/client/admin.py b/apps/client/admin.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/client/apps.py b/apps/client/apps.py deleted file mode 100644 index 1ec880e..0000000 --- a/apps/client/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class ClientConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.client" - label = "fonrey_client" diff --git a/apps/client/migrations/0001_initial.py b/apps/client/migrations/0001_initial.py deleted file mode 100644 index ccb642e..0000000 --- a/apps/client/migrations/0001_initial.py +++ /dev/null @@ -1,351 +0,0 @@ -# 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'), - ), - ] diff --git a/apps/client/migrations/0002_partitions_and_triggers.py b/apps/client/migrations/0002_partitions_and_triggers.py deleted file mode 100644 index 678c8e5..0000000 --- a/apps/client/migrations/0002_partitions_and_triggers.py +++ /dev/null @@ -1,99 +0,0 @@ -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), - ] diff --git a/apps/client/migrations/0003_alter_client_options_alter_clientcontact_options_and_more.py b/apps/client/migrations/0003_alter_client_options_alter_clientcontact_options_and_more.py deleted file mode 100644 index ce8bfe8..0000000 --- a/apps/client/migrations/0003_alter_client_options_alter_clientcontact_options_and_more.py +++ /dev/null @@ -1,57 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-29 11:07 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('fonrey_client', '0002_partitions_and_triggers'), - ] - - operations = [ - migrations.AlterModelOptions( - name='client', - options={'verbose_name': '客源', 'verbose_name_plural': '客源'}, - ), - migrations.AlterModelOptions( - name='clientcontact', - options={'verbose_name': '客源联系人', 'verbose_name_plural': '客源联系人'}, - ), - migrations.AlterModelOptions( - name='clientfavoritefolder', - options={'verbose_name': '私客收藏夹', 'verbose_name_plural': '私客收藏夹'}, - ), - migrations.AlterModelOptions( - name='clientfolderitem', - options={'verbose_name': '收藏夹中的客源', 'verbose_name_plural': '收藏夹中的客源'}, - ), - migrations.AlterModelOptions( - name='clientfollowlog', - options={'managed': False, 'verbose_name': '客源跟进日志', 'verbose_name_plural': '客源跟进日志'}, - ), - migrations.AlterModelOptions( - name='clientfollowlogattachment', - options={'verbose_name': '客源跟进附件', 'verbose_name_plural': '客源跟进附件'}, - ), - migrations.AlterModelOptions( - name='clientpropertymatch', - options={'verbose_name': '智能配房', 'verbose_name_plural': '智能配房'}, - ), - migrations.AlterModelOptions( - name='clientrequirement', - options={'verbose_name': '客源需求', 'verbose_name_plural': '客源需求'}, - ), - migrations.AlterModelOptions( - name='clientschoolpreference', - options={'verbose_name': '意向学校', 'verbose_name_plural': '意向学校'}, - ), - migrations.AlterModelOptions( - name='clientstatuslog', - options={'verbose_name': '客源状态变更日志', 'verbose_name_plural': '客源状态变更日志'}, - ), - migrations.AlterModelOptions( - name='clientviewing', - options={'verbose_name': '带看记录', 'verbose_name_plural': '带看记录'}, - ), - ] diff --git a/apps/client/migrations/0004_alter_client_activity_level_and_more.py b/apps/client/migrations/0004_alter_client_activity_level_and_more.py deleted file mode 100644 index 066e8cc..0000000 --- a/apps/client/migrations/0004_alter_client_activity_level_and_more.py +++ /dev/null @@ -1,667 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-30 01:46 - -import django.contrib.postgres.fields -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('org', '0003_alter_orgunit_address_city_and_more'), - ('fonrey_property', '0004_alter_commission_agent_and_more'), - ('fonrey_client', '0003_alter_client_options_alter_clientcontact_options_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='client', - name='activity_level', - field=models.CharField(blank=True, choices=[('new_matched', '新配对'), ('active_7d', '7日活跃'), ('active_30d', '30日活跃'), ('active_90d', '90日活跃'), ('expiring', '即将过期'), ('frozen', '暂缓中'), ('invalid', '无效')], default='', help_text='new_matched=新配偶 / active_7d / active_30d / active_90d / expiring / frozen / invalid(异步计算)', max_length=20, verbose_name='活跃度'), - ), - migrations.AlterField( - model_name='client', - name='buying_purpose', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('rigid', '刚需'), ('investment', '投资'), ('school_district', '学区'), ('upgrade', '改善'), ('commercial', '商用'), ('other', '其他')], max_length=20), blank=True, default=list, help_text='多选:rigid=刚需 / investment=投资 / school_district=学区 / upgrade=改善 / commercial=商用 / other=其他', size=None, verbose_name='购房目的'), - ), - migrations.AlterField( - model_name='client', - name='client_no', - field=models.CharField(help_text='系统生成的客源编号,格式由运营配置(如 KY20260424001)', max_length=30, unique=True, verbose_name='客源编号'), - ), - migrations.AlterField( - model_name='client', - name='client_type', - field=models.CharField(choices=[('private', '私客'), ('public', '公客'), ('transacted', '成交客')], default='private', help_text='private=私客 / public=公客 / transacted=成交客', max_length=20, verbose_name='客源分类'), - ), - migrations.AlterField( - model_name='client', - name='commission_date', - field=models.DateField(blank=True, null=True, verbose_name='委托日期'), - ), - migrations.AlterField( - model_name='client', - name='entrust_count', - field=models.SmallIntegerField(default=1, help_text='成交后再委托则累加', verbose_name='委托次数'), - ), - migrations.AlterField( - model_name='client', - name='first_recorder', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='first_recorded_clients', to='org.staff', verbose_name='首录人'), - ), - migrations.AlterField( - model_name='client', - name='grade', - field=models.CharField(choices=[('A', 'A(急迫)'), ('B', 'B(较强)'), ('C', 'C(一般)'), ('D', 'D(较弱)'), ('E', 'E(暂不关注)')], default='C', help_text='A=A急迫 / B=较强 / C=一般 / D=较弱 / E=暂不关注', max_length=5, verbose_name='客源等级'), - ), - migrations.AlterField( - model_name='client', - name='has_loan_record', - field=models.BooleanField(blank=True, null=True, verbose_name='有无贷款记录'), - ), - migrations.AlterField( - model_name='client', - name='id_number_enc', - field=models.BinaryField(blank=True, help_text='AES 加密存储', null=True, verbose_name='证件号码(加密)'), - ), - migrations.AlterField( - model_name='client', - name='id_type', - field=models.CharField(blank=True, choices=[('id_card', '身份证'), ('passport', '护照'), ('hk_macao', '港澳通行证'), ('other', '其他')], default='', help_text='id_card=身份证 / passport=护照 / hk_macao=港澳台 / other=其他', max_length=20, verbose_name='证件类型'), - ), - migrations.AlterField( - model_name='client', - name='invalid_reason', - field=models.CharField(blank=True, choices=[('invalid_phone', '号码无效'), ('peer_agent', '同行'), ('ad', '广告推销'), ('no_intent', '无意向'), ('other', '其他')], default='', help_text='invalid_phone=号码无效 / peer_agent=同行 / ad=广告推销 / no_intent=无意向 / other=其他', max_length=30, verbose_name='无效原因'), - ), - migrations.AlterField( - model_name='client', - name='invalidated_at', - field=models.DateTimeField(blank=True, null=True, verbose_name='标记无效时间'), - ), - migrations.AlterField( - model_name='client', - name='is_big_value', - field=models.BooleanField(default=False, help_text='影响筛选展示', verbose_name='是否大价值客户'), - ), - migrations.AlterField( - model_name='client', - name='is_pinned', - field=models.BooleanField(default=False, help_text='列表顶部置顶', verbose_name='是否置顶'), - ), - migrations.AlterField( - model_name='client', - name='is_protected', - field=models.BooleanField(default=False, help_text='影响转公逻辑', verbose_name='是否保护客'), - ), - migrations.AlterField( - model_name='client', - name='is_starred', - field=models.BooleanField(default=False, help_text='快速标记,详细收藏夹用 client_folder_items', verbose_name='是否收藏'), - ), - migrations.AlterField( - model_name='client', - name='last_active_at', - field=models.DateTimeField(blank=True, help_text='触发器维护', null=True, verbose_name='最后有效跟进时间'), - ), - migrations.AlterField( - model_name='client', - name='last_follow_at', - field=models.DateTimeField(blank=True, help_text='冗余字段,列表排序用', null=True, verbose_name='最后跟进时间'), - ), - migrations.AlterField( - model_name='client', - name='org_unit', - field=models.ForeignKey(blank=True, help_text='冗余字段,加速筛选', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='clients', to='org.orgunit', verbose_name='归属部门'), - ), - migrations.AlterField( - model_name='client', - name='owner', - field=models.ForeignKey(blank=True, help_text='私客独占跟进人', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owned_clients', to='org.staff', verbose_name='归属人'), - ), - migrations.AlterField( - model_name='client', - name='payment_method', - field=models.CharField(blank=True, choices=[('full', '全额'), ('mortgage', '商业贷款'), ('mortgage_fund', '商贷+公积金'), ('fund', '公积金')], default='', help_text='full=全额 / mortgage=商业贷款 / mortgage_fund=商贷+公积金 / fund=公积金', max_length=30, verbose_name='付款方式'), - ), - migrations.AlterField( - model_name='client', - name='prefers_new_house', - field=models.BooleanField(blank=True, help_text='用于筛选', null=True, verbose_name='偏好新房'), - ), - migrations.AlterField( - model_name='client', - name='properties_owned', - field=models.CharField(blank=True, choices=[('none', '无'), ('local_none', '本地无/外地有'), ('local_has', '本地有')], default='', help_text='none=无 / local_none=本地无外地有 / local_has=本地有', max_length=20, verbose_name='名下房产'), - ), - migrations.AlterField( - model_name='client', - name='property_usage', - field=models.CharField(choices=[('residential', '住宅'), ('villa', '别墅'), ('commercial_residential', '商住'), ('shop', '商铺'), ('office', '写字楼'), ('other', '其他')], default='residential', help_text='residential=住宅 / villa=别墅 / commercial_residential=商住 / shop=商铺 / office=写字楼 / other=其他', max_length=30, verbose_name='房屋用途'), - ), - migrations.AlterField( - model_name='client', - name='remarks', - field=models.TextField(blank=True, default='', help_text='最多200字', verbose_name='备注'), - ), - migrations.AlterField( - model_name='client', - name='source', - field=models.CharField(blank=True, default='', help_text='lookup_items 维护', max_length=50, verbose_name='客户来源'), - ), - migrations.AlterField( - model_name='client', - name='status', - field=models.CharField(choices=[('buying', '求购'), ('renting', '求租'), ('buy_or_rent', '租购'), ('suspended', '暂缓'), ('bought', '已购'), ('rented_done', '已租'), ('public', '公客'), ('invalid', '无效')], default='buying', help_text='buying=求购 / renting=求租 / buy_or_rent=租购 / suspended=暂缓 / bought=已购 / rented_done=已租 / public=公客 / invalid=无效(详见 ENUMS)', max_length=20, verbose_name='客源状态'), - ), - migrations.AlterField( - model_name='client', - name='transacted_at', - field=models.DateField(blank=True, null=True, verbose_name='成交日期'), - ), - migrations.AlterField( - model_name='client', - name='transacted_price', - field=models.DecimalField(blank=True, decimal_places=2, help_text='单位:万元', max_digits=12, null=True, verbose_name='成交价格'), - ), - migrations.AlterField( - model_name='client', - name='transacted_property', - field=models.ForeignKey(blank=True, help_text='成交关联的房源', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transacted_clients', to='fonrey_property.property', verbose_name='成交房源'), - ), - migrations.AlterField( - model_name='client', - name='transacted_property_type', - field=models.CharField(blank=True, choices=[('second_hand', '二手'), ('new_house', '新房')], default='', help_text='second_hand=二手 / new_house=新房', max_length=20, verbose_name='成交房源类型'), - ), - migrations.AlterField( - model_name='client', - name='transacted_type', - field=models.CharField(blank=True, choices=[('bought', '我购'), ('rented', '我租')], default='', help_text='bought=我购 / rented=我租', max_length=20, verbose_name='成交类型'), - ), - migrations.AlterField( - model_name='client', - name='transfer_to_public_type', - field=models.CharField(blank=True, choices=[('manual', '手动转公'), ('auto', '自动转公'), ('marketing_jump', '营销客跳公'), ('resource_public', '资料客素公')], default='', help_text='manual=手动转公 / auto=自动转公(超时) / marketing_jump=营销客跳公 / resource_public=资料客素公', max_length=20, verbose_name='转公客方式'), - ), - migrations.AlterField( - model_name='client', - name='transferred_public_at', - field=models.DateTimeField(blank=True, null=True, verbose_name='进入公客池时间'), - ), - migrations.AlterField( - model_name='client', - name='version', - field=models.IntegerField(default=1, help_text='乐观锁;每次 UPDATE +1;应用层检测 0 行受影响时抛 ConflictError', verbose_name='版本号'), - ), - migrations.AlterField( - model_name='clientcontact', - name='client', - field=models.ForeignKey(help_text='联系人随客源级联删除', on_delete=django.db.models.deletion.CASCADE, related_name='contacts', to='fonrey_client.client', verbose_name='所属客源'), - ), - migrations.AlterField( - model_name='clientcontact', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'), - ), - migrations.AlterField( - model_name='clientcontact', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_client_contacts', to='org.staff', verbose_name='创建人'), - ), - migrations.AlterField( - model_name='clientcontact', - name='deleted_at', - field=models.DateTimeField(blank=True, help_text='软删除时间戳;NULL=未删除(不影响客源本身)', null=True, verbose_name='删除时间'), - ), - migrations.AlterField( - model_name='clientcontact', - name='gender', - field=models.CharField(choices=[('male', '先生'), ('female', '女士')], default='male', help_text='male=先生 / female=女士', max_length=10, verbose_name='性别'), - ), - migrations.AlterField( - model_name='clientcontact', - name='name', - field=models.CharField(max_length=50, verbose_name='联系人姓名'), - ), - migrations.AlterField( - model_name='clientcontact', - name='phone2_enc', - field=models.BinaryField(blank=True, null=True, verbose_name='备用电话2(加密)'), - ), - migrations.AlterField( - model_name='clientcontact', - name='phone2_hash', - field=models.CharField(blank=True, default='', help_text='SHA-256,用于重复检测', max_length=64, verbose_name='备用电话2哈希'), - ), - migrations.AlterField( - model_name='clientcontact', - name='phone_country_code', - field=models.CharField(default='+86', max_length=10, verbose_name='国际区号'), - ), - migrations.AlterField( - model_name='clientcontact', - name='phone_enc', - field=models.BinaryField(help_text='AES-256-GCM 加密手机号(电话1)', verbose_name='手机号(加密)'), - ), - migrations.AlterField( - model_name='clientcontact', - name='phone_hash', - field=models.CharField(help_text='SHA-256 哈希(重复检测)', max_length=64, verbose_name='手机号哈希'), - ), - migrations.AlterField( - model_name='clientcontact', - name='phone_is_invalid', - field=models.BooleanField(default=False, help_text='标记无效后该号码不再参与重复检测', verbose_name='号码是否无效'), - ), - migrations.AlterField( - model_name='clientcontact', - name='qq', - field=models.CharField(blank=True, default='', max_length=20, verbose_name='QQ号'), - ), - migrations.AlterField( - model_name='clientcontact', - name='remarks', - field=models.CharField(blank=True, default='', help_text='最多200字', max_length=200, verbose_name='联系人备注'), - ), - migrations.AlterField( - model_name='clientcontact', - name='sort_order', - field=models.SmallIntegerField(default=0, help_text='sort_order=0 为主联系人,姓名用于客源姓名显示', verbose_name='排序顺序'), - ), - migrations.AlterField( - model_name='clientcontact', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='最后更新时间'), - ), - migrations.AlterField( - model_name='clientcontact', - name='wechat', - field=models.CharField(blank=True, default='', max_length=100, verbose_name='微信号'), - ), - migrations.AlterField( - model_name='clientfavoritefolder', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'), - ), - migrations.AlterField( - model_name='clientfavoritefolder', - name='deleted_at', - field=models.DateTimeField(blank=True, help_text='软删除时间戳;NULL=未删除', null=True, verbose_name='删除时间'), - ), - migrations.AlterField( - model_name='clientfavoritefolder', - name='is_default', - field=models.BooleanField(default=False, help_text='系统默认收藏夹,每个经纪人只能有一个', verbose_name='是否默认'), - ), - migrations.AlterField( - model_name='clientfavoritefolder', - name='name', - field=models.CharField(help_text='最多10字', max_length=10, verbose_name='收藏夹名称'), - ), - migrations.AlterField( - model_name='clientfavoritefolder', - name='sort_order', - field=models.IntegerField(default=0, help_text='升序排列', verbose_name='显示顺序'), - ), - migrations.AlterField( - model_name='clientfavoritefolder', - name='staff', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorite_folders', to='org.staff', verbose_name='所属经纪人'), - ), - migrations.AlterField( - model_name='clientfolderitem', - name='added_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='加入收藏夹时间'), - ), - migrations.AlterField( - model_name='clientfolderitem', - name='client', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='folder_items', to='fonrey_client.client', verbose_name='被收藏的客源'), - ), - migrations.AlterField( - model_name='clientfolderitem', - name='folder', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='fonrey_client.clientfavoritefolder', verbose_name='所属收藏夹'), - ), - migrations.AlterField( - model_name='clientfollowlogattachment', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'), - ), - migrations.AlterField( - model_name='clientfollowlogattachment', - name='file_key', - field=models.TextField(help_text='R2/S3 存储路径', verbose_name='文件存储路径'), - ), - migrations.AlterField( - model_name='clientfollowlogattachment', - name='file_name', - field=models.CharField(help_text='原始文件名(用于展示和下载)', max_length=255, verbose_name='文件名'), - ), - migrations.AlterField( - model_name='clientfollowlogattachment', - name='file_size', - field=models.IntegerField(help_text='单位:bytes,最大 20MB', verbose_name='文件大小'), - ), - migrations.AlterField( - model_name='clientfollowlogattachment', - name='file_type', - field=models.CharField(blank=True, default='', help_text='bmp / jpg / png / gif', max_length=10, verbose_name='文件类型'), - ), - migrations.AlterField( - model_name='clientfollowlogattachment', - name='follow_log_id', - field=models.UUIDField(help_text='跨分区 FK;不通过 Django FK 强制约束', verbose_name='所属跟进日志ID'), - ), - migrations.AlterField( - model_name='clientfollowlogattachment', - name='has_location', - field=models.BooleanField(default=False, help_text='是否含 GPS 位置信息', verbose_name='是否含位置信息'), - ), - migrations.AlterField( - model_name='clientfollowlogattachment', - name='sort_order', - field=models.SmallIntegerField(default=0, verbose_name='排序顺序'), - ), - migrations.AlterField( - model_name='clientpropertymatch', - name='calculated_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='配房计算时间'), - ), - migrations.AlterField( - model_name='clientpropertymatch', - name='client', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='property_matches', to='fonrey_client.client', verbose_name='所属客源'), - ), - migrations.AlterField( - model_name='clientpropertymatch', - name='created_by', - field=models.ForeignKey(blank=True, help_text='触发配房操作的员工(录客配房时记录,系统配房可为NULL)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_matches', to='org.staff', verbose_name='创建人'), - ), - migrations.AlterField( - model_name='clientpropertymatch', - name='feedback', - field=models.CharField(blank=True, default='', help_text='lookup_items 维护', max_length=50, verbose_name='反馈原因'), - ), - migrations.AlterField( - model_name='clientpropertymatch', - name='match_group', - field=models.CharField(blank=True, choices=[('quality_layout', '优质户型'), ('price_reduced', '降价'), ('hot', '热门'), ('newly_listed', '新上')], default='', help_text='quality_layout=优质户型 / price_reduced=降价 / hot=热门 / newly_listed=新上', max_length=30, verbose_name='匹配分组'), - ), - migrations.AlterField( - model_name='clientpropertymatch', - name='match_reasons', - field=models.JSONField(blank=True, help_text='格式:[{"key": "budget", "match": true}, ...]', null=True, verbose_name='匹配原因详情'), - ), - migrations.AlterField( - model_name='clientpropertymatch', - name='match_score', - field=models.DecimalField(blank=True, decimal_places=2, help_text='0-100', max_digits=5, null=True, verbose_name='匹配度评分'), - ), - migrations.AlterField( - model_name='clientpropertymatch', - name='match_source', - field=models.CharField(choices=[('recorded', '录客配房'), ('system', '系统配房')], default='recorded', help_text='recorded=录客配房(基于录入需求) / system=系统配房(算法推荐)', max_length=20, verbose_name='匹配来源'), - ), - migrations.AlterField( - model_name='clientpropertymatch', - name='property', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='client_matches', to='fonrey_property.property', verbose_name='匹配房源'), - ), - migrations.AlterField( - model_name='clientpropertymatch', - name='shared_at', - field=models.DateTimeField(blank=True, null=True, verbose_name='分享时间'), - ), - migrations.AlterField( - model_name='clientpropertymatch', - name='status', - field=models.CharField(choices=[('suggested', '待推送'), ('shared', '已分享'), ('rejected', '已反馈不合适'), ('viewed', '客户已查看')], default='suggested', help_text='suggested=待推送 / shared=已分享 / rejected=已反馈不合适 / viewed=客户已查看', max_length=20, verbose_name='状态'), - ), - migrations.AlterField( - model_name='clientrequirement', - name='area_max', - field=models.DecimalField(blank=True, decimal_places=2, help_text='单位:㎡', max_digits=8, null=True, verbose_name='最大面积'), - ), - migrations.AlterField( - model_name='clientrequirement', - name='area_min', - field=models.DecimalField(blank=True, decimal_places=2, help_text='单位:㎡', max_digits=8, null=True, verbose_name='最小面积'), - ), - migrations.AlterField( - model_name='clientrequirement', - name='bedroom_counts', - field=django.contrib.postgres.fields.ArrayField(base_field=models.SmallIntegerField(), blank=True, default=list, help_text='多选,如 [2,3]', size=None, verbose_name='可接受卧室数'), - ), - migrations.AlterField( - model_name='clientrequirement', - name='budget_max', - field=models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, verbose_name='最高预算'), - ), - migrations.AlterField( - model_name='clientrequirement', - name='budget_min', - field=models.DecimalField(blank=True, decimal_places=2, help_text='单位:万元/元,依据需求类型', max_digits=12, null=True, verbose_name='最低预算'), - ), - migrations.AlterField( - model_name='clientrequirement', - name='building_age_ranges', - field=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, help_text='多选:within_5y / 5_10y / 10_15y / 15_20y / over_20y', size=None, verbose_name='楼龄偏好'), - ), - migrations.AlterField( - model_name='clientrequirement', - name='client', - field=models.ForeignKey(help_text='需求随客源级联删除', on_delete=django.db.models.deletion.CASCADE, related_name='requirements', to='fonrey_client.client', verbose_name='所属客源'), - ), - migrations.AlterField( - model_name='clientrequirement', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'), - ), - migrations.AlterField( - model_name='clientrequirement', - name='decorations', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('rough', '毛坯'), ('plain', '清水'), ('simple', '简装'), ('medium', '中装'), ('fine', '精装'), ('luxury', '豪装')], max_length=10), blank=True, default=list, help_text='多选(枚举同 properties.decoration)', size=None, verbose_name='装修偏好'), - ), - migrations.AlterField( - model_name='clientrequirement', - name='floor_preferences', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('no_first', '不要一楼'), ('low', '低楼层'), ('mid', '中楼层'), ('high', '高楼层'), ('no_top', '不要顶楼')], max_length=20), blank=True, default=list, help_text='多选:no_first=不要一层 / low=低楼层 / mid=中楼层 / high=高楼层 / no_top=不要顶层', size=None, verbose_name='楼层偏好'), - ), - migrations.AlterField( - model_name='clientrequirement', - name='intent_business_area_ids', - field=django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(), blank=True, default=list, help_text='商圈 ID 数组', size=None, verbose_name='意向商圈'), - ), - migrations.AlterField( - model_name='clientrequirement', - name='intent_complex_names', - field=models.TextField(blank=True, default='', help_text='文本,逗号分隔,最多500字', verbose_name='意向小区'), - ), - migrations.AlterField( - model_name='clientrequirement', - name='intent_district_ids', - field=django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(), blank=True, default=list, help_text='行政区 ID 数组', size=None, verbose_name='意向行政区'), - ), - migrations.AlterField( - model_name='clientrequirement', - name='intent_school_names', - field=models.TextField(blank=True, default='', help_text='文本,逗号分隔', verbose_name='意向学校'), - ), - migrations.AlterField( - model_name='clientrequirement', - name='is_primary', - field=models.BooleanField(default=True, help_text='用于列表展示', verbose_name='是否主需求'), - ), - migrations.AlterField( - model_name='clientrequirement', - name='orientations', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('east', '东'), ('south', '南'), ('west', '西'), ('north', '北')], max_length=10), blank=True, default=list, help_text='多选:east=东 / south=南 / west=西 / north=北', size=None, verbose_name='朝向偏好'), - ), - migrations.AlterField( - model_name='clientrequirement', - name='requirement_notes', - field=models.CharField(blank=True, default='', help_text='最多200字', max_length=200, verbose_name='需求备注'), - ), - migrations.AlterField( - model_name='clientrequirement', - name='requirement_type', - field=models.CharField(choices=[('second_hand', '二手'), ('new_house', '新房'), ('rental', '租房')], help_text='second_hand=二手 / new_house=新房 / rental=租房', max_length=20, verbose_name='需求类型'), - ), - migrations.AlterField( - model_name='clientrequirement', - name='school_enrollment_date', - field=models.DateField(blank=True, help_text='月份精度,取该月1日存储', null=True, verbose_name='入学时间'), - ), - migrations.AlterField( - model_name='clientrequirement', - name='traffic_preference', - field=models.TextField(blank=True, default='', verbose_name='交通备注'), - ), - migrations.AlterField( - model_name='clientrequirement', - name='transportation', - field=models.CharField(blank=True, default='', help_text='最多50字', max_length=50, verbose_name='交通要求'), - ), - migrations.AlterField( - model_name='clientrequirement', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='最后更新时间'), - ), - migrations.AlterField( - model_name='clientschoolpreference', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'), - ), - migrations.AlterField( - model_name='clientschoolpreference', - name='requirement', - field=models.ForeignKey(help_text='意向学校随需求级联删除', on_delete=django.db.models.deletion.CASCADE, related_name='school_preferences', to='fonrey_client.clientrequirement', verbose_name='所属需求'), - ), - migrations.AlterField( - model_name='clientschoolpreference', - name='school_id', - field=models.UUIDField(blank=True, help_text='从学校表选择,允许为 NULL(自由输入)', null=True, verbose_name='学校ID'), - ), - migrations.AlterField( - model_name='clientschoolpreference', - name='school_name', - field=models.CharField(help_text='当 school_id 为 NULL 时为手动输入', max_length=100, verbose_name='学校名称'), - ), - migrations.AlterField( - model_name='clientstatuslog', - name='change_type', - field=models.CharField(choices=[('status_change', '改状态'), ('grade_change', '改等级'), ('to_public', '转公客'), ('to_transacted', '转成交'), ('to_invalid', '转无效'), ('owner_change', '改归属人'), ('source_change', '改来源'), ('merge', '合并客源')], help_text='status_change=改状态 / grade_change=改等级 / to_public=转公客 / to_transacted=转成交 / to_invalid=转无效 / owner_change=改归属人 / source_change=改来源', max_length=30, verbose_name='变更类型'), - ), - migrations.AlterField( - model_name='clientstatuslog', - name='client', - field=models.ForeignKey(help_text='状态日志永久保留,RESTRICT 防止删除客源', on_delete=django.db.models.deletion.RESTRICT, related_name='status_logs', to='fonrey_client.client', verbose_name='所属客源'), - ), - migrations.AlterField( - model_name='clientstatuslog', - name='id', - field=models.UUIDField(primary_key=True, serialize=False, verbose_name='主键'), - ), - migrations.AlterField( - model_name='clientstatuslog', - name='new_value', - field=models.JSONField(blank=True, null=True, verbose_name='变更后快照'), - ), - migrations.AlterField( - model_name='clientstatuslog', - name='old_value', - field=models.JSONField(blank=True, help_text='格式:{"status": "buying", "label": "求购"}', null=True, verbose_name='变更前快照'), - ), - migrations.AlterField( - model_name='clientstatuslog', - name='operated_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='操作时间'), - ), - migrations.AlterField( - model_name='clientstatuslog', - name='operator', - field=models.ForeignKey(help_text='必填,状态变更审计用', on_delete=django.db.models.deletion.RESTRICT, related_name='client_status_changes', to='org.staff', verbose_name='操作人'), - ), - migrations.AlterField( - model_name='clientstatuslog', - name='reason', - field=models.TextField(blank=True, default='', help_text='改状态必填,最多200字', verbose_name='变更理由'), - ), - migrations.AlterField( - model_name='clientviewing', - name='agent', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='led_viewings', to='org.staff', verbose_name='主带看经纪人'), - ), - migrations.AlterField( - model_name='clientviewing', - name='client', - field=models.ForeignKey(help_text='带看记录仅软删除,不随客源删除', on_delete=django.db.models.deletion.RESTRICT, related_name='viewings', to='fonrey_client.client', verbose_name='所属客源'), - ), - migrations.AlterField( - model_name='clientviewing', - name='client_intent', - field=models.CharField(blank=True, choices=[('interested', '感兴趣'), ('not_interested', '不感兴趣'), ('negotiating', '谈判中'), ('cancelled', '取消')], default='', help_text='interested=感兴趣 / not_interested=不感兴趣 / negotiating=谈判中 / cancelled=取消', max_length=20, verbose_name='客户意向'), - ), - migrations.AlterField( - model_name='clientviewing', - name='companion_ids', - field=django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(), blank=True, default=list, help_text='员工 ID 数组(最多5人)', size=None, verbose_name='陪看人员'), - ), - migrations.AlterField( - model_name='clientviewing', - name='cooperator_ids', - field=django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(), blank=True, default=list, help_text='员工 ID 数组(最多5人)', size=None, verbose_name='合作带看人'), - ), - migrations.AlterField( - model_name='clientviewing', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'), - ), - migrations.AlterField( - model_name='clientviewing', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_client_viewings', to='org.staff', verbose_name='创建人'), - ), - migrations.AlterField( - model_name='clientviewing', - name='deleted_at', - field=models.DateTimeField(blank=True, help_text='软删除时间戳;带看记录可软删除', null=True, verbose_name='删除时间'), - ), - migrations.AlterField( - model_name='clientviewing', - name='property', - field=models.ForeignKey(help_text='房源删除时保留带看记录', on_delete=django.db.models.deletion.RESTRICT, related_name='client_viewings', to='fonrey_property.property', verbose_name='带看房源'), - ), - migrations.AlterField( - model_name='clientviewing', - name='scheduled_at', - field=models.DateTimeField(blank=True, null=True, verbose_name='预约时间'), - ), - migrations.AlterField( - model_name='clientviewing', - name='situation', - field=models.TextField(blank=True, default='', help_text='必填,≥6字', verbose_name='带看情况'), - ), - migrations.AlterField( - model_name='clientviewing', - name='viewing_end_at', - field=models.DateTimeField(blank=True, null=True, verbose_name='带看结束时间'), - ), - migrations.AlterField( - model_name='clientviewing', - name='viewing_progress', - field=models.SmallIntegerField(blank=True, help_text='1=一看,2=二看…,冗余字段,触发器维护', null=True, verbose_name='带看进度'), - ), - migrations.AlterField( - model_name='clientviewing', - name='viewing_start_at', - field=models.DateTimeField(blank=True, null=True, verbose_name='实际带看开始时间'), - ), - migrations.AlterField( - model_name='clientviewing', - name='viewing_type', - field=models.CharField(choices=[('appointment', '预约'), ('viewing', '带看'), ('revisit', '复看'), ('empty', '空看')], default='viewing', help_text='appointment=预约 / viewing=带看 / revisit=复看 / empty=空看', max_length=20, verbose_name='带看类型'), - ), - ] diff --git a/apps/client/migrations/__init__.py b/apps/client/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/client/models/__init__.py b/apps/client/models/__init__.py deleted file mode 100644 index 83c3698..0000000 --- a/apps/client/models/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -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", -] diff --git a/apps/client/models/contacts.py b/apps/client/models/contacts.py deleted file mode 100644 index fd449f0..0000000 --- a/apps/client/models/contacts.py +++ /dev/null @@ -1,318 +0,0 @@ -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", - verbose_name="所属客源", - help_text="联系人随客源级联删除", - ) - sort_order = models.SmallIntegerField( - default=0, - verbose_name="排序顺序", - help_text="sort_order=0 为主联系人,姓名用于客源姓名显示", - ) - name = models.CharField( - max_length=50, - verbose_name="联系人姓名", - ) - gender = models.CharField( - max_length=10, - choices=ClientContactGender.choices, - default=ClientContactGender.MALE, - verbose_name="性别", - help_text="male=先生 / female=女士", - ) - - phone_enc = models.BinaryField( - verbose_name="手机号(加密)", - help_text="AES-256-GCM 加密手机号(电话1)", - ) - phone_hash = models.CharField( - max_length=64, - verbose_name="手机号哈希", - help_text="SHA-256 哈希(重复检测)", - ) - phone_country_code = models.CharField( - max_length=10, - default="+86", - verbose_name="国际区号", - ) - phone_is_invalid = models.BooleanField( - default=False, - verbose_name="号码是否无效", - help_text="标记无效后该号码不再参与重复检测", - ) - - phone2_enc = models.BinaryField( - null=True, - blank=True, - verbose_name="备用电话2(加密)", - ) - phone2_hash = models.CharField( - max_length=64, - blank=True, - default="", - verbose_name="备用电话2哈希", - help_text="SHA-256,用于重复检测", - ) - - wechat = models.CharField( - max_length=100, - blank=True, - default="", - verbose_name="微信号", - ) - qq = models.CharField( - max_length=20, - blank=True, - default="", - verbose_name="QQ号", - ) - remarks = models.CharField( - max_length=200, - blank=True, - default="", - verbose_name="联系人备注", - help_text="最多200字", - ) - - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name="创建时间", - ) - updated_at = models.DateTimeField( - auto_now=True, - verbose_name="最后更新时间", - ) - deleted_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="删除时间", - help_text="软删除时间戳;NULL=未删除(不影响客源本身)", - ) - created_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="created_client_contacts", - verbose_name="创建人", - ) - - class Meta: - db_table = "client_contacts" - verbose_name = "客源联系人" - verbose_name_plural = "客源联系人" - 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", - verbose_name="所属客源", - help_text="需求随客源级联删除", - ) - requirement_type = models.CharField( - max_length=20, - choices=ClientRequirementType.choices, - verbose_name="需求类型", - help_text="second_hand=二手 / new_house=新房 / rental=租房", - ) - is_primary = models.BooleanField( - default=True, - verbose_name="是否主需求", - help_text="用于列表展示", - ) - - budget_min = models.DecimalField( - max_digits=12, - decimal_places=2, - null=True, - blank=True, - verbose_name="最低预算", - help_text="单位:万元/元,依据需求类型", - ) - budget_max = models.DecimalField( - max_digits=12, - decimal_places=2, - null=True, - blank=True, - verbose_name="最高预算", - ) - area_min = models.DecimalField( - max_digits=8, - decimal_places=2, - null=True, - blank=True, - verbose_name="最小面积", - help_text="单位:㎡", - ) - area_max = models.DecimalField( - max_digits=8, - decimal_places=2, - null=True, - blank=True, - verbose_name="最大面积", - help_text="单位:㎡", - ) - - bedroom_counts = ArrayField( - models.SmallIntegerField(), - blank=True, - default=list, - verbose_name="可接受卧室数", - help_text="多选,如 [2,3]", - ) - floor_preferences = ArrayField( - models.CharField(max_length=20, choices=ClientFloorPreference.choices), - blank=True, - default=list, - verbose_name="楼层偏好", - help_text="多选:no_first=不要一层 / low=低楼层 / mid=中楼层 / high=高楼层 / no_top=不要顶层", - ) - orientations = ArrayField( - models.CharField(max_length=10, choices=ClientOrientation.choices), - blank=True, - default=list, - verbose_name="朝向偏好", - help_text="多选:east=东 / south=南 / west=西 / north=北", - ) - decorations = ArrayField( - models.CharField(max_length=10, choices=ClientDecoration.choices), - blank=True, - default=list, - verbose_name="装修偏好", - help_text="多选(枚举同 properties.decoration)", - ) - building_age_ranges = ArrayField( - models.CharField(max_length=20, choices=ClientBuildingAgeRange.choices), - blank=True, - default=list, - verbose_name="楼龄偏好", - help_text="多选:within_5y / 5_10y / 10_15y / 15_20y / over_20y", - ) - - intent_district_ids = ArrayField( - models.UUIDField(), - blank=True, - default=list, - verbose_name="意向行政区", - help_text="行政区 ID 数组", - ) - intent_business_area_ids = ArrayField( - models.UUIDField(), - blank=True, - default=list, - verbose_name="意向商圈", - help_text="商圈 ID 数组", - ) - intent_complex_names = models.TextField( - blank=True, - default="", - verbose_name="意向小区", - help_text="文本,逗号分隔,最多500字", - ) - transportation = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="交通要求", - help_text="最多50字", - ) - intent_school_names = models.TextField( - blank=True, - default="", - verbose_name="意向学校", - help_text="文本,逗号分隔", - ) - school_enrollment_date = models.DateField( - null=True, - blank=True, - verbose_name="入学时间", - help_text="月份精度,取该月1日存储", - ) - traffic_preference = models.TextField( - blank=True, - default="", - verbose_name="交通备注", - ) - requirement_notes = models.CharField( - max_length=200, - blank=True, - default="", - verbose_name="需求备注", - help_text="最多200字", - ) - - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name="创建时间", - ) - updated_at = models.DateTimeField( - auto_now=True, - verbose_name="最后更新时间", - ) - - class Meta: - db_table = "client_requirements" - verbose_name = "客源需求" - verbose_name_plural = "客源需求" - 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", - verbose_name="所属需求", - help_text="意向学校随需求级联删除", - ) - school_id = models.UUIDField( - null=True, - blank=True, - verbose_name="学校ID", - help_text="从学校表选择,允许为 NULL(自由输入)", - ) - school_name = models.CharField( - max_length=100, - verbose_name="学校名称", - help_text="当 school_id 为 NULL 时为手动输入", - ) - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name="创建时间", - ) - - class Meta: - db_table = "client_school_preferences" - verbose_name = "意向学校" - verbose_name_plural = "意向学校" - indexes = [ - models.Index(fields=["requirement"], name="idx_csp_requirement"), - ] diff --git a/apps/client/models/core.py b/apps/client/models/core.py deleted file mode 100644 index f3fe79f..0000000 --- a/apps/client/models/core.py +++ /dev/null @@ -1,291 +0,0 @@ -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, - verbose_name="客源编号", - help_text="系统生成的客源编号,格式由运营配置(如 KY20260424001)", - ) - client_type = models.CharField( - max_length=20, - choices=ClientType.choices, - default=ClientType.PRIVATE, - verbose_name="客源分类", - help_text="private=私客 / public=公客 / transacted=成交客", - ) - status = models.CharField( - max_length=20, - choices=ClientStatus.choices, - default=ClientStatus.BUYING, - verbose_name="客源状态", - help_text="buying=求购 / renting=求租 / buy_or_rent=租购 / suspended=暂缓 / bought=已购 / rented_done=已租 / public=公客 / invalid=无效(详见 ENUMS)", - ) - grade = models.CharField( - max_length=5, - choices=ClientGrade.choices, - default=ClientGrade.C, - verbose_name="客源等级", - help_text="A=A急迫 / B=较强 / C=一般 / D=较弱 / E=暂不关注", - ) - property_usage = models.CharField( - max_length=30, - choices=ClientPropertyUsage.choices, - default=ClientPropertyUsage.RESIDENTIAL, - verbose_name="房屋用途", - help_text="residential=住宅 / villa=别墅 / commercial_residential=商住 / shop=商铺 / office=写字楼 / other=其他", - ) - buying_purpose = ArrayField( - models.CharField(max_length=20, choices=ClientBuyingPurpose.choices), - blank=True, - default=list, - verbose_name="购房目的", - help_text="多选:rigid=刚需 / investment=投资 / school_district=学区 / upgrade=改善 / commercial=商用 / other=其他", - ) - payment_method = models.CharField( - max_length=30, - choices=ClientPaymentMethod.choices, - blank=True, - default="", - verbose_name="付款方式", - help_text="full=全额 / mortgage=商业贷款 / mortgage_fund=商贷+公积金 / fund=公积金", - ) - properties_owned = models.CharField( - max_length=20, - choices=ClientPropertiesOwned.choices, - blank=True, - default="", - verbose_name="名下房产", - help_text="none=无 / local_none=本地无外地有 / local_has=本地有", - ) - has_loan_record = models.BooleanField( - null=True, - blank=True, - verbose_name="有无贷款记录", - ) - - id_type = models.CharField( - max_length=20, - choices=ClientIdType.choices, - blank=True, - default="", - verbose_name="证件类型", - help_text="id_card=身份证 / passport=护照 / hk_macao=港澳台 / other=其他", - ) - id_number_enc = models.BinaryField( - null=True, - blank=True, - verbose_name="证件号码(加密)", - help_text="AES 加密存储", - ) - - source = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="客户来源", - help_text="lookup_items 维护", - ) - remarks = models.TextField( - blank=True, - default="", - verbose_name="备注", - help_text="最多200字", - ) - - is_starred = models.BooleanField( - default=False, - verbose_name="是否收藏", - help_text="快速标记,详细收藏夹用 client_folder_items", - ) - is_pinned = models.BooleanField( - default=False, - verbose_name="是否置顶", - help_text="列表顶部置顶", - ) - is_big_value = models.BooleanField( - default=False, - verbose_name="是否大价值客户", - help_text="影响筛选展示", - ) - is_protected = models.BooleanField( - default=False, - verbose_name="是否保护客", - help_text="影响转公逻辑", - ) - prefers_new_house = models.BooleanField( - null=True, - blank=True, - verbose_name="偏好新房", - help_text="用于筛选", - ) - - transfer_to_public_type = models.CharField( - max_length=20, - choices=ClientTransferToPublicType.choices, - blank=True, - default="", - verbose_name="转公客方式", - help_text="manual=手动转公 / auto=自动转公(超时) / marketing_jump=营销客跳公 / resource_public=资料客素公", - ) - transferred_public_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="进入公客池时间", - ) - - invalid_reason = models.CharField( - max_length=30, - choices=ClientInvalidReason.choices, - blank=True, - default="", - verbose_name="无效原因", - help_text="invalid_phone=号码无效 / peer_agent=同行 / ad=广告推销 / no_intent=无意向 / other=其他", - ) - invalidated_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="标记无效时间", - ) - - transacted_at = models.DateField( - null=True, - blank=True, - verbose_name="成交日期", - ) - transacted_property = models.ForeignKey( - "fonrey_property.Property", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="transacted_clients", - verbose_name="成交房源", - help_text="成交关联的房源", - ) - transacted_price = models.DecimalField( - max_digits=12, - decimal_places=2, - null=True, - blank=True, - verbose_name="成交价格", - help_text="单位:万元", - ) - transacted_type = models.CharField( - max_length=20, - choices=ClientTransactedType.choices, - blank=True, - default="", - verbose_name="成交类型", - help_text="bought=我购 / rented=我租", - ) - transacted_property_type = models.CharField( - max_length=20, - choices=ClientTransactedPropertyType.choices, - blank=True, - default="", - verbose_name="成交房源类型", - help_text="second_hand=二手 / new_house=新房", - ) - - first_recorder = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="first_recorded_clients", - verbose_name="首录人", - ) - owner = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="owned_clients", - verbose_name="归属人", - help_text="私客独占跟进人", - ) - org_unit = models.ForeignKey( - "org.OrgUnit", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="clients", - verbose_name="归属部门", - help_text="冗余字段,加速筛选", - ) - - activity_level = models.CharField( - max_length=20, - choices=ClientActivityLevel.choices, - blank=True, - default="", - verbose_name="活跃度", - help_text="new_matched=新配偶 / active_7d / active_30d / active_90d / expiring / frozen / invalid(异步计算)", - ) - last_active_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="最后有效跟进时间", - help_text="触发器维护", - ) - last_follow_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="最后跟进时间", - help_text="冗余字段,列表排序用", - ) - - commission_date = models.DateField( - null=True, - blank=True, - verbose_name="委托日期", - ) - entrust_count = models.SmallIntegerField( - default=1, - verbose_name="委托次数", - help_text="成交后再委托则累加", - ) - - version = models.IntegerField( - default=1, - verbose_name="版本号", - help_text="乐观锁;每次 UPDATE +1;应用层检测 0 行受影响时抛 ConflictError", - ) - - class Meta: - db_table = "clients" - verbose_name = "客源" - verbose_name_plural = "客源" - 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"), - ] diff --git a/apps/client/models/folders.py b/apps/client/models/folders.py deleted file mode 100644 index 9efa815..0000000 --- a/apps/client/models/folders.py +++ /dev/null @@ -1,84 +0,0 @@ -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", - verbose_name="所属经纪人", - ) - name = models.CharField( - max_length=10, - verbose_name="收藏夹名称", - help_text="最多10字", - ) - is_default = models.BooleanField( - default=False, - verbose_name="是否默认", - help_text="系统默认收藏夹,每个经纪人只能有一个", - ) - sort_order = models.IntegerField( - default=0, - verbose_name="显示顺序", - help_text="升序排列", - ) - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name="创建时间", - ) - deleted_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="删除时间", - help_text="软删除时间戳;NULL=未删除", - ) - - class Meta: - db_table = "client_favorite_folders" - verbose_name = "私客收藏夹" - verbose_name_plural = "私客收藏夹" - 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", - verbose_name="所属收藏夹", - ) - client = models.ForeignKey( - "fonrey_client.Client", - on_delete=models.CASCADE, - related_name="folder_items", - verbose_name="被收藏的客源", - ) - added_at = models.DateTimeField( - auto_now_add=True, - verbose_name="加入收藏夹时间", - ) - - class Meta: - db_table = "client_folder_items" - verbose_name = "收藏夹中的客源" - verbose_name_plural = "收藏夹中的客源" - constraints = [ - models.UniqueConstraint( - fields=["folder", "client"], name="uq_cfi_folder_client" - ), - ] - indexes = [ - models.Index(fields=["client"], name="idx_cfi_client"), - ] diff --git a/apps/client/models/follow.py b/apps/client/models/follow.py deleted file mode 100644 index 87e43ec..0000000 --- a/apps/client/models/follow.py +++ /dev/null @@ -1,145 +0,0 @@ -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, - verbose_name="主键", - ) - created_at = models.DateTimeField( - verbose_name="创建时间", - help_text="分区键", - ) - client = models.ForeignKey( - "fonrey_client.Client", - on_delete=models.CASCADE, - related_name="follow_logs", - verbose_name="所属客源", - help_text="跟进日志随客源级联删除", - ) - - log_type = models.CharField( - max_length=30, - choices=ClientFollowLogType.choices, - verbose_name="跟进类型", - help_text="written=写入跟进 / modified=修改跟进 / sensitive_view=敏感信息查看(不可删) / other=其他跟进 / system=系统日志", - ) - purpose = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="跟进目的", - help_text="lookup_items 维护,23项", - ) - content = models.TextField( - blank=True, - default="", - verbose_name="跟进内容", - help_text="最少6字,最多500字", - ) - log_tag = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="跟进标签", - help_text="has_recording=有录音 / has_photo=有图片 / not_satisfied=对房源不满意 / still_considering=还在考虑 / ready_to_deposit=可交定金", - ) - change_detail = models.JSONField( - null=True, - blank=True, - verbose_name="字段变更明细", - help_text='修改跟进专用,格式:{"field": "grade", "old": "C", "new": "B", "label": "等级"}', - ) - - is_public = models.BooleanField( - default=True, - verbose_name="是否公开", - help_text="FALSE=仅本人及上级可见", - ) - is_deletable = models.BooleanField( - default=True, - verbose_name="是否可删除", - help_text="敏感信息查看类型为 FALSE,不可删除", - ) - - operator = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - verbose_name="操作人", - ) - operator_snapshot = models.JSONField( - null=True, - blank=True, - verbose_name="操作人快照", - help_text="{name, store_group, role}(防止人员调动后显示异常)", - ) - - deleted_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="删除时间", - help_text="仅 is_deletable=TRUE 时可软删", - ) - - class Meta: - db_table = "client_follow_logs" - verbose_name = "客源跟进日志" - verbose_name_plural = "客源跟进日志" - managed = False - unique_together = (("id", "created_at"),) - - -class ClientFollowLogAttachment(UUIDPrimaryKeyModel): - follow_log_id = models.UUIDField( - verbose_name="所属跟进日志ID", - help_text="跨分区 FK;不通过 Django FK 强制约束", - ) - file_key = models.TextField( - verbose_name="文件存储路径", - help_text="R2/S3 存储路径", - ) - file_name = models.CharField( - max_length=255, - verbose_name="文件名", - help_text="原始文件名(用于展示和下载)", - ) - file_size = models.IntegerField( - verbose_name="文件大小", - help_text="单位:bytes,最大 20MB", - ) - file_type = models.CharField( - max_length=10, - blank=True, - default="", - verbose_name="文件类型", - help_text="bmp / jpg / png / gif", - ) - has_location = models.BooleanField( - default=False, - verbose_name="是否含位置信息", - help_text="是否含 GPS 位置信息", - ) - sort_order = models.SmallIntegerField( - default=0, - verbose_name="排序顺序", - ) - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name="创建时间", - ) - - class Meta: - db_table = "client_follow_log_attachments" - verbose_name = "客源跟进附件" - verbose_name_plural = "客源跟进附件" - indexes = [models.Index(fields=["follow_log_id"], name="idx_cfla_log")] diff --git a/apps/client/models/viewing_match.py b/apps/client/models/viewing_match.py deleted file mode 100644 index f9df19e..0000000 --- a/apps/client/models/viewing_match.py +++ /dev/null @@ -1,281 +0,0 @@ -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", - verbose_name="所属客源", - help_text="带看记录仅软删除,不随客源删除", - ) - property = models.ForeignKey( - "fonrey_property.Property", - on_delete=models.RESTRICT, - related_name="client_viewings", - verbose_name="带看房源", - help_text="房源删除时保留带看记录", - ) - viewing_type = models.CharField( - max_length=20, - choices=ClientViewingType.choices, - default=ClientViewingType.VIEWING, - verbose_name="带看类型", - help_text="appointment=预约 / viewing=带看 / revisit=复看 / empty=空看", - ) - - agent = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="led_viewings", - verbose_name="主带看经纪人", - ) - companion_ids = ArrayField( - models.UUIDField(), - blank=True, - default=list, - verbose_name="陪看人员", - help_text="员工 ID 数组(最多5人)", - ) - cooperator_ids = ArrayField( - models.UUIDField(), - blank=True, - default=list, - verbose_name="合作带看人", - help_text="员工 ID 数组(最多5人)", - ) - - scheduled_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="预约时间", - ) - viewing_start_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="实际带看开始时间", - ) - viewing_end_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="带看结束时间", - ) - - situation = models.TextField( - blank=True, - default="", - verbose_name="带看情况", - help_text="必填,≥6字", - ) - client_intent = models.CharField( - max_length=20, - choices=ClientViewingIntent.choices, - blank=True, - default="", - verbose_name="客户意向", - help_text="interested=感兴趣 / not_interested=不感兴趣 / negotiating=谈判中 / cancelled=取消", - ) - viewing_progress = models.SmallIntegerField( - null=True, - blank=True, - verbose_name="带看进度", - help_text="1=一看,2=二看…,冗余字段,触发器维护", - ) - - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name="创建时间", - ) - deleted_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="删除时间", - help_text="软删除时间戳;带看记录可软删除", - ) - created_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="created_client_viewings", - verbose_name="创建人", - ) - - class Meta: - db_table = "client_viewings" - verbose_name = "带看记录" - verbose_name_plural = "带看记录" - 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", - verbose_name="所属客源", - ) - property = models.ForeignKey( - "fonrey_property.Property", - on_delete=models.CASCADE, - related_name="client_matches", - verbose_name="匹配房源", - ) - - match_source = models.CharField( - max_length=20, - choices=ClientPropertyMatchSource.choices, - default=ClientPropertyMatchSource.RECORDED, - verbose_name="匹配来源", - help_text="recorded=录客配房(基于录入需求) / system=系统配房(算法推荐)", - ) - match_group = models.CharField( - max_length=30, - choices=ClientPropertyMatchGroup.choices, - blank=True, - default="", - verbose_name="匹配分组", - help_text="quality_layout=优质户型 / price_reduced=降价 / hot=热门 / newly_listed=新上", - ) - match_score = models.DecimalField( - max_digits=5, - decimal_places=2, - null=True, - blank=True, - verbose_name="匹配度评分", - help_text="0-100", - ) - match_reasons = models.JSONField( - null=True, - blank=True, - verbose_name="匹配原因详情", - help_text='格式:[{"key": "budget", "match": true}, ...]', - ) - - status = models.CharField( - max_length=20, - choices=ClientPropertyMatchStatus.choices, - default=ClientPropertyMatchStatus.SUGGESTED, - verbose_name="状态", - help_text="suggested=待推送 / shared=已分享 / rejected=已反馈不合适 / viewed=客户已查看", - ) - shared_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="分享时间", - ) - feedback = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="反馈原因", - help_text="lookup_items 维护", - ) - calculated_at = models.DateTimeField( - auto_now_add=True, - verbose_name="配房计算时间", - ) - created_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="created_matches", - verbose_name="创建人", - help_text="触发配房操作的员工(录客配房时记录,系统配房可为NULL)", - ) - - class Meta: - db_table = "client_property_matches" - verbose_name = "智能配房" - verbose_name_plural = "智能配房" - 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, - verbose_name="主键", - ) - client = models.ForeignKey( - "fonrey_client.Client", - on_delete=models.RESTRICT, - related_name="status_logs", - verbose_name="所属客源", - help_text="状态日志永久保留,RESTRICT 防止删除客源", - ) - change_type = models.CharField( - max_length=30, - choices=ClientStatusLogChangeType.choices, - verbose_name="变更类型", - help_text="status_change=改状态 / grade_change=改等级 / to_public=转公客 / to_transacted=转成交 / to_invalid=转无效 / owner_change=改归属人 / source_change=改来源", - ) - old_value = models.JSONField( - null=True, - blank=True, - verbose_name="变更前快照", - help_text='格式:{"status": "buying", "label": "求购"}', - ) - new_value = models.JSONField( - null=True, - blank=True, - verbose_name="变更后快照", - ) - reason = models.TextField( - blank=True, - default="", - verbose_name="变更理由", - help_text="改状态必填,最多200字", - ) - operator = models.ForeignKey( - "org.Staff", - on_delete=models.RESTRICT, - related_name="client_status_changes", - verbose_name="操作人", - help_text="必填,状态变更审计用", - ) - operated_at = models.DateTimeField( - auto_now_add=True, - verbose_name="操作时间", - ) - - class Meta: - db_table = "client_status_logs" - verbose_name = "客源状态变更日志" - verbose_name_plural = "客源状态变更日志" - indexes = [ - models.Index(fields=["client", "-operated_at"], name="idx_csl_client"), - models.Index(fields=["change_type", "-operated_at"], name="idx_csl_type"), - ] diff --git a/apps/client/serializers.py b/apps/client/serializers.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/client/services/__init__.py b/apps/client/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/client/tasks.py b/apps/client/tasks.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/client/templates/client/.gitkeep b/apps/client/templates/client/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/apps/client/tests/__init__.py b/apps/client/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/client/urls.py b/apps/client/urls.py deleted file mode 100644 index a5f04de..0000000 --- a/apps/client/urls.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.urls import path - -app_name = "client" - -urlpatterns: list = [] diff --git a/apps/client/views.py b/apps/client/views.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/complex/__init__.py b/apps/complex/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/complex/admin.py b/apps/complex/admin.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/complex/apps.py b/apps/complex/apps.py deleted file mode 100644 index 8e97024..0000000 --- a/apps/complex/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class ComplexConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.complex" - label = "fonrey_complex" diff --git a/apps/complex/migrations/0001_initial.py b/apps/complex/migrations/0001_initial.py deleted file mode 100644 index 064045c..0000000 --- a/apps/complex/migrations/0001_initial.py +++ /dev/null @@ -1,332 +0,0 @@ -# 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'), - ), - ] diff --git a/apps/complex/migrations/0002_pg_trgm_and_search_vector.py b/apps/complex/migrations/0002_pg_trgm_and_search_vector.py deleted file mode 100644 index 6a676f0..0000000 --- a/apps/complex/migrations/0002_pg_trgm_and_search_vector.py +++ /dev/null @@ -1,76 +0,0 @@ -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), - ] diff --git a/apps/complex/migrations/0003_alter_building_options_alter_complex_options_and_more.py b/apps/complex/migrations/0003_alter_building_options_alter_complex_options_and_more.py deleted file mode 100644 index e19e875..0000000 --- a/apps/complex/migrations/0003_alter_building_options_alter_complex_options_and_more.py +++ /dev/null @@ -1,53 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-29 11:07 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('fonrey_complex', '0002_pg_trgm_and_search_vector'), - ] - - operations = [ - migrations.AlterModelOptions( - name='building', - options={'ordering': ['complex_id', 'name'], 'verbose_name': '楼栋', 'verbose_name_plural': '楼栋'}, - ), - migrations.AlterModelOptions( - name='complex', - options={'ordering': ['name'], 'verbose_name': '楼盘', 'verbose_name_plural': '楼盘'}, - ), - migrations.AlterModelOptions( - name='complexalias', - options={'ordering': ['complex_id', 'alias'], 'verbose_name': '楼盘别名', 'verbose_name_plural': '楼盘别名'}, - ), - migrations.AlterModelOptions( - name='complexattachment', - options={'ordering': ['complex_id', 'sort_order'], 'verbose_name': '楼盘附件', 'verbose_name_plural': '楼盘附件'}, - ), - migrations.AlterModelOptions( - name='complexbusinessarea', - options={'verbose_name': '楼盘商圈关联', 'verbose_name_plural': '楼盘商圈关联'}, - ), - migrations.AlterModelOptions( - name='complexmetrostation', - options={'verbose_name': '楼盘地铁站关联', 'verbose_name_plural': '楼盘地铁站关联'}, - ), - migrations.AlterModelOptions( - name='complexphoto', - options={'ordering': ['complex_id', 'sort_order'], 'verbose_name': '楼盘照片', 'verbose_name_plural': '楼盘照片'}, - ), - migrations.AlterModelOptions( - name='complexpricetrend', - options={'ordering': ['complex_id', '-record_month'], 'verbose_name': '楼盘价格走势', 'verbose_name_plural': '楼盘价格走势'}, - ), - migrations.AlterModelOptions( - name='complexschool', - options={'verbose_name': '楼盘学校关联', 'verbose_name_plural': '楼盘学校关联'}, - ), - migrations.AlterModelOptions( - name='roomunit', - options={'ordering': ['building_id', '-floor', 'room_no'], 'verbose_name': '房号单元', 'verbose_name_plural': '房号单元'}, - ), - ] diff --git a/apps/complex/migrations/0004_alter_building_built_year_alter_building_complex_and_more.py b/apps/complex/migrations/0004_alter_building_built_year_alter_building_complex_and_more.py deleted file mode 100644 index 2040cca..0000000 --- a/apps/complex/migrations/0004_alter_building_built_year_alter_building_complex_and_more.py +++ /dev/null @@ -1,528 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-30 01:46 - -import django.contrib.postgres.fields -import django.contrib.postgres.search -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('org', '0003_alter_orgunit_address_city_and_more'), - ('region', '0003_alter_businessarea_district_and_more'), - ('fonrey_complex', '0003_alter_building_options_alter_complex_options_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='building', - name='built_year', - field=models.SmallIntegerField(blank=True, null=True, verbose_name='竣工年份'), - ), - migrations.AlterField( - model_name='building', - name='complex', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='buildings', to='fonrey_complex.complex', verbose_name='所属楼盘'), - ), - migrations.AlterField( - model_name='building', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_buildings', to='org.staff', verbose_name='创建人'), - ), - migrations.AlterField( - model_name='building', - name='has_elevator', - field=models.BooleanField(blank=True, null=True, verbose_name='是否有电梯'), - ), - migrations.AlterField( - model_name='building', - name='is_active', - field=models.BooleanField(default=True, help_text='FALSE=已停用(楼栋被删除或合并)', verbose_name='是否启用'), - ), - migrations.AlterField( - model_name='building', - name='is_standard', - field=models.BooleanField(default=False, help_text='TRUE=已经运营核准', verbose_name='是否标准结构'), - ), - migrations.AlterField( - model_name='building', - name='land_use_years', - field=models.CharField(blank=True, default='', max_length=30, verbose_name='土地使用年限'), - ), - migrations.AlterField( - model_name='building', - name='name', - field=models.CharField(help_text='如「1号楼」「A栋2单元」', max_length=50, verbose_name='楼栋名称'), - ), - migrations.AlterField( - model_name='building', - name='property_usage_type', - field=models.CharField(blank=True, choices=[('residential', '住宅'), ('villa', '别墅'), ('commercial_residential', '商住'), ('commercial', '商业'), ('office', '写字楼'), ('other', '其他')], default='', help_text='可与楼盘不同,如商住楼盘内有纯商铺楼栋', max_length=30, verbose_name='物业类型'), - ), - migrations.AlterField( - model_name='building', - name='school', - field=models.ForeignKey(blank=True, help_text='楼栋级别的学区差异', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='buildings', to='region.school', verbose_name='对口学校'), - ), - migrations.AlterField( - model_name='building', - name='total_floors', - field=models.SmallIntegerField(blank=True, null=True, verbose_name='总层数'), - ), - migrations.AlterField( - model_name='complex', - name='address', - field=models.CharField(blank=True, default='', help_text='不可在编辑页修改,需走纠错流程', max_length=500, verbose_name='详细地址'), - ), - migrations.AlterField( - model_name='complex', - name='address_summary', - field=models.CharField(blank=True, default='', help_text='如「海波路1000弄」,可编辑', max_length=100, verbose_name='概要地址'), - ), - migrations.AlterField( - model_name='complex', - name='building_structure', - field=models.CharField(blank=True, choices=[('unit_room', '单元-房号'), ('other', '其他')], default='', help_text='unit_room=单元-房号 / other=其他', max_length=30, verbose_name='楼栋结构'), - ), - migrations.AlterField( - model_name='complex', - name='building_type', - field=models.CharField(blank=True, choices=[('slab', '板楼'), ('tower', '塔楼'), ('slab_tower', '板塔结合')], default='', help_text='slab=板楼 / tower=塔楼 / slab_tower=板塔结合', max_length=20, verbose_name='建筑类型'), - ), - migrations.AlterField( - model_name='complex', - name='built_year', - field=models.SmallIntegerField(blank=True, help_text='可多选时存最早竣工年', null=True, verbose_name='竣工年份'), - ), - migrations.AlterField( - model_name='complex', - name='built_years', - field=django.contrib.postgres.fields.ArrayField(base_field=models.SmallIntegerField(), blank=True, default=list, help_text='楼盘分期竣工', size=None, verbose_name='竣工年份多值'), - ), - migrations.AlterField( - model_name='complex', - name='business_areas', - field=models.ManyToManyField(related_name='complexes', through='fonrey_complex.ComplexBusinessArea', to='region.businessarea', verbose_name='关联商圈'), - ), - migrations.AlterField( - 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', verbose_name='创建人'), - ), - migrations.AlterField( - model_name='complex', - name='developer', - field=models.CharField(blank=True, default='', max_length=200, verbose_name='开发商'), - ), - migrations.AlterField( - 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', verbose_name='所属城区'), - ), - migrations.AlterField( - model_name='complex', - name='electricity_type', - field=models.CharField(blank=True, choices=[('civil', '民电'), ('commercial', '商电')], default='', help_text='civil=民电 / commercial=商电', max_length=10, verbose_name='电费类型'), - ), - migrations.AlterField( - model_name='complex', - name='green_rate', - field=models.DecimalField(blank=True, decimal_places=2, help_text='单位:%', max_digits=5, null=True, verbose_name='绿化率'), - ), - migrations.AlterField( - model_name='complex', - name='has_central_heating', - field=models.BooleanField(blank=True, null=True, verbose_name='是否统一供暖'), - ), - migrations.AlterField( - model_name='complex', - name='has_gas', - field=models.BooleanField(blank=True, null=True, verbose_name='是否有燃气'), - ), - migrations.AlterField( - model_name='complex', - name='is_active', - field=models.BooleanField(default=True, help_text='FALSE=已停用楼盘', verbose_name='是否启用'), - ), - migrations.AlterField( - model_name='complex', - name='land_use_years', - field=models.CharField(blank=True, default='', help_text='如「70年」', max_length=30, verbose_name='土地使用年限'), - ), - migrations.AlterField( - model_name='complex', - name='latitude', - field=models.DecimalField(blank=True, decimal_places=7, help_text='WGS84,完整度目标 ≥ 90%', max_digits=10, null=True, verbose_name='纬度'), - ), - migrations.AlterField( - model_name='complex', - name='lock_building', - field=models.BooleanField(default=False, help_text='锁定后不可增删楼栋', verbose_name='楼栋锁'), - ), - migrations.AlterField( - model_name='complex', - name='lock_info', - field=models.BooleanField(default=False, help_text='锁定后基本信息只读', verbose_name='信息锁'), - ), - migrations.AlterField( - model_name='complex', - name='lock_room', - field=models.BooleanField(default=False, verbose_name='房号锁'), - ), - migrations.AlterField( - model_name='complex', - name='lock_standard_room', - field=models.BooleanField(default=False, verbose_name='标准房号锁'), - ), - migrations.AlterField( - model_name='complex', - name='longitude', - field=models.DecimalField(blank=True, decimal_places=7, help_text='WGS84', max_digits=10, null=True, verbose_name='经度'), - ), - migrations.AlterField( - model_name='complex', - name='metro_stations', - field=models.ManyToManyField(related_name='complexes', through='fonrey_complex.ComplexMetroStation', to='region.metrostation', verbose_name='周边地铁站'), - ), - migrations.AlterField( - model_name='complex', - name='name', - field=models.CharField(help_text='标准楼盘名称,不可在编辑页直接修改(需走合并/申请流程)', max_length=200, verbose_name='楼盘名称'), - ), - migrations.AlterField( - model_name='complex', - name='ownership_category', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=30), blank=True, default=list, help_text='多选(运营维护枚举)', size=None, verbose_name='权属类别'), - ), - migrations.AlterField( - model_name='complex', - name='parking_ratio', - field=models.CharField(blank=True, default='', help_text='如「100:63」', max_length=20, verbose_name='车位配比'), - ), - migrations.AlterField( - model_name='complex', - name='parking_total', - field=models.IntegerField(blank=True, null=True, verbose_name='车位总数'), - ), - migrations.AlterField( - model_name='complex', - name='parking_underground', - field=models.IntegerField(blank=True, null=True, verbose_name='地下车位数'), - ), - migrations.AlterField( - model_name='complex', - name='plot_area', - field=models.DecimalField(blank=True, decimal_places=2, help_text='单位:m²', max_digits=12, null=True, verbose_name='小区占地面积'), - ), - migrations.AlterField( - model_name='complex', - name='plot_ratio', - field=models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True, verbose_name='容积率'), - ), - migrations.AlterField( - model_name='complex', - name='property_company', - field=models.CharField(blank=True, default='', max_length=200, verbose_name='物业公司'), - ), - migrations.AlterField( - model_name='complex', - name='property_fee', - field=models.DecimalField(blank=True, decimal_places=2, help_text='单位:元/m²/月', max_digits=8, null=True, verbose_name='物业费'), - ), - migrations.AlterField( - model_name='complex', - name='property_phone', - field=models.CharField(blank=True, default='', max_length=30, verbose_name='物业电话'), - ), - migrations.AlterField( - model_name='complex', - name='property_usage_types', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('residential', '住宅'), ('villa', '别墅'), ('commercial_residential', '商住'), ('commercial', '商业'), ('office', '写字楼'), ('other', '其他')], max_length=30), blank=True, default=list, help_text='多选:residential / villa / commercial_residential / commercial / office / other', size=None, verbose_name='物业类型'), - ), - migrations.AlterField( - model_name='complex', - name='remarks', - field=models.TextField(blank=True, default='', verbose_name='备注'), - ), - migrations.AlterField( - model_name='complex', - name='schools', - field=models.ManyToManyField(related_name='complexes', through='fonrey_complex.ComplexSchool', to='region.school', verbose_name='对口学校'), - ), - migrations.AlterField( - model_name='complex', - name='search_vector', - field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='由触发器自动维护(name + alias + address)', null=True, verbose_name='全文检索向量'), - ), - migrations.AlterField( - model_name='complex', - name='total_floor_area', - field=models.DecimalField(blank=True, decimal_places=2, help_text='单位:m²', max_digits=12, null=True, verbose_name='小区总建筑面积'), - ), - migrations.AlterField( - model_name='complex', - name='total_households', - field=models.IntegerField(blank=True, null=True, verbose_name='总户数'), - ), - migrations.AlterField( - model_name='complex', - name='total_units', - field=models.IntegerField(blank=True, null=True, verbose_name='单元总数'), - ), - migrations.AlterField( - 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', verbose_name='最后更新人'), - ), - migrations.AlterField( - model_name='complex', - name='version', - field=models.IntegerField(default=1, help_text='乐观锁;UPDATE 时 +1;应用层检测 0 行受影响时抛 ConflictError', verbose_name='版本号'), - ), - migrations.AlterField( - model_name='complex', - name='water_type', - field=models.CharField(blank=True, choices=[('civil', '民水'), ('commercial', '商水')], default='', help_text='civil=民水 / commercial=商水', max_length=10, verbose_name='水费类型'), - ), - migrations.AlterField( - model_name='complexalias', - name='alias', - field=models.CharField(help_text='最多20字/条,多别名多行存储', max_length=200, verbose_name='别名'), - ), - migrations.AlterField( - model_name='complexalias', - name='complex', - field=models.ForeignKey(help_text='别名随楼盘级联删除', on_delete=django.db.models.deletion.CASCADE, related_name='aliases', to='fonrey_complex.complex', verbose_name='所属楼盘'), - ), - migrations.AlterField( - model_name='complexalias', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'), - ), - migrations.AlterField( - model_name='complexalias', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_complex_aliases', to='org.staff', verbose_name='创建人'), - ), - migrations.AlterField( - model_name='complexalias', - name='is_system', - field=models.BooleanField(default=False, help_text='TRUE=系统/标准别名(只读),FALSE=用户自定义', verbose_name='是否系统别名'), - ), - migrations.AlterField( - model_name='complexattachment', - name='complex', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='fonrey_complex.complex', verbose_name='所属楼盘'), - ), - migrations.AlterField( - model_name='complexattachment', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'), - ), - migrations.AlterField( - model_name='complexattachment', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_complex_attachments', to='org.staff', verbose_name='上传人'), - ), - migrations.AlterField( - model_name='complexattachment', - name='file_key', - field=models.TextField(help_text='R2/S3 存储路径', verbose_name='文件存储路径'), - ), - migrations.AlterField( - model_name='complexattachment', - name='file_name', - field=models.CharField(max_length=255, verbose_name='原始文件名'), - ), - migrations.AlterField( - model_name='complexattachment', - name='file_size', - field=models.IntegerField(blank=True, help_text='单位:bytes', null=True, verbose_name='文件大小'), - ), - migrations.AlterField( - model_name='complexattachment', - name='file_type', - field=models.CharField(blank=True, default='', help_text='MIME type', max_length=50, verbose_name='文件类型'), - ), - migrations.AlterField( - model_name='complexattachment', - name='sort_order', - field=models.SmallIntegerField(default=0, verbose_name='排序顺序'), - ), - migrations.AlterField( - model_name='complexbusinessarea', - name='business_area', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='complex_links', to='region.businessarea', verbose_name='关联商圈'), - ), - migrations.AlterField( - model_name='complexbusinessarea', - name='complex', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='complex_business_areas', to='fonrey_complex.complex', verbose_name='所属楼盘'), - ), - migrations.AlterField( - model_name='complexbusinessarea', - name='is_primary', - field=models.BooleanField(default=False, help_text='主商圈唯一,用于列表显示', verbose_name='是否主商圈'), - ), - migrations.AlterField( - model_name='complexmetrostation', - name='complex', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='complex_metro_stations', to='fonrey_complex.complex', verbose_name='所属楼盘'), - ), - migrations.AlterField( - model_name='complexmetrostation', - name='distance_meters', - field=models.IntegerField(blank=True, help_text='单位:米', null=True, verbose_name='步行距离'), - ), - migrations.AlterField( - model_name='complexmetrostation', - name='station', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='complex_links', to='region.metrostation', verbose_name='关联地铁站'), - ), - migrations.AlterField( - model_name='complexphoto', - name='category', - field=models.CharField(choices=[('complex', '楼盘图'), ('layout', '户型图'), ('vr', 'VR图'), ('other', '其他')], help_text='complex=楼盘图 / layout=户型图 / vr=VR全景 / other=其他', max_length=20, verbose_name='照片类别'), - ), - migrations.AlterField( - model_name='complexphoto', - name='complex', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='photos', to='fonrey_complex.complex', verbose_name='所属楼盘'), - ), - migrations.AlterField( - model_name='complexphoto', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'), - ), - migrations.AlterField( - model_name='complexphoto', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_complex_photos', to='org.staff', verbose_name='上传人'), - ), - migrations.AlterField( - model_name='complexphoto', - name='file_key', - field=models.TextField(help_text='R2/S3 路径', verbose_name='文件存储路径'), - ), - migrations.AlterField( - model_name='complexphoto', - name='file_name', - field=models.CharField(blank=True, default='', max_length=255, verbose_name='原始文件名'), - ), - migrations.AlterField( - model_name='complexphoto', - name='file_size', - field=models.IntegerField(blank=True, help_text='单位:bytes', null=True, verbose_name='文件大小'), - ), - migrations.AlterField( - model_name='complexphoto', - name='height', - field=models.IntegerField(blank=True, help_text='单位:px', null=True, verbose_name='图片高度'), - ), - migrations.AlterField( - model_name='complexphoto', - name='is_cover', - field=models.BooleanField(default=False, help_text='楼盘封面图(每楼盘唯一)', verbose_name='是否封面图'), - ), - migrations.AlterField( - model_name='complexphoto', - name='sort_order', - field=models.SmallIntegerField(default=0, help_text='同类别内的排序顺序', verbose_name='排序顺序'), - ), - migrations.AlterField( - model_name='complexphoto', - name='thumbnail_key', - field=models.TextField(blank=True, default='', verbose_name='缩略图路径'), - ), - migrations.AlterField( - model_name='complexphoto', - name='width', - field=models.IntegerField(blank=True, help_text='单位:px', null=True, verbose_name='图片宽度'), - ), - migrations.AlterField( - model_name='complexpricetrend', - name='avg_sale_price', - field=models.DecimalField(blank=True, decimal_places=2, help_text='单位:万元/套', max_digits=12, null=True, verbose_name='月均售价'), - ), - migrations.AlterField( - model_name='complexpricetrend', - name='avg_unit_price', - field=models.DecimalField(blank=True, decimal_places=2, help_text='单位:元/m²', max_digits=10, null=True, verbose_name='月均单价'), - ), - migrations.AlterField( - model_name='complexpricetrend', - name='complex', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='price_trends', to='fonrey_complex.complex', verbose_name='所属楼盘'), - ), - migrations.AlterField( - model_name='complexpricetrend', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'), - ), - migrations.AlterField( - model_name='complexpricetrend', - name='listing_count', - field=models.IntegerField(blank=True, null=True, verbose_name='当月挂牌套数'), - ), - migrations.AlterField( - model_name='complexpricetrend', - name='record_month', - field=models.DateField(help_text='统一存为该月1日,如 2026-04-01', verbose_name='月份'), - ), - migrations.AlterField( - model_name='complexpricetrend', - name='transaction_count', - field=models.IntegerField(blank=True, null=True, verbose_name='成交套数'), - ), - migrations.AlterField( - model_name='complexschool', - name='complex', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='complex_schools', to='fonrey_complex.complex', verbose_name='所属楼盘'), - ), - migrations.AlterField( - model_name='complexschool', - name='school', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='complex_links', to='region.school', verbose_name='对口学校'), - ), - migrations.AlterField( - model_name='complexschool', - name='zone_type', - field=models.CharField(blank=True, choices=[('guaranteed', '对口'), ('reference', '参考'), ('lottery', '摇号')], default='', help_text='guaranteed=对口(直升) / reference=参考(可能入读) / lottery=摇号', max_length=30, verbose_name='学区类型'), - ), - migrations.AlterField( - model_name='roomunit', - name='building', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='room_units', to='fonrey_complex.building', verbose_name='所属楼栋'), - ), - migrations.AlterField( - model_name='roomunit', - name='display_no', - field=models.CharField(blank=True, default='', help_text='展示用完整房号,如「3-1-101」', max_length=50, verbose_name='展示房号'), - ), - migrations.AlterField( - model_name='roomunit', - name='floor', - field=models.SmallIntegerField(help_text='实际层数,地下为负数', verbose_name='楼层'), - ), - migrations.AlterField( - model_name='roomunit', - name='floor_name', - field=models.CharField(blank=True, default='', help_text='如「1层」「B1层」', max_length=20, verbose_name='楼层名称'), - ), - migrations.AlterField( - model_name='roomunit', - name='is_active', - field=models.BooleanField(default=True, help_text='FALSE=已拆除/不存在', verbose_name='是否启用'), - ), - migrations.AlterField( - model_name='roomunit', - name='is_standard', - field=models.BooleanField(default=False, help_text='TRUE=已归一化为标准结构', verbose_name='是否标准化'), - ), - migrations.AlterField( - model_name='roomunit', - name='room_no', - field=models.CharField(help_text='如「01」「101」', max_length=30, verbose_name='房号'), - ), - ] diff --git a/apps/complex/migrations/__init__.py b/apps/complex/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/complex/models/__init__.py b/apps/complex/models/__init__.py deleted file mode 100644 index a72a661..0000000 --- a/apps/complex/models/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -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", -] diff --git a/apps/complex/models/complex.py b/apps/complex/models/complex.py deleted file mode 100644 index b85a3c9..0000000 --- a/apps/complex/models/complex.py +++ /dev/null @@ -1,841 +0,0 @@ -from django.contrib.postgres.fields import ArrayField -from django.contrib.postgres.indexes import GinIndex -from django.contrib.postgres.search import SearchVectorField -from django.db import models - -from core.enums import ( - ComplexBuildingStructure, - ComplexBuildingType, - ComplexElectricityType, - ComplexPhotoCategory, - ComplexPropertyUsageType, - ComplexWaterType, - SchoolZoneType, -) -from core.models.base import SoftDeleteModel, TimeStampedModel, UUIDPrimaryKeyModel - - -class Complex(SoftDeleteModel): - name = models.CharField( - max_length=200, - verbose_name="楼盘名称", - help_text="标准楼盘名称,不可在编辑页直接修改(需走合并/申请流程)", - ) - district = models.ForeignKey( - "region.District", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="complexes", - verbose_name="所属城区", - ) - address = models.CharField( - max_length=500, - blank=True, - default="", - verbose_name="详细地址", - help_text="不可在编辑页修改,需走纠错流程", - ) - address_summary = models.CharField( - max_length=100, - blank=True, - default="", - verbose_name="概要地址", - help_text='如「海波路1000弄」,可编辑', - ) - latitude = models.DecimalField( - max_digits=10, - decimal_places=7, - null=True, - blank=True, - verbose_name="纬度", - help_text="WGS84,完整度目标 ≥ 90%", - ) - longitude = models.DecimalField( - max_digits=10, - decimal_places=7, - null=True, - blank=True, - verbose_name="经度", - help_text="WGS84", - ) - - property_usage_types = ArrayField( - models.CharField(max_length=30, choices=ComplexPropertyUsageType.choices), - default=list, - blank=True, - verbose_name="物业类型", - help_text="多选:residential / villa / commercial_residential / commercial / office / other", - ) - building_structure = models.CharField( - max_length=30, - blank=True, - default="", - choices=ComplexBuildingStructure.choices, - verbose_name="楼栋结构", - help_text="unit_room=单元-房号 / other=其他", - ) - building_type = models.CharField( - max_length=20, - blank=True, - default="", - choices=ComplexBuildingType.choices, - verbose_name="建筑类型", - help_text="slab=板楼 / tower=塔楼 / slab_tower=板塔结合", - ) - land_use_years = models.CharField( - max_length=30, - blank=True, - default="", - verbose_name="土地使用年限", - help_text='如「70年」', - ) - built_year = models.SmallIntegerField( - null=True, - blank=True, - verbose_name="竣工年份", - help_text="可多选时存最早竣工年", - ) - built_years = ArrayField( - models.SmallIntegerField(), - default=list, - blank=True, - verbose_name="竣工年份多值", - help_text="楼盘分期竣工", - ) - ownership_category = ArrayField( - models.CharField(max_length=30), - default=list, - blank=True, - verbose_name="权属类别", - help_text="多选(运营维护枚举)", - ) - total_units = models.IntegerField( - null=True, - blank=True, - verbose_name="单元总数", - ) - total_households = models.IntegerField( - null=True, - blank=True, - verbose_name="总户数", - ) - - total_floor_area = models.DecimalField( - max_digits=12, - decimal_places=2, - null=True, - blank=True, - verbose_name="小区总建筑面积", - help_text="单位:m²", - ) - plot_area = models.DecimalField( - max_digits=12, - decimal_places=2, - null=True, - blank=True, - verbose_name="小区占地面积", - help_text="单位:m²", - ) - plot_ratio = models.DecimalField( - max_digits=5, - decimal_places=2, - null=True, - blank=True, - verbose_name="容积率", - ) - green_rate = models.DecimalField( - max_digits=5, - decimal_places=2, - null=True, - blank=True, - verbose_name="绿化率", - help_text="单位:%", - ) - developer = models.CharField( - max_length=200, - blank=True, - default="", - verbose_name="开发商", - ) - - property_company = models.CharField( - max_length=200, - blank=True, - default="", - verbose_name="物业公司", - ) - property_fee = models.DecimalField( - max_digits=8, - decimal_places=2, - null=True, - blank=True, - verbose_name="物业费", - help_text="单位:元/m²/月", - ) - property_phone = models.CharField( - max_length=30, - blank=True, - default="", - verbose_name="物业电话", - ) - - parking_total = models.IntegerField( - null=True, - blank=True, - verbose_name="车位总数", - ) - parking_underground = models.IntegerField( - null=True, - blank=True, - verbose_name="地下车位数", - ) - parking_ratio = models.CharField( - max_length=20, - blank=True, - default="", - verbose_name="车位配比", - help_text='如「100:63」', - ) - - water_type = models.CharField( - max_length=10, - blank=True, - default="", - choices=ComplexWaterType.choices, - verbose_name="水费类型", - help_text="civil=民水 / commercial=商水", - ) - electricity_type = models.CharField( - max_length=10, - blank=True, - default="", - choices=ComplexElectricityType.choices, - verbose_name="电费类型", - help_text="civil=民电 / commercial=商电", - ) - has_central_heating = models.BooleanField( - null=True, - blank=True, - verbose_name="是否统一供暖", - ) - has_gas = models.BooleanField( - null=True, - blank=True, - verbose_name="是否有燃气", - ) - remarks = models.TextField( - blank=True, - default="", - verbose_name="备注", - ) - - lock_building = models.BooleanField( - default=False, - verbose_name="楼栋锁", - help_text="锁定后不可增删楼栋", - ) - lock_room = models.BooleanField( - default=False, - verbose_name="房号锁", - ) - lock_info = models.BooleanField( - default=False, - verbose_name="信息锁", - help_text="锁定后基本信息只读", - ) - lock_standard_room = models.BooleanField( - default=False, - verbose_name="标准房号锁", - ) - - search_vector = SearchVectorField( - null=True, - blank=True, - verbose_name="全文检索向量", - help_text="由触发器自动维护(name + alias + address)", - ) - - is_active = models.BooleanField( - default=True, - verbose_name="是否启用", - help_text="FALSE=已停用楼盘", - ) - created_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="created_complexes", - verbose_name="创建人", - ) - updated_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="updated_complexes", - verbose_name="最后更新人", - ) - version = models.IntegerField( - default=1, - verbose_name="版本号", - help_text="乐观锁;UPDATE 时 +1;应用层检测 0 行受影响时抛 ConflictError", - ) - - business_areas = models.ManyToManyField( - "region.BusinessArea", - through="fonrey_complex.ComplexBusinessArea", - related_name="complexes", - verbose_name="关联商圈", - ) - schools = models.ManyToManyField( - "region.School", - through="fonrey_complex.ComplexSchool", - related_name="complexes", - verbose_name="对口学校", - ) - metro_stations = models.ManyToManyField( - "region.MetroStation", - through="fonrey_complex.ComplexMetroStation", - related_name="complexes", - verbose_name="周边地铁站", - ) - - class Meta: - db_table = "complexes" - verbose_name = "楼盘" - verbose_name_plural = "楼盘" - indexes = [ - models.Index( - fields=["district"], - name="idx_complexes_district", - condition=models.Q(deleted_at__isnull=True), - ), - GinIndex(fields=["search_vector"], name="idx_complexes_search"), - models.Index( - fields=["latitude", "longitude"], - name="idx_complexes_geo", - condition=models.Q(deleted_at__isnull=True, latitude__isnull=False), - ), - models.Index( - fields=["is_active"], - name="idx_complexes_active", - condition=models.Q(deleted_at__isnull=True), - ), - ] - ordering = ["name"] - - def __str__(self) -> str: - return self.name - - -class ComplexAlias(UUIDPrimaryKeyModel): - complex = models.ForeignKey( - "fonrey_complex.Complex", - on_delete=models.CASCADE, - related_name="aliases", - verbose_name="所属楼盘", - help_text="别名随楼盘级联删除", - ) - alias = models.CharField( - max_length=200, - verbose_name="别名", - help_text="最多20字/条,多别名多行存储", - ) - is_system = models.BooleanField( - default=False, - verbose_name="是否系统别名", - help_text="TRUE=系统/标准别名(只读),FALSE=用户自定义", - ) - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name="创建时间", - ) - created_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="created_complex_aliases", - verbose_name="创建人", - ) - - class Meta: - db_table = "complex_aliases" - verbose_name = "楼盘别名" - verbose_name_plural = "楼盘别名" - indexes = [ - models.Index(fields=["complex"], name="idx_complex_aliases_complex"), - ] - ordering = ["complex_id", "alias"] - - def __str__(self) -> str: - return self.alias - - -class ComplexBusinessArea(models.Model): - complex = models.ForeignKey( - "fonrey_complex.Complex", - on_delete=models.CASCADE, - related_name="complex_business_areas", - verbose_name="所属楼盘", - ) - business_area = models.ForeignKey( - "region.BusinessArea", - on_delete=models.CASCADE, - related_name="complex_links", - verbose_name="关联商圈", - ) - is_primary = models.BooleanField( - default=False, - verbose_name="是否主商圈", - help_text="主商圈唯一,用于列表显示", - ) - - class Meta: - db_table = "complex_business_areas" - verbose_name = "楼盘商圈关联" - verbose_name_plural = "楼盘商圈关联" - constraints = [ - models.UniqueConstraint( - fields=["complex", "business_area"], - name="pk_complex_business_areas", - ), - models.UniqueConstraint( - fields=["complex"], - condition=models.Q(is_primary=True), - name="uq_complex_biz_area_primary", - ), - ] - - -class ComplexSchool(models.Model): - complex = models.ForeignKey( - "fonrey_complex.Complex", - on_delete=models.CASCADE, - related_name="complex_schools", - verbose_name="所属楼盘", - ) - school = models.ForeignKey( - "region.School", - on_delete=models.CASCADE, - related_name="complex_links", - verbose_name="对口学校", - ) - zone_type = models.CharField( - max_length=30, - blank=True, - default="", - choices=SchoolZoneType.choices, - verbose_name="学区类型", - help_text="guaranteed=对口(直升) / reference=参考(可能入读) / lottery=摇号", - ) - - class Meta: - db_table = "complex_schools" - verbose_name = "楼盘学校关联" - verbose_name_plural = "楼盘学校关联" - constraints = [ - models.UniqueConstraint( - fields=["complex", "school"], - name="pk_complex_schools", - ), - ] - indexes = [ - models.Index(fields=["school"], name="idx_complex_schools_school"), - ] - - -class ComplexMetroStation(models.Model): - complex = models.ForeignKey( - "fonrey_complex.Complex", - on_delete=models.CASCADE, - related_name="complex_metro_stations", - verbose_name="所属楼盘", - ) - station = models.ForeignKey( - "region.MetroStation", - on_delete=models.CASCADE, - related_name="complex_links", - verbose_name="关联地铁站", - ) - distance_meters = models.IntegerField( - null=True, - blank=True, - verbose_name="步行距离", - help_text="单位:米", - ) - - class Meta: - db_table = "complex_metro_stations" - verbose_name = "楼盘地铁站关联" - verbose_name_plural = "楼盘地铁站关联" - constraints = [ - models.UniqueConstraint( - fields=["complex", "station"], - name="pk_complex_metro_stations", - ), - ] - indexes = [ - models.Index(fields=["complex"], name="idx_complex_metro_complex"), - models.Index(fields=["station"], name="idx_complex_metro_station"), - ] - - -class Building(TimeStampedModel): - complex = models.ForeignKey( - "fonrey_complex.Complex", - on_delete=models.CASCADE, - related_name="buildings", - verbose_name="所属楼盘", - ) - name = models.CharField( - max_length=50, - verbose_name="楼栋名称", - help_text='如「1号楼」「A栋2单元」', - ) - is_standard = models.BooleanField( - default=False, - verbose_name="是否标准结构", - help_text="TRUE=已经运营核准", - ) - property_usage_type = models.CharField( - max_length=30, - blank=True, - default="", - choices=ComplexPropertyUsageType.choices, - verbose_name="物业类型", - help_text="可与楼盘不同,如商住楼盘内有纯商铺楼栋", - ) - built_year = models.SmallIntegerField( - null=True, - blank=True, - verbose_name="竣工年份", - ) - total_floors = models.SmallIntegerField( - null=True, - blank=True, - verbose_name="总层数", - ) - land_use_years = models.CharField( - max_length=30, - blank=True, - default="", - verbose_name="土地使用年限", - ) - has_elevator = models.BooleanField( - null=True, - blank=True, - verbose_name="是否有电梯", - ) - school = models.ForeignKey( - "region.School", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="buildings", - verbose_name="对口学校", - help_text="楼栋级别的学区差异", - ) - is_active = models.BooleanField( - default=True, - verbose_name="是否启用", - help_text="FALSE=已停用(楼栋被删除或合并)", - ) - created_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="created_buildings", - verbose_name="创建人", - ) - - class Meta: - db_table = "buildings" - verbose_name = "楼栋" - verbose_name_plural = "楼栋" - indexes = [ - models.Index( - fields=["complex"], - name="idx_buildings_complex", - condition=models.Q(is_active=True), - ), - ] - constraints = [ - models.UniqueConstraint( - fields=["complex", "name"], - condition=models.Q(is_active=True), - name="uq_buildings_complex_name", - ), - ] - ordering = ["complex_id", "name"] - - def __str__(self) -> str: - return self.name - - -class RoomUnit(TimeStampedModel): - building = models.ForeignKey( - "fonrey_complex.Building", - on_delete=models.CASCADE, - related_name="room_units", - verbose_name="所属楼栋", - ) - floor = models.SmallIntegerField( - verbose_name="楼层", - help_text="实际层数,地下为负数", - ) - floor_name = models.CharField( - max_length=20, - blank=True, - default="", - verbose_name="楼层名称", - help_text='如「1层」「B1层」', - ) - room_no = models.CharField( - max_length=30, - verbose_name="房号", - help_text='如「01」「101」', - ) - display_no = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="展示房号", - help_text='展示用完整房号,如「3-1-101」', - ) - is_standard = models.BooleanField( - default=False, - verbose_name="是否标准化", - help_text="TRUE=已归一化为标准结构", - ) - is_active = models.BooleanField( - default=True, - verbose_name="是否启用", - help_text="FALSE=已拆除/不存在", - ) - - class Meta: - db_table = "room_units" - verbose_name = "房号单元" - verbose_name_plural = "房号单元" - indexes = [ - models.Index( - fields=["building"], - name="idx_room_units_building", - condition=models.Q(is_active=True), - ), - ] - constraints = [ - models.UniqueConstraint( - fields=["building", "floor", "room_no"], - condition=models.Q(is_active=True), - name="uq_room_units_unique", - ), - ] - ordering = ["building_id", "-floor", "room_no"] - - def __str__(self) -> str: - return self.display_no or f"{self.floor}/{self.room_no}" - - -class ComplexPhoto(UUIDPrimaryKeyModel): - complex = models.ForeignKey( - "fonrey_complex.Complex", - on_delete=models.CASCADE, - related_name="photos", - verbose_name="所属楼盘", - ) - category = models.CharField( - max_length=20, - choices=ComplexPhotoCategory.choices, - verbose_name="照片类别", - help_text="complex=楼盘图 / layout=户型图 / vr=VR全景 / other=其他", - ) - file_key = models.TextField( - verbose_name="文件存储路径", - help_text="R2/S3 路径", - ) - thumbnail_key = models.TextField( - blank=True, - default="", - verbose_name="缩略图路径", - ) - file_name = models.CharField( - max_length=255, - blank=True, - default="", - verbose_name="原始文件名", - ) - file_size = models.IntegerField( - null=True, - blank=True, - verbose_name="文件大小", - help_text="单位:bytes", - ) - width = models.IntegerField( - null=True, - blank=True, - verbose_name="图片宽度", - help_text="单位:px", - ) - height = models.IntegerField( - null=True, - blank=True, - verbose_name="图片高度", - help_text="单位:px", - ) - is_cover = models.BooleanField( - default=False, - verbose_name="是否封面图", - help_text="楼盘封面图(每楼盘唯一)", - ) - sort_order = models.SmallIntegerField( - default=0, - verbose_name="排序顺序", - help_text="同类别内的排序顺序", - ) - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name="创建时间", - ) - created_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="created_complex_photos", - verbose_name="上传人", - ) - - class Meta: - db_table = "complex_photos" - verbose_name = "楼盘照片" - verbose_name_plural = "楼盘照片" - indexes = [ - models.Index(fields=["complex"], name="idx_complex_photos_complex"), - models.Index(fields=["complex", "category"], name="idx_complex_photos_category"), - ] - constraints = [ - models.UniqueConstraint( - fields=["complex"], - condition=models.Q(is_cover=True), - name="uq_complex_photos_cover", - ), - ] - ordering = ["complex_id", "sort_order"] - - -class ComplexAttachment(UUIDPrimaryKeyModel): - complex = models.ForeignKey( - "fonrey_complex.Complex", - on_delete=models.CASCADE, - related_name="attachments", - verbose_name="所属楼盘", - ) - file_key = models.TextField( - verbose_name="文件存储路径", - help_text="R2/S3 存储路径", - ) - file_name = models.CharField( - max_length=255, - verbose_name="原始文件名", - ) - file_size = models.IntegerField( - null=True, - blank=True, - verbose_name="文件大小", - help_text="单位:bytes", - ) - file_type = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="文件类型", - help_text="MIME type", - ) - sort_order = models.SmallIntegerField( - default=0, - verbose_name="排序顺序", - ) - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name="创建时间", - ) - created_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="created_complex_attachments", - verbose_name="上传人", - ) - - class Meta: - db_table = "complex_attachments" - verbose_name = "楼盘附件" - verbose_name_plural = "楼盘附件" - ordering = ["complex_id", "sort_order"] - - -class ComplexPriceTrend(UUIDPrimaryKeyModel): - complex = models.ForeignKey( - "fonrey_complex.Complex", - on_delete=models.CASCADE, - related_name="price_trends", - verbose_name="所属楼盘", - ) - record_month = models.DateField( - verbose_name="月份", - help_text="统一存为该月1日,如 2026-04-01", - ) - avg_sale_price = models.DecimalField( - max_digits=12, - decimal_places=2, - null=True, - blank=True, - verbose_name="月均售价", - help_text="单位:万元/套", - ) - avg_unit_price = models.DecimalField( - max_digits=10, - decimal_places=2, - null=True, - blank=True, - verbose_name="月均单价", - help_text="单位:元/m²", - ) - transaction_count = models.IntegerField( - null=True, - blank=True, - verbose_name="成交套数", - ) - listing_count = models.IntegerField( - null=True, - blank=True, - verbose_name="当月挂牌套数", - ) - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name="创建时间", - ) - - class Meta: - db_table = "complex_price_trends" - verbose_name = "楼盘价格走势" - verbose_name_plural = "楼盘价格走势" - constraints = [ - models.UniqueConstraint( - fields=["complex", "record_month"], - name="uq_complex_price_trend_month", - ), - ] - indexes = [ - models.Index( - fields=["complex", "-record_month"], - name="idx_cpx_price_trend_complex", - ), - ] - ordering = ["complex_id", "-record_month"] diff --git a/apps/complex/serializers.py b/apps/complex/serializers.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/complex/services/__init__.py b/apps/complex/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/complex/tasks.py b/apps/complex/tasks.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/complex/templates/complex/.gitkeep b/apps/complex/templates/complex/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/apps/complex/tests/__init__.py b/apps/complex/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/complex/urls.py b/apps/complex/urls.py deleted file mode 100644 index 98ca7b1..0000000 --- a/apps/complex/urls.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.urls import path - -app_name = "complex" - -urlpatterns: list = [] diff --git a/apps/complex/views.py b/apps/complex/views.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/org/__init__.py b/apps/org/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/org/admin.py b/apps/org/admin.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/org/apps.py b/apps/org/apps.py deleted file mode 100644 index ac67c6e..0000000 --- a/apps/org/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class OrgConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.org" - label = "org" diff --git a/apps/org/migrations/0001_initial.py b/apps/org/migrations/0001_initial.py deleted file mode 100644 index 454bffb..0000000 --- a/apps/org/migrations/0001_initial.py +++ /dev/null @@ -1,300 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-29 08:42 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='OrgUnit', - 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(max_length=100)), - ('type', models.CharField(choices=[('company', '公司'), ('division', '事业部'), ('region', '大区'), ('area', '区域'), ('district', '片区'), ('store', '门店'), ('group', '店组'), ('functional', '职能部门')], max_length=20)), - ('path', models.TextField(help_text='Materialized path: /root_id/.../self_id/ for subtree queries.')), - ('depth', models.SmallIntegerField(default=0)), - ('sort_order', models.IntegerField(default=0)), - ('attribute', models.CharField(blank=True, choices=[('direct', '直营'), ('franchise', '加盟')], max_length=10, null=True)), - ('address_city', models.CharField(blank=True, default='', max_length=50)), - ('address_district', models.CharField(blank=True, default='', max_length=50)), - ('address_detail', models.CharField(blank=True, default='', max_length=200)), - ('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)), - ('established_at', models.DateField(blank=True, null=True)), - ('phone', models.CharField(blank=True, default='', max_length=30)), - ('ext_start', models.IntegerField(blank=True, null=True)), - ('ext_end', models.IntegerField(blank=True, null=True)), - ('is_active', models.BooleanField(default=True)), - ], - options={ - 'db_table': 'org_units', - 'ordering': ['sort_order', 'name'], - }, - ), - migrations.CreateModel( - name='Staff', - 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(max_length=50)), - ('nickname', models.CharField(blank=True, default='', max_length=50)), - ('employee_no', models.CharField(blank=True, max_length=30, null=True, unique=True)), - ('role', models.CharField(choices=[('agent', '经纪人'), ('store_manager', '店长'), ('area_manager', '区域经理'), ('admin', '系统管理员'), ('operator', '运营/行政'), ('system', '系统账号')], max_length=30)), - ('job_title', models.CharField(blank=True, default='', max_length=100)), - ('job_category', models.CharField(blank=True, default='', help_text="Job classification (e.g. '置业顾问' = agent qualification flag).", max_length=50)), - ('job_level', models.SmallIntegerField(blank=True, null=True)), - ('status', models.CharField(choices=[('active', '在职'), ('probation', '试用'), ('resigned', '离职'), ('frozen', '冻结')], default='active', max_length=20)), - ('phone_enc', models.BinaryField(blank=True, help_text='AES-256-GCM encrypted phone (DATA_MODEL_ORG §3.2).', null=True)), - ('phone_hash', models.CharField(blank=True, db_index=True, max_length=64, null=True)), - ('phone_hide', models.BooleanField(default=False)), - ('email', models.EmailField(blank=True, default='', max_length=255)), - ('extension', models.CharField(blank=True, default='', max_length=20)), - ('avatar_key', models.TextField(blank=True, default='')), - ('is_active', models.BooleanField(default=True)), - ('is_system_admin', models.BooleanField(default=False)), - ('first_joined_at', models.DateField(blank=True, null=True)), - ('rejoined_at', models.DateField(blank=True, null=True)), - ('resigned_at', models.DateField(blank=True, null=True)), - ('joined_count', models.SmallIntegerField(default=1)), - ('industry_exp_years', models.SmallIntegerField(blank=True, null=True)), - ('business_type', models.CharField(blank=True, default='', max_length=50)), - ('bank_name', models.CharField(blank=True, default='', max_length=100)), - ('bank_account', models.CharField(blank=True, default='', max_length=50)), - ('partner_no', models.CharField(blank=True, default='', max_length=50)), - ('recruit_source', models.CharField(blank=True, default='', max_length=50)), - ('mentor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mentees', to='org.staff')), - ('org_unit', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='staff_members', to='org.orgunit')), - ('recruit_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='recruited_staff', to='org.staff')), - ('referrer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='referred_staff', to='org.staff')), - ('supervisor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='subordinates', to='org.staff')), - ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='staff_profile', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'db_table': 'staff', - }, - ), - migrations.CreateModel( - name='StaffWorkExperience', - 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)), - ('company', models.CharField(max_length=200)), - ('job_title', models.CharField(blank=True, default='', max_length=100)), - ('start_date', models.DateField(blank=True, null=True)), - ('end_date', models.DateField(blank=True, null=True)), - ('reason', models.CharField(blank=True, default='', max_length=200)), - ('reference_name', models.CharField(blank=True, default='', max_length=50)), - ('reference_phone', models.CharField(blank=True, default='', max_length=30)), - ('staff', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='work_experiences', to='org.staff')), - ], - options={ - 'db_table': 'staff_work_experiences', - }, - ), - migrations.CreateModel( - name='StaffTraining', - 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)), - ('training_name', models.CharField(max_length=200)), - ('training_date', models.DateField(blank=True, null=True)), - ('certificate', models.CharField(blank=True, default='', max_length=200)), - ('staff', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trainings', to='org.staff')), - ], - options={ - 'db_table': 'staff_trainings', - }, - ), - migrations.CreateModel( - name='StaffRewardPunish', - 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)), - ('rp_date', models.DateField()), - ('category', models.CharField(help_text='Configurable lookup_items domain: org.reward_punish_category.', max_length=50)), - ('name', models.CharField(max_length=100)), - ('remarks', models.TextField(blank=True, default='')), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_reward_punish', to='org.staff')), - ('staff', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reward_punish_records', to='org.staff')), - ], - options={ - 'db_table': 'staff_reward_punish', - }, - ), - migrations.CreateModel( - name='StaffRemark', - 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)), - ('content', models.TextField()), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_remarks', to='org.staff')), - ('staff', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='remarks', to='org.staff')), - ], - options={ - 'db_table': 'staff_remarks', - }, - ), - migrations.CreateModel( - name='StaffFamilyMember', - 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)), - ('relation', models.CharField(max_length=30)), - ('name', models.CharField(max_length=50)), - ('birthdate', models.DateField(blank=True, null=True)), - ('occupation', models.CharField(blank=True, default='', max_length=100)), - ('work_unit', models.CharField(blank=True, default='', max_length=200)), - ('phone_enc', models.BinaryField(blank=True, null=True)), - ('staff', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='family_members', to='org.staff')), - ], - options={ - 'db_table': 'staff_family_members', - }, - ), - migrations.CreateModel( - name='StaffEducation', - 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)), - ('stage', models.CharField(blank=True, default='', max_length=30)), - ('school', models.CharField(max_length=200)), - ('major', models.CharField(blank=True, default='', max_length=100)), - ('start_date', models.DateField(blank=True, null=True)), - ('end_date', models.DateField(blank=True, null=True)), - ('enrollment_status', models.CharField(blank=True, default='', max_length=30)), - ('degree', models.CharField(blank=True, default='', max_length=30)), - ('staff', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='educations', to='org.staff')), - ], - options={ - 'db_table': 'staff_educations', - }, - ), - migrations.CreateModel( - name='StaffAccount', - 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)), - ('platform', models.CharField(choices=[('fonrey', '房睿主账号'), ('58anjuke', '58安居客'), ('cnreic', '中国网络经纪人'), ('wechat_mp', '微信公众号')], max_length=30)), - ('account_no', models.CharField(blank=True, default='', max_length=100)), - ('is_real_name_match', models.BooleanField(blank=True, null=True)), - ('is_bound', models.BooleanField(default=False)), - ('bound_at', models.DateTimeField(blank=True, null=True)), - ('staff', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='external_accounts', to='org.staff')), - ], - options={ - 'db_table': 'staff_accounts', - }, - ), - migrations.AddField( - model_name='orgunit', - name='manager', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='managed_org_units', to='org.staff'), - ), - migrations.AddField( - model_name='orgunit', - name='parent', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.RESTRICT, related_name='children', to='org.orgunit'), - ), - migrations.CreateModel( - name='StaffTransferLog', - 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)), - ('transfer_type', models.CharField(choices=[('onboard', '入职'), ('transfer', '调动'), ('resign', '离职'), ('rejoin', '复职'), ('supervisor_change', '上级变更'), ('role_change', '角色变更'), ('freeze', '冻结账号'), ('unfreeze', '恢复账号')], max_length=30)), - ('old_value', models.JSONField(blank=True, null=True)), - ('new_value', models.JSONField(blank=True, null=True)), - ('transfer_date', models.DateField()), - ('remarks', models.CharField(blank=True, default='', max_length=50)), - ('operated_at', models.DateTimeField(auto_now_add=True)), - ('operator', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='operated_transfers', to='org.staff')), - ('staff', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='transfer_logs', to='org.staff')), - ], - options={ - 'db_table': 'staff_transfer_logs', - 'indexes': [models.Index(fields=['staff', '-transfer_date'], name='idx_transfer_logs_staff'), models.Index(fields=['transfer_type', '-operated_at'], name='idx_transfer_logs_type'), models.Index(fields=['operator'], name='idx_transfer_logs_operator')], - }, - ), - migrations.CreateModel( - name='StaffPersonalInfo', - fields=[ - ('staff', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='personal_info', serialize=False, to='org.staff')), - ('gender', models.CharField(blank=True, choices=[('male', '男'), ('female', '女'), ('unknown', '未知')], default='', max_length=10)), - ('id_type', models.CharField(blank=True, choices=[('id_card', '身份证'), ('passport', '护照'), ('other', '其他')], default='', max_length=20)), - ('id_number_enc', models.BinaryField(blank=True, null=True)), - ('id_number_hash', models.CharField(blank=True, db_index=True, max_length=64, null=True)), - ('id_verified', models.BooleanField(default=False)), - ('id_verified_at', models.DateTimeField(blank=True, null=True)), - ('birthdate', models.DateField(blank=True, null=True)), - ('native_place', models.CharField(blank=True, default='', max_length=100)), - ('domicile_type', models.CharField(blank=True, default='', max_length=20)), - ('marital_status', models.CharField(blank=True, default='', max_length=20)), - ('political_status', models.CharField(blank=True, default='', max_length=20)), - ('has_children', models.BooleanField(blank=True, null=True)), - ('education_level', models.CharField(blank=True, default='', max_length=20)), - ('ethnicity', models.CharField(blank=True, default='', max_length=20)), - ('domicile_address', models.CharField(blank=True, default='', max_length=200)), - ('residence_address', models.CharField(blank=True, default='', max_length=200)), - ('work_start_date', models.DateField(blank=True, null=True)), - ('emergency_contact', models.CharField(blank=True, default='', max_length=50)), - ('emergency_phone_enc', models.BinaryField(blank=True, null=True)), - ('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_personal_info', to='org.staff')), - ], - options={ - 'db_table': 'staff_personal_info', - }, - ), - migrations.AddConstraint( - model_name='staffaccount', - constraint=models.UniqueConstraint(fields=('staff', 'platform'), name='uq_staff_accounts_staff_platform'), - ), - migrations.AddIndex( - model_name='staff', - index=models.Index(fields=['org_unit'], name='idx_staff_org_unit'), - ), - migrations.AddIndex( - model_name='staff', - index=models.Index(fields=['supervisor'], name='idx_staff_supervisor'), - ), - migrations.AddIndex( - model_name='staff', - index=models.Index(fields=['status'], name='idx_staff_status'), - ), - migrations.AddIndex( - model_name='orgunit', - index=models.Index(fields=['parent'], name='idx_org_units_parent'), - ), - migrations.AddIndex( - model_name='orgunit', - index=models.Index(fields=['type'], name='idx_org_units_type'), - ), - migrations.AddIndex( - model_name='orgunit', - index=models.Index(fields=['path'], name='idx_org_units_path'), - ), - ] diff --git a/apps/org/migrations/0002_alter_orgunit_options_alter_staff_options_and_more.py b/apps/org/migrations/0002_alter_orgunit_options_alter_staff_options_and_more.py deleted file mode 100644 index 40133b2..0000000 --- a/apps/org/migrations/0002_alter_orgunit_options_alter_staff_options_and_more.py +++ /dev/null @@ -1,57 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-29 11:07 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('org', '0001_initial'), - ] - - operations = [ - migrations.AlterModelOptions( - name='orgunit', - options={'ordering': ['sort_order', 'name'], 'verbose_name': '组织节点', 'verbose_name_plural': '组织节点'}, - ), - migrations.AlterModelOptions( - name='staff', - options={'verbose_name': '员工', 'verbose_name_plural': '员工'}, - ), - migrations.AlterModelOptions( - name='staffaccount', - options={'verbose_name': '员工第三方账号', 'verbose_name_plural': '员工第三方账号'}, - ), - migrations.AlterModelOptions( - name='staffeducation', - options={'verbose_name': '教育经历', 'verbose_name_plural': '教育经历'}, - ), - migrations.AlterModelOptions( - name='stafffamilymember', - options={'verbose_name': '家庭成员', 'verbose_name_plural': '家庭成员'}, - ), - migrations.AlterModelOptions( - name='staffpersonalinfo', - options={'verbose_name': '员工个人信息', 'verbose_name_plural': '员工个人信息'}, - ), - migrations.AlterModelOptions( - name='staffremark', - options={'verbose_name': '员工备注', 'verbose_name_plural': '员工备注'}, - ), - migrations.AlterModelOptions( - name='staffrewardpunish', - options={'verbose_name': '奖惩记录', 'verbose_name_plural': '奖惩记录'}, - ), - migrations.AlterModelOptions( - name='stafftraining', - options={'verbose_name': '培训记录', 'verbose_name_plural': '培训记录'}, - ), - migrations.AlterModelOptions( - name='stafftransferlog', - options={'verbose_name': '人事异动记录', 'verbose_name_plural': '人事异动记录'}, - ), - migrations.AlterModelOptions( - name='staffworkexperience', - options={'verbose_name': '工作经历', 'verbose_name_plural': '工作经历'}, - ), - ] diff --git a/apps/org/migrations/0003_alter_orgunit_address_city_and_more.py b/apps/org/migrations/0003_alter_orgunit_address_city_and_more.py deleted file mode 100644 index e02cdf2..0000000 --- a/apps/org/migrations/0003_alter_orgunit_address_city_and_more.py +++ /dev/null @@ -1,626 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-30 01:46 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('org', '0002_alter_orgunit_options_alter_staff_options_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='orgunit', - name='address_city', - field=models.CharField(blank=True, default='', max_length=50, verbose_name='所在城市'), - ), - migrations.AlterField( - model_name='orgunit', - name='address_detail', - field=models.CharField(blank=True, default='', max_length=200, verbose_name='详细地址'), - ), - migrations.AlterField( - model_name='orgunit', - name='address_district', - field=models.CharField(blank=True, default='', max_length=50, verbose_name='所在县区'), - ), - migrations.AlterField( - model_name='orgunit', - name='attribute', - field=models.CharField(blank=True, choices=[('direct', '直营'), ('franchise', '加盟')], help_text='direct=直营 / franchise=加盟', max_length=10, null=True, verbose_name='经营属性'), - ), - migrations.AlterField( - model_name='orgunit', - name='depth', - field=models.SmallIntegerField(default=0, help_text='根=0,最大支持 8 层', verbose_name='节点深度'), - ), - migrations.AlterField( - model_name='orgunit', - name='established_at', - field=models.DateField(blank=True, null=True, verbose_name='成立时间'), - ), - migrations.AlterField( - model_name='orgunit', - name='ext_end', - field=models.IntegerField(blank=True, null=True, verbose_name='分机号结束'), - ), - migrations.AlterField( - model_name='orgunit', - name='ext_start', - field=models.IntegerField(blank=True, null=True, verbose_name='分机号起始'), - ), - migrations.AlterField( - model_name='orgunit', - name='is_active', - field=models.BooleanField(default=True, help_text='FALSE=已关闭部门,仍可在筛选中显示', verbose_name='是否启用'), - ), - migrations.AlterField( - model_name='orgunit', - name='latitude', - field=models.DecimalField(blank=True, decimal_places=7, help_text='部门定位针 WGS84', max_digits=10, null=True, verbose_name='纬度'), - ), - migrations.AlterField( - model_name='orgunit', - name='longitude', - field=models.DecimalField(blank=True, decimal_places=7, help_text='部门定位针 WGS84', max_digits=10, null=True, verbose_name='经度'), - ), - migrations.AlterField( - model_name='orgunit', - name='manager', - field=models.ForeignKey(blank=True, help_text='循环依赖,Application 层维护', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='managed_org_units', to='org.staff', verbose_name='部门负责人'), - ), - migrations.AlterField( - model_name='orgunit', - name='name', - field=models.CharField(help_text='部门/组织名称', max_length=100, verbose_name='部门名称'), - ), - migrations.AlterField( - model_name='orgunit', - name='parent', - field=models.ForeignKey(blank=True, help_text='父节点,根节点为 NULL', null=True, on_delete=django.db.models.deletion.RESTRICT, related_name='children', to='org.orgunit', verbose_name='父节点'), - ), - migrations.AlterField( - model_name='orgunit', - name='path', - field=models.TextField(help_text='/root_id/.../self_id/,用于子树查询', verbose_name='物化路径'), - ), - migrations.AlterField( - model_name='orgunit', - name='phone', - field=models.CharField(blank=True, default='', max_length=30, verbose_name='部门联系电话'), - ), - migrations.AlterField( - model_name='orgunit', - name='sort_order', - field=models.IntegerField(default=0, help_text='同级排序', verbose_name='排序顺序'), - ), - migrations.AlterField( - model_name='orgunit', - name='type', - field=models.CharField(choices=[('company', '公司'), ('division', '事业部'), ('region', '大区'), ('area', '区域'), ('district', '片区'), ('store', '门店'), ('group', '店组'), ('functional', '职能部门')], help_text='company=公司 / division=事业部 / region=大区 / area=区域 / district=片区 / store=门店 / group=店组 / functional=职能', max_length=20, verbose_name='组织类型'), - ), - migrations.AlterField( - model_name='staff', - name='avatar_key', - field=models.TextField(blank=True, default='', help_text='R2/S3 头像路径', verbose_name='头像存储路径'), - ), - migrations.AlterField( - model_name='staff', - name='bank_account', - field=models.CharField(blank=True, default='', help_text='内部财务用', max_length=50, verbose_name='银行卡号'), - ), - migrations.AlterField( - model_name='staff', - name='bank_name', - field=models.CharField(blank=True, default='', max_length=100, verbose_name='银行名称'), - ), - migrations.AlterField( - model_name='staff', - name='business_type', - field=models.CharField(blank=True, default='', max_length=50, verbose_name='业务类型'), - ), - migrations.AlterField( - model_name='staff', - name='email', - field=models.EmailField(blank=True, default='', max_length=255, verbose_name='邮箱'), - ), - migrations.AlterField( - model_name='staff', - name='employee_no', - field=models.CharField(blank=True, help_text='系统自动生成或手动录入', max_length=30, null=True, unique=True, verbose_name='员工工号'), - ), - migrations.AlterField( - model_name='staff', - name='extension', - field=models.CharField(blank=True, default='', max_length=20, verbose_name='分机号'), - ), - migrations.AlterField( - model_name='staff', - name='first_joined_at', - field=models.DateField(blank=True, help_text='计算工龄起点', null=True, verbose_name='首次入职日期'), - ), - migrations.AlterField( - model_name='staff', - name='industry_exp_years', - field=models.SmallIntegerField(blank=True, help_text='单位:年', null=True, verbose_name='行业经验'), - ), - migrations.AlterField( - model_name='staff', - name='is_active', - field=models.BooleanField(default=True, help_text='FALSE 时账号不可登录(联动 auth_user.is_active)', verbose_name='是否启用'), - ), - migrations.AlterField( - model_name='staff', - name='is_system_admin', - field=models.BooleanField(default=False, help_text='影响权限上限', verbose_name='是否系统管理员'), - ), - migrations.AlterField( - model_name='staff', - name='job_category', - field=models.CharField(blank=True, default='', help_text='如「置业顾问」(经纪人判定字段)', max_length=50, verbose_name='职务类别'), - ), - migrations.AlterField( - model_name='staff', - name='job_level', - field=models.SmallIntegerField(blank=True, null=True, verbose_name='职级'), - ), - migrations.AlterField( - model_name='staff', - name='job_title', - field=models.CharField(blank=True, default='', help_text='如「高级业务员」', max_length=100, verbose_name='职务名称'), - ), - migrations.AlterField( - model_name='staff', - name='joined_count', - field=models.SmallIntegerField(default=1, verbose_name='累计入职次数'), - ), - migrations.AlterField( - model_name='staff', - name='mentor', - field=models.ForeignKey(blank=True, help_text='带教员工', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mentees', to='org.staff', verbose_name='师傅'), - ), - migrations.AlterField( - model_name='staff', - name='name', - field=models.CharField(max_length=50, verbose_name='真实姓名'), - ), - migrations.AlterField( - model_name='staff', - name='nickname', - field=models.CharField(blank=True, default='', help_text='通讯录/显示名', max_length=50, verbose_name='昵称'), - ), - migrations.AlterField( - model_name='staff', - name='org_unit', - field=models.ForeignKey(help_text='当前所属组织节点(门店或店组)', on_delete=django.db.models.deletion.RESTRICT, related_name='staff_members', to='org.orgunit', verbose_name='所属组织节点'), - ), - migrations.AlterField( - model_name='staff', - name='partner_no', - field=models.CharField(blank=True, default='', max_length=50, verbose_name='联号'), - ), - migrations.AlterField( - model_name='staff', - name='phone_enc', - field=models.BinaryField(blank=True, help_text='AES-256-GCM 加密手机号', null=True, verbose_name='手机号(加密)'), - ), - migrations.AlterField( - model_name='staff', - name='phone_hash', - field=models.CharField(blank=True, db_index=True, help_text='SHA-256 哈希,用于唯一性索引', max_length=64, null=True, verbose_name='手机号哈希'), - ), - migrations.AlterField( - model_name='staff', - name='phone_hide', - field=models.BooleanField(default=False, verbose_name='通讯录隐藏手机号'), - ), - migrations.AlterField( - model_name='staff', - name='recruit_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='recruited_staff', to='org.staff', verbose_name='招聘人'), - ), - migrations.AlterField( - model_name='staff', - name='recruit_source', - field=models.CharField(blank=True, default='', max_length=50, verbose_name='招聘来源'), - ), - migrations.AlterField( - model_name='staff', - name='referrer', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='referred_staff', to='org.staff', verbose_name='转介人'), - ), - migrations.AlterField( - model_name='staff', - name='rejoined_at', - field=models.DateField(blank=True, null=True, verbose_name='最近复职日期'), - ), - migrations.AlterField( - model_name='staff', - name='resigned_at', - field=models.DateField(blank=True, null=True, verbose_name='最近离职日期'), - ), - migrations.AlterField( - model_name='staff', - name='role', - field=models.CharField(choices=[('agent', '经纪人'), ('store_manager', '店长'), ('area_manager', '区域经理'), ('admin', '系统管理员'), ('operator', '运营/行政'), ('system', '系统账号')], help_text='agent=经纪人 / store_manager=店长 / area_manager=区域经理 / admin=管理员 / operator=运营 / system=系统账号', max_length=30, verbose_name='系统角色'), - ), - migrations.AlterField( - model_name='staff', - name='status', - field=models.CharField(choices=[('active', '在职'), ('probation', '试用'), ('resigned', '离职'), ('frozen', '冻结')], default='active', help_text='active=在职 / probation=试用期 / resigned=已离职 / frozen=账号冻结', max_length=20, verbose_name='员工状态'), - ), - migrations.AlterField( - model_name='staff', - name='supervisor', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='subordinates', to='org.staff', verbose_name='直属上级'), - ), - migrations.AlterField( - model_name='staff', - name='user', - field=models.OneToOneField(blank=True, help_text='Django auth 登录账号', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='staff_profile', to=settings.AUTH_USER_MODEL, verbose_name='登录账号'), - ), - migrations.AlterField( - model_name='staffaccount', - name='account_no', - field=models.CharField(blank=True, default='', max_length=100, verbose_name='账号/手机号'), - ), - migrations.AlterField( - model_name='staffaccount', - name='bound_at', - field=models.DateTimeField(blank=True, null=True, verbose_name='绑定时间'), - ), - migrations.AlterField( - model_name='staffaccount', - name='is_bound', - field=models.BooleanField(default=False, verbose_name='是否已绑定'), - ), - migrations.AlterField( - model_name='staffaccount', - name='is_real_name_match', - field=models.BooleanField(blank=True, help_text='中国网络经纪人专用', null=True, verbose_name='实名信息一致'), - ), - migrations.AlterField( - model_name='staffaccount', - name='platform', - field=models.CharField(choices=[('fonrey', '房睿主账号'), ('58anjuke', '58安居客'), ('cnreic', '中国网络经纪人'), ('wechat_mp', '微信公众号')], help_text='fonrey=主账号 / 58anjuke=58安居客 / cnreic=中国网络经纪人 / wechat_mp=微信公众号', max_length=30, verbose_name='平台'), - ), - migrations.AlterField( - model_name='staffaccount', - name='staff', - field=models.ForeignKey(help_text='证件信息随员工关联', on_delete=django.db.models.deletion.CASCADE, related_name='external_accounts', to='org.staff', verbose_name='所属员工'), - ), - migrations.AlterField( - model_name='staffeducation', - name='degree', - field=models.CharField(blank=True, default='', max_length=30, verbose_name='学位'), - ), - migrations.AlterField( - model_name='staffeducation', - name='end_date', - field=models.DateField(blank=True, null=True, verbose_name='结束日期'), - ), - migrations.AlterField( - model_name='staffeducation', - name='enrollment_status', - field=models.CharField(blank=True, default='', max_length=30, verbose_name='就读状态'), - ), - migrations.AlterField( - model_name='staffeducation', - name='major', - field=models.CharField(blank=True, default='', max_length=100, verbose_name='专业'), - ), - migrations.AlterField( - model_name='staffeducation', - name='school', - field=models.CharField(max_length=200, verbose_name='学校'), - ), - migrations.AlterField( - model_name='staffeducation', - name='staff', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='educations', to='org.staff', verbose_name='所属员工'), - ), - migrations.AlterField( - model_name='staffeducation', - name='stage', - field=models.CharField(blank=True, default='', max_length=30, verbose_name='教育阶段'), - ), - migrations.AlterField( - model_name='staffeducation', - name='start_date', - field=models.DateField(blank=True, null=True, verbose_name='开始日期'), - ), - migrations.AlterField( - model_name='stafffamilymember', - name='birthdate', - field=models.DateField(blank=True, null=True, verbose_name='出生日期'), - ), - migrations.AlterField( - model_name='stafffamilymember', - name='name', - field=models.CharField(max_length=50, verbose_name='姓名'), - ), - migrations.AlterField( - model_name='stafffamilymember', - name='occupation', - field=models.CharField(blank=True, default='', max_length=100, verbose_name='职业'), - ), - migrations.AlterField( - model_name='stafffamilymember', - name='phone_enc', - field=models.BinaryField(blank=True, help_text='AES-256-GCM 加密', null=True, verbose_name='电话(加密)'), - ), - migrations.AlterField( - model_name='stafffamilymember', - name='relation', - field=models.CharField(max_length=30, verbose_name='称谓'), - ), - migrations.AlterField( - model_name='stafffamilymember', - name='staff', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='family_members', to='org.staff', verbose_name='所属员工'), - ), - migrations.AlterField( - model_name='stafffamilymember', - name='work_unit', - field=models.CharField(blank=True, default='', max_length=200, verbose_name='工作单位'), - ), - migrations.AlterField( - model_name='staffpersonalinfo', - name='birthdate', - field=models.DateField(blank=True, null=True, verbose_name='出生日期'), - ), - migrations.AlterField( - model_name='staffpersonalinfo', - name='domicile_address', - field=models.CharField(blank=True, default='', max_length=200, verbose_name='户口所在地'), - ), - migrations.AlterField( - model_name='staffpersonalinfo', - name='domicile_type', - field=models.CharField(blank=True, default='', max_length=20, verbose_name='户籍性质'), - ), - migrations.AlterField( - model_name='staffpersonalinfo', - name='education_level', - field=models.CharField(blank=True, default='', max_length=20, verbose_name='最高学历'), - ), - migrations.AlterField( - model_name='staffpersonalinfo', - name='emergency_contact', - field=models.CharField(blank=True, default='', max_length=50, verbose_name='紧急联系人'), - ), - migrations.AlterField( - model_name='staffpersonalinfo', - name='emergency_phone_enc', - field=models.BinaryField(blank=True, null=True, verbose_name='紧急联系人电话(加密)'), - ), - migrations.AlterField( - model_name='staffpersonalinfo', - name='ethnicity', - field=models.CharField(blank=True, default='', max_length=20, verbose_name='民族'), - ), - migrations.AlterField( - model_name='staffpersonalinfo', - name='gender', - field=models.CharField(blank=True, choices=[('male', '男'), ('female', '女'), ('unknown', '未知')], default='', help_text='male=男 / female=女 / unknown=未知', max_length=10, verbose_name='性别'), - ), - migrations.AlterField( - model_name='staffpersonalinfo', - name='has_children', - field=models.BooleanField(blank=True, null=True, verbose_name='有无子女'), - ), - migrations.AlterField( - model_name='staffpersonalinfo', - name='id_number_enc', - field=models.BinaryField(blank=True, help_text='AES 加密', null=True, verbose_name='证件号码(加密)'), - ), - migrations.AlterField( - model_name='staffpersonalinfo', - name='id_number_hash', - field=models.CharField(blank=True, db_index=True, help_text='SHA-256 哈希,实名认证比对用', max_length=64, null=True, verbose_name='证件号码哈希'), - ), - migrations.AlterField( - model_name='staffpersonalinfo', - name='id_type', - field=models.CharField(blank=True, choices=[('id_card', '身份证'), ('passport', '护照'), ('other', '其他')], default='', help_text='id_card=身份证 / passport=护照 / other=其他', max_length=20, verbose_name='证件类型'), - ), - migrations.AlterField( - model_name='staffpersonalinfo', - name='id_verified', - field=models.BooleanField(default=False, verbose_name='是否实名认证'), - ), - migrations.AlterField( - model_name='staffpersonalinfo', - name='id_verified_at', - field=models.DateTimeField(blank=True, null=True, verbose_name='认证时间'), - ), - migrations.AlterField( - model_name='staffpersonalinfo', - name='marital_status', - field=models.CharField(blank=True, default='', max_length=20, verbose_name='婚姻状况'), - ), - migrations.AlterField( - model_name='staffpersonalinfo', - name='native_place', - field=models.CharField(blank=True, default='', max_length=100, verbose_name='籍贯'), - ), - migrations.AlterField( - model_name='staffpersonalinfo', - name='political_status', - field=models.CharField(blank=True, default='', max_length=20, verbose_name='政治面貌'), - ), - migrations.AlterField( - model_name='staffpersonalinfo', - name='residence_address', - field=models.CharField(blank=True, default='', max_length=200, verbose_name='居住地址'), - ), - migrations.AlterField( - model_name='staffpersonalinfo', - name='staff', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='personal_info', serialize=False, to='org.staff', verbose_name='所属员工'), - ), - migrations.AlterField( - model_name='staffpersonalinfo', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='最后更新时间'), - ), - migrations.AlterField( - model_name='staffpersonalinfo', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_personal_info', to='org.staff', verbose_name='最后修改人'), - ), - migrations.AlterField( - model_name='staffpersonalinfo', - name='work_start_date', - field=models.DateField(blank=True, null=True, verbose_name='参加工作时间'), - ), - migrations.AlterField( - model_name='staffremark', - name='content', - field=models.TextField(verbose_name='备注内容'), - ), - migrations.AlterField( - model_name='staffremark', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_remarks', to='org.staff', verbose_name='创建人'), - ), - migrations.AlterField( - model_name='staffremark', - name='staff', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='remarks', to='org.staff', verbose_name='所属员工'), - ), - migrations.AlterField( - model_name='staffrewardpunish', - name='category', - field=models.CharField(help_text='枚举由 lookup_items 维护:org.reward_punish_category', max_length=50, verbose_name='奖惩类别'), - ), - migrations.AlterField( - model_name='staffrewardpunish', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_reward_punish', to='org.staff', verbose_name='录入人'), - ), - migrations.AlterField( - model_name='staffrewardpunish', - name='name', - field=models.CharField(help_text='与类别联动', max_length=100, verbose_name='奖惩名称'), - ), - migrations.AlterField( - model_name='staffrewardpunish', - name='remarks', - field=models.TextField(blank=True, default='', verbose_name='备注'), - ), - migrations.AlterField( - model_name='staffrewardpunish', - name='rp_date', - field=models.DateField(verbose_name='奖惩日期'), - ), - migrations.AlterField( - model_name='staffrewardpunish', - name='staff', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reward_punish_records', to='org.staff', verbose_name='被奖惩员工'), - ), - migrations.AlterField( - model_name='stafftraining', - name='certificate', - field=models.CharField(blank=True, default='', max_length=200, verbose_name='证书'), - ), - migrations.AlterField( - model_name='stafftraining', - name='staff', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trainings', to='org.staff', verbose_name='所属员工'), - ), - migrations.AlterField( - model_name='stafftraining', - name='training_date', - field=models.DateField(blank=True, null=True, verbose_name='培训日期'), - ), - migrations.AlterField( - model_name='stafftraining', - name='training_name', - field=models.CharField(max_length=200, verbose_name='培训名称'), - ), - migrations.AlterField( - model_name='stafftransferlog', - name='new_value', - field=models.JSONField(blank=True, help_text='结构同 old_value', null=True, verbose_name='变动后值'), - ), - migrations.AlterField( - model_name='stafftransferlog', - name='old_value', - field=models.JSONField(blank=True, help_text='格式:{"field": "org_unit_id", "value": "...", "label": "门店A"}', null=True, verbose_name='变动前值'), - ), - migrations.AlterField( - model_name='stafftransferlog', - name='operated_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='系统操作时间'), - ), - migrations.AlterField( - model_name='stafftransferlog', - name='operator', - field=models.ForeignKey(help_text='必填,异动审计必须记录', on_delete=django.db.models.deletion.RESTRICT, related_name='operated_transfers', to='org.staff', verbose_name='操作人'), - ), - migrations.AlterField( - model_name='stafftransferlog', - name='remarks', - field=models.CharField(blank=True, default='', help_text='最多50字', max_length=50, verbose_name='备注'), - ), - migrations.AlterField( - model_name='stafftransferlog', - name='staff', - field=models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='transfer_logs', to='org.staff', verbose_name='被操作员工'), - ), - migrations.AlterField( - model_name='stafftransferlog', - name='transfer_date', - field=models.DateField(help_text='可以是过去日期', verbose_name='异动生效日期'), - ), - migrations.AlterField( - model_name='stafftransferlog', - name='transfer_type', - field=models.CharField(choices=[('onboard', '入职'), ('transfer', '调动'), ('resign', '离职'), ('rejoin', '复职'), ('supervisor_change', '上级变更'), ('role_change', '角色变更'), ('freeze', '冻结账号'), ('unfreeze', '恢复账号')], help_text='onboard=入职 / transfer=调动 / resign=离职 / rejoin=复职 / supervisor_change=上级变动 / role_change=角色变更 / freeze=账号冻结 / unfreeze=账号恢复', max_length=30, verbose_name='异动类型'), - ), - migrations.AlterField( - model_name='staffworkexperience', - name='company', - field=models.CharField(max_length=200, verbose_name='公司名称'), - ), - migrations.AlterField( - model_name='staffworkexperience', - name='end_date', - field=models.DateField(blank=True, null=True, verbose_name='结束日期'), - ), - migrations.AlterField( - model_name='staffworkexperience', - name='job_title', - field=models.CharField(blank=True, default='', max_length=100, verbose_name='职位'), - ), - migrations.AlterField( - model_name='staffworkexperience', - name='reason', - field=models.CharField(blank=True, default='', max_length=200, verbose_name='离职原因'), - ), - migrations.AlterField( - model_name='staffworkexperience', - name='reference_name', - field=models.CharField(blank=True, default='', max_length=50, verbose_name='证明人姓名'), - ), - migrations.AlterField( - model_name='staffworkexperience', - name='reference_phone', - field=models.CharField(blank=True, default='', max_length=30, verbose_name='证明人电话'), - ), - migrations.AlterField( - model_name='staffworkexperience', - name='staff', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='work_experiences', to='org.staff', verbose_name='所属员工'), - ), - migrations.AlterField( - model_name='staffworkexperience', - name='start_date', - field=models.DateField(blank=True, null=True, verbose_name='开始日期'), - ), - ] diff --git a/apps/org/migrations/__init__.py b/apps/org/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/org/models/__init__.py b/apps/org/models/__init__.py deleted file mode 100644 index 7d23e33..0000000 --- a/apps/org/models/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -from apps.org.models.org_unit import OrgUnit -from apps.org.models.staff import Staff, StaffPersonalInfo -from apps.org.models.staff_logs import ( - StaffAccount, - StaffEducation, - StaffFamilyMember, - StaffRemark, - StaffRewardPunish, - StaffTraining, - StaffTransferLog, - StaffWorkExperience, -) - -__all__ = [ - "OrgUnit", - "Staff", - "StaffPersonalInfo", - "StaffAccount", - "StaffEducation", - "StaffFamilyMember", - "StaffRemark", - "StaffRewardPunish", - "StaffTraining", - "StaffTransferLog", - "StaffWorkExperience", -] diff --git a/apps/org/models/org_unit.py b/apps/org/models/org_unit.py deleted file mode 100644 index b2470a7..0000000 --- a/apps/org/models/org_unit.py +++ /dev/null @@ -1,133 +0,0 @@ -from django.db import models - -from core.enums import OrgUnitAttribute, OrgUnitType -from core.models.base import SoftDeleteModel - - -class OrgUnit(SoftDeleteModel): - name = models.CharField( - max_length=100, - verbose_name="部门名称", - help_text="部门/组织名称", - ) - type = models.CharField( - max_length=20, - choices=OrgUnitType.choices, - verbose_name="组织类型", - help_text="company=公司 / division=事业部 / region=大区 / area=区域 / district=片区 / store=门店 / group=店组 / functional=职能", - ) - parent = models.ForeignKey( - "self", - null=True, - blank=True, - on_delete=models.RESTRICT, - related_name="children", - db_index=True, - verbose_name="父节点", - help_text="父节点,根节点为 NULL", - ) - path = models.TextField( - verbose_name="物化路径", - help_text='/root_id/.../self_id/,用于子树查询', - ) - depth = models.SmallIntegerField( - default=0, - verbose_name="节点深度", - help_text="根=0,最大支持 8 层", - ) - sort_order = models.IntegerField( - default=0, - verbose_name="排序顺序", - help_text="同级排序", - ) - attribute = models.CharField( - max_length=10, - choices=OrgUnitAttribute.choices, - null=True, - blank=True, - verbose_name="经营属性", - help_text="direct=直营 / franchise=加盟", - ) - address_city = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="所在城市", - ) - address_district = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="所在县区", - ) - address_detail = models.CharField( - max_length=200, - blank=True, - default="", - verbose_name="详细地址", - ) - latitude = models.DecimalField( - max_digits=10, - decimal_places=7, - null=True, - blank=True, - verbose_name="纬度", - help_text="部门定位针 WGS84", - ) - longitude = models.DecimalField( - max_digits=10, - decimal_places=7, - null=True, - blank=True, - verbose_name="经度", - help_text="部门定位针 WGS84", - ) - manager = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="managed_org_units", - verbose_name="部门负责人", - help_text="循环依赖,Application 层维护", - ) - established_at = models.DateField( - null=True, - blank=True, - verbose_name="成立时间", - ) - phone = models.CharField( - max_length=30, - blank=True, - default="", - verbose_name="部门联系电话", - ) - ext_start = models.IntegerField( - null=True, - blank=True, - verbose_name="分机号起始", - ) - ext_end = models.IntegerField( - null=True, - blank=True, - verbose_name="分机号结束", - ) - is_active = models.BooleanField( - default=True, - verbose_name="是否启用", - help_text="FALSE=已关闭部门,仍可在筛选中显示", - ) - - class Meta: - db_table = "org_units" - verbose_name = "组织节点" - verbose_name_plural = "组织节点" - indexes = [ - models.Index(fields=["parent"], name="idx_org_units_parent"), - models.Index(fields=["type"], name="idx_org_units_type"), - models.Index(fields=["path"], name="idx_org_units_path"), - ] - ordering = ["sort_order", "name"] - - def __str__(self) -> str: - return f"{self.name} ({self.type})" diff --git a/apps/org/models/staff.py b/apps/org/models/staff.py deleted file mode 100644 index 284820c..0000000 --- a/apps/org/models/staff.py +++ /dev/null @@ -1,365 +0,0 @@ -from django.conf import settings -from django.db import models - -from core.enums import StaffGender, StaffIdType, StaffRole, StaffStatus -from core.models.base import SoftDeleteModel - - -class Staff(SoftDeleteModel): - org_unit = models.ForeignKey( - "org.OrgUnit", - on_delete=models.RESTRICT, - related_name="staff_members", - db_index=True, - verbose_name="所属组织节点", - help_text="当前所属组织节点(门店或店组)", - ) - user = models.OneToOneField( - settings.AUTH_USER_MODEL, - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="staff_profile", - verbose_name="登录账号", - help_text="Django auth 登录账号", - ) - name = models.CharField( - max_length=50, - verbose_name="真实姓名", - ) - nickname = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="昵称", - help_text="通讯录/显示名", - ) - employee_no = models.CharField( - max_length=30, - null=True, - blank=True, - unique=True, - verbose_name="员工工号", - help_text="系统自动生成或手动录入", - ) - role = models.CharField( - max_length=30, - choices=StaffRole.choices, - verbose_name="系统角色", - help_text="agent=经纪人 / store_manager=店长 / area_manager=区域经理 / admin=管理员 / operator=运营 / system=系统账号", - ) - job_title = models.CharField( - max_length=100, - blank=True, - default="", - verbose_name="职务名称", - help_text='如「高级业务员」', - ) - job_category = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="职务类别", - help_text='如「置业顾问」(经纪人判定字段)', - ) - job_level = models.SmallIntegerField( - null=True, - blank=True, - verbose_name="职级", - ) - supervisor = models.ForeignKey( - "self", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="subordinates", - verbose_name="直属上级", - ) - status = models.CharField( - max_length=20, - choices=StaffStatus.choices, - default=StaffStatus.ACTIVE, - verbose_name="员工状态", - help_text="active=在职 / probation=试用期 / resigned=已离职 / frozen=账号冻结", - ) - phone_enc = models.BinaryField( - null=True, - blank=True, - verbose_name="手机号(加密)", - help_text="AES-256-GCM 加密手机号", - ) - phone_hash = models.CharField( - max_length=64, - null=True, - blank=True, - db_index=True, - verbose_name="手机号哈希", - help_text="SHA-256 哈希,用于唯一性索引", - ) - phone_hide = models.BooleanField( - default=False, - verbose_name="通讯录隐藏手机号", - ) - email = models.EmailField( - max_length=255, - blank=True, - default="", - verbose_name="邮箱", - ) - extension = models.CharField( - max_length=20, - blank=True, - default="", - verbose_name="分机号", - ) - avatar_key = models.TextField( - blank=True, - default="", - verbose_name="头像存储路径", - help_text="R2/S3 头像路径", - ) - is_active = models.BooleanField( - default=True, - verbose_name="是否启用", - help_text="FALSE 时账号不可登录(联动 auth_user.is_active)", - ) - is_system_admin = models.BooleanField( - default=False, - verbose_name="是否系统管理员", - help_text="影响权限上限", - ) - first_joined_at = models.DateField( - null=True, - blank=True, - verbose_name="首次入职日期", - help_text="计算工龄起点", - ) - rejoined_at = models.DateField( - null=True, - blank=True, - verbose_name="最近复职日期", - ) - resigned_at = models.DateField( - null=True, - blank=True, - verbose_name="最近离职日期", - ) - joined_count = models.SmallIntegerField( - default=1, - verbose_name="累计入职次数", - ) - industry_exp_years = models.SmallIntegerField( - null=True, - blank=True, - verbose_name="行业经验", - help_text="单位:年", - ) - mentor = models.ForeignKey( - "self", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="mentees", - verbose_name="师傅", - help_text="带教员工", - ) - business_type = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="业务类型", - ) - bank_name = models.CharField( - max_length=100, - blank=True, - default="", - verbose_name="银行名称", - ) - bank_account = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="银行卡号", - help_text="内部财务用", - ) - partner_no = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="联号", - ) - recruit_by = models.ForeignKey( - "self", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="recruited_staff", - verbose_name="招聘人", - ) - recruit_source = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="招聘来源", - ) - referrer = models.ForeignKey( - "self", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="referred_staff", - verbose_name="转介人", - ) - - class Meta: - db_table = "staff" - verbose_name = "员工" - verbose_name_plural = "员工" - indexes = [ - models.Index(fields=["org_unit"], name="idx_staff_org_unit"), - models.Index(fields=["supervisor"], name="idx_staff_supervisor"), - models.Index(fields=["status"], name="idx_staff_status"), - ] - - def __str__(self) -> str: - return self.name - - -class StaffPersonalInfo(models.Model): - staff = models.OneToOneField( - "org.Staff", - on_delete=models.CASCADE, - related_name="personal_info", - primary_key=True, - verbose_name="所属员工", - ) - gender = models.CharField( - max_length=10, - choices=StaffGender.choices, - blank=True, - default="", - verbose_name="性别", - help_text="male=男 / female=女 / unknown=未知", - ) - id_type = models.CharField( - max_length=20, - choices=StaffIdType.choices, - blank=True, - default="", - verbose_name="证件类型", - help_text="id_card=身份证 / passport=护照 / other=其他", - ) - id_number_enc = models.BinaryField( - null=True, - blank=True, - verbose_name="证件号码(加密)", - help_text="AES 加密", - ) - id_number_hash = models.CharField( - max_length=64, - null=True, - blank=True, - db_index=True, - verbose_name="证件号码哈希", - help_text="SHA-256 哈希,实名认证比对用", - ) - id_verified = models.BooleanField( - default=False, - verbose_name="是否实名认证", - ) - id_verified_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="认证时间", - ) - birthdate = models.DateField( - null=True, - blank=True, - verbose_name="出生日期", - ) - native_place = models.CharField( - max_length=100, - blank=True, - default="", - verbose_name="籍贯", - ) - domicile_type = models.CharField( - max_length=20, - blank=True, - default="", - verbose_name="户籍性质", - ) - marital_status = models.CharField( - max_length=20, - blank=True, - default="", - verbose_name="婚姻状况", - ) - political_status = models.CharField( - max_length=20, - blank=True, - default="", - verbose_name="政治面貌", - ) - has_children = models.BooleanField( - null=True, - blank=True, - verbose_name="有无子女", - ) - education_level = models.CharField( - max_length=20, - blank=True, - default="", - verbose_name="最高学历", - ) - ethnicity = models.CharField( - max_length=20, - blank=True, - default="", - verbose_name="民族", - ) - domicile_address = models.CharField( - max_length=200, - blank=True, - default="", - verbose_name="户口所在地", - ) - residence_address = models.CharField( - max_length=200, - blank=True, - default="", - verbose_name="居住地址", - ) - work_start_date = models.DateField( - null=True, - blank=True, - verbose_name="参加工作时间", - ) - emergency_contact = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="紧急联系人", - ) - emergency_phone_enc = models.BinaryField( - null=True, - blank=True, - verbose_name="紧急联系人电话(加密)", - ) - updated_at = models.DateTimeField( - auto_now=True, - verbose_name="最后更新时间", - ) - updated_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="updated_personal_info", - verbose_name="最后修改人", - ) - - class Meta: - db_table = "staff_personal_info" - verbose_name = "员工个人信息" - verbose_name_plural = "员工个人信息" diff --git a/apps/org/models/staff_logs.py b/apps/org/models/staff_logs.py deleted file mode 100644 index a095a17..0000000 --- a/apps/org/models/staff_logs.py +++ /dev/null @@ -1,354 +0,0 @@ -from django.db import models - -from core.enums import StaffAccountPlatform, StaffTransferType -from core.models.base import SoftDeleteModel, TimeStampedModel - - -class StaffTransferLog(TimeStampedModel): - staff = models.ForeignKey( - "org.Staff", - on_delete=models.RESTRICT, - related_name="transfer_logs", - verbose_name="被操作员工", - ) - transfer_type = models.CharField( - max_length=30, - choices=StaffTransferType.choices, - verbose_name="异动类型", - help_text="onboard=入职 / transfer=调动 / resign=离职 / rejoin=复职 / supervisor_change=上级变动 / role_change=角色变更 / freeze=账号冻结 / unfreeze=账号恢复", - ) - old_value = models.JSONField( - null=True, - blank=True, - verbose_name="变动前值", - help_text='格式:{"field": "org_unit_id", "value": "...", "label": "门店A"}', - ) - new_value = models.JSONField( - null=True, - blank=True, - verbose_name="变动后值", - help_text="结构同 old_value", - ) - transfer_date = models.DateField( - verbose_name="异动生效日期", - help_text="可以是过去日期", - ) - remarks = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="备注", - help_text="最多50字", - ) - operator = models.ForeignKey( - "org.Staff", - on_delete=models.RESTRICT, - related_name="operated_transfers", - verbose_name="操作人", - help_text="必填,异动审计必须记录", - ) - operated_at = models.DateTimeField( - auto_now_add=True, - verbose_name="系统操作时间", - ) - - class Meta: - db_table = "staff_transfer_logs" - verbose_name = "人事异动记录" - verbose_name_plural = "人事异动记录" - indexes = [ - models.Index(fields=["staff", "-transfer_date"], name="idx_transfer_logs_staff"), - models.Index(fields=["transfer_type", "-operated_at"], name="idx_transfer_logs_type"), - models.Index(fields=["operator"], name="idx_transfer_logs_operator"), - ] - - -class StaffRewardPunish(SoftDeleteModel): - staff = models.ForeignKey( - "org.Staff", - on_delete=models.CASCADE, - related_name="reward_punish_records", - verbose_name="被奖惩员工", - ) - rp_date = models.DateField( - verbose_name="奖惩日期", - ) - category = models.CharField( - max_length=50, - verbose_name="奖惩类别", - help_text="枚举由 lookup_items 维护:org.reward_punish_category", - ) - name = models.CharField( - max_length=100, - verbose_name="奖惩名称", - help_text="与类别联动", - ) - remarks = models.TextField( - blank=True, - default="", - verbose_name="备注", - ) - created_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="created_reward_punish", - verbose_name="录入人", - ) - - class Meta: - db_table = "staff_reward_punish" - verbose_name = "奖惩记录" - verbose_name_plural = "奖惩记录" - - -class StaffAccount(TimeStampedModel): - staff = models.ForeignKey( - "org.Staff", - on_delete=models.CASCADE, - related_name="external_accounts", - verbose_name="所属员工", - help_text="证件信息随员工关联", - ) - platform = models.CharField( - max_length=30, - choices=StaffAccountPlatform.choices, - verbose_name="平台", - help_text="fonrey=主账号 / 58anjuke=58安居客 / cnreic=中国网络经纪人 / wechat_mp=微信公众号", - ) - account_no = models.CharField( - max_length=100, - blank=True, - default="", - verbose_name="账号/手机号", - ) - is_real_name_match = models.BooleanField( - null=True, - blank=True, - verbose_name="实名信息一致", - help_text="中国网络经纪人专用", - ) - is_bound = models.BooleanField( - default=False, - verbose_name="是否已绑定", - ) - bound_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="绑定时间", - ) - - class Meta: - db_table = "staff_accounts" - verbose_name = "员工第三方账号" - verbose_name_plural = "员工第三方账号" - constraints = [ - models.UniqueConstraint( - fields=["staff", "platform"], - name="uq_staff_accounts_staff_platform", - ), - ] - - -class StaffWorkExperience(TimeStampedModel): - staff = models.ForeignKey( - "org.Staff", - on_delete=models.CASCADE, - related_name="work_experiences", - verbose_name="所属员工", - ) - company = models.CharField( - max_length=200, - verbose_name="公司名称", - ) - job_title = models.CharField( - max_length=100, - blank=True, - default="", - verbose_name="职位", - ) - start_date = models.DateField( - null=True, - blank=True, - verbose_name="开始日期", - ) - end_date = models.DateField( - null=True, - blank=True, - verbose_name="结束日期", - ) - reason = models.CharField( - max_length=200, - blank=True, - default="", - verbose_name="离职原因", - ) - reference_name = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="证明人姓名", - ) - reference_phone = models.CharField( - max_length=30, - blank=True, - default="", - verbose_name="证明人电话", - ) - - class Meta: - db_table = "staff_work_experiences" - verbose_name = "工作经历" - verbose_name_plural = "工作经历" - - -class StaffEducation(TimeStampedModel): - staff = models.ForeignKey( - "org.Staff", - on_delete=models.CASCADE, - related_name="educations", - verbose_name="所属员工", - ) - stage = models.CharField( - max_length=30, - blank=True, - default="", - verbose_name="教育阶段", - ) - school = models.CharField( - max_length=200, - verbose_name="学校", - ) - major = models.CharField( - max_length=100, - blank=True, - default="", - verbose_name="专业", - ) - start_date = models.DateField( - null=True, - blank=True, - verbose_name="开始日期", - ) - end_date = models.DateField( - null=True, - blank=True, - verbose_name="结束日期", - ) - enrollment_status = models.CharField( - max_length=30, - blank=True, - default="", - verbose_name="就读状态", - ) - degree = models.CharField( - max_length=30, - blank=True, - default="", - verbose_name="学位", - ) - - class Meta: - db_table = "staff_educations" - verbose_name = "教育经历" - verbose_name_plural = "教育经历" - - -class StaffTraining(TimeStampedModel): - staff = models.ForeignKey( - "org.Staff", - on_delete=models.CASCADE, - related_name="trainings", - verbose_name="所属员工", - ) - training_name = models.CharField( - max_length=200, - verbose_name="培训名称", - ) - training_date = models.DateField( - null=True, - blank=True, - verbose_name="培训日期", - ) - certificate = models.CharField( - max_length=200, - blank=True, - default="", - verbose_name="证书", - ) - - class Meta: - db_table = "staff_trainings" - verbose_name = "培训记录" - verbose_name_plural = "培训记录" - - -class StaffFamilyMember(TimeStampedModel): - staff = models.ForeignKey( - "org.Staff", - on_delete=models.CASCADE, - related_name="family_members", - verbose_name="所属员工", - ) - relation = models.CharField( - max_length=30, - verbose_name="称谓", - ) - name = models.CharField( - max_length=50, - verbose_name="姓名", - ) - birthdate = models.DateField( - null=True, - blank=True, - verbose_name="出生日期", - ) - occupation = models.CharField( - max_length=100, - blank=True, - default="", - verbose_name="职业", - ) - work_unit = models.CharField( - max_length=200, - blank=True, - default="", - verbose_name="工作单位", - ) - phone_enc = models.BinaryField( - null=True, - blank=True, - verbose_name="电话(加密)", - help_text="AES-256-GCM 加密", - ) - - class Meta: - db_table = "staff_family_members" - verbose_name = "家庭成员" - verbose_name_plural = "家庭成员" - - -class StaffRemark(SoftDeleteModel): - staff = models.ForeignKey( - "org.Staff", - on_delete=models.CASCADE, - related_name="remarks", - verbose_name="所属员工", - ) - content = models.TextField( - verbose_name="备注内容", - ) - created_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="created_remarks", - verbose_name="创建人", - ) - - class Meta: - db_table = "staff_remarks" - verbose_name = "员工备注" - verbose_name_plural = "员工备注" diff --git a/apps/org/serializers.py b/apps/org/serializers.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/org/services/__init__.py b/apps/org/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/org/tasks.py b/apps/org/tasks.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/org/templates/org/.gitkeep b/apps/org/templates/org/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/apps/org/tests/__init__.py b/apps/org/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/org/urls.py b/apps/org/urls.py deleted file mode 100644 index 32ebb5b..0000000 --- a/apps/org/urls.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.urls import path - -app_name = "org" - -urlpatterns: list = [] diff --git a/apps/org/views.py b/apps/org/views.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/permission/__init__.py b/apps/permission/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/permission/admin.py b/apps/permission/admin.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/permission/apps.py b/apps/permission/apps.py deleted file mode 100644 index b069ea2..0000000 --- a/apps/permission/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class PermissionConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.permission" - label = "fonrey_permission" diff --git a/apps/permission/migrations/0001_initial.py b/apps/permission/migrations/0001_initial.py deleted file mode 100644 index 73daeb1..0000000 --- a/apps/permission/migrations/0001_initial.py +++ /dev/null @@ -1,249 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-29 08:47 - -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'), - ] - - operations = [ - migrations.CreateModel( - name='PermissionChangeLog', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('target_type', models.CharField(choices=[('role', '角色'), ('role_permission', '角色权限'), ('staff_role', '员工角色'), ('staff_override', '员工权限覆盖'), ('staff_scope', '员工数据范围')], max_length=30)), - ('target_id', models.UUIDField()), - ('permission_code', models.CharField(blank=True, default='', max_length=150)), - ('action', models.CharField(choices=[('create', '创建'), ('update', '更新'), ('delete', '删除'), ('assign', '分配'), ('revoke', '撤销')], max_length=20)), - ('old_value', models.JSONField(blank=True, null=True)), - ('new_value', models.JSONField(blank=True, null=True)), - ('operator_ip', models.GenericIPAddressField(blank=True, null=True)), - ('user_agent', models.TextField(blank=True, default='')), - ('reason', models.TextField(blank=True, default='')), - ('operated_at', models.DateTimeField(auto_now_add=True)), - ], - options={ - 'db_table': 'permission_change_logs', - 'ordering': ['-operated_at'], - }, - ), - migrations.CreateModel( - name='PermissionDef', - 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)), - ('code', models.CharField(max_length=150, unique=True)), - ('module', models.CharField(choices=[('home', '首页'), ('property', '房源'), ('new_house', '新房'), ('client', '客源'), ('transaction', '交易'), ('data', '数据'), ('marketing', '营销'), ('hr', '人事OA'), ('contract', '合同'), ('trinet', '三网'), ('system', '系统'), ('mobile', '移动端'), ('smart_store', '智能门店'), ('recharge', '在线充值')], max_length=50)), - ('sub_module', models.CharField(blank=True, default='', max_length=50)), - ('group_name', models.CharField(max_length=100)), - ('name', models.CharField(max_length=200)), - ('description', models.TextField(blank=True, default='')), - ('value_type', models.CharField(choices=[('boolean', '开关型'), ('scope', '范围型'), ('integer', '数值型')], max_length=20)), - ('scope_choices', models.JSONField(blank=True, default=list)), - ('integer_min', models.IntegerField(blank=True, null=True)), - ('integer_max', models.IntegerField(blank=True, null=True)), - ('default_value', models.JSONField(default=dict)), - ('max_allowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=50), blank=True, default=list, size=None)), - ('sort_order', models.PositiveIntegerField(default=0)), - ('is_active', models.BooleanField(default=True)), - ('is_deprecated', models.BooleanField(default=False)), - ('version', models.PositiveIntegerField(default=1)), - ], - options={ - 'db_table': 'permission_defs', - }, - ), - migrations.CreateModel( - name='Role', - 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(max_length=100)), - ('category', models.CharField(choices=[('agent', '置业顾问'), ('store_manager', '店管'), ('director', '总经'), ('operator', '运营/行政'), ('custom', '自定义')], max_length=30)), - ('description', models.TextField(blank=True, default='')), - ('is_system_builtin', models.BooleanField(default=False)), - ('is_active', models.BooleanField(default=True)), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='permission_roles_created', to='org.staff')), - ('template_role', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='derived_roles', to='fonrey_permission.role')), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='permission_roles_updated', to='org.staff')), - ], - options={ - 'db_table': 'roles', - }, - ), - migrations.CreateModel( - name='StaffRole', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('is_primary', models.BooleanField(default=False)), - ('assigned_at', models.DateTimeField(auto_now_add=True)), - ('valid_from', models.DateField(blank=True, null=True)), - ('valid_until', models.DateField(blank=True, null=True)), - ('assigned_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='staff_role_assignments_made', to='org.staff')), - ('role', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='staff_links', to='fonrey_permission.role')), - ('staff', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='staff_roles', to='org.staff')), - ], - options={ - 'db_table': 'staff_roles', - }, - ), - migrations.CreateModel( - name='StaffPermissionOverride', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('value', models.JSONField()), - ('override_mode', models.CharField(choices=[('replace', '覆盖'), ('restrict', '限制'), ('grant', '授予')], default='replace', max_length=10)), - ('reason', models.TextField(blank=True, default='')), - ('modified_at', models.DateTimeField(auto_now=True)), - ('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='staff_overrides_modified', to='org.staff')), - ('permission_def', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='staff_overrides', to='fonrey_permission.permissiondef')), - ('staff', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='permission_overrides', to='org.staff')), - ], - options={ - 'db_table': 'staff_permission_overrides', - }, - ), - migrations.CreateModel( - name='StaffDataScope', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('scope_type', models.CharField(choices=[('self', '本人'), ('group', '本组'), ('store', '本门店'), ('area', '本区域'), ('region', '本大区'), ('company', '全公司'), ('custom_unit', '自定义组织单元')], max_length=20)), - ('is_readable', models.BooleanField(default=True)), - ('is_writable', models.BooleanField(default=False)), - ('granted_at', models.DateTimeField(auto_now_add=True)), - ('expires_at', models.DateTimeField(blank=True, null=True)), - ('reason', models.TextField(blank=True, default='')), - ('granted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='data_scopes_granted', to='org.staff')), - ('org_unit', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='data_scope_grants', to='org.orgunit')), - ('staff', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='data_scopes', to='org.staff')), - ], - options={ - 'db_table': 'staff_data_scopes', - }, - ), - migrations.CreateModel( - name='RolePermission', - 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)), - ('value', models.JSONField()), - ('permission_def', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='role_assignments', to='fonrey_permission.permissiondef')), - ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='permissions', to='fonrey_permission.role')), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='role_permissions_updated', to='org.staff')), - ], - options={ - 'db_table': 'role_permissions', - }, - ), - migrations.AddIndex( - model_name='permissiondef', - index=models.Index(condition=models.Q(('is_active', True)), fields=['module', 'sub_module', 'sort_order'], name='idx_perm_defs_module'), - ), - migrations.AddIndex( - model_name='permissiondef', - index=models.Index(condition=models.Q(('is_active', True)), fields=['is_active'], name='idx_perm_defs_active'), - ), - migrations.AddField( - model_name='permissionchangelog', - name='operator', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='permission_changes_operated', to='org.staff'), - ), - migrations.AddField( - model_name='permissionchangelog', - name='role', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='change_logs', to='fonrey_permission.role'), - ), - migrations.AddField( - model_name='permissionchangelog', - name='staff', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='permission_change_logs_affecting', to='org.staff'), - ), - migrations.AddIndex( - model_name='staffrole', - index=models.Index(fields=['role'], name='idx_staff_roles_role'), - ), - migrations.AddConstraint( - model_name='staffrole', - constraint=models.UniqueConstraint(fields=('staff', 'role'), name='uq_staff_roles'), - ), - migrations.AddConstraint( - model_name='staffrole', - constraint=models.UniqueConstraint(condition=models.Q(('is_primary', True)), fields=('staff',), name='uq_staff_roles_primary'), - ), - migrations.AddIndex( - model_name='staffpermissionoverride', - index=models.Index(fields=['staff'], name='idx_staff_overrides_staff'), - ), - migrations.AddConstraint( - model_name='staffpermissionoverride', - constraint=models.UniqueConstraint(fields=('staff', 'permission_def'), name='uq_staff_overrides'), - ), - migrations.AddIndex( - model_name='staffdatascope', - index=models.Index(fields=['staff'], name='idx_data_scopes_staff'), - ), - migrations.AddIndex( - model_name='staffdatascope', - index=models.Index(fields=['org_unit'], name='idx_data_scopes_org'), - ), - migrations.AddIndex( - model_name='staffdatascope', - index=models.Index(condition=models.Q(('expires_at__isnull', False)), fields=['expires_at'], name='idx_data_scopes_expires'), - ), - migrations.AddIndex( - model_name='rolepermission', - index=models.Index(fields=['role'], name='idx_role_permissions_role'), - ), - migrations.AddIndex( - model_name='rolepermission', - index=models.Index(fields=['permission_def'], name='idx_role_permissions_def'), - ), - migrations.AddConstraint( - model_name='rolepermission', - constraint=models.UniqueConstraint(fields=('role', 'permission_def'), name='uq_role_permissions'), - ), - migrations.AddIndex( - model_name='role', - index=models.Index(condition=models.Q(('deleted_at__isnull', True)), fields=['category'], name='idx_roles_category'), - ), - migrations.AddIndex( - model_name='role', - index=models.Index(fields=['template_role'], name='idx_roles_template'), - ), - migrations.AddConstraint( - model_name='role', - constraint=models.UniqueConstraint(condition=models.Q(('deleted_at__isnull', True)), fields=('name',), name='uq_roles_name_active'), - ), - migrations.AddIndex( - model_name='permissionchangelog', - index=models.Index(condition=models.Q(('staff__isnull', False)), fields=['staff', '-operated_at'], name='idx_perm_log_staff'), - ), - migrations.AddIndex( - model_name='permissionchangelog', - index=models.Index(condition=models.Q(('role__isnull', False)), fields=['role', '-operated_at'], name='idx_perm_log_role'), - ), - migrations.AddIndex( - model_name='permissionchangelog', - index=models.Index(fields=['target_type', 'target_id', '-operated_at'], name='idx_perm_log_target'), - ), - migrations.AddIndex( - model_name='permissionchangelog', - index=models.Index(fields=['operator', '-operated_at'], name='idx_perm_log_operator'), - ), - migrations.AddIndex( - model_name='permissionchangelog', - index=models.Index(fields=['-operated_at'], name='idx_perm_log_time'), - ), - ] diff --git a/apps/permission/migrations/0002_alter_permissionchangelog_options_and_more.py b/apps/permission/migrations/0002_alter_permissionchangelog_options_and_more.py deleted file mode 100644 index d7dd139..0000000 --- a/apps/permission/migrations/0002_alter_permissionchangelog_options_and_more.py +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-29 11:07 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('fonrey_permission', '0001_initial'), - ] - - operations = [ - migrations.AlterModelOptions( - name='permissionchangelog', - options={'ordering': ['-operated_at'], 'verbose_name': '权限变更流水', 'verbose_name_plural': '权限变更流水'}, - ), - migrations.AlterModelOptions( - name='permissiondef', - options={'verbose_name': '权限定义', 'verbose_name_plural': '权限定义'}, - ), - migrations.AlterModelOptions( - name='role', - options={'verbose_name': '角色', 'verbose_name_plural': '角色'}, - ), - migrations.AlterModelOptions( - name='rolepermission', - options={'verbose_name': '角色权限', 'verbose_name_plural': '角色权限'}, - ), - migrations.AlterModelOptions( - name='staffdatascope', - options={'verbose_name': '员工数据范围', 'verbose_name_plural': '员工数据范围'}, - ), - migrations.AlterModelOptions( - name='staffpermissionoverride', - options={'verbose_name': '个人权限覆盖', 'verbose_name_plural': '个人权限覆盖'}, - ), - migrations.AlterModelOptions( - name='staffrole', - options={'verbose_name': '员工角色', 'verbose_name_plural': '员工角色'}, - ), - ] diff --git a/apps/permission/migrations/0003_alter_permissionchangelog_action_and_more.py b/apps/permission/migrations/0003_alter_permissionchangelog_action_and_more.py deleted file mode 100644 index 53cb962..0000000 --- a/apps/permission/migrations/0003_alter_permissionchangelog_action_and_more.py +++ /dev/null @@ -1,336 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-30 01:46 - -import django.contrib.postgres.fields -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('org', '0003_alter_orgunit_address_city_and_more'), - ('fonrey_permission', '0002_alter_permissionchangelog_options_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='permissionchangelog', - name='action', - field=models.CharField(choices=[('create', '创建'), ('update', '更新'), ('delete', '删除'), ('assign', '分配'), ('revoke', '撤销')], help_text='create / update / delete / assign / revoke', max_length=20, verbose_name='操作动作'), - ), - migrations.AlterField( - model_name='permissionchangelog', - name='new_value', - field=models.JSONField(blank=True, null=True, verbose_name='变更后快照'), - ), - migrations.AlterField( - model_name='permissionchangelog', - name='old_value', - field=models.JSONField(blank=True, null=True, verbose_name='变更前快照'), - ), - migrations.AlterField( - model_name='permissionchangelog', - name='operated_at', - field=models.DateTimeField(auto_now_add=True, help_text='append-only 流水,分区键', verbose_name='操作时间'), - ), - migrations.AlterField( - model_name='permissionchangelog', - name='operator', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='permission_changes_operated', to='org.staff', verbose_name='操作人'), - ), - migrations.AlterField( - model_name='permissionchangelog', - name='operator_ip', - field=models.GenericIPAddressField(blank=True, null=True, verbose_name='操作来源 IP'), - ), - migrations.AlterField( - model_name='permissionchangelog', - name='permission_code', - field=models.CharField(blank=True, default='', help_text='用 code 而非 FK,避免 PermissionDef 删除后日志丢失', max_length=150, verbose_name='权限编码'), - ), - migrations.AlterField( - model_name='permissionchangelog', - name='reason', - field=models.TextField(blank=True, default='', help_text='批量设置角色等场景强制填写', verbose_name='操作原因'), - ), - migrations.AlterField( - model_name='permissionchangelog', - name='role', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='change_logs', to='fonrey_permission.role', verbose_name='被影响角色'), - ), - migrations.AlterField( - model_name='permissionchangelog', - name='staff', - field=models.ForeignKey(blank=True, help_text='target 是 staff_role/staff_override/staff_scope 时必填', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='permission_change_logs_affecting', to='org.staff', verbose_name='被影响员工'), - ), - migrations.AlterField( - model_name='permissionchangelog', - name='target_id', - field=models.UUIDField(verbose_name='变更对象 ID'), - ), - migrations.AlterField( - model_name='permissionchangelog', - name='target_type', - field=models.CharField(choices=[('role', '角色'), ('role_permission', '角色权限'), ('staff_role', '员工角色'), ('staff_override', '员工权限覆盖'), ('staff_scope', '员工数据范围')], help_text='role / role_permission / staff_role / staff_override / staff_scope', max_length=30, verbose_name='变更对象类型'), - ), - migrations.AlterField( - model_name='permissionchangelog', - name='user_agent', - field=models.TextField(blank=True, default='', verbose_name='操作终端 UA'), - ), - migrations.AlterField( - model_name='permissiondef', - name='code', - field=models.CharField(help_text='规则:{module}.{sub_module}.{action}[.{qualifier}]', max_length=150, unique=True, verbose_name='权限编码'), - ), - migrations.AlterField( - model_name='permissiondef', - name='default_value', - field=models.JSONField(default=dict, help_text='系统最小默认值,格式 {"v": }', verbose_name='默认值'), - ), - migrations.AlterField( - model_name='permissiondef', - name='description', - field=models.TextField(blank=True, default='', verbose_name='权限作用描述'), - ), - migrations.AlterField( - model_name='permissiondef', - name='group_name', - field=models.CharField(help_text='如「私客基础权限」「联系人基础权限」', max_length=100, verbose_name='分组标题'), - ), - migrations.AlterField( - model_name='permissiondef', - name='integer_max', - field=models.IntegerField(blank=True, help_text='仅 INTEGER 类型有效;NULL=无上限(业务上 0 通常代表不限制)', null=True, verbose_name='最大值'), - ), - migrations.AlterField( - model_name='permissiondef', - name='integer_min', - field=models.IntegerField(blank=True, help_text='仅 INTEGER 类型有效', null=True, verbose_name='最小值'), - ), - migrations.AlterField( - model_name='permissiondef', - name='is_active', - field=models.BooleanField(default=True, help_text='下线权限项置 FALSE,历史记录保留', verbose_name='是否启用'), - ), - migrations.AlterField( - model_name='permissiondef', - name='is_deprecated', - field=models.BooleanField(default=False, help_text='不再推荐使用但保持兼容', verbose_name='是否废弃'), - ), - migrations.AlterField( - model_name='permissiondef', - name='max_allowed_categories', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=50), blank=True, default=list, help_text='允许配置此权限的角色类别列表,空数组=所有类别均可', size=None, verbose_name='可配置角色类别'), - ), - migrations.AlterField( - model_name='permissiondef', - name='module', - field=models.CharField(choices=[('home', '首页'), ('property', '房源'), ('new_house', '新房'), ('client', '客源'), ('transaction', '交易'), ('data', '数据'), ('marketing', '营销'), ('hr', '人事OA'), ('contract', '合同'), ('trinet', '三网'), ('system', '系统'), ('mobile', '移动端'), ('smart_store', '智能门店'), ('recharge', '在线充值')], help_text='home/property/new_house/client/transaction/data/marketing/hr/contract/trinet/system/mobile/smart_store/recharge', max_length=50, verbose_name='一级模块'), - ), - migrations.AlterField( - model_name='permissiondef', - name='name', - field=models.CharField(max_length=200, verbose_name='显示名称'), - ), - migrations.AlterField( - model_name='permissiondef', - name='scope_choices', - field=models.JSONField(blank=True, default=list, help_text='仅 SCOPE 类型有效,可选枚举 code 列表,如 ["none","self","store","company"]', verbose_name='可选范围'), - ), - migrations.AlterField( - model_name='permissiondef', - name='sort_order', - field=models.PositiveIntegerField(default=0, help_text='分组内排序', verbose_name='排序顺序'), - ), - migrations.AlterField( - model_name='permissiondef', - name='sub_module', - field=models.CharField(blank=True, default='', help_text='如「二手&租赁」「商圈精耕」', max_length=50, verbose_name='二级模块'), - ), - migrations.AlterField( - model_name='permissiondef', - name='value_type', - field=models.CharField(choices=[('boolean', '开关型'), ('scope', '范围型'), ('integer', '数值型')], help_text='BOOLEAN=开关型 / SCOPE=范围型 / INTEGER=数值型', max_length=20, verbose_name='权限值类型'), - ), - migrations.AlterField( - model_name='permissiondef', - name='version', - field=models.PositiveIntegerField(default=1, help_text='变更时递增,用于缓存失效', verbose_name='定义版本'), - ), - migrations.AlterField( - model_name='role', - name='category', - field=models.CharField(choices=[('agent', '置业顾问'), ('store_manager', '店管'), ('director', '总经'), ('operator', '运营/行政'), ('custom', '自定义')], help_text='agent=置业顾问 / store_manager=店管 / director=总经 / operator=运营 / custom=自定义', max_length=30, verbose_name='角色类别'), - ), - migrations.AlterField( - model_name='role', - name='created_by', - field=models.ForeignKey(blank=True, help_text='角色类别只能由创建者修改', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='permission_roles_created', to='org.staff', verbose_name='创建人'), - ), - migrations.AlterField( - model_name='role', - name='description', - field=models.TextField(blank=True, default='', verbose_name='角色描述'), - ), - migrations.AlterField( - model_name='role', - name='is_active', - field=models.BooleanField(default=True, help_text='FALSE=禁用(员工无法继承该角色权限)', verbose_name='是否启用'), - ), - migrations.AlterField( - model_name='role', - name='is_system_builtin', - field=models.BooleanField(default=False, help_text='如「最大权限角色」,不可删除、不可改名', verbose_name='是否系统内置'), - ), - migrations.AlterField( - model_name='role', - name='name', - field=models.CharField(max_length=100, verbose_name='角色名称'), - ), - migrations.AlterField( - model_name='role', - name='template_role', - field=models.ForeignKey(blank=True, help_text='PRD「引用该角色配置」列', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='derived_roles', to='fonrey_permission.role', verbose_name='权限模板来源'), - ), - migrations.AlterField( - model_name='role', - name='updated_by', - field=models.ForeignKey(blank=True, help_text='权限管理审计用', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='permission_roles_updated', to='org.staff', verbose_name='最后修改人'), - ), - migrations.AlterField( - model_name='rolepermission', - name='permission_def', - field=models.ForeignKey(help_text='RESTRICT 防止删除仍被引用的权限项', on_delete=django.db.models.deletion.PROTECT, related_name='role_assignments', to='fonrey_permission.permissiondef', verbose_name='权限定义'), - ), - migrations.AlterField( - model_name='rolepermission', - name='role', - field=models.ForeignKey(help_text='稀疏存储:角色删除时级联清理权限值', on_delete=django.db.models.deletion.CASCADE, related_name='permissions', to='fonrey_permission.role', verbose_name='所属角色'), - ), - migrations.AlterField( - model_name='rolepermission', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='role_permissions_updated', to='org.staff', verbose_name='最后修改人'), - ), - migrations.AlterField( - model_name='rolepermission', - name='value', - field=models.JSONField(help_text='统一格式 {"v": }', verbose_name='权限值'), - ), - migrations.AlterField( - model_name='staffdatascope', - name='expires_at', - field=models.DateTimeField(blank=True, null=True, verbose_name='临时授权失效时间'), - ), - migrations.AlterField( - model_name='staffdatascope', - name='granted_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='授权时间'), - ), - migrations.AlterField( - model_name='staffdatascope', - name='granted_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='data_scopes_granted', to='org.staff', verbose_name='授权操作人'), - ), - migrations.AlterField( - model_name='staffdatascope', - name='is_readable', - field=models.BooleanField(default=True, verbose_name='可读'), - ), - migrations.AlterField( - model_name='staffdatascope', - name='is_writable', - field=models.BooleanField(default=False, help_text='默认只读', verbose_name='可写'), - ), - migrations.AlterField( - model_name='staffdatascope', - name='org_unit', - field=models.ForeignKey(blank=True, help_text='scope_type=custom_unit 时必填,其他类型为 NULL', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='data_scope_grants', to='org.orgunit', verbose_name='组织节点'), - ), - migrations.AlterField( - model_name='staffdatascope', - name='reason', - field=models.TextField(blank=True, default='', verbose_name='授予原因'), - ), - migrations.AlterField( - model_name='staffdatascope', - name='scope_type', - field=models.CharField(choices=[('self', '本人'), ('group', '本组'), ('store', '本门店'), ('area', '本区域'), ('region', '本大区'), ('company', '全公司'), ('custom_unit', '自定义组织单元')], help_text='self=本人 / group=本组 / store=本门店 / area=本区域 / region=本大区 / company=全公司 / custom_unit=指定节点', max_length=20, verbose_name='范围类型'), - ), - migrations.AlterField( - model_name='staffdatascope', - name='staff', - field=models.ForeignKey(help_text='员工删除时级联删除范围记录', on_delete=django.db.models.deletion.CASCADE, related_name='data_scopes', to='org.staff', verbose_name='所属员工'), - ), - migrations.AlterField( - model_name='staffpermissionoverride', - name='modified_at', - field=models.DateTimeField(auto_now=True, verbose_name='最近修改时间'), - ), - migrations.AlterField( - model_name='staffpermissionoverride', - name='modified_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='staff_overrides_modified', to='org.staff', verbose_name='修改人'), - ), - migrations.AlterField( - model_name='staffpermissionoverride', - name='override_mode', - field=models.CharField(choices=[('replace', '覆盖'), ('restrict', '限制'), ('grant', '授予')], default='replace', help_text='REPLACE=替换合并值 / RESTRICT=限制上限 / GRANT=仅扩展', max_length=10, verbose_name='覆盖模式'), - ), - migrations.AlterField( - model_name='staffpermissionoverride', - name='permission_def', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='staff_overrides', to='fonrey_permission.permissiondef', verbose_name='被覆盖权限项'), - ), - migrations.AlterField( - model_name='staffpermissionoverride', - name='reason', - field=models.TextField(blank=True, default='', help_text='管理员备注,建议强制填写以便审计', verbose_name='备注'), - ), - migrations.AlterField( - model_name='staffpermissionoverride', - name='staff', - field=models.ForeignKey(help_text='员工删除时级联删除覆盖记录', on_delete=django.db.models.deletion.CASCADE, related_name='permission_overrides', to='org.staff', verbose_name='所属员工'), - ), - migrations.AlterField( - model_name='staffpermissionoverride', - name='value', - field=models.JSONField(help_text='统一格式 {"v": }', verbose_name='个人权限值'), - ), - migrations.AlterField( - model_name='staffrole', - name='assigned_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='分配时间'), - ), - migrations.AlterField( - model_name='staffrole', - name='assigned_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='staff_role_assignments_made', to='org.staff', verbose_name='分配操作人'), - ), - migrations.AlterField( - model_name='staffrole', - name='is_primary', - field=models.BooleanField(default=False, help_text='每个员工有且仅有一个主角色', verbose_name='是否主角色'), - ), - migrations.AlterField( - model_name='staffrole', - name='role', - field=models.ForeignKey(help_text='角色被员工引用时禁止删除', on_delete=django.db.models.deletion.PROTECT, related_name='staff_links', to='fonrey_permission.role', verbose_name='角色'), - ), - migrations.AlterField( - model_name='staffrole', - name='staff', - field=models.ForeignKey(help_text='员工删除时级联删除角色关联', on_delete=django.db.models.deletion.CASCADE, related_name='staff_roles', to='org.staff', verbose_name='所属员工'), - ), - migrations.AlterField( - model_name='staffrole', - name='valid_from', - field=models.DateField(blank=True, help_text='预留未来「定时生效」功能', null=True, verbose_name='生效日'), - ), - migrations.AlterField( - model_name='staffrole', - name='valid_until', - field=models.DateField(blank=True, null=True, verbose_name='失效日'), - ), - ] diff --git a/apps/permission/migrations/0004_alter_rolepermission_permission_def_and_more.py b/apps/permission/migrations/0004_alter_rolepermission_permission_def_and_more.py deleted file mode 100644 index f8e3dbb..0000000 --- a/apps/permission/migrations/0004_alter_rolepermission_permission_def_and_more.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-30 04:44 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('fonrey_permission_def', '0001_initial'), - ('fonrey_permission', '0003_alter_permissionchangelog_action_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='rolepermission', - name='permission_def', - field=models.ForeignKey(help_text='RESTRICT 防止删除仍被引用的权限项', on_delete=django.db.models.deletion.PROTECT, related_name='role_assignments', to='fonrey_permission_def.permissiondef', verbose_name='权限定义'), - ), - migrations.AlterField( - model_name='staffpermissionoverride', - name='permission_def', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='staff_overrides', to='fonrey_permission_def.permissiondef', verbose_name='被覆盖权限项'), - ), - migrations.DeleteModel( - name='PermissionDef', - ), - ] diff --git a/apps/permission/migrations/__init__.py b/apps/permission/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/permission/models/__init__.py b/apps/permission/models/__init__.py deleted file mode 100644 index 8d59fb9..0000000 --- a/apps/permission/models/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from apps.permission.models.role import Role, RolePermission -from apps.permission.models.staff_perm import ( - PermissionChangeLog, - StaffDataScope, - StaffPermissionOverride, - StaffRole, -) - -__all__ = [ - "PermissionChangeLog", - "Role", - "RolePermission", - "StaffDataScope", - "StaffPermissionOverride", - "StaffRole", -] diff --git a/apps/permission/models/role.py b/apps/permission/models/role.py deleted file mode 100644 index 77759da..0000000 --- a/apps/permission/models/role.py +++ /dev/null @@ -1,129 +0,0 @@ -from django.db import models - -from core.enums import PermissionRoleCategory -from core.models.base import SoftDeleteModel, TimeStampedModel - - -class Role(SoftDeleteModel): - name = models.CharField( - max_length=100, - verbose_name="角色名称", - ) - category = models.CharField( - max_length=30, - choices=PermissionRoleCategory.choices, - verbose_name="角色类别", - help_text="agent=置业顾问 / store_manager=店管 / director=总经 / operator=运营 / custom=自定义", - ) - description = models.TextField( - blank=True, - default="", - verbose_name="角色描述", - ) - template_role = models.ForeignKey( - "fonrey_permission.Role", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="derived_roles", - verbose_name="权限模板来源", - help_text='PRD「引用该角色配置」列', - ) - is_system_builtin = models.BooleanField( - default=False, - verbose_name="是否系统内置", - help_text='如「最大权限角色」,不可删除、不可改名', - ) - is_active = models.BooleanField( - default=True, - verbose_name="是否启用", - help_text="FALSE=禁用(员工无法继承该角色权限)", - ) - created_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="permission_roles_created", - verbose_name="创建人", - help_text="角色类别只能由创建者修改", - ) - updated_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="permission_roles_updated", - verbose_name="最后修改人", - help_text="权限管理审计用", - ) - - class Meta: - db_table = "roles" - verbose_name = "角色" - verbose_name_plural = "角色" - constraints = [ - models.UniqueConstraint( - fields=["name"], - name="uq_roles_name_active", - condition=models.Q(deleted_at__isnull=True), - ), - ] - indexes = [ - models.Index( - fields=["category"], - name="idx_roles_category", - condition=models.Q(deleted_at__isnull=True), - ), - models.Index(fields=["template_role"], name="idx_roles_template"), - ] - - def __str__(self) -> str: - return f"{self.name} ({self.category})" - - -class RolePermission(TimeStampedModel): - role = models.ForeignKey( - "fonrey_permission.Role", - on_delete=models.CASCADE, - related_name="permissions", - verbose_name="所属角色", - help_text="稀疏存储:角色删除时级联清理权限值", - ) - permission_def = models.ForeignKey( - "fonrey_permission_def.PermissionDef", - on_delete=models.PROTECT, - related_name="role_assignments", - verbose_name="权限定义", - help_text="RESTRICT 防止删除仍被引用的权限项", - ) - value = models.JSONField( - verbose_name="权限值", - help_text='统一格式 {"v": }', - ) - updated_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="role_permissions_updated", - verbose_name="最后修改人", - ) - - class Meta: - db_table = "role_permissions" - verbose_name = "角色权限" - verbose_name_plural = "角色权限" - constraints = [ - models.UniqueConstraint( - fields=["role", "permission_def"], - name="uq_role_permissions", - ), - ] - indexes = [ - models.Index(fields=["role"], name="idx_role_permissions_role"), - models.Index(fields=["permission_def"], name="idx_role_permissions_def"), - ] - - def __str__(self) -> str: - return f"{self.role.name} → {self.permission_def.code}" diff --git a/apps/permission/models/staff_perm.py b/apps/permission/models/staff_perm.py deleted file mode 100644 index d1f42d9..0000000 --- a/apps/permission/models/staff_perm.py +++ /dev/null @@ -1,315 +0,0 @@ -from django.db import models - -from core.enums import ( - PermissionChangeAction, - PermissionChangeTargetType, - PermissionDataScopeType, - PermissionOverrideMode, -) -from core.models.base import TimeStampedModel, UUIDPrimaryKeyModel - - -class StaffRole(UUIDPrimaryKeyModel): - staff = models.ForeignKey( - "org.Staff", - on_delete=models.CASCADE, - related_name="staff_roles", - verbose_name="所属员工", - help_text="员工删除时级联删除角色关联", - ) - role = models.ForeignKey( - "fonrey_permission.Role", - on_delete=models.PROTECT, - related_name="staff_links", - verbose_name="角色", - help_text="角色被员工引用时禁止删除", - ) - is_primary = models.BooleanField( - default=False, - verbose_name="是否主角色", - help_text="每个员工有且仅有一个主角色", - ) - assigned_at = models.DateTimeField( - auto_now_add=True, - verbose_name="分配时间", - ) - assigned_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="staff_role_assignments_made", - verbose_name="分配操作人", - ) - valid_from = models.DateField( - null=True, - blank=True, - verbose_name="生效日", - help_text='预留未来「定时生效」功能', - ) - valid_until = models.DateField( - null=True, - blank=True, - verbose_name="失效日", - ) - - class Meta: - db_table = "staff_roles" - verbose_name = "员工角色" - verbose_name_plural = "员工角色" - constraints = [ - models.UniqueConstraint( - fields=["staff", "role"], - name="uq_staff_roles", - ), - models.UniqueConstraint( - fields=["staff"], - condition=models.Q(is_primary=True), - name="uq_staff_roles_primary", - ), - ] - indexes = [ - models.Index(fields=["role"], name="idx_staff_roles_role"), - ] - - def __str__(self) -> str: - marker = " [primary]" if self.is_primary else "" - return f"{self.staff_id} → {self.role_id}{marker}" - - -class StaffPermissionOverride(UUIDPrimaryKeyModel): - staff = models.ForeignKey( - "org.Staff", - on_delete=models.CASCADE, - related_name="permission_overrides", - verbose_name="所属员工", - help_text="员工删除时级联删除覆盖记录", - ) - permission_def = models.ForeignKey( - "fonrey_permission_def.PermissionDef", - on_delete=models.PROTECT, - related_name="staff_overrides", - verbose_name="被覆盖权限项", - ) - value = models.JSONField( - verbose_name="个人权限值", - help_text='统一格式 {"v": }', - ) - override_mode = models.CharField( - max_length=10, - choices=PermissionOverrideMode.choices, - default=PermissionOverrideMode.REPLACE, - verbose_name="覆盖模式", - help_text="REPLACE=替换合并值 / RESTRICT=限制上限 / GRANT=仅扩展", - ) - reason = models.TextField( - blank=True, - default="", - verbose_name="备注", - help_text="管理员备注,建议强制填写以便审计", - ) - modified_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="staff_overrides_modified", - verbose_name="修改人", - ) - modified_at = models.DateTimeField( - auto_now=True, - verbose_name="最近修改时间", - ) - - class Meta: - db_table = "staff_permission_overrides" - verbose_name = "个人权限覆盖" - verbose_name_plural = "个人权限覆盖" - constraints = [ - models.UniqueConstraint( - fields=["staff", "permission_def"], - name="uq_staff_overrides", - ), - ] - indexes = [ - models.Index(fields=["staff"], name="idx_staff_overrides_staff"), - ] - - -class StaffDataScope(UUIDPrimaryKeyModel): - staff = models.ForeignKey( - "org.Staff", - on_delete=models.CASCADE, - related_name="data_scopes", - verbose_name="所属员工", - help_text="员工删除时级联删除范围记录", - ) - scope_type = models.CharField( - max_length=20, - choices=PermissionDataScopeType.choices, - verbose_name="范围类型", - help_text="self=本人 / group=本组 / store=本门店 / area=本区域 / region=本大区 / company=全公司 / custom_unit=指定节点", - ) - org_unit = models.ForeignKey( - "org.OrgUnit", - null=True, - blank=True, - on_delete=models.PROTECT, - related_name="data_scope_grants", - verbose_name="组织节点", - help_text="scope_type=custom_unit 时必填,其他类型为 NULL", - ) - is_readable = models.BooleanField( - default=True, - verbose_name="可读", - ) - is_writable = models.BooleanField( - default=False, - verbose_name="可写", - help_text="默认只读", - ) - granted_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="data_scopes_granted", - verbose_name="授权操作人", - ) - granted_at = models.DateTimeField( - auto_now_add=True, - verbose_name="授权时间", - ) - expires_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="临时授权失效时间", - ) - reason = models.TextField( - blank=True, - default="", - verbose_name="授予原因", - ) - - class Meta: - db_table = "staff_data_scopes" - verbose_name = "员工数据范围" - verbose_name_plural = "员工数据范围" - indexes = [ - models.Index(fields=["staff"], name="idx_data_scopes_staff"), - models.Index(fields=["org_unit"], name="idx_data_scopes_org"), - models.Index( - fields=["expires_at"], - name="idx_data_scopes_expires", - condition=models.Q(expires_at__isnull=False), - ), - ] - - -class PermissionChangeLog(UUIDPrimaryKeyModel): - target_type = models.CharField( - max_length=30, - choices=PermissionChangeTargetType.choices, - verbose_name="变更对象类型", - help_text="role / role_permission / staff_role / staff_override / staff_scope", - ) - target_id = models.UUIDField( - verbose_name="变更对象 ID", - ) - staff = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="permission_change_logs_affecting", - verbose_name="被影响员工", - help_text="target 是 staff_role/staff_override/staff_scope 时必填", - ) - role = models.ForeignKey( - "fonrey_permission.Role", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="change_logs", - verbose_name="被影响角色", - ) - permission_code = models.CharField( - max_length=150, - blank=True, - default="", - verbose_name="权限编码", - help_text="用 code 而非 FK,避免 PermissionDef 删除后日志丢失", - ) - action = models.CharField( - max_length=20, - choices=PermissionChangeAction.choices, - verbose_name="操作动作", - help_text="create / update / delete / assign / revoke", - ) - old_value = models.JSONField( - null=True, - blank=True, - verbose_name="变更前快照", - ) - new_value = models.JSONField( - null=True, - blank=True, - verbose_name="变更后快照", - ) - operator = models.ForeignKey( - "org.Staff", - on_delete=models.PROTECT, - related_name="permission_changes_operated", - verbose_name="操作人", - ) - operator_ip = models.GenericIPAddressField( - null=True, - blank=True, - verbose_name="操作来源 IP", - ) - user_agent = models.TextField( - blank=True, - default="", - verbose_name="操作终端 UA", - ) - reason = models.TextField( - blank=True, - default="", - verbose_name="操作原因", - help_text="批量设置角色等场景强制填写", - ) - operated_at = models.DateTimeField( - auto_now_add=True, - verbose_name="操作时间", - help_text="append-only 流水,分区键", - ) - - class Meta: - db_table = "permission_change_logs" - verbose_name = "权限变更流水" - verbose_name_plural = "权限变更流水" - ordering = ["-operated_at"] - indexes = [ - models.Index( - fields=["staff", "-operated_at"], - name="idx_perm_log_staff", - condition=models.Q(staff__isnull=False), - ), - models.Index( - fields=["role", "-operated_at"], - name="idx_perm_log_role", - condition=models.Q(role__isnull=False), - ), - models.Index( - fields=["target_type", "target_id", "-operated_at"], - name="idx_perm_log_target", - ), - models.Index( - fields=["operator", "-operated_at"], - name="idx_perm_log_operator", - ), - models.Index(fields=["-operated_at"], name="idx_perm_log_time"), - ] - - def delete(self, *args, **kwargs): - raise NotImplementedError("PermissionChangeLog is append-only and cannot be deleted.") diff --git a/apps/permission/serializers.py b/apps/permission/serializers.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/permission/services/__init__.py b/apps/permission/services/__init__.py deleted file mode 100644 index 06d5079..0000000 --- a/apps/permission/services/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from apps.permission.services.seed_default_roles import seed_default_roles - -__all__ = ["seed_default_roles"] diff --git a/apps/permission/services/seed_default_roles.py b/apps/permission/services/seed_default_roles.py deleted file mode 100644 index ebf3dc4..0000000 --- a/apps/permission/services/seed_default_roles.py +++ /dev/null @@ -1,218 +0,0 @@ -import logging - -from django_tenants.utils import schema_context - -logger = logging.getLogger(__name__) - -_ROLES = [ - {"name": "置业顾问", "category": "agent"}, - {"name": "店管", "category": "store_manager"}, - {"name": "区管", "category": "custom"}, - {"name": "区总", "category": "custom"}, - {"name": "副总", "category": "custom"}, - {"name": "总经", "category": "director"}, - {"name": "其他职能", "category": "operator"}, -] - -_T = True -_F = False -_SELF = "self" -_DEPT = "dept" -_ALL = "all" -_NONE = "none" - -_MATRIX = { - "property.listing.create": [_T, _T, _T, _T, _T, _T, _F], - "property.listing.view_scope": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.listing.view_public": [_T, _T, _T, _T, _T, _T, _F], - "property.listing.view_private": [_F, _T, _T, _T, _T, _T, _F], - "property.listing.set_public": [_T, _T, _T, _T, _T, _T, _F], - "property.listing.set_private": [_T, _T, _T, _T, _T, _T, _F], - "property.listing.set_locked": [_F, _T, _T, _T, _T, _T, _F], - "property.listing.set_special": [_F, _T, _T, _T, _T, _T, _F], - "property.listing.delete": [_F, _T, _T, _T, _T, _T, _F], - "property.listing.restore": [_F, _T, _T, _T, _T, _T, _F], - "property.listing.export": [_F, _T, _T, _T, _T, _T, _F], - "property.listing.edit_description": [_T, _T, _T, _T, _T, _T, _F], - "property.listing.view_deal": [_F, _T, _T, _T, _T, _T, _F], - "property.listing.price_read": [_T, _T, _T, _T, _T, _T, _F], - "property.listing.view_history": [_T, _T, _T, _T, _T, _T, _F], - "property.listing.view_owner_others": [_T, _T, _T, _T, _T, _T, _F], - "property.listing.set_protected": [_F, _T, _T, _T, _T, _T, _F], - "property.listing.view_protected": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.listing.change_keeper": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.listing.merge_duplicate": [_F, _T, _T, _T, _T, _T, _F], - "property.listing.status_sold": [_T, _T, _T, _T, _T, _T, _F], - "property.listing.grade_set_a": [_T, _T, _T, _T, _T, _T, _F], - "property.listing.grade_set_e": [_F, _T, _T, _T, _T, _T, _F], - "property.contact.view_phone": [_T, _T, _T, _T, _T, _T, _F], - "property.contact.view_phone_limit": [20, -1, -1, -1, -1, -1, 0], - "property.contact.add_contact": [_T, _T, _T, _T, _T, _T, _F], - "property.contact.edit_core": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.contact.edit_basic": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.contact.delete_contact": [_F, _T, _T, _T, _T, _T, _F], - "property.contact.view_cert": [_T, _T, _T, _T, _T, _T, _F], - "property.contact.view_operation_log":[_F, _T, _T, _T, _T, _T, _F], - "property.address.view_detail": [_T, _T, _T, _T, _T, _T, _F], - "property.address.view_limit": [10, -1, -1, -1, -1, -1, 0], - "property.address.edit": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.key.create": [_T, _T, _T, _T, _T, _T, _F], - "property.key.edit": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.key.return": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.key.view_password": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.key.view_number": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.key.borrow": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.key.give_back": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.key.delete": [_NONE, _SELF, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.key.export": [_F, _T, _T, _T, _T, _T, _F], - "property.survey.create_photo": [_T, _T, _T, _T, _T, _T, _F], - "property.survey.download_photo": [_T, _T, _T, _T, _T, _T, _F], - "property.survey.delete_photo": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.survey.create": [_T, _T, _T, _T, _T, _T, _F], - "property.survey.view": [_T, _T, _T, _T, _T, _T, _F], - "property.survey.upload_video": [_T, _T, _T, _T, _T, _T, _F], - "property.survey.download_video": [_T, _T, _T, _T, _T, _T, _F], - "property.survey.play_video": [_T, _T, _T, _T, _T, _T, _F], - "property.mandate.create": [_T, _T, _T, _T, _T, _T, _F], - "property.mandate.renew": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.mandate.view": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.mandate.revoke": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.mandate.export": [_F, _T, _T, _T, _T, _T, _F], - "property.follow.view_scope": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.follow.hide": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.follow.view_hidden": [_NONE, _SELF, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.follow.pin": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.attachment.create": [_T, _T, _T, _T, _T, _T, _F], - "property.attachment.view": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.attachment.edit": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.attachment.download": [_T, _T, _T, _T, _T, _T, _F], - "property.attachment.delete": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "property.showing.view_scope": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "client.private.create": [_T, _T, _T, _T, _T, _T, _F], - "client.private.view": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "client.private.view_protected": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "client.private.edit": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "client.private.edit_protected": [_SELF, _SELF, _DEPT, _ALL, _ALL, _ALL, _NONE], - "client.private.set_protected": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "client.private.to_public": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "client.private.export": [_F, _T, _T, _T, _T, _T, _F], - "client.public.view": [_NONE, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "client.public.to_private": [_T, _T, _T, _T, _T, _T, _F], - "client.public.edit": [_T, _T, _T, _T, _T, _T, _F], - "client.public.change_status": [_F, _T, _T, _T, _T, _T, _F], - "client.deal.view": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "client.deal.view_public": [_NONE, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "client.deal.re_transaction": [_T, _T, _T, _T, _T, _T, _F], - "client.deal.export": [_F, _T, _T, _T, _T, _T, _F], - "client.contact.view_phone_private": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "client.contact.view_phone_protected":[_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "client.contact.view_phone_public": [_NONE, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "client.contact.view_phone_limit": [20, -1, -1, -1, -1, -1, 0], - "client.contact.edit_contact": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "client.contact.edit_phone": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "client.mgmt.delete": [_F, _T, _T, _T, _T, _T, _F], - "client.mgmt.to_deal": [_F, _T, _T, _T, _T, _T, _F], - "client.mgmt.change_staff": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "client.mgmt.batch_change_staff": [_NONE, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "client.mgmt.view_operation_log": [_F, _T, _T, _T, _T, _T, _F], - "client.mgmt.merge_private": [_T, _T, _T, _T, _T, _T, _F], - "client.showing.create": [_T, _T, _T, _T, _T, _T, _F], - "client.showing.view": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "client.showing.edit": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "client.archive.view": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "client.archive.import": [_F, _T, _T, _T, _T, _T, _F], - "client.archive.view_phone": [_F, _T, _T, _T, _T, _T, _F], - "client.archive.delete": [_F, _T, _T, _T, _T, _T, _F], - "client.archive.view_log": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "home.dashboard.view_version": [_T, _T, _T, _T, _T, _T, _T], - "home.dashboard.personal_rank": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "home.dashboard.dept_rank": [_NONE, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "home.dashboard.manage_praise": [_F, _T, _T, _T, _T, _T, _F], - "complex.view": [_T, _T, _T, _T, _T, _T, _T], - "complex.view_structure": [_T, _T, _T, _T, _T, _T, _T], - "complex.create": [_F, _T, _T, _T, _T, _T, _F], - "complex.create_unit": [_F, _T, _T, _T, _T, _T, _F], - "complex.edit": [_F, _T, _T, _T, _T, _T, _F], - "complex.edit_unit": [_F, _T, _T, _T, _T, _T, _F], - "complex.delete": [_F, _F, _T, _T, _T, _T, _F], - "complex.delete_unit": [_F, _T, _T, _T, _T, _T, _F], - "complex.delete_with_property": [_F, _F, _F, _T, _T, _T, _F], - "complex.merge": [_F, _F, _T, _T, _T, _T, _F], - "complex.move_unit": [_F, _F, _T, _T, _T, _T, _F], - "complex.lock": [_F, _T, _T, _T, _T, _T, _F], - "complex.view_deal": [_T, _T, _T, _T, _T, _T, _F], - "complex.view_deal_detail": [_F, _T, _T, _T, _T, _T, _F], - "complex.view_address_scope": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "complex.region_manage": [_F, _F, _T, _T, _T, _T, _F], - "complex.material.view_photo": [_T, _T, _T, _T, _T, _T, _T], - "complex.material.manage_photo": [_T, _T, _T, _T, _T, _T, _F], - "complex.material.delete_photo": [_F, _T, _T, _T, _T, _T, _F], - "complex.material.download_photo": [_T, _T, _T, _T, _T, _T, _T], - "complex.material.view_attachment": [_T, _T, _T, _T, _T, _T, _T], - "complex.material.manage_attachment": [_T, _T, _T, _T, _T, _T, _F], - "complex.material.download_attachment":[_T, _T, _T, _T, _T, _T, _T], - "complex.material.delete_attachment": [_F, _T, _T, _T, _T, _T, _F], - "complex.material.view_surrounding": [_T, _T, _T, _T, _T, _T, _T], - "complex.feedback.view": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "complex.feedback.handle": [_F, _T, _T, _T, _T, _T, _F], - "org.view_structure": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _SELF], - "org.view_dept": [_F, _T, _T, _T, _T, _T, _F], - "org.edit_dept": [_F, _F, _T, _T, _T, _T, _F], - "org.view_staff": [_F, _T, _T, _T, _T, _T, _F], - "org.edit_staff": [_F, _F, _T, _T, _T, _T, _F], - "org.edit_staff_detail": [_F, _T, _T, _T, _T, _T, _F], - "org.freeze_account": [_F, _F, _T, _T, _T, _T, _F], - "org.import_staff": [_F, _F, _T, _T, _T, _T, _F], - "org.export_staff": [_F, _T, _T, _T, _T, _T, _F], - "org.view_permission": [_F, _F, _F, _T, _T, _T, _F], - "org.edit_permission": [_F, _F, _F, _T, _T, _T, _F], - "org.export_permission": [_F, _F, _F, _T, _T, _T, _F], - "org.edit_position": [_F, _F, _F, _T, _T, _T, _F], - "org.edit_role": [_F, _F, _F, _T, _T, _T, _F], - "org.view_store_list": [_F, _T, _T, _T, _T, _T, _F], - "org.export_store_list": [_F, _F, _T, _T, _T, _T, _F], - "org.view_contact_book": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _SELF], - "org.transfer_business": [_NONE, _DEPT, _DEPT, _ALL, _ALL, _ALL, _NONE], - "org.resign_apply": [_SELF, _DEPT, _DEPT, _ALL, _ALL, _ALL, _SELF], - "org.invite_onboard": [_F, _T, _T, _T, _T, _T, _F], - "org.view_contact_phone_limit": [5, -1, -1, -1, -1, -1, 5], -} - - -def seed_default_roles(schema_name: str) -> None: - from django.apps import apps - - Role = apps.get_model("fonrey_permission", "Role") - RolePermission = apps.get_model("fonrey_permission", "RolePermission") - PermissionDef = apps.get_model("fonrey_permission_def", "PermissionDef") - - perm_map = {p.code: p for p in PermissionDef.objects.all()} - - roles = [] - for role_def in _ROLES: - role, _ = Role.objects.get_or_create( - name=role_def["name"], - defaults={ - "category": role_def["category"], - "is_system_builtin": True, - "is_active": True, - }, - ) - roles.append(role) - - rp_objects = [] - for code, values in _MATRIX.items(): - perm = perm_map.get(code) - if perm is None: - logger.warning("PermissionDef not found: %s", code) - continue - for role, val in zip(roles, values): - rp_objects.append( - RolePermission( - role=role, - permission_def=perm, - value={"v": val}, - ) - ) - - RolePermission.objects.bulk_create(rp_objects, ignore_conflicts=True) diff --git a/apps/permission/tasks.py b/apps/permission/tasks.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/permission/templates/permission/.gitkeep b/apps/permission/templates/permission/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/apps/permission/tests/__init__.py b/apps/permission/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/permission/urls.py b/apps/permission/urls.py deleted file mode 100644 index 2f2b5bf..0000000 --- a/apps/permission/urls.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.urls import path - -app_name = "permission" - -urlpatterns: list = [] diff --git a/apps/permission/views.py b/apps/permission/views.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/permission_def/__init__.py b/apps/permission_def/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/permission_def/apps.py b/apps/permission_def/apps.py deleted file mode 100644 index a3f03f8..0000000 --- a/apps/permission_def/apps.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.apps import AppConfig - - -class PermissionDefConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.permission_def" - label = "fonrey_permission_def" - verbose_name = "权限定义(全局共享)" diff --git a/apps/permission_def/migrations/0001_initial.py b/apps/permission_def/migrations/0001_initial.py deleted file mode 100644 index 6291ebc..0000000 --- a/apps/permission_def/migrations/0001_initial.py +++ /dev/null @@ -1,46 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-30 04:41 - -import django.contrib.postgres.fields -from django.db import migrations, models -import uuid - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='PermissionDef', - 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)), - ('code', models.CharField(help_text='规则:{module}.{sub_module}.{action}[.{qualifier}]', max_length=150, unique=True, verbose_name='权限编码')), - ('module', models.CharField(choices=[('home', '首页'), ('property', '房源'), ('new_house', '新房'), ('client', '客源'), ('transaction', '交易'), ('data', '数据'), ('marketing', '营销'), ('hr', '人事OA'), ('contract', '合同'), ('trinet', '三网'), ('system', '系统'), ('mobile', '移动端'), ('smart_store', '智能门店'), ('recharge', '在线充值')], help_text='home/property/new_house/client/transaction/data/marketing/hr/contract/trinet/system/mobile/smart_store/recharge', max_length=50, verbose_name='一级模块')), - ('sub_module', models.CharField(blank=True, default='', help_text='如「二手&租赁」「商圈精耕」', max_length=50, verbose_name='二级模块')), - ('group_name', models.CharField(help_text='如「私客基础权限」「联系人基础权限」', max_length=100, verbose_name='分组标题')), - ('name', models.CharField(max_length=200, verbose_name='显示名称')), - ('description', models.TextField(blank=True, default='', verbose_name='权限作用描述')), - ('value_type', models.CharField(choices=[('boolean', '开关型'), ('scope', '范围型'), ('integer', '数值型')], help_text='BOOLEAN=开关型 / SCOPE=范围型 / INTEGER=数值型', max_length=20, verbose_name='权限值类型')), - ('scope_choices', models.JSONField(blank=True, default=list, help_text='仅 SCOPE 类型有效,可选枚举 code 列表,如 ["none","self","store","company"]', verbose_name='可选范围')), - ('integer_min', models.IntegerField(blank=True, help_text='仅 INTEGER 类型有效', null=True, verbose_name='最小值')), - ('integer_max', models.IntegerField(blank=True, help_text='仅 INTEGER 类型有效;NULL=无上限(业务上 0 通常代表不限制)', null=True, verbose_name='最大值')), - ('default_value', models.JSONField(default=dict, help_text='系统最小默认值,格式 {"v": }', verbose_name='默认值')), - ('max_allowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=50), blank=True, default=list, help_text='允许配置此权限的角色类别列表,空数组=所有类别均可', size=None, verbose_name='可配置角色类别')), - ('sort_order', models.PositiveIntegerField(default=0, help_text='分组内排序', verbose_name='排序顺序')), - ('is_active', models.BooleanField(default=True, help_text='下线权限项置 FALSE,历史记录保留', verbose_name='是否启用')), - ('is_deprecated', models.BooleanField(default=False, help_text='不再推荐使用但保持兼容', verbose_name='是否废弃')), - ('version', models.PositiveIntegerField(default=1, help_text='变更时递增,用于缓存失效', verbose_name='定义版本')), - ], - options={ - 'verbose_name': '权限定义', - 'verbose_name_plural': '权限定义', - 'db_table': 'permission_defs', - 'indexes': [models.Index(condition=models.Q(('is_active', True)), fields=['module', 'sub_module', 'sort_order'], name='idx_perm_defs_module'), models.Index(condition=models.Q(('is_active', True)), fields=['is_active'], name='idx_perm_defs_active')], - }, - ), - ] diff --git a/apps/permission_def/migrations/0002_seed_permission_defs.py b/apps/permission_def/migrations/0002_seed_permission_defs.py deleted file mode 100644 index 9e32ff8..0000000 --- a/apps/permission_def/migrations/0002_seed_permission_defs.py +++ /dev/null @@ -1,2358 +0,0 @@ -from django.db import migrations - - -PERMISSION_DEFS = [ - { - "code": "property.listing.create", - "module": "property", - "sub_module": "listing", - "group_name": "房源基础", - "name": "新增房源", - "description": "是否可新增房源", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 1, - }, - { - "code": "property.listing.view_scope", - "module": "property", - "sub_module": "listing", - "group_name": "房源基础", - "name": "维护房源查看范围", - "description": "按维护人范围查看房源列表:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "self"}, - "max_allowed_categories": [], - "sort_order": 2, - }, - { - "code": "property.listing.view_public", - "module": "property", - "sub_module": "listing", - "group_name": "房源基础", - "name": "公盘查看", - "description": "是否可查看公盘房源", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 3, - }, - { - "code": "property.listing.view_private", - "module": "property", - "sub_module": "listing", - "group_name": "房源基础", - "name": "私盘查看", - "description": "是否可查看私盘房源", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 4, - }, - { - "code": "property.listing.set_public", - "module": "property", - "sub_module": "listing", - "group_name": "房源基础", - "name": "将房源改为公盘", - "description": "是否可将房源属性改为公盘", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 5, - }, - { - "code": "property.listing.set_private", - "module": "property", - "sub_module": "listing", - "group_name": "房源基础", - "name": "将房源改为私盘", - "description": "是否可将房源属性改为私盘", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 6, - }, - { - "code": "property.listing.set_locked", - "module": "property", - "sub_module": "listing", - "group_name": "房源基础", - "name": "将房源改为封盘", - "description": "是否可将房源属性改为封盘", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 7, - }, - { - "code": "property.listing.set_special", - "module": "property", - "sub_module": "listing", - "group_name": "房源基础", - "name": "将房源改为特盘", - "description": "是否可将房源属性改为特盘", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 8, - }, - { - "code": "property.listing.delete", - "module": "property", - "sub_module": "listing", - "group_name": "房源基础", - "name": "删除房源", - "description": "是否可删除房源", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 9, - }, - { - "code": "property.listing.restore", - "module": "property", - "sub_module": "listing", - "group_name": "房源基础", - "name": "恢复已删除房源", - "description": "是否可恢复已删除的房源", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 10, - }, - { - "code": "property.listing.export", - "module": "property", - "sub_module": "listing", - "group_name": "房源基础", - "name": "房源列表数据导出", - "description": "是否可将房源列表数据导出", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 11, - }, - { - "code": "property.listing.edit_description", - "module": "property", - "sub_module": "listing", - "group_name": "房源基础", - "name": "修改房屋介绍信息", - "description": "是否可修改营销标题、核心卖点、户型介绍等介绍信息", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 12, - }, - { - "code": "property.listing.view_deal", - "module": "property", - "sub_module": "listing", - "group_name": "房源基础", - "name": "成交房源列表及价格信息", - "description": "是否可查看成交房源列表及价格历史", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 13, - }, - { - "code": "property.listing.price_read", - "module": "property", - "sub_module": "listing", - "group_name": "房源基础", - "name": "价格解读", - "description": "是否可查看房源详情页的价格解读", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 14, - }, - { - "code": "property.listing.view_history", - "module": "property", - "sub_module": "listing", - "group_name": "房源基础", - "name": "查看房源挂牌历史", - "description": "是否可查看房源挂牌历史记录", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 15, - }, - { - "code": "property.listing.view_owner_others", - "module": "property", - "sub_module": "listing", - "group_name": "房源基础", - "name": "查看同业主其他房源", - "description": "是否可在房源详情页查看同业主的其他房源", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 16, - }, - { - "code": "property.listing.set_protected", - "module": "property", - "sub_module": "listing", - "group_name": "房源基础", - "name": "修改房源保护设置", - "description": "是否可修改房源的保护期/保护房设置", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 17, - }, - { - "code": "property.listing.view_protected", - "module": "property", - "sub_module": "listing", - "group_name": "房源基础", - "name": "查看保护期内房源", - "description": "是否可查看保护期内(我租/我售/已售/已租)房源", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 18, - }, - { - "code": "property.listing.change_keeper", - "module": "property", - "sub_module": "listing", - "group_name": "房源基础", - "name": "修改相关方范围", - "description": "可修改哪个范围内房源的相关方:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 19, - }, - { - "code": "property.listing.merge_duplicate", - "module": "property", - "sub_module": "listing", - "group_name": "房源基础", - "name": "重复房源合并", - "description": "是否可合并重复房源", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 20, - }, - { - "code": "property.listing.status_sold", - "module": "property", - "sub_module": "listing", - "group_name": "房源基础", - "name": "修改为我售/我租状态", - "description": "是否可修改房源为我售/我租状态", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 21, - }, - { - "code": "property.listing.grade_set_a", - "module": "property", - "sub_module": "listing", - "group_name": "房源基础", - "name": "将房源等级设为A", - "description": "是否可将挂牌中房源等级设为A(急迫)", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 22, - }, - { - "code": "property.listing.grade_set_e", - "module": "property", - "sub_module": "listing", - "group_name": "房源基础", - "name": "将房源等级设为E", - "description": "是否可将房源等级设为E(暂不关注)", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 23, - }, - { - "code": "property.contact.view_phone", - "module": "property", - "sub_module": "contact", - "group_name": "业主/联系人与号码", - "name": "查看业主/联系人号码", - "description": "是否可查看房源业主/联系人真实号码", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 1, - }, - { - "code": "property.contact.view_phone_limit", - "module": "property", - "sub_module": "contact", - "group_name": "业主/联系人与号码", - "name": "每日查看号码次数上限", - "description": "每天可查看房源真实号码次数,0=不允许,-1=不限制", - "value_type": "integer", - "scope_choices": [], - "integer_min": -1, - "integer_max": None, - "default_value": {"v": 0}, - "max_allowed_categories": [], - "sort_order": 2, - }, - { - "code": "property.contact.add_contact", - "module": "property", - "sub_module": "contact", - "group_name": "业主/联系人与号码", - "name": "新增业主/联系人", - "description": "是否可新增业主/联系人", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 3, - }, - { - "code": "property.contact.edit_core", - "module": "property", - "sub_module": "contact", - "group_name": "业主/联系人与号码", - "name": "修改业主核心信息", - "description": "可修改哪个范围的业主核心信息(电话/微信):无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 4, - }, - { - "code": "property.contact.edit_basic", - "module": "property", - "sub_module": "contact", - "group_name": "业主/联系人与号码", - "name": "修改业主非核心信息", - "description": "可修改哪个范围的业主基本信息(姓名/备注):无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "self"}, - "max_allowed_categories": [], - "sort_order": 5, - }, - { - "code": "property.contact.delete_contact", - "module": "property", - "sub_module": "contact", - "group_name": "业主/联系人与号码", - "name": "删除业主/联系人", - "description": "是否可删除业主/联系人", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 6, - }, - { - "code": "property.contact.view_cert", - "module": "property", - "sub_module": "contact", - "group_name": "业主/联系人与号码", - "name": "查看产证信息", - "description": "是否可查看房源详情页产证信息", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 7, - }, - { - "code": "property.contact.view_operation_log", - "module": "property", - "sub_module": "contact", - "group_name": "业主/联系人与号码", - "name": "查看业主联系人操作日志", - "description": "是否可查看业主/联系人的新增、修改、删除等记录", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 8, - }, - { - "code": "property.address.view_detail", - "module": "property", - "sub_module": "address", - "group_name": "房源地址", - "name": "查看楼栋/单元/楼层/房号", - "description": "是否可查看房源真实地址详情", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 1, - }, - { - "code": "property.address.view_limit", - "module": "property", - "sub_module": "address", - "group_name": "房源地址", - "name": "每日查看地址次数上限", - "description": "每天可查看房源真实地址总次数,0=不允许,-1=不限制", - "value_type": "integer", - "scope_choices": [], - "integer_min": -1, - "integer_max": None, - "default_value": {"v": 0}, - "max_allowed_categories": [], - "sort_order": 2, - }, - { - "code": "property.address.edit", - "module": "property", - "sub_module": "address", - "group_name": "房源地址", - "name": "修改楼栋/单元/楼层/房号", - "description": "是否可修改房源地址信息", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 3, - }, - { - "code": "property.key.create", - "module": "property", - "sub_module": "key", - "group_name": "房源钥匙", - "name": "新增钥匙", - "description": "是否可新增钥匙", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 1, - }, - { - "code": "property.key.edit", - "module": "property", - "sub_module": "key", - "group_name": "房源钥匙", - "name": "修改钥匙", - "description": "按钥匙方范围可修改钥匙:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 2, - }, - { - "code": "property.key.return", - "module": "property", - "sub_module": "key", - "group_name": "房源钥匙", - "name": "退还钥匙", - "description": "按钥匙方范围控制是否可退还钥匙:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 3, - }, - { - "code": "property.key.view_password", - "module": "property", - "sub_module": "key", - "group_name": "房源钥匙", - "name": "查看钥匙密码", - "description": "按钥匙方范围控制是否可查看密码:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 4, - }, - { - "code": "property.key.view_number", - "module": "property", - "sub_module": "key", - "group_name": "房源钥匙", - "name": "查看钥匙编号", - "description": "按钥匙方范围控制是否可查看钥匙编号:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 5, - }, - { - "code": "property.key.borrow", - "module": "property", - "sub_module": "key", - "group_name": "房源钥匙", - "name": "钥匙借出", - "description": "按钥匙保管部门范围借出钥匙:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 6, - }, - { - "code": "property.key.give_back", - "module": "property", - "sub_module": "key", - "group_name": "房源钥匙", - "name": "钥匙归还", - "description": "按钥匙保管部门范围归还钥匙:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 7, - }, - { - "code": "property.key.delete", - "module": "property", - "sub_module": "key", - "group_name": "房源钥匙", - "name": "删除钥匙", - "description": "按钥匙方范围控制是否可删除钥匙:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 8, - }, - { - "code": "property.key.export", - "module": "property", - "sub_module": "key", - "group_name": "房源钥匙", - "name": "钥匙列表数据导出", - "description": "是否可导出钥匙数据", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 9, - }, - { - "code": "property.survey.create_photo", - "module": "property", - "sub_module": "survey", - "group_name": "房源实勘", - "name": "新增实勘图片", - "description": "是否可新增实勘图片", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 1, - }, - { - "code": "property.survey.download_photo", - "module": "property", - "sub_module": "survey", - "group_name": "房源实勘", - "name": "下载图片", - "description": "是否可下载实勘图片", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 2, - }, - { - "code": "property.survey.delete_photo", - "module": "property", - "sub_module": "survey", - "group_name": "房源实勘", - "name": "删除图片", - "description": "按图片上传人范围删除:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 3, - }, - { - "code": "property.survey.create", - "module": "property", - "sub_module": "survey", - "group_name": "房源实勘", - "name": "新增实勘", - "description": "是否可新增实勘记录", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 4, - }, - { - "code": "property.survey.view", - "module": "property", - "sub_module": "survey", - "group_name": "房源实勘", - "name": "查看实勘", - "description": "是否可查看实勘记录", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 5, - }, - { - "code": "property.survey.upload_video", - "module": "property", - "sub_module": "survey", - "group_name": "房源实勘", - "name": "上传视频", - "description": "是否可上传房源视频", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 6, - }, - { - "code": "property.survey.download_video", - "module": "property", - "sub_module": "survey", - "group_name": "房源实勘", - "name": "下载视频", - "description": "是否可下载房源视频", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 7, - }, - { - "code": "property.survey.play_video", - "module": "property", - "sub_module": "survey", - "group_name": "房源实勘", - "name": "播放视频", - "description": "是否可播放房源视频", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 8, - }, - { - "code": "property.mandate.create", - "module": "property", - "sub_module": "mandate", - "group_name": "房源委托", - "name": "新增委托", - "description": "是否可新增房源委托", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 1, - }, - { - "code": "property.mandate.renew", - "module": "property", - "sub_module": "mandate", - "group_name": "房源委托", - "name": "续签/违约委托", - "description": "按委托方范围可续签或违约委托:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 2, - }, - { - "code": "property.mandate.view", - "module": "property", - "sub_module": "mandate", - "group_name": "房源委托", - "name": "委托列表查看", - "description": "按委托方范围查看委托列表:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 3, - }, - { - "code": "property.mandate.revoke", - "module": "property", - "sub_module": "mandate", - "group_name": "房源委托", - "name": "委托作废", - "description": "按委托方范围控制是否可作废委托:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 4, - }, - { - "code": "property.mandate.export", - "module": "property", - "sub_module": "mandate", - "group_name": "房源委托", - "name": "委托列表数据导出", - "description": "是否可导出委托列表数据", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 5, - }, - { - "code": "property.follow.view_scope", - "module": "property", - "sub_module": "follow", - "group_name": "房源跟进", - "name": "查看房源跟进范围", - "description": "控制房源详情页的跟进查看范围:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "self"}, - "max_allowed_categories": [], - "sort_order": 1, - }, - { - "code": "property.follow.hide", - "module": "property", - "sub_module": "follow", - "group_name": "房源跟进", - "name": "隐藏/开放跟进", - "description": "按跟进人范围隐藏/开放跟进:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 2, - }, - { - "code": "property.follow.view_hidden", - "module": "property", - "sub_module": "follow", - "group_name": "房源跟进", - "name": "查看隐藏跟进", - "description": "按跟进人范围查看被隐藏的跟进:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 3, - }, - { - "code": "property.follow.pin", - "module": "property", - "sub_module": "follow", - "group_name": "房源跟进", - "name": "置顶/取消置顶跟进", - "description": "按跟进人范围置顶或取消置顶跟进:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 4, - }, - { - "code": "property.attachment.create", - "module": "property", - "sub_module": "attachment", - "group_name": "房源附件", - "name": "新增附件", - "description": "是否可新增房源附件", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 1, - }, - { - "code": "property.attachment.view", - "module": "property", - "sub_module": "attachment", - "group_name": "房源附件", - "name": "查看附件", - "description": "按附件上传人范围查看:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 2, - }, - { - "code": "property.attachment.edit", - "module": "property", - "sub_module": "attachment", - "group_name": "房源附件", - "name": "修改附件", - "description": "按附件上传人范围修改:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 3, - }, - { - "code": "property.attachment.download", - "module": "property", - "sub_module": "attachment", - "group_name": "房源附件", - "name": "下载附件", - "description": "是否可下载房源附件", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 4, - }, - { - "code": "property.attachment.delete", - "module": "property", - "sub_module": "attachment", - "group_name": "房源附件", - "name": "删除附件", - "description": "按附件上传人范围删除:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 5, - }, - { - "code": "property.showing.view_scope", - "module": "property", - "sub_module": "showing", - "group_name": "房源带看", - "name": "查看房源带看数据", - "description": "按数据权限范围查看房源带看记录:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 1, - }, - { - "code": "client.private.create", - "module": "client", - "sub_module": "private", - "group_name": "私客", - "name": "新增私客", - "description": "是否可新增私客", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 1, - }, - { - "code": "client.private.view", - "module": "client", - "sub_module": "private", - "group_name": "私客", - "name": "查看私客(非保护客)", - "description": "按归属人范围查看非保护私客:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "self"}, - "max_allowed_categories": [], - "sort_order": 2, - }, - { - "code": "client.private.view_protected", - "module": "client", - "sub_module": "private", - "group_name": "私客", - "name": "查看私客(保护客)", - "description": "按归属人范围查看保护私客:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "self"}, - "max_allowed_categories": [], - "sort_order": 3, - }, - { - "code": "client.private.edit", - "module": "client", - "sub_module": "private", - "group_name": "私客", - "name": "编辑私客(非保护客)", - "description": "按归属人范围编辑非保护私客:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "self"}, - "max_allowed_categories": [], - "sort_order": 4, - }, - { - "code": "client.private.edit_protected", - "module": "client", - "sub_module": "private", - "group_name": "私客", - "name": "编辑私客(保护客)", - "description": "按归属人范围编辑保护私客:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "self"}, - "max_allowed_categories": [], - "sort_order": 5, - }, - { - "code": "client.private.set_protected", - "module": "client", - "sub_module": "private", - "group_name": "私客", - "name": "设置/取消保护客", - "description": "设置/取消哪个范围的保护客:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "self"}, - "max_allowed_categories": [], - "sort_order": 6, - }, - { - "code": "client.private.to_public", - "module": "client", - "sub_module": "private", - "group_name": "私客", - "name": "私客转公客", - "description": "按归属人范围将私客转为公客:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "self"}, - "max_allowed_categories": [], - "sort_order": 7, - }, - { - "code": "client.private.export", - "module": "client", - "sub_module": "private", - "group_name": "私客", - "name": "私客列表导出", - "description": "是否支持导出私客列表", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 8, - }, - { - "code": "client.public.view", - "module": "client", - "sub_module": "public", - "group_name": "公客", - "name": "公客查看范围", - "description": "控制公客查看范围:无/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 1, - }, - { - "code": "client.public.to_private", - "module": "client", - "sub_module": "public", - "group_name": "公客", - "name": "公客转私客", - "description": "是否可将可见范围内的公客转为私客", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 2, - }, - { - "code": "client.public.edit", - "module": "client", - "sub_module": "public", - "group_name": "公客", - "name": "编辑公客", - "description": "是否可编辑公客信息", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 3, - }, - { - "code": "client.public.change_status", - "module": "client", - "sub_module": "public", - "group_name": "公客", - "name": "改公客状态", - "description": "是否可修改公客状态", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 4, - }, - { - "code": "client.deal.view", - "module": "client", - "sub_module": "deal", - "group_name": "成交客", - "name": "查看成交客(私客类型)", - "description": "按归属人范围查看归属人为个人的成交客:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "self"}, - "max_allowed_categories": [], - "sort_order": 1, - }, - { - "code": "client.deal.view_public", - "module": "client", - "sub_module": "deal", - "group_name": "成交客", - "name": "查看成交客(公客类型)", - "description": "按归属人范围查看归属人为共享账号的成交客:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 2, - }, - { - "code": "client.deal.re_transaction", - "module": "client", - "sub_module": "deal", - "group_name": "成交客", - "name": "成交客再次租/购", - "description": "是否可对可见范围内成交客操作再次租/购", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 3, - }, - { - "code": "client.deal.export", - "module": "client", - "sub_module": "deal", - "group_name": "成交客", - "name": "导出成交客列表", - "description": "是否可导出成交客列表", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 4, - }, - { - "code": "client.contact.view_phone_private", - "module": "client", - "sub_module": "contact", - "group_name": "联系人号码", - "name": "查看私客/成交客号码", - "description": "控制查看非保护私客及成交客的号码范围:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 1, - }, - { - "code": "client.contact.view_phone_protected", - "module": "client", - "sub_module": "contact", - "group_name": "联系人号码", - "name": "查看保护客号码", - "description": "控制查看保护私客的号码范围:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 2, - }, - { - "code": "client.contact.view_phone_public", - "module": "client", - "sub_module": "contact", - "group_name": "联系人号码", - "name": "查看公客号码", - "description": "控制查看公客号码范围:无/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 3, - }, - { - "code": "client.contact.view_phone_limit", - "module": "client", - "sub_module": "contact", - "group_name": "联系人号码", - "name": "每日查看联系人号码次数上限", - "description": "每天可查看客源联系人真实号码次数,0=不允许,-1=不限制", - "value_type": "integer", - "scope_choices": [], - "integer_min": -1, - "integer_max": None, - "default_value": {"v": 0}, - "max_allowed_categories": [], - "sort_order": 4, - }, - { - "code": "client.contact.edit_contact", - "module": "client", - "sub_module": "contact", - "group_name": "联系人号码", - "name": "编辑私客/成交客联系人", - "description": "控制编辑非保护私客及成交客联系人信息范围:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 5, - }, - { - "code": "client.contact.edit_phone", - "module": "client", - "sub_module": "contact", - "group_name": "联系人号码", - "name": "编辑私客/成交客联系人号码", - "description": "控制编辑非保护私客及成交客联系人号码范围:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 6, - }, - { - "code": "client.mgmt.delete", - "module": "client", - "sub_module": "mgmt", - "group_name": "客源管理", - "name": "删除客源", - "description": "是否可删除客源及查看已删除客源", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 1, - }, - { - "code": "client.mgmt.to_deal", - "module": "client", - "sub_module": "mgmt", - "group_name": "客源管理", - "name": "手动客源转为成交客", - "description": "是否可手动将客源标记为成交客", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 2, - }, - { - "code": "client.mgmt.change_staff", - "module": "client", - "sub_module": "mgmt", - "group_name": "客源管理", - "name": "单个客源修改相关员工", - "description": "可修改哪个范围内客源的相关方:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 3, - }, - { - "code": "client.mgmt.batch_change_staff", - "module": "client", - "sub_module": "mgmt", - "group_name": "客源管理", - "name": "批量客源修改相关员工", - "description": "批量修改哪个范围内客源的相关方:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 4, - }, - { - "code": "client.mgmt.view_operation_log", - "module": "client", - "sub_module": "mgmt", - "group_name": "客源管理", - "name": "查看客户/联系人操作日志", - "description": "是否可查看客户详情页手机号修改/删除、客户合并等记录", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 5, - }, - { - "code": "client.mgmt.merge_private", - "module": "client", - "sub_module": "mgmt", - "group_name": "客源管理", - "name": "允许合并自己的私客", - "description": "是否可合并归属人为本人的私客", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 6, - }, - { - "code": "client.showing.create", - "module": "client", - "sub_module": "showing", - "group_name": "带看/预约", - "name": "带看/预约新增", - "description": "是否可新增带看/预约记录", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 1, - }, - { - "code": "client.showing.view", - "module": "client", - "sub_module": "showing", - "group_name": "带看/预约", - "name": "私客/成交客详情页带看单查看", - "description": "按带看人范围查看私客/成交客的带看单:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "self"}, - "max_allowed_categories": [], - "sort_order": 2, - }, - { - "code": "client.showing.edit", - "module": "client", - "sub_module": "showing", - "group_name": "带看/预约", - "name": "带看/预约编辑、作废", - "description": "按带看人范围编辑或作废带看:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "self"}, - "max_allowed_categories": [], - "sort_order": 3, - }, - { - "code": "client.archive.view", - "module": "client", - "sub_module": "archive", - "group_name": "资料客", - "name": "查看资料客", - "description": "按归属人范围查看资料客:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "self"}, - "max_allowed_categories": [], - "sort_order": 1, - }, - { - "code": "client.archive.import", - "module": "client", - "sub_module": "archive", - "group_name": "资料客", - "name": "导入资料客", - "description": "是否可导入资料客", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 2, - }, - { - "code": "client.archive.view_phone", - "module": "client", - "sub_module": "archive", - "group_name": "资料客", - "name": "查看资料客号码", - "description": "是否可查看资料客号码", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 3, - }, - { - "code": "client.archive.delete", - "module": "client", - "sub_module": "archive", - "group_name": "资料客", - "name": "删除资料客", - "description": "是否可删除资料客", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 4, - }, - { - "code": "client.archive.view_log", - "module": "client", - "sub_module": "archive", - "group_name": "资料客", - "name": "查看资料客操作日志", - "description": "查看哪个范围员工的资料客操作日志:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "self"}, - "max_allowed_categories": [], - "sort_order": 5, - }, - { - "code": "home.dashboard.view_version", - "module": "home", - "sub_module": "dashboard", - "group_name": "首页看板", - "name": "查看首页版本", - "description": "控制员工可查看的首页数据版本(置业顾问/店管/区管等)", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 1, - }, - { - "code": "home.dashboard.personal_rank", - "module": "home", - "sub_module": "dashboard", - "group_name": "首页看板", - "name": "个人排行榜权限", - "description": "控制个人排行榜可见数据范围:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "self"}, - "max_allowed_categories": [], - "sort_order": 2, - }, - { - "code": "home.dashboard.dept_rank", - "module": "home", - "sub_module": "dashboard", - "group_name": "首页看板", - "name": "部门排行榜权限", - "description": "控制部门排行榜可见数据范围:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 3, - }, - { - "code": "home.dashboard.manage_praise", - "module": "home", - "sub_module": "dashboard", - "group_name": "首页看板", - "name": "管理点赞信息和屏蔽点赞", - "description": "是否可删除首页点赞墙内容和禁止员工发布", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 4, - }, - { - "code": "complex.view", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘管理", - "name": "楼盘管理查看", - "description": "是否显示楼盘管理模块", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 1, - }, - { - "code": "complex.view_structure", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘管理", - "name": "楼盘结构查看", - "description": "是否可查看楼栋-单元-房号层级结构", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 2, - }, - { - "code": "complex.create", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘管理", - "name": "新增楼盘", - "description": "是否可新增或批量新增楼盘", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 3, - }, - { - "code": "complex.create_unit", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘管理", - "name": "新增楼栋/单元/房号", - "description": "是否可新增楼栋、单元、房号数据", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 4, - }, - { - "code": "complex.edit", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘管理", - "name": "编辑楼盘", - "description": "是否可编辑楼盘信息", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 5, - }, - { - "code": "complex.edit_unit", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘管理", - "name": "编辑楼栋/单元/房号", - "description": "是否可编辑楼栋、单元、房号信息", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 6, - }, - { - "code": "complex.delete", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘管理", - "name": "删除楼盘", - "description": "是否可删除楼盘", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 7, - }, - { - "code": "complex.delete_unit", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘管理", - "name": "删除楼栋/单元/房号", - "description": "是否可删除楼栋、单元、房号", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 8, - }, - { - "code": "complex.delete_with_property", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘管理", - "name": "删除楼盘数据(含房源)", - "description": "是否可无视房源直接删除楼盘及以下所有数据", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 9, - }, - { - "code": "complex.merge", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘管理", - "name": "合并楼盘", - "description": "是否可合并不同层级楼盘数据", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 10, - }, - { - "code": "complex.move_unit", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘管理", - "name": "移动楼栋/单元/房号数据", - "description": "是否可将楼栋单元数据移动至其他楼盘", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 11, - }, - { - "code": "complex.lock", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘管理", - "name": "锁定/解锁楼盘", - "description": "是否可操作锁定或解锁楼盘", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 12, - }, - { - "code": "complex.view_deal", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘管理", - "name": "楼盘挂牌成交数据", - "description": "是否显示楼盘挂牌及成交数据", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 13, - }, - { - "code": "complex.view_deal_detail", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘管理", - "name": "司内成交明细及套数", - "description": "是否显示公司成交房源明细信息及套数", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 14, - }, - { - "code": "complex.view_address_scope", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘管理", - "name": "楼街房源地址数据查看范围", - "description": "控制查看部门内其他员工楼街房源地址数据:本人/本部/全部", - "value_type": "scope", - "scope_choices": ["self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "self"}, - "max_allowed_categories": [], - "sort_order": 15, - }, - { - "code": "complex.region_manage", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘管理", - "name": "区域管理", - "description": "是否可对区域商圈进行新增、合并、关联操作", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 16, - }, - { - "code": "complex.material.view_photo", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘资料", - "name": "楼盘照片", - "description": "是否显示楼盘照片列表", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 1, - }, - { - "code": "complex.material.manage_photo", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘资料", - "name": "管理照片", - "description": "是否可上传照片、设为封面", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 2, - }, - { - "code": "complex.material.delete_photo", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘资料", - "name": "删除照片", - "description": "是否可删除楼盘照片", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 3, - }, - { - "code": "complex.material.download_photo", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘资料", - "name": "下载照片", - "description": "是否可下载楼盘照片", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 4, - }, - { - "code": "complex.material.view_attachment", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘资料", - "name": "楼盘附件", - "description": "是否显示楼盘附件模块", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 5, - }, - { - "code": "complex.material.manage_attachment", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘资料", - "name": "管理附件", - "description": "是否可上传楼盘附件", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 6, - }, - { - "code": "complex.material.download_attachment", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘资料", - "name": "下载附件", - "description": "是否可下载楼盘附件", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 7, - }, - { - "code": "complex.material.delete_attachment", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘资料", - "name": "删除附件", - "description": "是否可删除楼盘附件", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 8, - }, - { - "code": "complex.material.view_surrounding", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘资料", - "name": "周边配套", - "description": "是否显示周边配套模块", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 9, - }, - { - "code": "complex.feedback.view", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘反馈", - "name": "楼盘反馈列表", - "description": "可查看小区反馈列表的数据范围:本人/本部/全部", - "value_type": "scope", - "scope_choices": ["self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "self"}, - "max_allowed_categories": [], - "sort_order": 1, - }, - { - "code": "complex.feedback.handle", - "module": "property", - "sub_module": "complex", - "group_name": "楼盘反馈", - "name": "楼盘反馈处理", - "description": "是否可处理或不予处理楼盘反馈", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 2, - }, - { - "code": "org.view_structure", - "module": "hr", - "sub_module": "org", - "group_name": "组织管理", - "name": "组织结构查看", - "description": "控制组织结构页面的部门/员工查看范围:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "self"}, - "max_allowed_categories": [], - "sort_order": 1, - }, - { - "code": "org.view_dept", - "module": "hr", - "sub_module": "org", - "group_name": "组织管理", - "name": "部门查看", - "description": "是否可查看部门信息", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 2, - }, - { - "code": "org.edit_dept", - "module": "hr", - "sub_module": "org", - "group_name": "组织管理", - "name": "部门维护", - "description": "是否可对部门进行编辑操作", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 3, - }, - { - "code": "org.view_staff", - "module": "hr", - "sub_module": "org", - "group_name": "组织管理", - "name": "员工查看", - "description": "是否可查看员工详情", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 4, - }, - { - "code": "org.edit_staff", - "module": "hr", - "sub_module": "org", - "group_name": "组织管理", - "name": "员工维护", - "description": "是否可进行员工异动、批量设置员工上级等操作", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 5, - }, - { - "code": "org.edit_staff_detail", - "module": "hr", - "sub_module": "org", - "group_name": "组织管理", - "name": "员工详情编辑", - "description": "是否可编辑员工信息、新增奖惩记录、编辑账号信息", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 6, - }, - { - "code": "org.freeze_account", - "module": "hr", - "sub_module": "org", - "group_name": "组织管理", - "name": "员工账号冻结/解冻", - "description": "是否可冻结/解冻员工账号", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 7, - }, - { - "code": "org.import_staff", - "module": "hr", - "sub_module": "org", - "group_name": "组织管理", - "name": "批量导入员工", - "description": "是否可批量导入员工", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 8, - }, - { - "code": "org.export_staff", - "module": "hr", - "sub_module": "org", - "group_name": "组织管理", - "name": "导出员工", - "description": "是否可导出员工数据", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 9, - }, - { - "code": "org.view_permission", - "module": "hr", - "sub_module": "org", - "group_name": "组织管理", - "name": "员工权限查看", - "description": "是否可查看员工权限配置", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 10, - }, - { - "code": "org.edit_permission", - "module": "hr", - "sub_module": "org", - "group_name": "组织管理", - "name": "员工权限设置", - "description": "是否可编辑员工权限", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 11, - }, - { - "code": "org.export_permission", - "module": "hr", - "sub_module": "org", - "group_name": "组织管理", - "name": "权限管理页面导出", - "description": "是否可在权限管理页面导出员工角色及管理范围数据", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 12, - }, - { - "code": "org.edit_position", - "module": "hr", - "sub_module": "org", - "group_name": "组织管理", - "name": "职务维护", - "description": "是否可新增/编辑/删除/合并员工职务", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 13, - }, - { - "code": "org.edit_role", - "module": "hr", - "sub_module": "org", - "group_name": "组织管理", - "name": "角色维护", - "description": "是否可展示角色管理页面并进行角色编辑", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 14, - }, - { - "code": "org.view_store_list", - "module": "hr", - "sub_module": "org", - "group_name": "组织管理", - "name": "门店列表查看", - "description": "是否可查看门店列表", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 15, - }, - { - "code": "org.export_store_list", - "module": "hr", - "sub_module": "org", - "group_name": "组织管理", - "name": "门店列表导出", - "description": "是否可导出门店列表", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 16, - }, - { - "code": "org.view_contact_book", - "module": "hr", - "sub_module": "org", - "group_name": "组织管理", - "name": "员工通讯录查看", - "description": "控制查看员工通讯录的范围:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "self"}, - "max_allowed_categories": [], - "sort_order": 17, - }, - { - "code": "org.transfer_business", - "module": "hr", - "sub_module": "org", - "group_name": "组织管理", - "name": "转移业务归属", - "description": "控制转出/转入人的可选范围:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "none"}, - "max_allowed_categories": [], - "sort_order": 18, - }, - { - "code": "org.resign_apply", - "module": "hr", - "sub_module": "org", - "group_name": "组织管理", - "name": "离职申请范围", - "description": "控制员工离职申请范围:无/本人/本部/全部", - "value_type": "scope", - "scope_choices": ["none", "self", "dept", "all"], - "integer_min": None, - "integer_max": None, - "default_value": {"v": "self"}, - "max_allowed_categories": [], - "sort_order": 19, - }, - { - "code": "org.invite_onboard", - "module": "hr", - "sub_module": "org", - "group_name": "组织管理", - "name": "入职邀请", - "description": "是否可生成入职邀请链接/二维码", - "value_type": "boolean", - "scope_choices": [], - "integer_min": None, - "integer_max": None, - "default_value": {"v": False}, - "max_allowed_categories": [], - "sort_order": 20, - }, - { - "code": "org.view_contact_phone_limit", - "module": "hr", - "sub_module": "org", - "group_name": "组织管理", - "name": "每日查看通讯录电话次数上限", - "description": "每天可查看员工通讯录电话次数,0=不允许,-1=不限制", - "value_type": "integer", - "scope_choices": [], - "integer_min": -1, - "integer_max": None, - "default_value": {"v": 0}, - "max_allowed_categories": [], - "sort_order": 21, - }, -] - - -def forwards(apps, schema_editor): - PermissionDef = apps.get_model("fonrey_permission_def", "PermissionDef") - objs = [ - PermissionDef( - code=d["code"], - module=d["module"], - sub_module=d["sub_module"], - group_name=d["group_name"], - name=d["name"], - description=d["description"], - value_type=d["value_type"], - scope_choices=d["scope_choices"], - integer_min=d["integer_min"], - integer_max=d["integer_max"], - default_value=d["default_value"], - max_allowed_categories=d["max_allowed_categories"], - sort_order=d["sort_order"], - is_active=True, - is_deprecated=False, - is_system=True, - version=1, - ) - for d in PERMISSION_DEFS - ] - PermissionDef.objects.bulk_create(objs, ignore_conflicts=False) - - -def backwards(apps, schema_editor): - PermissionDef = apps.get_model("fonrey_permission_def", "PermissionDef") - codes = [d["code"] for d in PERMISSION_DEFS] - PermissionDef.objects.filter(code__in=codes).delete() - - -class Migration(migrations.Migration): - dependencies = [ - ("fonrey_permission_def", "0001_initial"), - ] - - operations = [ - migrations.RunPython(forwards, backwards), - ] diff --git a/apps/permission_def/migrations/__init__.py b/apps/permission_def/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/permission_def/models/__init__.py b/apps/permission_def/models/__init__.py deleted file mode 100644 index f350a5a..0000000 --- a/apps/permission_def/models/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from apps.permission_def.models.permission_def import PermissionDef - -__all__ = ["PermissionDef"] diff --git a/apps/permission_def/models/permission_def.py b/apps/permission_def/models/permission_def.py deleted file mode 100644 index 0fa0d8b..0000000 --- a/apps/permission_def/models/permission_def.py +++ /dev/null @@ -1,117 +0,0 @@ -from django.contrib.postgres.fields import ArrayField -from django.db import models - -from core.enums import PermissionModule, PermissionValueType -from core.models.base import TimeStampedModel - - -class PermissionDef(TimeStampedModel): - code = models.CharField( - max_length=150, - unique=True, - verbose_name="权限编码", - help_text='规则:{module}.{sub_module}.{action}[.{qualifier}]', - ) - module = models.CharField( - max_length=50, - choices=PermissionModule.choices, - verbose_name="一级模块", - help_text="home/property/new_house/client/transaction/data/marketing/hr/contract/trinet/system/mobile/smart_store/recharge", - ) - sub_module = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="二级模块", - help_text='如「二手&租赁」「商圈精耕」', - ) - group_name = models.CharField( - max_length=100, - verbose_name="分组标题", - help_text='如「私客基础权限」「联系人基础权限」', - ) - name = models.CharField( - max_length=200, - verbose_name="显示名称", - ) - description = models.TextField( - blank=True, - default="", - verbose_name="权限作用描述", - ) - value_type = models.CharField( - max_length=20, - choices=PermissionValueType.choices, - verbose_name="权限值类型", - help_text="BOOLEAN=开关型 / SCOPE=范围型 / INTEGER=数值型", - ) - scope_choices = models.JSONField( - default=list, - blank=True, - verbose_name="可选范围", - help_text='仅 SCOPE 类型有效,可选枚举 code 列表,如 ["none","self","store","company"]', - ) - integer_min = models.IntegerField( - null=True, - blank=True, - verbose_name="最小值", - help_text="仅 INTEGER 类型有效", - ) - integer_max = models.IntegerField( - null=True, - blank=True, - verbose_name="最大值", - help_text="仅 INTEGER 类型有效;NULL=无上限(业务上 0 通常代表不限制)", - ) - default_value = models.JSONField( - default=dict, - verbose_name="默认值", - help_text='系统最小默认值,格式 {"v": }', - ) - max_allowed_categories = ArrayField( - models.CharField(max_length=50), - default=list, - blank=True, - verbose_name="可配置角色类别", - help_text="允许配置此权限的角色类别列表,空数组=所有类别均可", - ) - sort_order = models.PositiveIntegerField( - default=0, - verbose_name="排序顺序", - help_text="分组内排序", - ) - is_active = models.BooleanField( - default=True, - verbose_name="是否启用", - help_text="下线权限项置 FALSE,历史记录保留", - ) - is_deprecated = models.BooleanField( - default=False, - verbose_name="是否废弃", - help_text="不再推荐使用但保持兼容", - ) - version = models.PositiveIntegerField( - default=1, - verbose_name="定义版本", - help_text="变更时递增,用于缓存失效", - ) - - class Meta: - db_table = "permission_defs" - verbose_name = "权限定义" - verbose_name_plural = "权限定义" - indexes = [ - models.Index( - fields=["module", "sub_module", "sort_order"], - name="idx_perm_defs_module", - condition=models.Q(is_active=True), - ), - models.Index( - fields=["is_active"], - name="idx_perm_defs_active", - condition=models.Q(is_active=True), - ), - ] - - def __str__(self) -> str: - return f"{self.code} ({self.value_type})" diff --git a/apps/property/__init__.py b/apps/property/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/property/admin.py b/apps/property/admin.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/property/apps.py b/apps/property/apps.py deleted file mode 100644 index 7d04673..0000000 --- a/apps/property/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class PropertyConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.property" - label = "fonrey_property" diff --git a/apps/property/migrations/0001_initial.py b/apps/property/migrations/0001_initial.py deleted file mode 100644 index 2cf393d..0000000 --- a/apps/property/migrations/0001_initial.py +++ /dev/null @@ -1,658 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-29 09:26 - -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 = [ - ('fonrey_complex', '0002_pg_trgm_and_search_vector'), - ('org', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='FollowLog', - fields=[ - ('id', models.UUIDField(primary_key=True, serialize=False)), - ('created_at', models.DateTimeField()), - ('log_type', models.CharField(choices=[('written', '手写跟进'), ('modified', '修改跟进'), ('sensitive_op', '敏感操作'), ('sensitive_view', '敏感查看'), ('other', '其他'), ('system', '系统')], max_length=30)), - ('purpose', models.CharField(blank=True, default='', max_length=50)), - ('content', models.TextField(blank=True, default='')), - ('ai_tag', models.CharField(blank=True, choices=[('ai_for_sale', 'AI判断可售'), ('ai_not_for_sale', 'AI判断不可售')], default='', max_length=20)), - ('change_detail', models.JSONField(blank=True, null=True)), - ('log_tag', models.CharField(blank=True, default='', max_length=50)), - ('is_public', models.BooleanField(default=True)), - ('operator_snapshot', models.JSONField(blank=True, null=True)), - ('is_deletable', models.BooleanField(default=True)), - ('deleted_at', models.DateTimeField(blank=True, null=True)), - ], - options={ - 'db_table': 'follow_logs', - 'managed': False, - }, - ), - migrations.CreateModel( - name='PropertyPhoto', - fields=[ - ('id', models.UUIDField(primary_key=True, serialize=False)), - ('created_at', models.DateTimeField()), - ('category', models.CharField(choices=[('cover', '封面'), ('entrance', '入户'), ('living_room', '客厅'), ('dining_room', '餐厅'), ('bedroom', '卧室'), ('bathroom', '卫生间'), ('kitchen', '厨房'), ('balcony', '阳台'), ('study', '书房'), ('indoor_other', '室内其他'), ('outdoor', '室外'), ('panorama', '全景')], 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, 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)), - ('updated_at', models.DateTimeField(auto_now=True)), - ], - options={ - 'db_table': 'property_photos', - 'managed': False, - }, - ), - migrations.CreateModel( - name='Commission', - 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)), - ('commission_type', models.CharField(max_length=50)), - ('period_start', models.DateField()), - ('period_end', models.DateField(blank=True, null=True)), - ('is_open_ended', models.BooleanField(default=False)), - ('agent_snapshot', models.JSONField(blank=True, null=True)), - ('signing_method', models.CharField(blank=True, default='', max_length=50)), - ('owner_type', models.CharField(choices=[('owner', '产权人本人'), ('authorized_third', '授权第三方')], default='owner', max_length=20)), - ('owner_name', models.CharField(blank=True, default='', max_length=50)), - ('owner_id_type', models.CharField(blank=True, default='', max_length=20)), - ('owner_id_number', models.CharField(blank=True, default='', max_length=50)), - ('owner_id_number_enc', models.BinaryField(blank=True, null=True)), - ('remarks', models.TextField(blank=True, default='')), - ('status', models.CharField(choices=[('active', '有效'), ('expired', '过期'), ('cancelled', '取消')], default='active', max_length=20)), - ('agent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='commissions_as_agent', to='org.staff')), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_commissions', to='org.staff')), - ], - options={ - 'db_table': 'commissions', - }, - ), - migrations.CreateModel( - name='Property', - 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)), - ('property_type', models.CharField(choices=[('residential', '住宅'), ('villa', '别墅'), ('commercial_residential', '商住'), ('shop', '商铺'), ('office', '写字楼'), ('other', '其他')], max_length=30)), - ('status', models.CharField(choices=[('for_sale', '出售'), ('for_rent', '出租'), ('for_sale_rent', '租售'), ('suspended', '暂缓'), ('sold_elsewhere', '他售'), ('rented_elsewhere', '他租'), ('sold', '成交'), ('unlisted', '未挂牌')], default='for_sale', max_length=20)), - ('attribute', models.CharField(choices=[('public', '公盘'), ('private', '私盘'), ('special', '特盘'), ('sealed', '封盘')], default='public', max_length=10)), - ('private_reason', models.TextField(blank=True, default='')), - ('block_no', models.CharField(blank=True, default='', max_length=30)), - ('unit_no', models.CharField(blank=True, default='', max_length=30)), - ('room_no', models.CharField(blank=True, default='', max_length=30)), - ('floor', models.SmallIntegerField()), - ('total_floors', models.SmallIntegerField()), - ('bedroom_count', models.SmallIntegerField(default=0)), - ('living_room_count', models.SmallIntegerField(default=0)), - ('bathroom_count', models.SmallIntegerField(default=0)), - ('kitchen_count', models.SmallIntegerField(default=0)), - ('balcony_count', models.SmallIntegerField(default=0)), - ('area', models.DecimalField(decimal_places=2, max_digits=8)), - ('inner_area', models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True)), - ('sale_price', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), - ('sale_bottom_price', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), - ('sale_record_price', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), - ('rent_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), - ('orientation', models.CharField(blank=True, choices=[('east', '东'), ('south', '南'), ('west', '西'), ('north', '北'), ('southeast', '东南'), ('northeast', '东北'), ('east_west', '东西'), ('south_north', '南北'), ('northwest', '西北'), ('southwest', '西南')], default='', max_length=15)), - ('decoration', models.CharField(blank=True, choices=[('rough', '毛坯'), ('plain', '清水'), ('simple', '简装'), ('medium', '中装'), ('fine', '精装'), ('luxury', '豪装')], default='', max_length=10)), - ('has_elevator', models.BooleanField(blank=True, null=True)), - ('built_year', models.SmallIntegerField(blank=True, null=True)), - ('usage_type', models.CharField(blank=True, default='', max_length=30)), - ('usage_subtype', models.CharField(blank=True, default='', max_length=30)), - ('shop_frontage', models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True)), - ('shop_depth', models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True)), - ('shop_height', models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True)), - ('shop_location', models.CharField(blank=True, choices=[('street', '临街商铺'), ('mall', '商场'), ('residential', '住宅底商'), ('ground_floor', '底层'), ('complex', '综合体')], default='', max_length=20)), - ('house_status', models.CharField(blank=True, choices=[('owner_occupied', '业主自住'), ('vacant', '空置'), ('tenant_occupied', '租客在住'), ('unknown', '未知')], default='', max_length=20)), - ('viewing_time', models.CharField(blank=True, choices=[('anytime', '随时看房'), ('by_appointment', '预约看房'), ('inconvenient', '不便看房')], default='', max_length=20)), - ('grade', models.CharField(blank=True, choices=[('a', 'A(急迫)'), ('b', 'B(较强)'), ('c', 'C(一般)'), ('d', 'D(较弱)')], default='', max_length=2)), - ('ownership_years', models.CharField(blank=True, default='', max_length=30)), - ('ownership_years_detail', models.CharField(blank=True, default='', max_length=20)), - ('ownership_nature', models.CharField(blank=True, choices=[('commercial', '商品房'), ('reform_housing', '房改房'), ('collective', '集资房'), ('economic', '经济适用房')], default='', max_length=20)), - ('is_only_house', models.BooleanField(blank=True, null=True)), - ('payment_method', models.CharField(blank=True, choices=[('full', '全款'), ('mortgage', '按揭'), ('installment', '分期'), ('advance', '垫资')], default='', max_length=15)), - ('tax_included', models.CharField(blank=True, choices=[('each_party', '各付'), ('net', '净到手'), ('inclusive', '包税')], default='', max_length=15)), - ('has_mortgage', models.BooleanField(blank=True, null=True)), - ('has_loan', models.BooleanField(blank=True, null=True)), - ('has_seal', models.BooleanField(blank=True, null=True)), - ('has_restriction', models.BooleanField(blank=True, null=True)), - ('original_price', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), - ('sale_reason', models.TextField(blank=True, default='')), - ('remarks', models.TextField(blank=True, default='')), - ('source', models.CharField(blank=True, default='', max_length=50)), - ('completeness_score', models.SmallIntegerField(default=0)), - ('listed_at', models.DateTimeField(blank=True, null=True)), - ('last_followed_at', models.DateTimeField(blank=True, null=True)), - ('search_vector', django.contrib.postgres.search.SearchVectorField(blank=True, null=True)), - ('version', models.IntegerField(default=1)), - ('building', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='properties', to='fonrey_complex.building')), - ('buyer_agent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='buying_properties', to='org.staff')), - ('complex', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='properties', to='fonrey_complex.complex')), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_properties', to='org.staff')), - ('first_recorder', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='first_recorded_properties', to='org.staff')), - ('number_holder', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='held_properties', to='org.staff')), - ('seller_agent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='selling_properties', to='org.staff')), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_properties', to='org.staff')), - ], - options={ - 'db_table': 'properties', - }, - ), - migrations.CreateModel( - name='PropertyTag', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=50)), - ('color', models.CharField(blank=True, default='', max_length=7)), - ('is_system', models.BooleanField(default=False)), - ('sort_order', models.IntegerField(default=0)), - ('is_active', models.BooleanField(default=True)), - ], - options={ - 'db_table': 'property_tags', - }, - ), - migrations.CreateModel( - name='PropertyProtection', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('is_protected', models.BooleanField(default=False)), - ('reason', models.TextField(blank=True, default='')), - ('start_at', models.DateTimeField(blank=True, null=True)), - ('end_at', models.DateTimeField(blank=True, null=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('property', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='protection', to='fonrey_property.property')), - ('set_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='org.staff')), - ], - options={ - 'db_table': 'property_protections', - }, - ), - migrations.CreateModel( - name='PropertyMarketing', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('marketing_title', models.CharField(blank=True, default='', max_length=30)), - ('core_selling_points', models.TextField(blank=True, default='')), - ('owner_attitude', models.TextField(blank=True, default='')), - ('layout_description', models.TextField(blank=True, default='')), - ('complex_description', models.TextField(blank=True, default='')), - ('ai_generated_points', models.BooleanField(default=False)), - ('ai_generated_attitude', models.BooleanField(default=False)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('property', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='marketing', to='fonrey_property.property')), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='org.staff')), - ], - options={ - 'db_table': 'property_marketing', - }, - ), - migrations.CreateModel( - name='PropertyKey', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('key_type', models.CharField(choices=[('mechanical', '机械钥匙'), ('password', '密码钥匙')], max_length=20)), - ('holder_snapshot', models.JSONField(blank=True, null=True)), - ('is_other_agency', models.BooleanField(default=False)), - ('other_agency_info', models.CharField(blank=True, default='', max_length=30)), - ('remarks', models.TextField(blank=True, default='')), - ('is_active', models.BooleanField(default=True)), - ('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_property_keys', to='org.staff')), - ('holder', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='held_keys', to='org.staff')), - ('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='keys', to='fonrey_property.property')), - ('storage_unit', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stored_keys', to='org.orgunit')), - ], - options={ - 'db_table': 'property_keys', - }, - ), - migrations.CreateModel( - name='PropertyFavorite', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorited_by', to='fonrey_property.property')), - ('staff', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorite_properties', to='org.staff')), - ], - options={ - 'db_table': 'property_favorites', - }, - ), - migrations.CreateModel( - name='PropertyContact', - 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(max_length=50)), - ('gender', models.CharField(choices=[('male', '先生'), ('female', '女士')], default='male', max_length=10)), - ('identity', models.CharField(choices=[('owner', '业主'), ('contact', '联系人'), ('subletter', '转租人'), ('tenant', '租客'), ('agent', '代理人'), ('corporate', '企业法人')], default='contact', max_length=20)), - ('phone_enc', models.BinaryField()), - ('phone_hash', models.CharField(max_length=64)), - ('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.TextField(blank=True, default='')), - ('is_number_holder', models.BooleanField(default=False)), - ('number_holder_approved_at', models.DateTimeField(blank=True, null=True)), - ('sort_order', models.IntegerField(default=0)), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_property_contacts', to='org.staff')), - ('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contacts', to='fonrey_property.property')), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_property_contacts', to='org.staff')), - ], - options={ - 'db_table': 'property_contacts', - }, - ), - migrations.CreateModel( - name='PropertyCompleteness', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('score_core_info', models.SmallIntegerField(default=0)), - ('score_attachment', models.SmallIntegerField(default=0)), - ('score_survey', models.SmallIntegerField(default=0)), - ('score_vr', models.SmallIntegerField(default=0)), - ('score_key', models.SmallIntegerField(default=0)), - ('score_commission', models.SmallIntegerField(default=0)), - ('score_verification', models.SmallIntegerField(default=0)), - ('score_follow_up', models.SmallIntegerField(default=0)), - ('score_viewing', models.SmallIntegerField(default=0)), - ('score_other', models.SmallIntegerField(default=0)), - ('total_score', models.SmallIntegerField(default=0)), - ('calculated_at', models.DateTimeField(auto_now=True)), - ('property', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='completeness', to='fonrey_property.property')), - ], - options={ - 'db_table': 'property_completeness', - }, - ), - migrations.CreateModel( - name='PropertyCertificate', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('owner_name', models.CharField(blank=True, default='', max_length=100)), - ('owner_id_number', models.CharField(blank=True, default='', max_length=50)), - ('owner_cert_type', models.CharField(blank=True, default='', max_length=20)), - ('property_location', models.CharField(blank=True, default='', max_length=500)), - ('cert_status', models.CharField(blank=True, default='', max_length=30)), - ('cert_no', models.CharField(blank=True, default='', max_length=100)), - ('first_registered_at', models.DateField(blank=True, null=True)), - ('ownership_nature', models.CharField(blank=True, default='', max_length=30)), - ('land_nature', models.CharField(blank=True, default='', max_length=30)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('property', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='certificate', to='fonrey_property.property')), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='org.staff')), - ], - options={ - 'db_table': 'property_certificates', - }, - ), - migrations.CreateModel( - name='PropertyAttachment', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('category', models.CharField(choices=[('id_card', '身份证件'), ('property_cert', '产权证明'), ('commission_letter', '委托书'), ('other', '其他')], default='other', max_length=20)), - ('file_key', models.TextField()), - ('file_name', models.CharField(max_length=255)), - ('file_size', models.IntegerField()), - ('file_type', models.CharField(blank=True, default='', max_length=50)), - ('sort_order', models.SmallIntegerField(default=0)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='org.staff')), - ('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='fonrey_property.property')), - ], - options={ - 'db_table': 'property_attachments', - }, - ), - migrations.CreateModel( - name='PriceChange', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('old_sale_price', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), - ('new_sale_price', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), - ('old_bottom_price', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), - ('new_bottom_price', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), - ('old_record_price', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), - ('new_record_price', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), - ('old_rent_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), - ('new_rent_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), - ('change_reason', models.TextField()), - ('changed_at', models.DateTimeField(auto_now_add=True)), - ('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to='org.staff')), - ('property', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='price_changes', to='fonrey_property.property')), - ], - options={ - 'db_table': 'price_changes', - }, - ), - migrations.CreateModel( - name='NumberHolderApproval', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('status', models.CharField(choices=[('pending', '待审批'), ('approved', '已通过'), ('rejected', '已驳回')], default='pending', max_length=20)), - ('remarks', models.TextField(blank=True, default='')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('decided_at', models.DateTimeField(blank=True, null=True)), - ('applicant', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='nh_applications', to='org.staff')), - ('approver', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nh_approvals', to='org.staff')), - ('contact', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='number_holder_approvals', to='fonrey_property.propertycontact')), - ('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='number_holder_approvals', to='fonrey_property.property')), - ], - options={ - 'db_table': 'number_holder_approvals', - }, - ), - migrations.CreateModel( - name='ListingHistory', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('listing_type', models.CharField(choices=[('for_sale', '出售挂牌'), ('for_rent', '出租挂牌')], max_length=20)), - ('status', models.CharField(choices=[('active', '生效中'), ('ended', '已结束')], default='active', max_length=10)), - ('sale_price', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), - ('rent_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), - ('sale_unit_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), - ('ownership_years', models.CharField(blank=True, default='', max_length=30)), - ('is_only_house', models.BooleanField(blank=True, null=True)), - ('tax_included', models.CharField(blank=True, default='', max_length=15)), - ('sale_reason', models.TextField(blank=True, default='')), - ('seller_agent_snapshot', models.JSONField(blank=True, null=True)), - ('started_at', models.DateTimeField()), - ('ended_at', models.DateTimeField(blank=True, null=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('property', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='listing_histories', to='fonrey_property.property')), - ('seller_agent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='org.staff')), - ], - options={ - 'db_table': 'listing_histories', - }, - ), - migrations.CreateModel( - name='KeyAttachment', - 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)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('key', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='fonrey_property.propertykey')), - ], - options={ - 'db_table': 'key_attachments', - }, - ), - migrations.CreateModel( - name='FollowLogRecording', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('follow_log_id', models.UUIDField()), - ('file_key', models.TextField()), - ('duration_seconds', models.IntegerField(blank=True, null=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ], - options={ - 'db_table': 'follow_log_recordings', - 'indexes': [models.Index(fields=['follow_log_id'], name='idx_flr_log')], - }, - ), - migrations.CreateModel( - name='FollowLogAttachment', - 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, choices=[('bmp', 'BMP'), ('jpg', 'JPG'), ('png', 'PNG'), ('svg', 'SVG'), ('gif', 'GIF')], default='', max_length=10)), - ('sort_order', models.SmallIntegerField(default=0)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ], - options={ - 'db_table': 'follow_log_attachments', - 'indexes': [models.Index(fields=['follow_log_id'], name='idx_fla_log')], - }, - ), - migrations.CreateModel( - name='FieldSurvey', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('status', models.CharField(choices=[('draft', '草稿'), ('submitted', '已提交')], default='draft', max_length=10)), - ('gps_latitude', models.DecimalField(blank=True, decimal_places=7, max_digits=10, null=True)), - ('gps_longitude', models.DecimalField(blank=True, decimal_places=7, max_digits=10, null=True)), - ('gps_accuracy', models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True)), - ('description', models.TextField(blank=True, default='')), - ('submitted_at', models.DateTimeField(blank=True, null=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to='org.staff')), - ('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='field_surveys', to='fonrey_property.property')), - ], - options={ - 'db_table': 'field_surveys', - }, - ), - migrations.CreateModel( - name='CommissionAttachment', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('category', models.CharField(choices=[('id_card', '身份证件'), ('property_cert', '产权证明'), ('commission_letter', '委托书'), ('other', '其他')], max_length=20)), - ('file_key', models.TextField()), - ('file_name', models.CharField(max_length=255)), - ('file_size', models.IntegerField(blank=True, null=True)), - ('sort_order', models.SmallIntegerField(default=0)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('commission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='fonrey_property.commission')), - ], - options={ - 'db_table': 'commission_attachments', - }, - ), - migrations.AddField( - model_name='commission', - name='property', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='commissions', to='fonrey_property.property'), - ), - migrations.AddField( - model_name='commission', - name='property_owner_contact', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='commissions', to='fonrey_property.propertycontact'), - ), - migrations.CreateModel( - name='SurveyPhoto', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('category', models.CharField(choices=[('layout', '户型图'), ('living_room', '客厅'), ('dining_room', '餐厅'), ('bedroom', '卧室'), ('bathroom', '卫生间'), ('kitchen', '厨房'), ('entrance', '入户'), ('balcony', '阳台'), ('study', '书房'), ('indoor_other', '室内其他'), ('outdoor', '室外')], max_length=20)), - ('file_key', models.TextField()), - ('thumbnail_key', models.TextField(blank=True, default='')), - ('file_size', models.IntegerField(blank=True, null=True)), - ('width', models.IntegerField(blank=True, null=True)), - ('height', models.IntegerField(blank=True, null=True)), - ('sort_order', models.SmallIntegerField(default=0)), - ('is_vr_screenshot', models.BooleanField(default=False)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('survey', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='photos', to='fonrey_property.fieldsurvey')), - ], - options={ - 'db_table': 'survey_photos', - 'indexes': [models.Index(fields=['survey'], name='idx_sp_survey'), models.Index(fields=['survey', 'category'], name='idx_sp_category')], - }, - ), - migrations.CreateModel( - name='PropertyTagRelation', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tag_relations', to='fonrey_property.property')), - ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='property_relations', to='fonrey_property.propertytag')), - ], - options={ - 'db_table': 'property_tag_relations', - 'indexes': [models.Index(fields=['property'], name='idx_ptr_property'), models.Index(fields=['tag'], name='idx_ptr_tag')], - }, - ), - migrations.AddConstraint( - model_name='propertytagrelation', - constraint=models.UniqueConstraint(fields=('property', 'tag'), name='uq_ptr_property_tag'), - ), - migrations.AddIndex( - model_name='propertykey', - index=models.Index(fields=['property'], name='idx_pk_property'), - ), - migrations.AddIndex( - model_name='propertyfavorite', - index=models.Index(fields=['staff'], name='idx_pfav_staff'), - ), - migrations.AddConstraint( - model_name='propertyfavorite', - constraint=models.UniqueConstraint(fields=('staff', 'property'), name='uq_pfav_staff_property'), - ), - migrations.AddIndex( - model_name='propertycontact', - index=models.Index(fields=['property'], name='idx_pc_property'), - ), - migrations.AddIndex( - model_name='propertycontact', - index=models.Index(fields=['phone_hash'], name='idx_pc_phone_hash'), - ), - migrations.AddIndex( - model_name='propertycontact', - index=models.Index(fields=['phone2_hash'], name='idx_pc_phone2_hash'), - ), - migrations.AddIndex( - model_name='propertyattachment', - index=models.Index(fields=['property'], name='idx_pa_property'), - ), - migrations.AddIndex( - model_name='propertyattachment', - index=models.Index(fields=['property', 'category'], name='idx_pa_category'), - ), - migrations.AddIndex( - model_name='property', - index=django.contrib.postgres.indexes.GinIndex(fields=['search_vector'], name='idx_properties_search'), - ), - migrations.AddIndex( - model_name='property', - index=models.Index(fields=['complex'], name='idx_properties_complex'), - ), - migrations.AddIndex( - model_name='property', - index=models.Index(fields=['status'], name='idx_properties_status'), - ), - migrations.AddIndex( - model_name='property', - index=models.Index(fields=['sale_price'], name='idx_properties_sale_price'), - ), - migrations.AddIndex( - model_name='property', - index=models.Index(fields=['area'], name='idx_properties_area'), - ), - migrations.AddIndex( - model_name='property', - index=models.Index(fields=['listed_at'], name='idx_properties_listed_at'), - ), - migrations.AddIndex( - model_name='property', - index=models.Index(fields=['last_followed_at'], name='idx_properties_last_followed'), - ), - migrations.AddIndex( - model_name='property', - index=models.Index(fields=['bedroom_count'], name='idx_properties_bedroom'), - ), - migrations.AddIndex( - model_name='property', - index=models.Index(fields=['grade'], name='idx_properties_grade'), - ), - migrations.AddIndex( - model_name='property', - index=models.Index(fields=['completeness_score'], name='idx_properties_completeness'), - ), - migrations.AddIndex( - model_name='property', - index=models.Index(fields=['seller_agent'], name='idx_properties_seller_agent'), - ), - migrations.AddIndex( - model_name='property', - index=models.Index(fields=['number_holder'], name='idx_properties_number_holder'), - ), - migrations.AddIndex( - model_name='property', - index=models.Index(fields=['status', 'attribute', 'complex', 'sale_price'], name='idx_properties_list_composite'), - ), - migrations.AddIndex( - model_name='property', - index=models.Index(fields=['seller_agent', 'status', 'listed_at'], name='idx_properties_my_properties'), - ), - migrations.AddConstraint( - model_name='property', - constraint=models.CheckConstraint(check=models.Q(('floor__gt', 0), ('floor__lte', models.F('total_floors'))), name='chk_property_floor'), - ), - migrations.AddIndex( - model_name='pricechange', - index=models.Index(fields=['property'], name='idx_pchg_property'), - ), - migrations.AddIndex( - model_name='pricechange', - index=models.Index(fields=['property', '-changed_at'], name='idx_pchg_time'), - ), - migrations.AddIndex( - model_name='numberholderapproval', - index=models.Index(fields=['status'], name='idx_nha_status'), - ), - migrations.AddIndex( - model_name='numberholderapproval', - index=models.Index(fields=['property'], name='idx_nha_property'), - ), - migrations.AddIndex( - model_name='listinghistory', - index=models.Index(fields=['property'], name='idx_lh_property'), - ), - migrations.AddIndex( - model_name='listinghistory', - index=models.Index(fields=['property', 'status'], name='idx_lh_active'), - ), - migrations.AddIndex( - model_name='keyattachment', - index=models.Index(fields=['key'], name='idx_ka_key'), - ), - migrations.AddIndex( - model_name='fieldsurvey', - index=models.Index(fields=['property'], name='idx_fs_property'), - ), - migrations.AddIndex( - model_name='fieldsurvey', - index=models.Index(fields=['property', 'status'], name='idx_fs_submitted'), - ), - migrations.AddIndex( - model_name='commissionattachment', - index=models.Index(fields=['commission'], name='idx_ca_commission'), - ), - migrations.AddIndex( - model_name='commission', - index=models.Index(fields=['property'], name='idx_commissions_property'), - ), - migrations.AddIndex( - model_name='commission', - index=models.Index(fields=['property', 'status'], name='idx_commissions_active'), - ), - ] diff --git a/apps/property/migrations/0002_partitions_and_triggers.py b/apps/property/migrations/0002_partitions_and_triggers.py deleted file mode 100644 index e769a15..0000000 --- a/apps/property/migrations/0002_partitions_and_triggers.py +++ /dev/null @@ -1,136 +0,0 @@ -from django.db import migrations - -CREATE_FOLLOW_LOGS = """ -CREATE TABLE follow_logs ( - id UUID NOT NULL DEFAULT gen_random_uuid(), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE, - log_type VARCHAR(30) NOT NULL - CHECK (log_type IN ('written','modified','sensitive_op', - 'sensitive_view','other','system')), - purpose VARCHAR(50), - content TEXT, - ai_tag VARCHAR(20) - CHECK (ai_tag IS NULL OR ai_tag IN ('ai_for_sale','ai_not_for_sale')), - change_detail JSONB, - log_tag VARCHAR(50), - is_public BOOLEAN NOT NULL DEFAULT TRUE, - operator_id UUID REFERENCES staff(id) ON DELETE SET NULL, - operator_snapshot JSONB, - is_deletable BOOLEAN NOT NULL DEFAULT TRUE, - deleted_at TIMESTAMPTZ, - PRIMARY KEY (id, created_at) -) PARTITION BY RANGE (created_at); - -CREATE TABLE follow_logs_2026_04 PARTITION OF follow_logs - FOR VALUES FROM ('2026-04-01') TO ('2026-05-01'); -CREATE TABLE follow_logs_2026_05 PARTITION OF follow_logs - FOR VALUES FROM ('2026-05-01') TO ('2026-06-01'); -CREATE TABLE follow_logs_default PARTITION OF follow_logs DEFAULT; - -CREATE INDEX idx_follow_logs_property_time ON follow_logs(property_id, created_at DESC) - WHERE deleted_at IS NULL; -CREATE INDEX idx_follow_logs_type ON follow_logs(property_id, log_type, created_at DESC) - WHERE deleted_at IS NULL; -CREATE INDEX idx_follow_logs_operator ON follow_logs(operator_id, created_at DESC) - WHERE deleted_at IS NULL; -CREATE INDEX idx_follow_logs_sensitive ON follow_logs(property_id, created_at DESC) - WHERE log_type IN ('sensitive_view','sensitive_op'); -""" - -DROP_FOLLOW_LOGS = "DROP TABLE IF EXISTS follow_logs CASCADE;" - -CREATE_PROPERTY_PHOTOS = """ -CREATE TABLE property_photos ( - id UUID NOT NULL DEFAULT gen_random_uuid(), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE, - category VARCHAR(20) NOT NULL - CHECK (category IN ('cover','entrance','living_room', - 'dining_room','bedroom','bathroom', - 'kitchen','balcony','study', - 'indoor_other','outdoor','panorama')), - file_key TEXT NOT NULL, - thumbnail_key TEXT, - file_name VARCHAR(255), - file_size INTEGER, - width INTEGER, - height INTEGER, - is_cover BOOLEAN NOT NULL DEFAULT FALSE, - sort_order SMALLINT NOT NULL DEFAULT 0, - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by_id UUID REFERENCES staff(id) ON DELETE SET NULL, - PRIMARY KEY (id, created_at) -) PARTITION BY RANGE (created_at); - -CREATE TABLE property_photos_2026_04 PARTITION OF property_photos - FOR VALUES FROM ('2026-04-01') TO ('2026-05-01'); -CREATE TABLE property_photos_2026_05 PARTITION OF property_photos - FOR VALUES FROM ('2026-05-01') TO ('2026-06-01'); -CREATE TABLE property_photos_default PARTITION OF property_photos DEFAULT; - -CREATE INDEX idx_property_photos_property ON property_photos(property_id); -CREATE INDEX idx_property_photos_cover ON property_photos(property_id) - WHERE is_cover = TRUE; -CREATE INDEX idx_property_photos_category ON property_photos(property_id, category); -CREATE UNIQUE INDEX idx_property_photos_unique_cover - ON property_photos(property_id) - WHERE is_cover = TRUE; -""" - -DROP_PROPERTY_PHOTOS = "DROP TABLE IF EXISTS property_photos CASCADE;" - -CREATE_TRIGGERS = """ -CREATE OR REPLACE FUNCTION update_property_search_vector() -RETURNS TRIGGER AS $$ -BEGIN - NEW.search_vector := - setweight(to_tsvector('simple', COALESCE(NEW.block_no, '') || - ' ' || COALESCE(NEW.unit_no, '') || - ' ' || COALESCE(NEW.room_no, '')), 'A') || - setweight(to_tsvector('simple', COALESCE(NEW.remarks, '')), 'C'); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER trg_property_search_vector - BEFORE INSERT OR UPDATE OF block_no, unit_no, room_no, remarks - ON properties - FOR EACH ROW EXECUTE FUNCTION update_property_search_vector(); - -CREATE OR REPLACE FUNCTION update_property_last_followed() -RETURNS TRIGGER AS $$ -BEGIN - IF NEW.log_type = 'written' THEN - UPDATE properties - SET last_followed_at = NEW.created_at, - updated_at = NOW() - WHERE id = NEW.property_id; - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER trg_update_last_followed - AFTER INSERT ON follow_logs - FOR EACH ROW EXECUTE FUNCTION update_property_last_followed(); -""" - -DROP_TRIGGERS = """ -DROP TRIGGER IF EXISTS trg_update_last_followed ON follow_logs; -DROP FUNCTION IF EXISTS update_property_last_followed(); -DROP TRIGGER IF EXISTS trg_property_search_vector ON properties; -DROP FUNCTION IF EXISTS update_property_search_vector(); -""" - - -class Migration(migrations.Migration): - dependencies = [ - ("fonrey_property", "0001_initial"), - ] - - operations = [ - migrations.RunSQL(CREATE_FOLLOW_LOGS, reverse_sql=DROP_FOLLOW_LOGS), - migrations.RunSQL(CREATE_PROPERTY_PHOTOS, reverse_sql=DROP_PROPERTY_PHOTOS), - migrations.RunSQL(CREATE_TRIGGERS, reverse_sql=DROP_TRIGGERS), - ] diff --git a/apps/property/migrations/0003_alter_commission_options_and_more.py b/apps/property/migrations/0003_alter_commission_options_and_more.py deleted file mode 100644 index 4ace973..0000000 --- a/apps/property/migrations/0003_alter_commission_options_and_more.py +++ /dev/null @@ -1,105 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-29 11:07 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('fonrey_property', '0002_partitions_and_triggers'), - ] - - operations = [ - migrations.AlterModelOptions( - name='commission', - options={'verbose_name': '委托管理', 'verbose_name_plural': '委托管理'}, - ), - migrations.AlterModelOptions( - name='commissionattachment', - options={'verbose_name': '委托附件', 'verbose_name_plural': '委托附件'}, - ), - migrations.AlterModelOptions( - name='fieldsurvey', - options={'verbose_name': '实勘记录', 'verbose_name_plural': '实勘记录'}, - ), - migrations.AlterModelOptions( - name='followlog', - options={'managed': False, 'verbose_name': '房源跟进日志', 'verbose_name_plural': '房源跟进日志'}, - ), - migrations.AlterModelOptions( - name='followlogattachment', - options={'verbose_name': '跟进附件', 'verbose_name_plural': '跟进附件'}, - ), - migrations.AlterModelOptions( - name='followlogrecording', - options={'verbose_name': '跟进录音', 'verbose_name_plural': '跟进录音'}, - ), - migrations.AlterModelOptions( - name='keyattachment', - options={'verbose_name': '钥匙附件', 'verbose_name_plural': '钥匙附件'}, - ), - migrations.AlterModelOptions( - name='listinghistory', - options={'verbose_name': '挂牌历史', 'verbose_name_plural': '挂牌历史'}, - ), - migrations.AlterModelOptions( - name='numberholderapproval', - options={'verbose_name': '号码方审批', 'verbose_name_plural': '号码方审批'}, - ), - migrations.AlterModelOptions( - name='pricechange', - options={'verbose_name': '调价记录', 'verbose_name_plural': '调价记录'}, - ), - migrations.AlterModelOptions( - name='property', - options={'verbose_name': '房源', 'verbose_name_plural': '房源'}, - ), - migrations.AlterModelOptions( - name='propertyattachment', - options={'verbose_name': '房源附件', 'verbose_name_plural': '房源附件'}, - ), - migrations.AlterModelOptions( - name='propertycertificate', - options={'verbose_name': '房源产证', 'verbose_name_plural': '房源产证'}, - ), - migrations.AlterModelOptions( - name='propertycompleteness', - options={'verbose_name': '房源完整度', 'verbose_name_plural': '房源完整度'}, - ), - migrations.AlterModelOptions( - name='propertycontact', - options={'verbose_name': '房源联系人', 'verbose_name_plural': '房源联系人'}, - ), - migrations.AlterModelOptions( - name='propertyfavorite', - options={'verbose_name': '房源收藏', 'verbose_name_plural': '房源收藏'}, - ), - migrations.AlterModelOptions( - name='propertykey', - options={'verbose_name': '房源钥匙', 'verbose_name_plural': '房源钥匙'}, - ), - migrations.AlterModelOptions( - name='propertymarketing', - options={'verbose_name': '房源营销信息', 'verbose_name_plural': '房源营销信息'}, - ), - migrations.AlterModelOptions( - name='propertyphoto', - options={'managed': False, 'verbose_name': '房源图片', 'verbose_name_plural': '房源图片'}, - ), - migrations.AlterModelOptions( - name='propertyprotection', - options={'verbose_name': '房源保护期', 'verbose_name_plural': '房源保护期'}, - ), - migrations.AlterModelOptions( - name='propertytag', - options={'verbose_name': '房源标签', 'verbose_name_plural': '房源标签'}, - ), - migrations.AlterModelOptions( - name='propertytagrelation', - options={'verbose_name': '房源标签关联', 'verbose_name_plural': '房源标签关联'}, - ), - migrations.AlterModelOptions( - name='surveyphoto', - options={'verbose_name': '实勘照片', 'verbose_name_plural': '实勘照片'}, - ), - ] diff --git a/apps/property/migrations/0004_alter_commission_agent_and_more.py b/apps/property/migrations/0004_alter_commission_agent_and_more.py deleted file mode 100644 index 014e76d..0000000 --- a/apps/property/migrations/0004_alter_commission_agent_and_more.py +++ /dev/null @@ -1,1232 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-30 01:46 - -import django.contrib.postgres.search -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('fonrey_complex', '0004_alter_building_built_year_alter_building_complex_and_more'), - ('org', '0003_alter_orgunit_address_city_and_more'), - ('fonrey_property', '0003_alter_commission_options_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='commission', - name='agent', - field=models.ForeignKey(blank=True, help_text='人员离职后置 NULL', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='commissions_as_agent', to='org.staff', verbose_name='委托经纪人'), - ), - migrations.AlterField( - model_name='commission', - name='agent_snapshot', - field=models.JSONField(blank=True, help_text='{name, store_group};防止人员变动后数据丢失', null=True, verbose_name='经纪人快照'), - ), - migrations.AlterField( - model_name='commission', - name='commission_type', - field=models.CharField(help_text='独家委托/非独家委托;由 lookup_items 维护', max_length=50, verbose_name='委托类型'), - ), - migrations.AlterField( - model_name='commission', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_commissions', to='org.staff', verbose_name='创建人'), - ), - migrations.AlterField( - model_name='commission', - name='is_open_ended', - field=models.BooleanField(default=False, help_text='true=长期委托/false=有截止日期', verbose_name='是否无固定结束日期'), - ), - migrations.AlterField( - model_name='commission', - name='owner_id_number', - field=models.CharField(blank=True, default='', help_text='仅供参考;加密版本见 owner_id_number_enc', max_length=50, verbose_name='委托人证件号明文'), - ), - migrations.AlterField( - model_name='commission', - name='owner_id_number_enc', - field=models.BinaryField(blank=True, help_text='AES-256-GCM 加密', null=True, verbose_name='委托人证件号密文'), - ), - migrations.AlterField( - model_name='commission', - name='owner_id_type', - field=models.CharField(blank=True, default='', help_text='如:身份证/护照', max_length=20, verbose_name='委托人证件类型'), - ), - migrations.AlterField( - model_name='commission', - name='owner_name', - field=models.CharField(blank=True, default='', max_length=50, verbose_name='委托人姓名'), - ), - migrations.AlterField( - model_name='commission', - name='owner_type', - field=models.CharField(choices=[('owner', '产权人本人'), ('authorized_third', '授权第三方')], default='owner', help_text='owner=产权人本人/authorized_third=被授权第三方', max_length=20, verbose_name='委托人类型'), - ), - migrations.AlterField( - model_name='commission', - name='period_end', - field=models.DateField(blank=True, help_text='is_open_ended=true 时为 NULL', null=True, verbose_name='委托结束日期'), - ), - migrations.AlterField( - model_name='commission', - name='period_start', - field=models.DateField(verbose_name='委托开始日期'), - ), - migrations.AlterField( - model_name='commission', - name='property', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='commissions', to='fonrey_property.property', verbose_name='所属房源'), - ), - migrations.AlterField( - model_name='commission', - name='property_owner_contact', - field=models.ForeignKey(blank=True, help_text='若委托人已录入联系人则关联,否则填写下方姓名/证件', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='commissions', to='fonrey_property.propertycontact', verbose_name='关联联系人'), - ), - migrations.AlterField( - model_name='commission', - name='remarks', - field=models.TextField(blank=True, default='', help_text='最多 200 字', verbose_name='备注'), - ), - migrations.AlterField( - model_name='commission', - name='signing_method', - field=models.CharField(blank=True, default='', help_text='选择后动态展示委托书模板', max_length=50, verbose_name='签约方式'), - ), - migrations.AlterField( - model_name='commission', - name='status', - field=models.CharField(choices=[('active', '有效'), ('expired', '过期'), ('cancelled', '取消')], default='active', help_text='active=有效/expired=已过期/cancelled=已取消', max_length=20, verbose_name='委托状态'), - ), - migrations.AlterField( - model_name='commissionattachment', - name='category', - field=models.CharField(choices=[('id_card', '身份证件'), ('property_cert', '产权证明'), ('commission_letter', '委托书'), ('other', '其他')], help_text='id_card=身份证/property_cert=产权证书/commission_letter=委托书/other=其他材料', max_length=20, verbose_name='附件分类'), - ), - migrations.AlterField( - model_name='commissionattachment', - name='commission', - field=models.ForeignKey(help_text='委托删除时联级删除', on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='fonrey_property.commission', verbose_name='所属委托'), - ), - migrations.AlterField( - model_name='commissionattachment', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='上传时间'), - ), - migrations.AlterField( - model_name='commissionattachment', - name='file_key', - field=models.TextField(help_text='Cloudflare R2 对象路径', verbose_name='附件存储路径'), - ), - migrations.AlterField( - model_name='commissionattachment', - name='file_name', - field=models.CharField(max_length=255, verbose_name='原始文件名'), - ), - migrations.AlterField( - model_name='commissionattachment', - name='file_size', - field=models.IntegerField(blank=True, help_text='bytes', null=True, verbose_name='文件大小'), - ), - migrations.AlterField( - model_name='commissionattachment', - name='sort_order', - field=models.SmallIntegerField(default=0, help_text='数值越小越靠前', verbose_name='排序权重'), - ), - migrations.AlterField( - model_name='fieldsurvey', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'), - ), - migrations.AlterField( - model_name='fieldsurvey', - name='created_by', - field=models.ForeignKey(help_text='禁止置 NULL 保留审计', on_delete=django.db.models.deletion.RESTRICT, to='org.staff', verbose_name='实勘人'), - ), - migrations.AlterField( - model_name='fieldsurvey', - name='description', - field=models.TextField(blank=True, default='', help_text='最多 200 字;经纪人现场情况描述', verbose_name='实勘说明'), - ), - migrations.AlterField( - model_name='fieldsurvey', - name='gps_accuracy', - field=models.DecimalField(blank=True, decimal_places=2, help_text='米;标注定位误差', max_digits=6, null=True, verbose_name='GPS 精度'), - ), - migrations.AlterField( - model_name='fieldsurvey', - name='gps_latitude', - field=models.DecimalField(blank=True, decimal_places=7, help_text='实勘打卡位置;精度 7 位小数', max_digits=10, null=True, verbose_name='GPS 纬度'), - ), - migrations.AlterField( - model_name='fieldsurvey', - name='gps_longitude', - field=models.DecimalField(blank=True, decimal_places=7, help_text='实勘打卡位置;精度 7 位小数', max_digits=10, null=True, verbose_name='GPS 经度'), - ), - migrations.AlterField( - model_name='fieldsurvey', - name='property', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='field_surveys', to='fonrey_property.property', verbose_name='所属房源'), - ), - migrations.AlterField( - model_name='fieldsurvey', - name='status', - field=models.CharField(choices=[('draft', '草稿'), ('submitted', '已提交')], default='draft', help_text='draft=草稿(未提交)/submitted=已提交(已完成)', max_length=10, verbose_name='实勘状态'), - ), - migrations.AlterField( - model_name='fieldsurvey', - name='submitted_at', - field=models.DateTimeField(blank=True, help_text='status 变为 submitted 时记录;NULL=尚未提交', null=True, verbose_name='提交时间'), - ), - migrations.AlterField( - model_name='fieldsurvey', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='最后更新时间'), - ), - migrations.AlterField( - model_name='followlogattachment', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='上传时间'), - ), - migrations.AlterField( - model_name='followlogattachment', - name='file_key', - field=models.TextField(help_text='Cloudflare R2 对象路径', verbose_name='图片存储路径'), - ), - migrations.AlterField( - model_name='followlogattachment', - name='file_name', - field=models.CharField(help_text='用户上传时的文件名', max_length=255, verbose_name='原始文件名'), - ), - migrations.AlterField( - model_name='followlogattachment', - name='file_size', - field=models.IntegerField(help_text='bytes;最大 20MB = 20971520', verbose_name='文件大小'), - ), - migrations.AlterField( - model_name='followlogattachment', - name='file_type', - field=models.CharField(blank=True, choices=[('bmp', 'BMP'), ('jpg', 'JPG'), ('png', 'PNG'), ('svg', 'SVG'), ('gif', 'GIF')], default='', help_text='bmp/jpg/png/svg/gif(PRD 限定格式)', max_length=10, verbose_name='文件格式'), - ), - migrations.AlterField( - model_name='followlogattachment', - name='follow_log_id', - field=models.UUIDField(help_text='跨分区外键,未通过 Django FK 强约束;日志删除时联级删除', verbose_name='所属跟进日志ID'), - ), - migrations.AlterField( - model_name='followlogattachment', - name='sort_order', - field=models.SmallIntegerField(default=0, help_text='控制同一跟进附件的显示顺序', verbose_name='排序权重'), - ), - migrations.AlterField( - model_name='followlogrecording', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='上传时间'), - ), - migrations.AlterField( - model_name='followlogrecording', - name='duration_seconds', - field=models.IntegerField(blank=True, help_text='秒;可空,上传时若能解析则填写', null=True, verbose_name='录音时长'), - ), - migrations.AlterField( - model_name='followlogrecording', - name='file_key', - field=models.TextField(help_text='Cloudflare R2 对象路径', verbose_name='录音文件存储路径'), - ), - migrations.AlterField( - model_name='followlogrecording', - name='follow_log_id', - field=models.UUIDField(help_text='跨分区外键,未通过 Django FK 强约束;日志删除时联级删除', verbose_name='所属跟进日志ID'), - ), - migrations.AlterField( - model_name='keyattachment', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='上传时间'), - ), - migrations.AlterField( - model_name='keyattachment', - name='file_key', - field=models.TextField(help_text='Cloudflare R2 对象路径', verbose_name='附件存储路径'), - ), - migrations.AlterField( - model_name='keyattachment', - name='file_name', - field=models.CharField(max_length=255, verbose_name='原始文件名'), - ), - migrations.AlterField( - model_name='keyattachment', - name='key', - field=models.ForeignKey(help_text='钥匙删除时联级删除', on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='fonrey_property.propertykey', verbose_name='所属钥匙记录'), - ), - migrations.AlterField( - model_name='listinghistory', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'), - ), - migrations.AlterField( - model_name='listinghistory', - name='ended_at', - field=models.DateTimeField(blank=True, help_text='NULL=当前仍在挂牌中', null=True, verbose_name='本次挂牌结束时间'), - ), - migrations.AlterField( - model_name='listinghistory', - name='is_only_house', - field=models.BooleanField(blank=True, help_text='本次挂牌时的唯一住房状态', null=True, verbose_name='唯一住房状态快照'), - ), - migrations.AlterField( - model_name='listinghistory', - name='listing_type', - field=models.CharField(choices=[('for_sale', '出售挂牌'), ('for_rent', '出租挂牌')], help_text='for_sale=出售挂牌/for_rent=出租挂牌', max_length=20, verbose_name='挂牌类型'), - ), - migrations.AlterField( - model_name='listinghistory', - name='ownership_years', - field=models.CharField(blank=True, default='', help_text='本次挂牌时的房本年限,如"满2年"', max_length=30, verbose_name='房本年限快照'), - ), - migrations.AlterField( - model_name='listinghistory', - name='property', - field=models.ForeignKey(help_text='禁止级联删除,保留历史', on_delete=django.db.models.deletion.RESTRICT, related_name='listing_histories', to='fonrey_property.property', verbose_name='所属房源'), - ), - migrations.AlterField( - model_name='listinghistory', - name='rent_price', - field=models.DecimalField(blank=True, decimal_places=2, help_text='元/月;出租挂牌时记录', max_digits=10, null=True, verbose_name='本次挂牌租价快照'), - ), - migrations.AlterField( - model_name='listinghistory', - name='sale_price', - field=models.DecimalField(blank=True, decimal_places=2, help_text='万元;出售挂牌时记录', max_digits=12, null=True, verbose_name='本次挂牌售价快照'), - ), - migrations.AlterField( - model_name='listinghistory', - name='sale_reason', - field=models.TextField(blank=True, default='', help_text='本次挂牌时的售房原因', verbose_name='售房原因快照'), - ), - migrations.AlterField( - model_name='listinghistory', - name='sale_unit_price', - field=models.DecimalField(blank=True, decimal_places=2, help_text='元/m²;由 sale_price ÷ area 计算后存储', max_digits=10, null=True, verbose_name='本次挂牌售价单价'), - ), - migrations.AlterField( - model_name='listinghistory', - name='seller_agent', - field=models.ForeignKey(blank=True, help_text='本次挂牌的出售经纪人;人员离职后置 NULL,但 snapshot 保留', null=True, on_delete=django.db.models.deletion.SET_NULL, to='org.staff', verbose_name='出售经纪人'), - ), - migrations.AlterField( - model_name='listinghistory', - name='seller_agent_snapshot', - field=models.JSONField(blank=True, help_text='{name, store_group, org_unit_name};防止人员变动后数据丢失', null=True, verbose_name='出售经纪人快照'), - ), - migrations.AlterField( - model_name='listinghistory', - name='started_at', - field=models.DateTimeField(verbose_name='本次挂牌开始时间'), - ), - migrations.AlterField( - model_name='listinghistory', - name='status', - field=models.CharField(choices=[('active', '生效中'), ('ended', '已结束')], default='active', help_text='active=挂牌中/ended=已结束', max_length=10, verbose_name='挂牌状态'), - ), - migrations.AlterField( - model_name='listinghistory', - name='tax_included', - field=models.CharField(blank=True, default='', help_text='each_party=各付/net=到手/inclusive=包税', max_length=15, verbose_name='包税费方式快照'), - ), - migrations.AlterField( - model_name='numberholderapproval', - name='applicant', - field=models.ForeignKey(help_text='提交号码方变更申请的经纪人;禁止置 NULL 保留审计', on_delete=django.db.models.deletion.RESTRICT, related_name='nh_applications', to='org.staff', verbose_name='申请人'), - ), - migrations.AlterField( - model_name='numberholderapproval', - name='approver', - field=models.ForeignKey(blank=True, help_text='上级审批人;审批前为 NULL', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nh_approvals', to='org.staff', verbose_name='审批人'), - ), - migrations.AlterField( - model_name='numberholderapproval', - name='contact', - field=models.ForeignKey(help_text='即号码方候选联系人', on_delete=django.db.models.deletion.CASCADE, related_name='number_holder_approvals', to='fonrey_property.propertycontact', verbose_name='申请变更的联系方'), - ), - migrations.AlterField( - model_name='numberholderapproval', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='申请提交时间'), - ), - migrations.AlterField( - model_name='numberholderapproval', - name='decided_at', - field=models.DateTimeField(blank=True, help_text='NULL=尚未审批', null=True, verbose_name='审批决定时间'), - ), - migrations.AlterField( - model_name='numberholderapproval', - name='property', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='number_holder_approvals', to='fonrey_property.property', verbose_name='所属房源'), - ), - migrations.AlterField( - model_name='numberholderapproval', - name='remarks', - field=models.TextField(blank=True, default='', help_text='审批人填写的意见或驳回原因', verbose_name='审批备注'), - ), - migrations.AlterField( - model_name='numberholderapproval', - name='status', - field=models.CharField(choices=[('pending', '待审批'), ('approved', '已通过'), ('rejected', '已驳回')], default='pending', help_text='pending=待审批/approved=已通过/rejected=已驳回', max_length=20, verbose_name='审批状态'), - ), - migrations.AlterField( - model_name='pricechange', - name='change_reason', - field=models.TextField(help_text='必填,最多 200 字;如"业主主动降价"', verbose_name='调价原因'), - ), - migrations.AlterField( - model_name='pricechange', - name='changed_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='调价操作时间'), - ), - migrations.AlterField( - model_name='pricechange', - name='changed_by', - field=models.ForeignKey(help_text='禁止置 NULL,保留审计追溯', on_delete=django.db.models.deletion.RESTRICT, to='org.staff', verbose_name='操作人'), - ), - migrations.AlterField( - model_name='pricechange', - name='new_bottom_price', - field=models.DecimalField(blank=True, decimal_places=2, help_text='万元;NULL=本次不变更底价', max_digits=12, null=True, verbose_name='调价后售底价'), - ), - migrations.AlterField( - model_name='pricechange', - name='new_record_price', - field=models.DecimalField(blank=True, decimal_places=2, help_text='万元;NULL=本次不变更', max_digits=12, null=True, verbose_name='调价后备案/核验价'), - ), - migrations.AlterField( - model_name='pricechange', - name='new_rent_price', - field=models.DecimalField(blank=True, decimal_places=2, help_text='元/月', max_digits=10, null=True, verbose_name='调价后挂牌租价'), - ), - migrations.AlterField( - model_name='pricechange', - name='new_sale_price', - field=models.DecimalField(blank=True, decimal_places=2, help_text='万元', max_digits=12, null=True, verbose_name='调价后挂牌售价'), - ), - migrations.AlterField( - model_name='pricechange', - name='old_bottom_price', - field=models.DecimalField(blank=True, decimal_places=2, help_text='万元;NULL=未设置', max_digits=12, null=True, verbose_name='调价前售底价'), - ), - migrations.AlterField( - model_name='pricechange', - name='old_record_price', - field=models.DecimalField(blank=True, decimal_places=2, help_text='万元;NULL=未设置', max_digits=12, null=True, verbose_name='调价前备案/核验价'), - ), - migrations.AlterField( - model_name='pricechange', - name='old_rent_price', - field=models.DecimalField(blank=True, decimal_places=2, help_text='元/月;NULL=非出租类或未设置', max_digits=10, null=True, verbose_name='调价前挂牌租价'), - ), - migrations.AlterField( - model_name='pricechange', - name='old_sale_price', - field=models.DecimalField(blank=True, decimal_places=2, help_text='万元;NULL=首次定价', max_digits=12, null=True, verbose_name='调价前挂牌售价'), - ), - migrations.AlterField( - model_name='pricechange', - name='property', - field=models.ForeignKey(help_text='禁止级联删除,保留调价历史', on_delete=django.db.models.deletion.RESTRICT, related_name='price_changes', to='fonrey_property.property', verbose_name='所属房源'), - ), - migrations.AlterField( - model_name='property', - name='area', - field=models.DecimalField(decimal_places=2, help_text='含公摊;录入必填', max_digits=8, verbose_name='建筑面积(m²)'), - ), - migrations.AlterField( - model_name='property', - name='attribute', - field=models.CharField(choices=[('public', '公盘'), ('private', '私盘'), ('special', '特盘'), ('sealed', '封盘')], default='public', help_text='public=公盘/private=私盘/special=特盘/sealed=封盘;控制可见范围', max_length=10, verbose_name='流通属性'), - ), - migrations.AlterField( - model_name='property', - name='balcony_count', - field=models.SmallIntegerField(default=0, help_text='0=无阳台', verbose_name='阳台数'), - ), - migrations.AlterField( - model_name='property', - name='bathroom_count', - field=models.SmallIntegerField(default=0, verbose_name='卫生间数(卫)'), - ), - migrations.AlterField( - model_name='property', - name='bedroom_count', - field=models.SmallIntegerField(default=0, verbose_name='卧室数(室)'), - ), - migrations.AlterField( - model_name='property', - name='block_no', - field=models.CharField(blank=True, default='', help_text="如'3栋'、'A幢'", max_length=30, verbose_name='栋/幢/弄号'), - ), - migrations.AlterField( - model_name='property', - name='building', - field=models.ForeignKey(blank=True, help_text='楼栋被删除时置 NULL', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='properties', to='fonrey_complex.building', verbose_name='所属楼栋'), - ), - migrations.AlterField( - model_name='property', - name='built_year', - field=models.SmallIntegerField(blank=True, help_text='如 2018;可空(老房源无记录),影响营销发房', null=True, verbose_name='建成年份'), - ), - migrations.AlterField( - model_name='property', - name='buyer_agent', - field=models.ForeignKey(blank=True, help_text='促成成交的买方经纪人', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='buying_properties', to='org.staff', verbose_name='实买方'), - ), - migrations.AlterField( - model_name='property', - name='completeness_score', - field=models.SmallIntegerField(default=0, help_text='0-100;由 Celery 异步计算,非实时;前端列表页展示徽章', verbose_name='维护完成度评分'), - ), - migrations.AlterField( - model_name='property', - name='complex', - field=models.ForeignKey(help_text='房源必须挂在楼盘下,禁止级联删除', on_delete=django.db.models.deletion.RESTRICT, related_name='properties', to='fonrey_complex.complex', verbose_name='所属楼盘'), - ), - migrations.AlterField( - model_name='property', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_properties', to='org.staff', verbose_name='创建人'), - ), - migrations.AlterField( - model_name='property', - name='decoration', - field=models.CharField(blank=True, choices=[('rough', '毛坯'), ('plain', '清水'), ('simple', '简装'), ('medium', '中装'), ('fine', '精装'), ('luxury', '豪装')], default='', help_text='rough=毛坯/plain=清水/simple=简装/medium=中装/fine=精装/luxury=豪装', max_length=10, verbose_name='装修情况'), - ), - migrations.AlterField( - model_name='property', - name='first_recorder', - field=models.ForeignKey(blank=True, help_text='最初录入该房源的经纪人;人员离职后置 NULL', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='first_recorded_properties', to='org.staff', verbose_name='首录方'), - ), - migrations.AlterField( - model_name='property', - name='floor', - field=models.SmallIntegerField(help_text='正整数;不超过 total_floors(CheckConstraint 校验)', verbose_name='所在楼层'), - ), - migrations.AlterField( - model_name='property', - name='grade', - field=models.CharField(blank=True, choices=[('a', 'A(急迫)'), ('b', 'B(较强)'), ('c', 'C(一般)'), ('d', 'D(较弱)')], default='', help_text='A=急迫/B=较强/C=一般/D=较弱(业主出售意向)', max_length=2, verbose_name='房源等级'), - ), - migrations.AlterField( - model_name='property', - name='has_elevator', - field=models.BooleanField(blank=True, help_text='true=有/false=无/NULL=未确认', null=True, verbose_name='是否有电梯'), - ), - migrations.AlterField( - model_name='property', - name='has_loan', - field=models.BooleanField(blank=True, help_text='true=有/false=无/NULL=未确认', null=True, verbose_name='是否有贷款(未还清)'), - ), - migrations.AlterField( - model_name='property', - name='has_mortgage', - field=models.BooleanField(blank=True, help_text='true=有/false=无/NULL=未确认', null=True, verbose_name='是否有抵押'), - ), - migrations.AlterField( - model_name='property', - name='has_restriction', - field=models.BooleanField(blank=True, help_text='true=有/false=无/NULL=未确认', null=True, verbose_name='是否有其他限制'), - ), - migrations.AlterField( - model_name='property', - name='has_seal', - field=models.BooleanField(blank=True, help_text='true=有/false=无/NULL=未确认', null=True, verbose_name='是否被查封'), - ), - migrations.AlterField( - model_name='property', - name='house_status', - field=models.CharField(blank=True, choices=[('owner_occupied', '业主自住'), ('vacant', '空置'), ('tenant_occupied', '租客在住'), ('unknown', '未知')], default='', help_text='owner_occupied=业主自住/vacant=空置/tenant_occupied=租客租住/unknown=未知;影响带看安排', max_length=20, verbose_name='房屋现状'), - ), - migrations.AlterField( - model_name='property', - name='inner_area', - field=models.DecimalField(blank=True, decimal_places=2, help_text='不含公摊;选填,编辑页专属字段', max_digits=8, null=True, verbose_name='套内面积(m²)'), - ), - migrations.AlterField( - model_name='property', - name='is_only_house', - field=models.BooleanField(blank=True, help_text='true=唯一/false=非唯一/NULL=未确认;影响交易税费计算', null=True, verbose_name='是否唯一住房'), - ), - migrations.AlterField( - model_name='property', - name='kitchen_count', - field=models.SmallIntegerField(default=0, verbose_name='厨房数(厨)'), - ), - migrations.AlterField( - model_name='property', - name='last_followed_at', - field=models.DateTimeField(blank=True, help_text='冗余字段,由触发器自动维护,加速超时未跟进排序', null=True, verbose_name='最后跟进时间'), - ), - migrations.AlterField( - model_name='property', - name='listed_at', - field=models.DateTimeField(blank=True, help_text='每次重新挂牌时更新', null=True, verbose_name='最近一次挂牌时间'), - ), - migrations.AlterField( - model_name='property', - name='living_room_count', - field=models.SmallIntegerField(default=0, verbose_name='客厅/餐厅数(厅)'), - ), - migrations.AlterField( - model_name='property', - name='number_holder', - field=models.ForeignKey(blank=True, help_text='持有业主联系号码的经纪人;变更需走审批流', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='held_properties', to='org.staff', verbose_name='号码方'), - ), - migrations.AlterField( - model_name='property', - name='orientation', - field=models.CharField(blank=True, choices=[('east', '东'), ('south', '南'), ('west', '西'), ('north', '北'), ('southeast', '东南'), ('northeast', '东北'), ('east_west', '东西'), ('south_north', '南北'), ('northwest', '西北'), ('southwest', '西南')], default='', help_text='east=东/south=南/west=西/north=北/southeast=东南/northeast=东北/east_west=东西/south_north=南北/northwest=西北/southwest=西南', max_length=15, verbose_name='朝向'), - ), - migrations.AlterField( - model_name='property', - name='original_price', - field=models.DecimalField(blank=True, decimal_places=2, help_text='业主当年购入价,用于计算增值', max_digits=12, null=True, verbose_name='原购价(万元)'), - ), - migrations.AlterField( - model_name='property', - name='ownership_nature', - field=models.CharField(blank=True, choices=[('commercial', '商品房'), ('reform_housing', '房改房'), ('collective', '集资房'), ('economic', '经济适用房')], default='', help_text='commercial=商品房/reform_housing=房改房/collective=集资房/economic=经济活用房', max_length=20, verbose_name='产权性质'), - ), - migrations.AlterField( - model_name='property', - name='ownership_years', - field=models.CharField(blank=True, default='', help_text='不满2年/满2年/满5年 等(影响交易税费)', max_length=30, verbose_name='房本年限'), - ), - migrations.AlterField( - model_name='property', - name='ownership_years_detail', - field=models.CharField(blank=True, default='', help_text='满五/不满五(与 ownership_years 组合使用)', max_length=20, verbose_name='房本年限辅助说明'), - ), - migrations.AlterField( - model_name='property', - name='payment_method', - field=models.CharField(blank=True, choices=[('full', '全款'), ('mortgage', '按揭'), ('installment', '分期'), ('advance', '垫资')], default='', help_text='full=一次付清/mortgage=按揭付款/installment=分批次付款/advance=垫资解按', max_length=15, verbose_name='购房付款方式'), - ), - migrations.AlterField( - model_name='property', - name='private_reason', - field=models.TextField(blank=True, default='', help_text='attribute 为 private/sealed 时必填,最多 200 字', verbose_name='私盘/封盘原因'), - ), - migrations.AlterField( - model_name='property', - name='property_type', - field=models.CharField(choices=[('residential', '住宅'), ('villa', '别墅'), ('commercial_residential', '商住'), ('shop', '商铺'), ('office', '写字楼'), ('other', '其他')], help_text='residential=住宅/villa=别墅/commercial_residential=商住/shop=商铺/office=写字楼/other=其他(详见 ENUMS)', max_length=30, verbose_name='房源类型'), - ), - migrations.AlterField( - model_name='property', - name='remarks', - field=models.TextField(blank=True, default='', help_text='经纪人内部备注,最多 500 字,不对外展示', verbose_name='房源备注'), - ), - migrations.AlterField( - model_name='property', - name='rent_price', - field=models.DecimalField(blank=True, decimal_places=2, help_text='出租类房源使用', max_digits=10, null=True, verbose_name='挂牌租价(元/月)'), - ), - migrations.AlterField( - model_name='property', - name='room_no', - field=models.CharField(blank=True, default='', help_text="如'0301'、'1502'", max_length=30, verbose_name='房号/门牌号'), - ), - migrations.AlterField( - model_name='property', - name='sale_bottom_price', - field=models.DecimalField(blank=True, decimal_places=2, help_text='业主心理底价,仅内部可见,不对外展示', max_digits=12, null=True, verbose_name='售底价(万元)'), - ), - migrations.AlterField( - model_name='property', - name='sale_price', - field=models.DecimalField(blank=True, decimal_places=2, help_text='出售类房源必填,出租类可为 NULL', max_digits=12, null=True, verbose_name='挂牌售价(万元)'), - ), - migrations.AlterField( - model_name='property', - name='sale_reason', - field=models.TextField(blank=True, default='', help_text="业主出售理由,最多 200 字;如'置换'", verbose_name='售房原因'), - ), - migrations.AlterField( - model_name='property', - name='sale_record_price', - field=models.DecimalField(blank=True, decimal_places=2, help_text='填写后同步至营销库', max_digits=12, null=True, verbose_name='备案/核验价(万元)'), - ), - migrations.AlterField( - model_name='property', - name='search_vector', - field=django.contrib.postgres.search.SearchVectorField(blank=True, help_text='由触发器自动维护,覆盖栋号/单元/房号/备注', null=True, verbose_name='全文检索向量'), - ), - migrations.AlterField( - model_name='property', - name='seller_agent', - field=models.ForeignKey(blank=True, help_text='负责出售跟进的经纪人', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='selling_properties', to='org.staff', verbose_name='出售方'), - ), - migrations.AlterField( - model_name='property', - name='shop_depth', - field=models.DecimalField(blank=True, decimal_places=2, help_text='商铺专属', max_digits=6, null=True, verbose_name='进深(米)'), - ), - migrations.AlterField( - model_name='property', - name='shop_frontage', - field=models.DecimalField(blank=True, decimal_places=2, help_text='商铺专属,住宅类为 NULL', max_digits=6, null=True, verbose_name='开间(米)'), - ), - migrations.AlterField( - model_name='property', - name='shop_height', - field=models.DecimalField(blank=True, decimal_places=2, help_text='商铺专属', max_digits=6, null=True, verbose_name='层高(米)'), - ), - migrations.AlterField( - model_name='property', - name='shop_location', - field=models.CharField(blank=True, choices=[('street', '临街商铺'), ('mall', '商场'), ('residential', '住宅底商'), ('ground_floor', '底层'), ('complex', '综合体')], default='', help_text='street=沿街/mall=商场内/residential=住宅底商/ground_floor=楼栋底层/complex=综合体(商铺专属)', max_length=20, verbose_name='商铺位置类型'), - ), - migrations.AlterField( - model_name='property', - name='source', - field=models.CharField(blank=True, default='', help_text='枚举值由 lookup_items 维护,如:门店拓客/转介绍/网络等', max_length=50, verbose_name='房源来源渠道'), - ), - migrations.AlterField( - model_name='property', - name='status', - field=models.CharField(choices=[('for_sale', '出售'), ('for_rent', '出租'), ('for_sale_rent', '租售'), ('suspended', '暂缓'), ('sold_elsewhere', '他售'), ('rented_elsewhere', '他租'), ('sold', '成交'), ('unlisted', '未挂牌')], default='for_sale', help_text='for_sale=出售/for_rent=出租/for_sale_rent=租售/suspended=暂缓/sold_elsewhere=他售/rented_elsewhere=他租/sold=成交/unlisted=未挂牌(详见 ENUMS)', max_length=20, verbose_name='交易状态'), - ), - migrations.AlterField( - model_name='property', - name='tax_included', - field=models.CharField(blank=True, choices=[('each_party', '各付'), ('net', '净到手'), ('inclusive', '包税')], default='', help_text='each_party=各付/net=到手/inclusive=包税', max_length=15, verbose_name='包税费方式'), - ), - migrations.AlterField( - model_name='property', - name='total_floors', - field=models.SmallIntegerField(help_text='正整数', verbose_name='楼栋总层数'), - ), - migrations.AlterField( - model_name='property', - name='unit_no', - field=models.CharField(blank=True, default='', help_text="如'1单元'、'055'", max_length=30, verbose_name='单元号'), - ), - migrations.AlterField( - model_name='property', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_properties', to='org.staff', verbose_name='最后修改人'), - ), - migrations.AlterField( - model_name='property', - name='usage_subtype', - field=models.CharField(blank=True, default='', help_text='如:普通住宅/花园洋房;对应更改用途浮窗第二级下拉', max_length=30, verbose_name='房屋用途细分小类'), - ), - migrations.AlterField( - model_name='property', - name='usage_type', - field=models.CharField(blank=True, default='', help_text='如:住宅/商住/商业;对应更改用途浮窗第一级下拉', max_length=30, verbose_name='房屋用途大类'), - ), - migrations.AlterField( - model_name='property', - name='version', - field=models.IntegerField(default=1, help_text='每次 UPDATE 必须 +1;应用层检测 0 行受影响时抛 ConflictError', verbose_name='乐观锁版本号'), - ), - migrations.AlterField( - model_name='property', - name='viewing_time', - field=models.CharField(blank=True, choices=[('anytime', '随时看房'), ('by_appointment', '预约看房'), ('inconvenient', '不便看房')], default='', help_text='anytime=随时可看/by_appointment=提前预约/inconvenient=不方便看', max_length=20, verbose_name='看房时间安排'), - ), - migrations.AlterField( - model_name='propertyattachment', - name='category', - field=models.CharField(choices=[('id_card', '身份证件'), ('property_cert', '产权证明'), ('commission_letter', '委托书'), ('other', '其他')], default='other', help_text='id_card=身份证/property_cert=产权证书/commission_letter=委托书/other=其他材料', max_length=20, verbose_name='附件分类'), - ), - migrations.AlterField( - model_name='propertyattachment', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='上传时间'), - ), - migrations.AlterField( - model_name='propertyattachment', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='org.staff', verbose_name='上传人'), - ), - migrations.AlterField( - model_name='propertyattachment', - name='file_key', - field=models.TextField(help_text='Cloudflare R2 对象路径', verbose_name='附件存储路径'), - ), - migrations.AlterField( - model_name='propertyattachment', - name='file_name', - field=models.CharField(max_length=255, verbose_name='原始文件名'), - ), - migrations.AlterField( - model_name='propertyattachment', - name='file_size', - field=models.IntegerField(help_text='bytes', verbose_name='文件大小'), - ), - migrations.AlterField( - model_name='propertyattachment', - name='file_type', - field=models.CharField(blank=True, default='', help_text='如 application/pdf、image/jpeg', max_length=50, verbose_name='MIME 类型'), - ), - migrations.AlterField( - model_name='propertyattachment', - name='property', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='fonrey_property.property', verbose_name='所属房源'), - ), - migrations.AlterField( - model_name='propertyattachment', - name='sort_order', - field=models.SmallIntegerField(default=0, help_text='控制同一房源附件的显示顺序', verbose_name='排序权重'), - ), - migrations.AlterField( - model_name='propertycertificate', - name='cert_no', - field=models.CharField(blank=True, default='', help_text='不动产权证书编号', max_length=100, verbose_name='产权证号'), - ), - migrations.AlterField( - model_name='propertycertificate', - name='cert_status', - field=models.CharField(blank=True, default='', help_text='如:已过户/抵押中/查封/正常', max_length=30, verbose_name='产证状态'), - ), - migrations.AlterField( - model_name='propertycertificate', - name='first_registered_at', - field=models.DateField(blank=True, help_text='产权证上的初始登记日期', null=True, verbose_name='首次登记时间'), - ), - migrations.AlterField( - model_name='propertycertificate', - name='land_nature', - field=models.CharField(blank=True, default='', help_text='如:国有/集体/划拨/出让', max_length=30, verbose_name='土地性质'), - ), - migrations.AlterField( - model_name='propertycertificate', - name='owner_cert_type', - field=models.CharField(blank=True, default='', help_text='如:身份证/护照/营业执照', max_length=20, verbose_name='产权人证件类型'), - ), - migrations.AlterField( - model_name='propertycertificate', - name='owner_id_number', - field=models.CharField(blank=True, default='', help_text='身份证号/统一社会信用代码等', max_length=50, verbose_name='产权人证件号码'), - ), - migrations.AlterField( - model_name='propertycertificate', - name='owner_name', - field=models.CharField(blank=True, default='', help_text='产权证书上登记的所有权人', max_length=100, verbose_name='产权人姓名'), - ), - migrations.AlterField( - model_name='propertycertificate', - name='ownership_nature', - field=models.CharField(blank=True, default='', help_text='如:商品房/经济适用房/回迁房', max_length=30, verbose_name='权属性质'), - ), - migrations.AlterField( - model_name='propertycertificate', - name='property', - field=models.OneToOneField(help_text='1:1 关联 properties 表', on_delete=django.db.models.deletion.CASCADE, related_name='certificate', to='fonrey_property.property', verbose_name='所属房源'), - ), - migrations.AlterField( - model_name='propertycertificate', - name='property_location', - field=models.CharField(blank=True, default='', help_text='产权证书上的完整地址,最多 500 字', max_length=500, verbose_name='房屋坐落'), - ), - migrations.AlterField( - model_name='propertycertificate', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='最后更新时间'), - ), - migrations.AlterField( - model_name='propertycertificate', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='org.staff', verbose_name='最后修改人'), - ), - migrations.AlterField( - model_name='propertycompleteness', - name='calculated_at', - field=models.DateTimeField(auto_now=True, help_text='最近一次 Celery 任务异步计算完成时间', verbose_name='最近计算时间'), - ), - migrations.AlterField( - model_name='propertycompleteness', - name='property', - field=models.OneToOneField(help_text='1:1 关联 properties 表', on_delete=django.db.models.deletion.CASCADE, related_name='completeness', to='fonrey_property.property', verbose_name='所属房源'), - ), - migrations.AlterField( - model_name='propertycompleteness', - name='score_attachment', - field=models.SmallIntegerField(default=0, help_text='满分 8;身份证/产权证/委托书等材料上传情况', verbose_name='附件得分'), - ), - migrations.AlterField( - model_name='propertycompleteness', - name='score_commission', - field=models.SmallIntegerField(default=0, help_text='满分 10;独家/普通委托书情况', verbose_name='委托得分'), - ), - migrations.AlterField( - model_name='propertycompleteness', - name='score_core_info', - field=models.SmallIntegerField(default=0, help_text='满分 8;包含房源核心字段完整度', verbose_name='重点信息得分'), - ), - migrations.AlterField( - model_name='propertycompleteness', - name='score_follow_up', - field=models.SmallIntegerField(default=0, help_text='满分 8;近期跟进记录情况', verbose_name='跟进得分'), - ), - migrations.AlterField( - model_name='propertycompleteness', - name='score_key', - field=models.SmallIntegerField(default=0, help_text='满分 10;钥匙托管情况', verbose_name='钥匙得分'), - ), - migrations.AlterField( - model_name='propertycompleteness', - name='score_other', - field=models.SmallIntegerField(default=0, help_text='满分 7;其他加分项', verbose_name='其他得分'), - ), - migrations.AlterField( - model_name='propertycompleteness', - name='score_survey', - field=models.SmallIntegerField(default=0, help_text='满分 16;实勘照片和报告完整度', verbose_name='实勘得分'), - ), - migrations.AlterField( - model_name='propertycompleteness', - name='score_verification', - field=models.SmallIntegerField(default=0, help_text='满分 7;房源信息核实情况', verbose_name='验证得分'), - ), - migrations.AlterField( - model_name='propertycompleteness', - name='score_viewing', - field=models.SmallIntegerField(default=0, help_text='满分 8;带看记录完整度', verbose_name='带看得分'), - ), - migrations.AlterField( - model_name='propertycompleteness', - name='score_vr', - field=models.SmallIntegerField(default=0, help_text='满分 8;VR/全景照片上传情况', verbose_name='VR得分'), - ), - migrations.AlterField( - model_name='propertycompleteness', - name='total_score', - field=models.SmallIntegerField(default=0, help_text='0-100;供列表排序用,与 properties.completeness_score 冗余', verbose_name='维护完成度总分'), - ), - migrations.AlterField( - model_name='propertycontact', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_property_contacts', to='org.staff', verbose_name='创建人'), - ), - migrations.AlterField( - model_name='propertycontact', - name='gender', - field=models.CharField(choices=[('male', '先生'), ('female', '女士')], default='male', help_text='male=先生/female=女士', max_length=10, verbose_name='性别'), - ), - migrations.AlterField( - model_name='propertycontact', - name='identity', - field=models.CharField(choices=[('owner', '业主'), ('contact', '联系人'), ('subletter', '转租人'), ('tenant', '租客'), ('agent', '代理人'), ('corporate', '企业法人')], default='contact', help_text='owner=业主/contact=联系人/subletter=二房东/tenant=租客/agent=代理人/corporate=企业法人', max_length=20, verbose_name='联系人身份'), - ), - migrations.AlterField( - model_name='propertycontact', - name='is_number_holder', - field=models.BooleanField(default=False, help_text='true=是号码方(审批通过)/false=否;号码方变更须走审批流', verbose_name='是否为号码方'), - ), - migrations.AlterField( - model_name='propertycontact', - name='name', - field=models.CharField(help_text="如'张先生';业主或其代理人的真实姓名", max_length=50, verbose_name='联系人姓名'), - ), - migrations.AlterField( - model_name='propertycontact', - name='number_holder_approved_at', - field=models.DateTimeField(blank=True, help_text='NULL=尚未成为号码方', null=True, verbose_name='号码方审批通过时间'), - ), - migrations.AlterField( - model_name='propertycontact', - name='phone2_enc', - field=models.BinaryField(blank=True, help_text='AES-256-GCM 加密;选填', null=True, verbose_name='手机号2密文'), - ), - migrations.AlterField( - model_name='propertycontact', - name='phone2_hash', - field=models.CharField(blank=True, default='', help_text='SHA-256;phone2_enc 存在时必填', max_length=64, verbose_name='手机号2哈希'), - ), - migrations.AlterField( - model_name='propertycontact', - name='phone_enc', - field=models.BinaryField(help_text='AES-256-GCM 加密,不可直接查询', verbose_name='手机号1密文'), - ), - migrations.AlterField( - model_name='propertycontact', - name='phone_hash', - field=models.CharField(help_text='SHA-256,用于重复房源检测和精确查询', max_length=64, verbose_name='手机号1哈希'), - ), - migrations.AlterField( - model_name='propertycontact', - name='property', - field=models.ForeignKey(help_text='房源删除时联级删除', on_delete=django.db.models.deletion.CASCADE, related_name='contacts', to='fonrey_property.property', verbose_name='所属房源'), - ), - migrations.AlterField( - model_name='propertycontact', - name='qq', - field=models.CharField(blank=True, default='', help_text="选填;无数据时前端展示'-'", max_length=20, verbose_name='QQ号'), - ), - migrations.AlterField( - model_name='propertycontact', - name='remarks', - field=models.TextField(blank=True, default='', help_text='最多 200 字;补充说明联系人情况', verbose_name='备注'), - ), - migrations.AlterField( - model_name='propertycontact', - name='sort_order', - field=models.IntegerField(default=0, help_text='数值越小越靠前;控制联系人在面板中的显示顺序', verbose_name='排序权重'), - ), - migrations.AlterField( - model_name='propertycontact', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_property_contacts', to='org.staff', verbose_name='最后修改人'), - ), - migrations.AlterField( - model_name='propertycontact', - name='wechat', - field=models.CharField(blank=True, default='', help_text="选填;无数据时前端展示'-'", max_length=100, verbose_name='微信号'), - ), - migrations.AlterField( - model_name='propertyfavorite', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='收藏时间'), - ), - migrations.AlterField( - model_name='propertyfavorite', - name='property', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorited_by', to='fonrey_property.property', verbose_name='收藏的房源'), - ), - migrations.AlterField( - model_name='propertyfavorite', - name='staff', - field=models.ForeignKey(help_text='员工注销时删除收藏记录', on_delete=django.db.models.deletion.CASCADE, related_name='favorite_properties', to='org.staff', verbose_name='收藏人'), - ), - migrations.AlterField( - model_name='propertykey', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'), - ), - migrations.AlterField( - model_name='propertykey', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_property_keys', to='org.staff', verbose_name='创建人'), - ), - migrations.AlterField( - model_name='propertykey', - name='holder', - field=models.ForeignKey(blank=True, help_text='人员离职后置 NULL', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='held_keys', to='org.staff', verbose_name='持有人'), - ), - migrations.AlterField( - model_name='propertykey', - name='holder_snapshot', - field=models.JSONField(blank=True, help_text='{name, store_group};防止人员离职后丢失显示信息', null=True, verbose_name='持有人快照'), - ), - migrations.AlterField( - model_name='propertykey', - name='is_active', - field=models.BooleanField(default=True, help_text='true=在管中/false=已归还或失效', verbose_name='是否有效'), - ), - migrations.AlterField( - model_name='propertykey', - name='is_other_agency', - field=models.BooleanField(default=False, help_text='true=是他中介公司的钥匙/false=本司钥匙', verbose_name='是否他司钥匙'), - ), - migrations.AlterField( - model_name='propertykey', - name='key_type', - field=models.CharField(choices=[('mechanical', '机械钥匙'), ('password', '密码钥匙')], help_text='mechanical=机械钥匙/password=密码(如密码门锁)', max_length=20, verbose_name='钥匙类型'), - ), - migrations.AlterField( - model_name='propertykey', - name='other_agency_info', - field=models.CharField(blank=True, default='', help_text='最多 30 字;is_other_agency=true 时填写,如"链家"', max_length=30, verbose_name='他司中介信息'), - ), - migrations.AlterField( - model_name='propertykey', - name='property', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='keys', to='fonrey_property.property', verbose_name='所属房源'), - ), - migrations.AlterField( - model_name='propertykey', - name='remarks', - field=models.TextField(blank=True, default='', help_text='最多 200 字;如密码内容等补充说明', verbose_name='备注'), - ), - migrations.AlterField( - model_name='propertykey', - name='storage_unit', - field=models.ForeignKey(blank=True, help_text='钥匙存放在哪个部门', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stored_keys', to='org.orgunit', verbose_name='保管部门'), - ), - migrations.AlterField( - model_name='propertykey', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='最后更新时间'), - ), - migrations.AlterField( - model_name='propertymarketing', - name='ai_generated_attitude', - field=models.BooleanField(default=False, help_text='true=AI辅助生成', verbose_name='业主心态AI生成'), - ), - migrations.AlterField( - model_name='propertymarketing', - name='ai_generated_points', - field=models.BooleanField(default=False, help_text='true=AI辅助生成(经纪人确认后使用)', verbose_name='核心卖点AI生成'), - ), - migrations.AlterField( - model_name='propertymarketing', - name='complex_description', - field=models.TextField(blank=True, default='', help_text='最多 200 字;楼盘/小区周边配套描述', verbose_name='小区介绍'), - ), - migrations.AlterField( - model_name='propertymarketing', - name='core_selling_points', - field=models.TextField(blank=True, default='', help_text='最多 200 字;展示给买家的重点卖点说明', verbose_name='核心卖点'), - ), - migrations.AlterField( - model_name='propertymarketing', - name='layout_description', - field=models.TextField(blank=True, default='', help_text='最多 200 字;房源户型特点描述,面向买家展示', verbose_name='户型介绍'), - ), - migrations.AlterField( - model_name='propertymarketing', - name='marketing_title', - field=models.CharField(blank=True, default='', help_text='0-30 字;前端发房时展示给买家的吸睛标题', max_length=30, verbose_name='营销标题'), - ), - migrations.AlterField( - model_name='propertymarketing', - name='owner_attitude', - field=models.TextField(blank=True, default='', help_text='最多 200 字;仅内部可见,描述业主议价空间和心理状态', verbose_name='业主心态'), - ), - migrations.AlterField( - model_name='propertymarketing', - name='property', - field=models.OneToOneField(help_text='1:1 关联 properties 表', on_delete=django.db.models.deletion.CASCADE, related_name='marketing', to='fonrey_property.property', verbose_name='所属房源'), - ), - migrations.AlterField( - model_name='propertymarketing', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='最后更新时间'), - ), - migrations.AlterField( - model_name='propertymarketing', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='org.staff', verbose_name='最后修改人'), - ), - migrations.AlterField( - model_name='propertyprotection', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'), - ), - migrations.AlterField( - model_name='propertyprotection', - name='end_at', - field=models.DateTimeField(blank=True, help_text='NULL=长期保护', null=True, verbose_name='保护到期时间'), - ), - migrations.AlterField( - model_name='propertyprotection', - name='is_protected', - field=models.BooleanField(default=False, help_text='true=受保护(防止被他人抢单/公盘化)/false=未保护', verbose_name='是否处于保护状态'), - ), - migrations.AlterField( - model_name='propertyprotection', - name='property', - field=models.OneToOneField(help_text='1:1 关联 properties 表', on_delete=django.db.models.deletion.CASCADE, related_name='protection', to='fonrey_property.property', verbose_name='所属房源'), - ), - migrations.AlterField( - model_name='propertyprotection', - name='reason', - field=models.TextField(blank=True, default='', help_text='说明为何启用保护', verbose_name='保护原因'), - ), - migrations.AlterField( - model_name='propertyprotection', - name='set_by', - field=models.ForeignKey(blank=True, help_text='人员离职后置 NULL', null=True, on_delete=django.db.models.deletion.SET_NULL, to='org.staff', verbose_name='设置人'), - ), - migrations.AlterField( - model_name='propertyprotection', - name='start_at', - field=models.DateTimeField(blank=True, help_text='NULL=尚未生效', null=True, verbose_name='保护开始时间'), - ), - migrations.AlterField( - model_name='propertytag', - name='color', - field=models.CharField(blank=True, default='', help_text='HEX 色值,如 #FF5733;前端标签徽章颜色', max_length=7, verbose_name='显示颜色'), - ), - migrations.AlterField( - model_name='propertytag', - name='is_active', - field=models.BooleanField(default=True, help_text='false=已停用不再展示', verbose_name='是否启用'), - ), - migrations.AlterField( - model_name='propertytag', - name='is_system', - field=models.BooleanField(default=False, help_text='true=系统内置标签不可删除;false=运营自定义标签可删', verbose_name='是否系统预置'), - ), - migrations.AlterField( - model_name='propertytag', - name='name', - field=models.CharField(help_text='最多 50 字;如:学区/地铁口/满五唯一', max_length=50, verbose_name='标签名称'), - ), - migrations.AlterField( - model_name='propertytag', - name='sort_order', - field=models.IntegerField(default=0, help_text='数值越小越靠前', verbose_name='排序权重'), - ), - migrations.AlterField( - model_name='propertytagrelation', - name='property', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tag_relations', to='fonrey_property.property', verbose_name='所属房源'), - ), - migrations.AlterField( - model_name='propertytagrelation', - name='tag', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='property_relations', to='fonrey_property.propertytag', verbose_name='所属标签'), - ), - migrations.AlterField( - model_name='surveyphoto', - name='category', - field=models.CharField(choices=[('layout', '户型图'), ('living_room', '客厅'), ('dining_room', '餐厅'), ('bedroom', '卧室'), ('bathroom', '卫生间'), ('kitchen', '厨房'), ('entrance', '入户'), ('balcony', '阳台'), ('study', '书房'), ('indoor_other', '室内其他'), ('outdoor', '室外')], help_text='layout=户型图/living_room=客厅/dining_room=餐厅/bedroom=卧室/bathroom=卫生间/kitchen=厨房/entrance=门厅/balcony=阳台/study=书房/indoor_other=室内其他/outdoor=外景', max_length=20, verbose_name='照片空间分类'), - ), - migrations.AlterField( - model_name='surveyphoto', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='上传时间'), - ), - migrations.AlterField( - model_name='surveyphoto', - name='file_key', - field=models.TextField(help_text='Cloudflare R2 对象路径', verbose_name='原图存储路径'), - ), - migrations.AlterField( - model_name='surveyphoto', - name='file_size', - field=models.IntegerField(blank=True, help_text='bytes', null=True, verbose_name='文件大小'), - ), - migrations.AlterField( - model_name='surveyphoto', - name='height', - field=models.IntegerField(blank=True, help_text='像素;上传时解析', null=True, verbose_name='图片高度'), - ), - migrations.AlterField( - model_name='surveyphoto', - name='is_vr_screenshot', - field=models.BooleanField(default=False, help_text='true=全景/VR截图(区别于普通实拍照片)', verbose_name='是否为VR截图'), - ), - migrations.AlterField( - model_name='surveyphoto', - name='sort_order', - field=models.SmallIntegerField(default=0, help_text='同一空间分类内,数值越小越靠前', verbose_name='排序权重'), - ), - migrations.AlterField( - model_name='surveyphoto', - name='survey', - field=models.ForeignKey(help_text='实勘删除时联级删除', on_delete=django.db.models.deletion.CASCADE, related_name='photos', to='fonrey_property.fieldsurvey', verbose_name='所属实勘'), - ), - migrations.AlterField( - model_name='surveyphoto', - name='thumbnail_key', - field=models.TextField(blank=True, default='', help_text='Cloudflare Images 自动生成', verbose_name='缩略图路径'), - ), - migrations.AlterField( - model_name='surveyphoto', - name='width', - field=models.IntegerField(blank=True, help_text='像素;上传时解析', null=True, verbose_name='图片宽度'), - ), - ] diff --git a/apps/property/migrations/__init__.py b/apps/property/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/property/models/__init__.py b/apps/property/models/__init__.py deleted file mode 100644 index a0a6181..0000000 --- a/apps/property/models/__init__.py +++ /dev/null @@ -1,57 +0,0 @@ -from apps.property.models.core import ( - Property, - PropertyCertificate, - PropertyCompleteness, - PropertyContact, - PropertyMarketing, - PropertyProtection, -) -from apps.property.models.follow_keys import ( - FollowLog, - FollowLogAttachment, - FollowLogRecording, - KeyAttachment, - PropertyKey, -) -from apps.property.models.listings import ( - Commission, - CommissionAttachment, - ListingHistory, - NumberHolderApproval, - PriceChange, -) -from apps.property.models.media import ( - FieldSurvey, - PropertyAttachment, - PropertyFavorite, - PropertyPhoto, - PropertyTag, - PropertyTagRelation, - SurveyPhoto, -) - -__all__ = [ - "Commission", - "CommissionAttachment", - "FieldSurvey", - "FollowLog", - "FollowLogAttachment", - "FollowLogRecording", - "KeyAttachment", - "ListingHistory", - "NumberHolderApproval", - "PriceChange", - "Property", - "PropertyAttachment", - "PropertyCertificate", - "PropertyCompleteness", - "PropertyContact", - "PropertyFavorite", - "PropertyKey", - "PropertyMarketing", - "PropertyPhoto", - "PropertyProtection", - "PropertyTag", - "PropertyTagRelation", - "SurveyPhoto", -] diff --git a/apps/property/models/core.py b/apps/property/models/core.py deleted file mode 100644 index dc7df4b..0000000 --- a/apps/property/models/core.py +++ /dev/null @@ -1,862 +0,0 @@ -from django.contrib.postgres.indexes import GinIndex -from django.contrib.postgres.search import SearchVectorField -from django.db import models - -from core.enums import ( - PropertyAttribute, - PropertyContactGender, - PropertyContactIdentity, - PropertyDecoration, - PropertyGrade, - PropertyHouseStatus, - PropertyOrientation, - PropertyOwnershipNature, - PropertyPaymentMethod, - PropertyShopLocation, - PropertyStatus, - PropertyTaxIncluded, - PropertyType, - PropertyViewingTime, -) -from core.models.base import SoftDeleteModel, TimeStampedModel, UUIDPrimaryKeyModel - - -class Property(SoftDeleteModel): - property_type = models.CharField( - max_length=30, - choices=PropertyType.choices, - verbose_name="房源类型", - help_text="residential=住宅/villa=别墅/commercial_residential=商住/shop=商铺/office=写字楼/other=其他(详见 ENUMS)", - ) - status = models.CharField( - max_length=20, - choices=PropertyStatus.choices, - default=PropertyStatus.FOR_SALE, - verbose_name="交易状态", - help_text="for_sale=出售/for_rent=出租/for_sale_rent=租售/suspended=暂缓/sold_elsewhere=他售/rented_elsewhere=他租/sold=成交/unlisted=未挂牌(详见 ENUMS)", - ) - attribute = models.CharField( - max_length=10, - choices=PropertyAttribute.choices, - default=PropertyAttribute.PUBLIC, - verbose_name="流通属性", - help_text="public=公盘/private=私盘/special=特盘/sealed=封盘;控制可见范围", - ) - private_reason = models.TextField( - blank=True, - default="", - verbose_name="私盘/封盘原因", - help_text="attribute 为 private/sealed 时必填,最多 200 字", - ) - - complex = models.ForeignKey( - "fonrey_complex.Complex", - on_delete=models.RESTRICT, - related_name="properties", - verbose_name="所属楼盘", - help_text="房源必须挂在楼盘下,禁止级联删除", - ) - building = models.ForeignKey( - "fonrey_complex.Building", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="properties", - verbose_name="所属楼栋", - help_text="楼栋被删除时置 NULL", - ) - block_no = models.CharField( - max_length=30, - blank=True, - default="", - verbose_name="栋/幢/弄号", - help_text="如'3栋'、'A幢'", - ) - unit_no = models.CharField( - max_length=30, - blank=True, - default="", - verbose_name="单元号", - help_text="如'1单元'、'055'", - ) - room_no = models.CharField( - max_length=30, - blank=True, - default="", - verbose_name="房号/门牌号", - help_text="如'0301'、'1502'", - ) - floor = models.SmallIntegerField( - verbose_name="所在楼层", - help_text="正整数;不超过 total_floors(CheckConstraint 校验)", - ) - total_floors = models.SmallIntegerField( - verbose_name="楼栋总层数", - help_text="正整数", - ) - - bedroom_count = models.SmallIntegerField(default=0, verbose_name="卧室数(室)") - living_room_count = models.SmallIntegerField(default=0, verbose_name="客厅/餐厅数(厅)") - bathroom_count = models.SmallIntegerField(default=0, verbose_name="卫生间数(卫)") - kitchen_count = models.SmallIntegerField(default=0, verbose_name="厨房数(厨)") - balcony_count = models.SmallIntegerField(default=0, verbose_name="阳台数", help_text="0=无阳台") - - area = models.DecimalField( - max_digits=8, - decimal_places=2, - verbose_name="建筑面积(m²)", - help_text="含公摊;录入必填", - ) - inner_area = models.DecimalField( - max_digits=8, - decimal_places=2, - null=True, - blank=True, - verbose_name="套内面积(m²)", - help_text="不含公摊;选填,编辑页专属字段", - ) - - sale_price = models.DecimalField( - max_digits=12, - decimal_places=2, - null=True, - blank=True, - verbose_name="挂牌售价(万元)", - help_text="出售类房源必填,出租类可为 NULL", - ) - sale_bottom_price = models.DecimalField( - max_digits=12, - decimal_places=2, - null=True, - blank=True, - verbose_name="售底价(万元)", - help_text="业主心理底价,仅内部可见,不对外展示", - ) - sale_record_price = models.DecimalField( - max_digits=12, - decimal_places=2, - null=True, - blank=True, - verbose_name="备案/核验价(万元)", - help_text="填写后同步至营销库", - ) - rent_price = models.DecimalField( - max_digits=10, - decimal_places=2, - null=True, - blank=True, - verbose_name="挂牌租价(元/月)", - help_text="出租类房源使用", - ) - - orientation = models.CharField( - max_length=15, - blank=True, - default="", - choices=PropertyOrientation.choices, - verbose_name="朝向", - help_text="east=东/south=南/west=西/north=北/southeast=东南/northeast=东北/east_west=东西/south_north=南北/northwest=西北/southwest=西南", - ) - decoration = models.CharField( - max_length=10, - blank=True, - default="", - choices=PropertyDecoration.choices, - verbose_name="装修情况", - help_text="rough=毛坯/plain=清水/simple=简装/medium=中装/fine=精装/luxury=豪装", - ) - has_elevator = models.BooleanField( - null=True, - blank=True, - verbose_name="是否有电梯", - help_text="true=有/false=无/NULL=未确认", - ) - built_year = models.SmallIntegerField( - null=True, - blank=True, - verbose_name="建成年份", - help_text="如 2018;可空(老房源无记录),影响营销发房", - ) - - usage_type = models.CharField( - max_length=30, - blank=True, - default="", - verbose_name="房屋用途大类", - help_text="如:住宅/商住/商业;对应更改用途浮窗第一级下拉", - ) - usage_subtype = models.CharField( - max_length=30, - blank=True, - default="", - verbose_name="房屋用途细分小类", - help_text="如:普通住宅/花园洋房;对应更改用途浮窗第二级下拉", - ) - - shop_frontage = models.DecimalField( - max_digits=6, - decimal_places=2, - null=True, - blank=True, - verbose_name="开间(米)", - help_text="商铺专属,住宅类为 NULL", - ) - shop_depth = models.DecimalField( - max_digits=6, - decimal_places=2, - null=True, - blank=True, - verbose_name="进深(米)", - help_text="商铺专属", - ) - shop_height = models.DecimalField( - max_digits=6, - decimal_places=2, - null=True, - blank=True, - verbose_name="层高(米)", - help_text="商铺专属", - ) - shop_location = models.CharField( - max_length=20, - blank=True, - default="", - choices=PropertyShopLocation.choices, - verbose_name="商铺位置类型", - help_text="street=沿街/mall=商场内/residential=住宅底商/ground_floor=楼栋底层/complex=综合体(商铺专属)", - ) - - house_status = models.CharField( - max_length=20, - blank=True, - default="", - choices=PropertyHouseStatus.choices, - verbose_name="房屋现状", - help_text="owner_occupied=业主自住/vacant=空置/tenant_occupied=租客租住/unknown=未知;影响带看安排", - ) - viewing_time = models.CharField( - max_length=20, - blank=True, - default="", - choices=PropertyViewingTime.choices, - verbose_name="看房时间安排", - help_text="anytime=随时可看/by_appointment=提前预约/inconvenient=不方便看", - ) - - grade = models.CharField( - max_length=2, - blank=True, - default="", - choices=PropertyGrade.choices, - verbose_name="房源等级", - help_text="A=急迫/B=较强/C=一般/D=较弱(业主出售意向)", - ) - - ownership_years = models.CharField( - max_length=30, - blank=True, - default="", - verbose_name="房本年限", - help_text="不满2年/满2年/满5年 等(影响交易税费)", - ) - ownership_years_detail = models.CharField( - max_length=20, - blank=True, - default="", - verbose_name="房本年限辅助说明", - help_text="满五/不满五(与 ownership_years 组合使用)", - ) - ownership_nature = models.CharField( - max_length=20, - blank=True, - default="", - choices=PropertyOwnershipNature.choices, - verbose_name="产权性质", - help_text="commercial=商品房/reform_housing=房改房/collective=集资房/economic=经济活用房", - ) - is_only_house = models.BooleanField( - null=True, - blank=True, - verbose_name="是否唯一住房", - help_text="true=唯一/false=非唯一/NULL=未确认;影响交易税费计算", - ) - payment_method = models.CharField( - max_length=15, - blank=True, - default="", - choices=PropertyPaymentMethod.choices, - verbose_name="购房付款方式", - help_text="full=一次付清/mortgage=按揭付款/installment=分批次付款/advance=垫资解按", - ) - tax_included = models.CharField( - max_length=15, - blank=True, - default="", - choices=PropertyTaxIncluded.choices, - verbose_name="包税费方式", - help_text="each_party=各付/net=到手/inclusive=包税", - ) - has_mortgage = models.BooleanField( - null=True, - blank=True, - verbose_name="是否有抵押", - help_text="true=有/false=无/NULL=未确认", - ) - has_loan = models.BooleanField( - null=True, - blank=True, - verbose_name="是否有贷款(未还清)", - help_text="true=有/false=无/NULL=未确认", - ) - has_seal = models.BooleanField( - null=True, - blank=True, - verbose_name="是否被查封", - help_text="true=有/false=无/NULL=未确认", - ) - has_restriction = models.BooleanField( - null=True, - blank=True, - verbose_name="是否有其他限制", - help_text="true=有/false=无/NULL=未确认", - ) - original_price = models.DecimalField( - max_digits=12, - decimal_places=2, - null=True, - blank=True, - verbose_name="原购价(万元)", - help_text="业主当年购入价,用于计算增值", - ) - sale_reason = models.TextField( - blank=True, - default="", - verbose_name="售房原因", - help_text="业主出售理由,最多 200 字;如'置换'", - ) - - remarks = models.TextField( - blank=True, - default="", - verbose_name="房源备注", - help_text="经纪人内部备注,最多 500 字,不对外展示", - ) - - first_recorder = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="first_recorded_properties", - verbose_name="首录方", - help_text="最初录入该房源的经纪人;人员离职后置 NULL", - ) - number_holder = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="held_properties", - verbose_name="号码方", - help_text="持有业主联系号码的经纪人;变更需走审批流", - ) - seller_agent = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="selling_properties", - verbose_name="出售方", - help_text="负责出售跟进的经纪人", - ) - buyer_agent = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="buying_properties", - verbose_name="实买方", - help_text="促成成交的买方经纪人", - ) - - source = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="房源来源渠道", - help_text="枚举值由 lookup_items 维护,如:门店拓客/转介绍/网络等", - ) - - completeness_score = models.SmallIntegerField( - default=0, - verbose_name="维护完成度评分", - help_text="0-100;由 Celery 异步计算,非实时;前端列表页展示徽章", - ) - - listed_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="最近一次挂牌时间", - help_text="每次重新挂牌时更新", - ) - last_followed_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="最后跟进时间", - help_text="冗余字段,由触发器自动维护,加速超时未跟进排序", - ) - - created_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="created_properties", - verbose_name="创建人", - ) - updated_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="updated_properties", - verbose_name="最后修改人", - ) - - search_vector = SearchVectorField( - null=True, - blank=True, - verbose_name="全文检索向量", - help_text="由触发器自动维护,覆盖栋号/单元/房号/备注", - ) - version = models.IntegerField( - default=1, - verbose_name="乐观锁版本号", - help_text="每次 UPDATE 必须 +1;应用层检测 0 行受影响时抛 ConflictError", - ) - - class Meta: - db_table = "properties" - verbose_name = "房源" - verbose_name_plural = "房源" - constraints = [ - models.CheckConstraint( - check=models.Q(floor__gt=0) & models.Q(floor__lte=models.F("total_floors")), - name="chk_property_floor", - ), - ] - indexes = [ - GinIndex(fields=["search_vector"], name="idx_properties_search"), - models.Index(fields=["complex"], name="idx_properties_complex"), - models.Index(fields=["status"], name="idx_properties_status"), - models.Index(fields=["sale_price"], name="idx_properties_sale_price"), - models.Index(fields=["area"], name="idx_properties_area"), - models.Index(fields=["listed_at"], name="idx_properties_listed_at"), - models.Index(fields=["last_followed_at"], name="idx_properties_last_followed"), - models.Index(fields=["bedroom_count"], name="idx_properties_bedroom"), - models.Index(fields=["grade"], name="idx_properties_grade"), - models.Index(fields=["completeness_score"], name="idx_properties_completeness"), - models.Index(fields=["seller_agent"], name="idx_properties_seller_agent"), - models.Index(fields=["number_holder"], name="idx_properties_number_holder"), - models.Index( - fields=["status", "attribute", "complex", "sale_price"], - name="idx_properties_list_composite", - ), - models.Index( - fields=["seller_agent", "status", "listed_at"], - name="idx_properties_my_properties", - ), - ] - - -class PropertyContact(SoftDeleteModel): - property = models.ForeignKey( - Property, - on_delete=models.CASCADE, - related_name="contacts", - verbose_name="所属房源", - help_text="房源删除时联级删除", - ) - name = models.CharField( - max_length=50, - verbose_name="联系人姓名", - help_text="如'张先生';业主或其代理人的真实姓名", - ) - gender = models.CharField( - max_length=10, - choices=PropertyContactGender.choices, - default=PropertyContactGender.MALE, - verbose_name="性别", - help_text="male=先生/female=女士", - ) - identity = models.CharField( - max_length=20, - choices=PropertyContactIdentity.choices, - default=PropertyContactIdentity.CONTACT, - verbose_name="联系人身份", - help_text="owner=业主/contact=联系人/subletter=二房东/tenant=租客/agent=代理人/corporate=企业法人", - ) - - phone_enc = models.BinaryField( - verbose_name="手机号1密文", - help_text="AES-256-GCM 加密,不可直接查询", - ) - phone_hash = models.CharField( - max_length=64, - verbose_name="手机号1哈希", - help_text="SHA-256,用于重复房源检测和精确查询", - ) - phone2_enc = models.BinaryField( - null=True, - blank=True, - verbose_name="手机号2密文", - help_text="AES-256-GCM 加密;选填", - ) - phone2_hash = models.CharField( - max_length=64, - blank=True, - default="", - verbose_name="手机号2哈希", - help_text="SHA-256;phone2_enc 存在时必填", - ) - - wechat = models.CharField( - max_length=100, - blank=True, - default="", - verbose_name="微信号", - help_text="选填;无数据时前端展示'-'", - ) - qq = models.CharField( - max_length=20, - blank=True, - default="", - verbose_name="QQ号", - help_text="选填;无数据时前端展示'-'", - ) - remarks = models.TextField( - blank=True, - default="", - verbose_name="备注", - help_text="最多 200 字;补充说明联系人情况", - ) - - is_number_holder = models.BooleanField( - default=False, - verbose_name="是否为号码方", - help_text="true=是号码方(审批通过)/false=否;号码方变更须走审批流", - ) - number_holder_approved_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="号码方审批通过时间", - help_text="NULL=尚未成为号码方", - ) - - sort_order = models.IntegerField( - default=0, - verbose_name="排序权重", - help_text="数值越小越靠前;控制联系人在面板中的显示顺序", - ) - created_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="created_property_contacts", - verbose_name="创建人", - ) - updated_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="updated_property_contacts", - verbose_name="最后修改人", - ) - - class Meta: - db_table = "property_contacts" - verbose_name = "房源联系人" - verbose_name_plural = "房源联系人" - indexes = [ - models.Index(fields=["property"], name="idx_pc_property"), - models.Index(fields=["phone_hash"], name="idx_pc_phone_hash"), - models.Index(fields=["phone2_hash"], name="idx_pc_phone2_hash"), - ] - - -class PropertyMarketing(UUIDPrimaryKeyModel): - property = models.OneToOneField( - Property, - on_delete=models.CASCADE, - related_name="marketing", - verbose_name="所属房源", - help_text="1:1 关联 properties 表", - ) - marketing_title = models.CharField( - max_length=30, - blank=True, - default="", - verbose_name="营销标题", - help_text="0-30 字;前端发房时展示给买家的吸睛标题", - ) - core_selling_points = models.TextField( - blank=True, - default="", - verbose_name="核心卖点", - help_text="最多 200 字;展示给买家的重点卖点说明", - ) - owner_attitude = models.TextField( - blank=True, - default="", - verbose_name="业主心态", - help_text="最多 200 字;仅内部可见,描述业主议价空间和心理状态", - ) - layout_description = models.TextField( - blank=True, - default="", - verbose_name="户型介绍", - help_text="最多 200 字;房源户型特点描述,面向买家展示", - ) - complex_description = models.TextField( - blank=True, - default="", - verbose_name="小区介绍", - help_text="最多 200 字;楼盘/小区周边配套描述", - ) - - ai_generated_points = models.BooleanField( - default=False, - verbose_name="核心卖点AI生成", - help_text="true=AI辅助生成(经纪人确认后使用)", - ) - ai_generated_attitude = models.BooleanField( - default=False, - verbose_name="业主心态AI生成", - help_text="true=AI辅助生成", - ) - - updated_at = models.DateTimeField(auto_now=True, verbose_name="最后更新时间") - updated_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - verbose_name="最后修改人", - ) - - class Meta: - db_table = "property_marketing" - verbose_name = "房源营销信息" - verbose_name_plural = "房源营销信息" - - -class PropertyCertificate(UUIDPrimaryKeyModel): - property = models.OneToOneField( - Property, - on_delete=models.CASCADE, - related_name="certificate", - verbose_name="所属房源", - help_text="1:1 关联 properties 表", - ) - owner_name = models.CharField( - max_length=100, - blank=True, - default="", - verbose_name="产权人姓名", - help_text="产权证书上登记的所有权人", - ) - owner_id_number = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="产权人证件号码", - help_text="身份证号/统一社会信用代码等", - ) - owner_cert_type = models.CharField( - max_length=20, - blank=True, - default="", - verbose_name="产权人证件类型", - help_text="如:身份证/护照/营业执照", - ) - property_location = models.CharField( - max_length=500, - blank=True, - default="", - verbose_name="房屋坐落", - help_text="产权证书上的完整地址,最多 500 字", - ) - - cert_status = models.CharField( - max_length=30, - blank=True, - default="", - verbose_name="产证状态", - help_text="如:已过户/抵押中/查封/正常", - ) - cert_no = models.CharField( - max_length=100, - blank=True, - default="", - verbose_name="产权证号", - help_text="不动产权证书编号", - ) - first_registered_at = models.DateField( - null=True, - blank=True, - verbose_name="首次登记时间", - help_text="产权证上的初始登记日期", - ) - ownership_nature = models.CharField( - max_length=30, - blank=True, - default="", - verbose_name="权属性质", - help_text="如:商品房/经济适用房/回迁房", - ) - land_nature = models.CharField( - max_length=30, - blank=True, - default="", - verbose_name="土地性质", - help_text="如:国有/集体/划拨/出让", - ) - - updated_at = models.DateTimeField(auto_now=True, verbose_name="最后更新时间") - updated_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - verbose_name="最后修改人", - ) - - class Meta: - db_table = "property_certificates" - verbose_name = "房源产证" - verbose_name_plural = "房源产证" - - -class PropertyCompleteness(UUIDPrimaryKeyModel): - property = models.OneToOneField( - Property, - on_delete=models.CASCADE, - related_name="completeness", - verbose_name="所属房源", - help_text="1:1 关联 properties 表", - ) - score_core_info = models.SmallIntegerField( - default=0, - verbose_name="重点信息得分", - help_text="满分 8;包含房源核心字段完整度", - ) - score_attachment = models.SmallIntegerField( - default=0, - verbose_name="附件得分", - help_text="满分 8;身份证/产权证/委托书等材料上传情况", - ) - score_survey = models.SmallIntegerField( - default=0, - verbose_name="实勘得分", - help_text="满分 16;实勘照片和报告完整度", - ) - score_vr = models.SmallIntegerField( - default=0, - verbose_name="VR得分", - help_text="满分 8;VR/全景照片上传情况", - ) - score_key = models.SmallIntegerField( - default=0, - verbose_name="钥匙得分", - help_text="满分 10;钥匙托管情况", - ) - score_commission = models.SmallIntegerField( - default=0, - verbose_name="委托得分", - help_text="满分 10;独家/普通委托书情况", - ) - score_verification = models.SmallIntegerField( - default=0, - verbose_name="验证得分", - help_text="满分 7;房源信息核实情况", - ) - score_follow_up = models.SmallIntegerField( - default=0, - verbose_name="跟进得分", - help_text="满分 8;近期跟进记录情况", - ) - score_viewing = models.SmallIntegerField( - default=0, - verbose_name="带看得分", - help_text="满分 8;带看记录完整度", - ) - score_other = models.SmallIntegerField( - default=0, - verbose_name="其他得分", - help_text="满分 7;其他加分项", - ) - total_score = models.SmallIntegerField( - default=0, - verbose_name="维护完成度总分", - help_text="0-100;供列表排序用,与 properties.completeness_score 冗余", - ) - - calculated_at = models.DateTimeField( - auto_now=True, - verbose_name="最近计算时间", - help_text="最近一次 Celery 任务异步计算完成时间", - ) - - class Meta: - db_table = "property_completeness" - verbose_name = "房源完整度" - verbose_name_plural = "房源完整度" - - -class PropertyProtection(UUIDPrimaryKeyModel): - property = models.OneToOneField( - Property, - on_delete=models.CASCADE, - related_name="protection", - verbose_name="所属房源", - help_text="1:1 关联 properties 表", - ) - is_protected = models.BooleanField( - default=False, - verbose_name="是否处于保护状态", - help_text="true=受保护(防止被他人抢单/公盘化)/false=未保护", - ) - reason = models.TextField( - blank=True, - default="", - verbose_name="保护原因", - help_text="说明为何启用保护", - ) - start_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="保护开始时间", - help_text="NULL=尚未生效", - ) - end_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="保护到期时间", - help_text="NULL=长期保护", - ) - set_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - verbose_name="设置人", - help_text="人员离职后置 NULL", - ) - created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") - - class Meta: - db_table = "property_protections" - verbose_name = "房源保护期" - verbose_name_plural = "房源保护期" diff --git a/apps/property/models/follow_keys.py b/apps/property/models/follow_keys.py deleted file mode 100644 index 7231bc2..0000000 --- a/apps/property/models/follow_keys.py +++ /dev/null @@ -1,278 +0,0 @@ -from django.db import models - -from core.enums import ( - PropertyFollowAiTag, - PropertyFollowAttachmentFileType, - PropertyFollowLogType, - PropertyKeyType, -) -from core.models.base import UUIDPrimaryKeyModel - - -class FollowLog(models.Model): - """Partitioned table (PARTITION BY RANGE created_at). - - Managed via RunSQL; Django ORM treats parent as unmanaged. - """ - - id = models.UUIDField(primary_key=True, verbose_name="主键") - created_at = models.DateTimeField( - verbose_name="创建时间", - help_text="分区键,必须在最前声明;系统自动", - ) - property = models.ForeignKey( - "fonrey_property.Property", - on_delete=models.CASCADE, - related_name="follow_logs", - verbose_name="所属房源", - ) - - log_type = models.CharField( - max_length=30, - choices=PropertyFollowLogType.choices, - verbose_name="跟进日志类型", - help_text="written=经纪人主动写入/modified=字段变更自动生成/sensitive_op=敏感操作跟进/sensitive_view=敏感信息查看(不可删)/other=其他/system=系统日志", - ) - purpose = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="跟进目的", - help_text="枚举值由 lookup_items 维护,如:电话/业主跟进/议价/带看;仅 written 类型使用", - ) - content = models.TextField( - blank=True, - default="", - verbose_name="跟进内容", - help_text="最少 6 字,最多 500 字;仅 written 类型必填", - ) - - ai_tag = models.CharField( - max_length=20, - blank=True, - default="", - choices=PropertyFollowAiTag.choices, - verbose_name="AI 辅助标签", - help_text="ai_for_sale=AI判断业主在售/ai_not_for_sale=AI判断业主不售;由系统智能分析后打标", - ) - - change_detail = models.JSONField( - null=True, - blank=True, - verbose_name="字段变更明细", - help_text='格式:{"field": "sale_price", "old": 850, "new": 800, "label": "售价"};modified 类型使用', - ) - log_tag = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="前端展示标签", - help_text="如:查看号码/图片下载/改状态/改价格/改等级/修改相关方;对应跟进时间线显示的方括号标签", - ) - - is_public = models.BooleanField( - default=True, - verbose_name="是否公开", - help_text="true=全员可见/false=仅本人及上级可见", - ) - - operator = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - verbose_name="操作人", - help_text="人员离职后置 NULL,但 snapshot 保留", - ) - operator_snapshot = models.JSONField( - null=True, - blank=True, - verbose_name="操作人快照", - help_text="{name, role, org_unit_name, store_group};防止人员离职后丢失显示信息", - ) - - is_deletable = models.BooleanField( - default=True, - verbose_name="是否可软删除", - help_text="false=敏感信息查看类型,合规要求不可删除", - ) - deleted_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="软删除时间戳", - help_text="仅 is_deletable=TRUE 时可软删;NULL=未删除", - ) - - class Meta: - db_table = "follow_logs" - verbose_name = "房源跟进日志" - verbose_name_plural = "房源跟进日志" - managed = False - unique_together = (("id", "created_at"),) - - -class FollowLogAttachment(UUIDPrimaryKeyModel): - follow_log_id = models.UUIDField( - verbose_name="所属跟进日志ID", - help_text="跨分区外键,未通过 Django FK 强约束;日志删除时联级删除", - ) - file_key = models.TextField( - verbose_name="图片存储路径", - help_text="Cloudflare R2 对象路径", - ) - file_name = models.CharField( - max_length=255, - verbose_name="原始文件名", - help_text="用户上传时的文件名", - ) - file_size = models.IntegerField( - verbose_name="文件大小", - help_text="bytes;最大 20MB = 20971520", - ) - file_type = models.CharField( - max_length=10, - blank=True, - default="", - choices=PropertyFollowAttachmentFileType.choices, - verbose_name="文件格式", - help_text="bmp/jpg/png/svg/gif(PRD 限定格式)", - ) - sort_order = models.SmallIntegerField( - default=0, - verbose_name="排序权重", - help_text="控制同一跟进附件的显示顺序", - ) - created_at = models.DateTimeField(auto_now_add=True, verbose_name="上传时间") - - class Meta: - db_table = "follow_log_attachments" - verbose_name = "跟进附件" - verbose_name_plural = "跟进附件" - indexes = [models.Index(fields=["follow_log_id"], name="idx_fla_log")] - - -class FollowLogRecording(UUIDPrimaryKeyModel): - follow_log_id = models.UUIDField( - verbose_name="所属跟进日志ID", - help_text="跨分区外键,未通过 Django FK 强约束;日志删除时联级删除", - ) - file_key = models.TextField( - verbose_name="录音文件存储路径", - help_text="Cloudflare R2 对象路径", - ) - duration_seconds = models.IntegerField( - null=True, - blank=True, - verbose_name="录音时长", - help_text="秒;可空,上传时若能解析则填写", - ) - created_at = models.DateTimeField(auto_now_add=True, verbose_name="上传时间") - - class Meta: - db_table = "follow_log_recordings" - verbose_name = "跟进录音" - verbose_name_plural = "跟进录音" - indexes = [models.Index(fields=["follow_log_id"], name="idx_flr_log")] - - -class PropertyKey(UUIDPrimaryKeyModel): - property = models.ForeignKey( - "fonrey_property.Property", - on_delete=models.CASCADE, - related_name="keys", - verbose_name="所属房源", - ) - key_type = models.CharField( - max_length=20, - choices=PropertyKeyType.choices, - verbose_name="钥匙类型", - help_text="mechanical=机械钥匙/password=密码(如密码门锁)", - ) - - holder = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="held_keys", - verbose_name="持有人", - help_text="人员离职后置 NULL", - ) - holder_snapshot = models.JSONField( - null=True, - blank=True, - verbose_name="持有人快照", - help_text="{name, store_group};防止人员离职后丢失显示信息", - ) - storage_unit = models.ForeignKey( - "org.OrgUnit", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="stored_keys", - verbose_name="保管部门", - help_text="钥匙存放在哪个部门", - ) - - is_other_agency = models.BooleanField( - default=False, - verbose_name="是否他司钥匙", - help_text="true=是他中介公司的钥匙/false=本司钥匙", - ) - other_agency_info = models.CharField( - max_length=30, - blank=True, - default="", - verbose_name="他司中介信息", - help_text='最多 30 字;is_other_agency=true 时填写,如"链家"', - ) - remarks = models.TextField( - blank=True, - default="", - verbose_name="备注", - help_text="最多 200 字;如密码内容等补充说明", - ) - - is_active = models.BooleanField( - default=True, - verbose_name="是否有效", - help_text="true=在管中/false=已归还或失效", - ) - created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") - updated_at = models.DateTimeField(auto_now=True, verbose_name="最后更新时间") - created_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="created_property_keys", - verbose_name="创建人", - ) - - class Meta: - db_table = "property_keys" - verbose_name = "房源钥匙" - verbose_name_plural = "房源钥匙" - indexes = [models.Index(fields=["property"], name="idx_pk_property")] - - -class KeyAttachment(UUIDPrimaryKeyModel): - key = models.ForeignKey( - PropertyKey, - on_delete=models.CASCADE, - related_name="attachments", - verbose_name="所属钥匙记录", - help_text="钥匙删除时联级删除", - ) - file_key = models.TextField( - verbose_name="附件存储路径", - help_text="Cloudflare R2 对象路径", - ) - file_name = models.CharField(max_length=255, verbose_name="原始文件名") - created_at = models.DateTimeField(auto_now_add=True, verbose_name="上传时间") - - class Meta: - db_table = "key_attachments" - verbose_name = "钥匙附件" - verbose_name_plural = "钥匙附件" - indexes = [models.Index(fields=["key"], name="idx_ka_key")] diff --git a/apps/property/models/listings.py b/apps/property/models/listings.py deleted file mode 100644 index 68f3590..0000000 --- a/apps/property/models/listings.py +++ /dev/null @@ -1,445 +0,0 @@ -from django.db import models - -from core.enums import ( - PropertyCommissionAttachmentCategory, - PropertyCommissionOwnerType, - PropertyCommissionStatus, - PropertyListingHistoryStatus, - PropertyListingType, - PropertyNumberHolderApprovalStatus, -) -from core.models.base import TimeStampedModel, UUIDPrimaryKeyModel - - -class ListingHistory(UUIDPrimaryKeyModel): - property = models.ForeignKey( - "fonrey_property.Property", - on_delete=models.RESTRICT, - related_name="listing_histories", - verbose_name="所属房源", - help_text="禁止级联删除,保留历史", - ) - listing_type = models.CharField( - max_length=20, - choices=PropertyListingType.choices, - verbose_name="挂牌类型", - help_text="for_sale=出售挂牌/for_rent=出租挂牌", - ) - status = models.CharField( - max_length=10, - choices=PropertyListingHistoryStatus.choices, - default=PropertyListingHistoryStatus.ACTIVE, - verbose_name="挂牌状态", - help_text="active=挂牌中/ended=已结束", - ) - - sale_price = models.DecimalField( - max_digits=12, - decimal_places=2, - null=True, - blank=True, - verbose_name="本次挂牌售价快照", - help_text="万元;出售挂牌时记录", - ) - rent_price = models.DecimalField( - max_digits=10, - decimal_places=2, - null=True, - blank=True, - verbose_name="本次挂牌租价快照", - help_text="元/月;出租挂牌时记录", - ) - sale_unit_price = models.DecimalField( - max_digits=10, - decimal_places=2, - null=True, - blank=True, - verbose_name="本次挂牌售价单价", - help_text="元/m²;由 sale_price ÷ area 计算后存储", - ) - - ownership_years = models.CharField( - max_length=30, - blank=True, - default="", - verbose_name="房本年限快照", - help_text='本次挂牌时的房本年限,如"满2年"', - ) - is_only_house = models.BooleanField( - null=True, - blank=True, - verbose_name="唯一住房状态快照", - help_text="本次挂牌时的唯一住房状态", - ) - tax_included = models.CharField( - max_length=15, - blank=True, - default="", - verbose_name="包税费方式快照", - help_text="each_party=各付/net=到手/inclusive=包税", - ) - sale_reason = models.TextField( - blank=True, - default="", - verbose_name="售房原因快照", - help_text="本次挂牌时的售房原因", - ) - - seller_agent = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - verbose_name="出售经纪人", - help_text="本次挂牌的出售经纪人;人员离职后置 NULL,但 snapshot 保留", - ) - seller_agent_snapshot = models.JSONField( - null=True, - blank=True, - verbose_name="出售经纪人快照", - help_text="{name, store_group, org_unit_name};防止人员变动后数据丢失", - ) - - started_at = models.DateTimeField( - auto_now_add=False, - verbose_name="本次挂牌开始时间", - ) - ended_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="本次挂牌结束时间", - help_text="NULL=当前仍在挂牌中", - ) - created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") - - class Meta: - db_table = "listing_histories" - verbose_name = "挂牌历史" - verbose_name_plural = "挂牌历史" - indexes = [ - models.Index(fields=["property"], name="idx_lh_property"), - models.Index(fields=["property", "status"], name="idx_lh_active"), - ] - - -class PriceChange(UUIDPrimaryKeyModel): - property = models.ForeignKey( - "fonrey_property.Property", - on_delete=models.RESTRICT, - related_name="price_changes", - verbose_name="所属房源", - help_text="禁止级联删除,保留调价历史", - ) - old_sale_price = models.DecimalField( - max_digits=12, - decimal_places=2, - null=True, - blank=True, - verbose_name="调价前挂牌售价", - help_text="万元;NULL=首次定价", - ) - new_sale_price = models.DecimalField( - max_digits=12, - decimal_places=2, - null=True, - blank=True, - verbose_name="调价后挂牌售价", - help_text="万元", - ) - old_bottom_price = models.DecimalField( - max_digits=12, - decimal_places=2, - null=True, - blank=True, - verbose_name="调价前售底价", - help_text="万元;NULL=未设置", - ) - new_bottom_price = models.DecimalField( - max_digits=12, - decimal_places=2, - null=True, - blank=True, - verbose_name="调价后售底价", - help_text="万元;NULL=本次不变更底价", - ) - old_record_price = models.DecimalField( - max_digits=12, - decimal_places=2, - null=True, - blank=True, - verbose_name="调价前备案/核验价", - help_text="万元;NULL=未设置", - ) - new_record_price = models.DecimalField( - max_digits=12, - decimal_places=2, - null=True, - blank=True, - verbose_name="调价后备案/核验价", - help_text="万元;NULL=本次不变更", - ) - old_rent_price = models.DecimalField( - max_digits=10, - decimal_places=2, - null=True, - blank=True, - verbose_name="调价前挂牌租价", - help_text="元/月;NULL=非出租类或未设置", - ) - new_rent_price = models.DecimalField( - max_digits=10, - decimal_places=2, - null=True, - blank=True, - verbose_name="调价后挂牌租价", - help_text="元/月", - ) - - change_reason = models.TextField( - verbose_name="调价原因", - help_text='必填,最多 200 字;如"业主主动降价"', - ) - changed_at = models.DateTimeField(auto_now_add=True, verbose_name="调价操作时间") - changed_by = models.ForeignKey( - "org.Staff", - on_delete=models.RESTRICT, - verbose_name="操作人", - help_text="禁止置 NULL,保留审计追溯", - ) - - class Meta: - db_table = "price_changes" - verbose_name = "调价记录" - verbose_name_plural = "调价记录" - indexes = [ - models.Index(fields=["property"], name="idx_pchg_property"), - models.Index(fields=["property", "-changed_at"], name="idx_pchg_time"), - ] - - -class Commission(TimeStampedModel): - property = models.ForeignKey( - "fonrey_property.Property", - on_delete=models.CASCADE, - related_name="commissions", - verbose_name="所属房源", - ) - commission_type = models.CharField( - max_length=50, - verbose_name="委托类型", - help_text="独家委托/非独家委托;由 lookup_items 维护", - ) - period_start = models.DateField(verbose_name="委托开始日期") - period_end = models.DateField( - null=True, - blank=True, - verbose_name="委托结束日期", - help_text="is_open_ended=true 时为 NULL", - ) - is_open_ended = models.BooleanField( - default=False, - verbose_name="是否无固定结束日期", - help_text="true=长期委托/false=有截止日期", - ) - - agent = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="commissions_as_agent", - verbose_name="委托经纪人", - help_text="人员离职后置 NULL", - ) - agent_snapshot = models.JSONField( - null=True, - blank=True, - verbose_name="经纪人快照", - help_text="{name, store_group};防止人员变动后数据丢失", - ) - - signing_method = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="签约方式", - help_text="选择后动态展示委托书模板", - ) - - owner_type = models.CharField( - max_length=20, - choices=PropertyCommissionOwnerType.choices, - default=PropertyCommissionOwnerType.OWNER, - verbose_name="委托人类型", - help_text="owner=产权人本人/authorized_third=被授权第三方", - ) - property_owner_contact = models.ForeignKey( - "fonrey_property.PropertyContact", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="commissions", - verbose_name="关联联系人", - help_text="若委托人已录入联系人则关联,否则填写下方姓名/证件", - ) - owner_name = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="委托人姓名", - ) - owner_id_type = models.CharField( - max_length=20, - blank=True, - default="", - verbose_name="委托人证件类型", - help_text="如:身份证/护照", - ) - owner_id_number = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="委托人证件号明文", - help_text="仅供参考;加密版本见 owner_id_number_enc", - ) - owner_id_number_enc = models.BinaryField( - null=True, - blank=True, - verbose_name="委托人证件号密文", - help_text="AES-256-GCM 加密", - ) - - remarks = models.TextField( - blank=True, - default="", - verbose_name="备注", - help_text="最多 200 字", - ) - - status = models.CharField( - max_length=20, - choices=PropertyCommissionStatus.choices, - default=PropertyCommissionStatus.ACTIVE, - verbose_name="委托状态", - help_text="active=有效/expired=已过期/cancelled=已取消", - ) - - created_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="created_commissions", - verbose_name="创建人", - ) - - class Meta: - db_table = "commissions" - verbose_name = "委托管理" - verbose_name_plural = "委托管理" - indexes = [ - models.Index(fields=["property"], name="idx_commissions_property"), - models.Index(fields=["property", "status"], name="idx_commissions_active"), - ] - - -class CommissionAttachment(UUIDPrimaryKeyModel): - commission = models.ForeignKey( - Commission, - on_delete=models.CASCADE, - related_name="attachments", - verbose_name="所属委托", - help_text="委托删除时联级删除", - ) - category = models.CharField( - max_length=20, - choices=PropertyCommissionAttachmentCategory.choices, - verbose_name="附件分类", - help_text="id_card=身份证/property_cert=产权证书/commission_letter=委托书/other=其他材料", - ) - file_key = models.TextField( - verbose_name="附件存储路径", - help_text="Cloudflare R2 对象路径", - ) - file_name = models.CharField(max_length=255, verbose_name="原始文件名") - file_size = models.IntegerField( - null=True, - blank=True, - verbose_name="文件大小", - help_text="bytes", - ) - sort_order = models.SmallIntegerField( - default=0, - verbose_name="排序权重", - help_text="数值越小越靠前", - ) - created_at = models.DateTimeField(auto_now_add=True, verbose_name="上传时间") - - class Meta: - db_table = "commission_attachments" - verbose_name = "委托附件" - verbose_name_plural = "委托附件" - indexes = [models.Index(fields=["commission"], name="idx_ca_commission")] - - -class NumberHolderApproval(UUIDPrimaryKeyModel): - property = models.ForeignKey( - "fonrey_property.Property", - on_delete=models.CASCADE, - related_name="number_holder_approvals", - verbose_name="所属房源", - ) - contact = models.ForeignKey( - "fonrey_property.PropertyContact", - on_delete=models.CASCADE, - related_name="number_holder_approvals", - verbose_name="申请变更的联系方", - help_text="即号码方候选联系人", - ) - - applicant = models.ForeignKey( - "org.Staff", - on_delete=models.RESTRICT, - related_name="nh_applications", - verbose_name="申请人", - help_text="提交号码方变更申请的经纪人;禁止置 NULL 保留审计", - ) - approver = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="nh_approvals", - verbose_name="审批人", - help_text="上级审批人;审批前为 NULL", - ) - - status = models.CharField( - max_length=20, - choices=PropertyNumberHolderApprovalStatus.choices, - default=PropertyNumberHolderApprovalStatus.PENDING, - verbose_name="审批状态", - help_text="pending=待审批/approved=已通过/rejected=已驳回", - ) - remarks = models.TextField( - blank=True, - default="", - verbose_name="审批备注", - help_text="审批人填写的意见或驳回原因", - ) - created_at = models.DateTimeField(auto_now_add=True, verbose_name="申请提交时间") - decided_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="审批决定时间", - help_text="NULL=尚未审批", - ) - - class Meta: - db_table = "number_holder_approvals" - verbose_name = "号码方审批" - verbose_name_plural = "号码方审批" - indexes = [ - models.Index(fields=["status"], name="idx_nha_status"), - models.Index(fields=["property"], name="idx_nha_property"), - ] diff --git a/apps/property/models/media.py b/apps/property/models/media.py deleted file mode 100644 index 8504167..0000000 --- a/apps/property/models/media.py +++ /dev/null @@ -1,374 +0,0 @@ -from django.db import models - -from core.enums import ( - PropertyAttachmentCategory, - PropertyFieldSurveyStatus, - PropertyPhotoCategory, - PropertySurveyPhotoCategory, -) -from core.models.base import UUIDPrimaryKeyModel - - -class FieldSurvey(UUIDPrimaryKeyModel): - property = models.ForeignKey( - "fonrey_property.Property", - on_delete=models.CASCADE, - related_name="field_surveys", - verbose_name="所属房源", - ) - status = models.CharField( - max_length=10, - choices=PropertyFieldSurveyStatus.choices, - default=PropertyFieldSurveyStatus.DRAFT, - verbose_name="实勘状态", - help_text="draft=草稿(未提交)/submitted=已提交(已完成)", - ) - - gps_latitude = models.DecimalField( - max_digits=10, - decimal_places=7, - null=True, - blank=True, - verbose_name="GPS 纬度", - help_text="实勘打卡位置;精度 7 位小数", - ) - gps_longitude = models.DecimalField( - max_digits=10, - decimal_places=7, - null=True, - blank=True, - verbose_name="GPS 经度", - help_text="实勘打卡位置;精度 7 位小数", - ) - gps_accuracy = models.DecimalField( - max_digits=6, - decimal_places=2, - null=True, - blank=True, - verbose_name="GPS 精度", - help_text="米;标注定位误差", - ) - - description = models.TextField( - blank=True, - default="", - verbose_name="实勘说明", - help_text="最多 200 字;经纪人现场情况描述", - ) - - submitted_at = models.DateTimeField( - null=True, - blank=True, - verbose_name="提交时间", - help_text="status 变为 submitted 时记录;NULL=尚未提交", - ) - created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") - updated_at = models.DateTimeField(auto_now=True, verbose_name="最后更新时间") - created_by = models.ForeignKey( - "org.Staff", - on_delete=models.RESTRICT, - verbose_name="实勘人", - help_text="禁止置 NULL 保留审计", - ) - - class Meta: - db_table = "field_surveys" - verbose_name = "实勘记录" - verbose_name_plural = "实勘记录" - indexes = [ - models.Index(fields=["property"], name="idx_fs_property"), - models.Index(fields=["property", "status"], name="idx_fs_submitted"), - ] - - -class SurveyPhoto(UUIDPrimaryKeyModel): - survey = models.ForeignKey( - FieldSurvey, - on_delete=models.CASCADE, - related_name="photos", - verbose_name="所属实勘", - help_text="实勘删除时联级删除", - ) - category = models.CharField( - max_length=20, - choices=PropertySurveyPhotoCategory.choices, - verbose_name="照片空间分类", - help_text="layout=户型图/living_room=客厅/dining_room=餐厅/bedroom=卧室/bathroom=卫生间/kitchen=厨房/entrance=门厅/balcony=阳台/study=书房/indoor_other=室内其他/outdoor=外景", - ) - file_key = models.TextField( - verbose_name="原图存储路径", - help_text="Cloudflare R2 对象路径", - ) - thumbnail_key = models.TextField( - blank=True, - default="", - verbose_name="缩略图路径", - help_text="Cloudflare Images 自动生成", - ) - file_size = models.IntegerField( - null=True, - blank=True, - verbose_name="文件大小", - help_text="bytes", - ) - width = models.IntegerField( - null=True, - blank=True, - verbose_name="图片宽度", - help_text="像素;上传时解析", - ) - height = models.IntegerField( - null=True, - blank=True, - verbose_name="图片高度", - help_text="像素;上传时解析", - ) - sort_order = models.SmallIntegerField( - default=0, - verbose_name="排序权重", - help_text="同一空间分类内,数值越小越靠前", - ) - is_vr_screenshot = models.BooleanField( - default=False, - verbose_name="是否为VR截图", - help_text="true=全景/VR截图(区别于普通实拍照片)", - ) - created_at = models.DateTimeField(auto_now_add=True, verbose_name="上传时间") - - class Meta: - db_table = "survey_photos" - verbose_name = "实勘照片" - verbose_name_plural = "实勘照片" - indexes = [ - models.Index(fields=["survey"], name="idx_sp_survey"), - models.Index(fields=["survey", "category"], name="idx_sp_category"), - ] - - -class PropertyPhoto(models.Model): - """Partitioned table (PARTITION BY RANGE created_at). - - Managed via RunSQL; Django ORM treats parent as unmanaged. - """ - - id = models.UUIDField(primary_key=True, verbose_name="主键") - created_at = models.DateTimeField( - verbose_name="上传时间", - help_text="分区键;系统自动", - ) - property = models.ForeignKey( - "fonrey_property.Property", - on_delete=models.CASCADE, - related_name="photos", - verbose_name="所属房源", - ) - - category = models.CharField( - max_length=20, - choices=PropertyPhotoCategory.choices, - verbose_name="照片分类", - help_text="cover=封面/entrance=门厅/living_room=客厅/dining_room=餐厅/bedroom=卧室/bathroom=卫生间/kitchen=厨房/balcony=阳台/study=书房/indoor_other=室内其他/outdoor=外景/panorama=全景", - ) - file_key = models.TextField( - verbose_name="原图存储路径", - help_text="Cloudflare R2/S3 对象路径", - ) - thumbnail_key = models.TextField( - blank=True, - default="", - verbose_name="缩略图路径", - help_text="Cloudflare Images 自动生成", - ) - file_name = models.CharField( - max_length=255, - blank=True, - default="", - verbose_name="原始文件名", - ) - file_size = models.IntegerField( - null=True, - blank=True, - verbose_name="文件大小", - help_text="bytes", - ) - width = models.IntegerField( - null=True, - blank=True, - verbose_name="图片宽度", - help_text="像素;上传时解析", - ) - height = models.IntegerField( - null=True, - blank=True, - verbose_name="图片高度", - help_text="像素;上传时解析", - ) - - is_cover = models.BooleanField( - default=False, - verbose_name="是否为封面图", - help_text="true=封面;每套房源只能有一张封面(唯一约束保证)", - ) - sort_order = models.SmallIntegerField( - default=0, - verbose_name="排序权重", - help_text="同一房源内,数值越小越靠前", - ) - - updated_at = models.DateTimeField(auto_now=True, verbose_name="最后更新时间") - created_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - verbose_name="上传人", - ) - - class Meta: - db_table = "property_photos" - verbose_name = "房源图片" - verbose_name_plural = "房源图片" - managed = False - unique_together = (("id", "created_at"),) - - -class PropertyAttachment(UUIDPrimaryKeyModel): - property = models.ForeignKey( - "fonrey_property.Property", - on_delete=models.CASCADE, - related_name="attachments", - verbose_name="所属房源", - ) - category = models.CharField( - max_length=20, - choices=PropertyAttachmentCategory.choices, - default=PropertyAttachmentCategory.OTHER, - verbose_name="附件分类", - help_text="id_card=身份证/property_cert=产权证书/commission_letter=委托书/other=其他材料", - ) - file_key = models.TextField( - verbose_name="附件存储路径", - help_text="Cloudflare R2 对象路径", - ) - file_name = models.CharField(max_length=255, verbose_name="原始文件名") - file_size = models.IntegerField( - verbose_name="文件大小", - help_text="bytes", - ) - file_type = models.CharField( - max_length=50, - blank=True, - default="", - verbose_name="MIME 类型", - help_text="如 application/pdf、image/jpeg", - ) - sort_order = models.SmallIntegerField( - default=0, - verbose_name="排序权重", - help_text="控制同一房源附件的显示顺序", - ) - created_at = models.DateTimeField(auto_now_add=True, verbose_name="上传时间") - created_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - verbose_name="上传人", - ) - - class Meta: - db_table = "property_attachments" - verbose_name = "房源附件" - verbose_name_plural = "房源附件" - indexes = [ - models.Index(fields=["property"], name="idx_pa_property"), - models.Index(fields=["property", "category"], name="idx_pa_category"), - ] - - -class PropertyTag(UUIDPrimaryKeyModel): - name = models.CharField( - max_length=50, - verbose_name="标签名称", - help_text="最多 50 字;如:学区/地铁口/满五唯一", - ) - color = models.CharField( - max_length=7, - blank=True, - default="", - verbose_name="显示颜色", - help_text="HEX 色值,如 #FF5733;前端标签徽章颜色", - ) - is_system = models.BooleanField( - default=False, - verbose_name="是否系统预置", - help_text="true=系统内置标签不可删除;false=运营自定义标签可删", - ) - sort_order = models.IntegerField( - default=0, - verbose_name="排序权重", - help_text="数值越小越靠前", - ) - is_active = models.BooleanField( - default=True, - verbose_name="是否启用", - help_text="false=已停用不再展示", - ) - - class Meta: - db_table = "property_tags" - verbose_name = "房源标签" - verbose_name_plural = "房源标签" - - -class PropertyTagRelation(models.Model): - property = models.ForeignKey( - "fonrey_property.Property", - on_delete=models.CASCADE, - related_name="tag_relations", - verbose_name="所属房源", - ) - tag = models.ForeignKey( - PropertyTag, - on_delete=models.CASCADE, - related_name="property_relations", - verbose_name="所属标签", - ) - - class Meta: - db_table = "property_tag_relations" - verbose_name = "房源标签关联" - verbose_name_plural = "房源标签关联" - constraints = [ - models.UniqueConstraint(fields=["property", "tag"], name="uq_ptr_property_tag"), - ] - indexes = [ - models.Index(fields=["property"], name="idx_ptr_property"), - models.Index(fields=["tag"], name="idx_ptr_tag"), - ] - - -class PropertyFavorite(models.Model): - staff = models.ForeignKey( - "org.Staff", - on_delete=models.CASCADE, - related_name="favorite_properties", - verbose_name="收藏人", - help_text="员工注销时删除收藏记录", - ) - property = models.ForeignKey( - "fonrey_property.Property", - on_delete=models.CASCADE, - related_name="favorited_by", - verbose_name="收藏的房源", - ) - created_at = models.DateTimeField(auto_now_add=True, verbose_name="收藏时间") - - class Meta: - db_table = "property_favorites" - verbose_name = "房源收藏" - verbose_name_plural = "房源收藏" - constraints = [ - models.UniqueConstraint(fields=["staff", "property"], name="uq_pfav_staff_property"), - ] - indexes = [models.Index(fields=["staff"], name="idx_pfav_staff")] diff --git a/apps/property/serializers.py b/apps/property/serializers.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/property/services/__init__.py b/apps/property/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/property/tasks.py b/apps/property/tasks.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/property/templates/property/.gitkeep b/apps/property/templates/property/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/apps/property/tests/__init__.py b/apps/property/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/property/urls.py b/apps/property/urls.py deleted file mode 100644 index c2e559d..0000000 --- a/apps/property/urls.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.urls import path - -app_name = "property" - -urlpatterns: list = [] diff --git a/apps/property/views.py b/apps/property/views.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/region/__init__.py b/apps/region/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/region/admin.py b/apps/region/admin.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/region/apps.py b/apps/region/apps.py deleted file mode 100644 index 4cbd8bf..0000000 --- a/apps/region/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class RegionConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.region" - label = "region" diff --git a/apps/region/migrations/0001_initial.py b/apps/region/migrations/0001_initial.py deleted file mode 100644 index e929c9b..0000000 --- a/apps/region/migrations/0001_initial.py +++ /dev/null @@ -1,128 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-29 08:57 - -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='BusinessArea', - 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(max_length=100)), - ('sort_order', models.IntegerField(default=0)), - ('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)), - ('is_active', models.BooleanField(default=True)), - ], - options={ - 'db_table': 'business_areas', - 'ordering': ['district_id', 'sort_order', 'name'], - }, - ), - migrations.CreateModel( - name='District', - 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)), - ('city', models.CharField(max_length=50)), - ('name', models.CharField(max_length=50)), - ('short_name', models.CharField(blank=True, default='', max_length=20)), - ('sort_order', models.IntegerField(default=0)), - ('is_active', models.BooleanField(default=True)), - ], - options={ - 'db_table': 'districts', - 'ordering': ['city', 'sort_order', 'name'], - }, - ), - migrations.CreateModel( - name='MetroLine', - 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)), - ('city', models.CharField(max_length=50)), - ('name', models.CharField(max_length=50)), - ('color', models.CharField(blank=True, default='', max_length=7)), - ('sort_order', models.IntegerField(default=0)), - ('is_active', models.BooleanField(default=True)), - ], - options={ - 'db_table': 'metro_lines', - 'ordering': ['city', 'sort_order', 'name'], - }, - ), - migrations.CreateModel( - name='School', - 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(max_length=100)), - ('type', models.CharField(blank=True, choices=[('primary', '小学'), ('middle', '初中'), ('high', '高中'), ('k9', '九年一贯制'), ('k12', '十二年一贯制')], default='', max_length=20)), - ('nature', models.CharField(blank=True, choices=[('public', '公立'), ('private', '私立'), ('international', '国际')], default='', max_length=20)), - ('level', models.CharField(blank=True, choices=[('normal', '普通'), ('key', '重点'), ('top', '名校')], default='', max_length=20)), - ('is_active', models.BooleanField(default=True)), - ('district', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='schools', to='region.district')), - ], - options={ - 'db_table': 'schools', - 'ordering': ['name'], - }, - ), - migrations.CreateModel( - name='MetroStation', - 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(max_length=50)), - ('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)), - ('sort_order', models.IntegerField(default=0)), - ('is_active', models.BooleanField(default=True)), - ('metro_line', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stations', to='region.metroline')), - ], - options={ - 'db_table': 'metro_stations', - 'ordering': ['metro_line_id', 'sort_order'], - }, - ), - migrations.AddConstraint( - model_name='district', - constraint=models.UniqueConstraint(condition=models.Q(('is_active', True)), fields=('city', 'name'), name='uq_districts_city_name'), - ), - migrations.AddField( - model_name='businessarea', - name='district', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='business_areas', to='region.district'), - ), - migrations.AddIndex( - model_name='school', - index=models.Index(condition=models.Q(('is_active', True)), fields=['district'], name='idx_schools_district'), - ), - migrations.AddIndex( - model_name='metrostation', - index=models.Index(condition=models.Q(('is_active', True)), fields=['metro_line'], name='idx_metro_stations_line'), - ), - migrations.AddIndex( - model_name='businessarea', - index=models.Index(condition=models.Q(('is_active', True)), fields=['district'], name='idx_business_areas_district'), - ), - migrations.AddConstraint( - model_name='businessarea', - constraint=models.UniqueConstraint(fields=('district', 'name'), name='uq_business_areas_name'), - ), - ] diff --git a/apps/region/migrations/0002_alter_businessarea_options_alter_district_options_and_more.py b/apps/region/migrations/0002_alter_businessarea_options_alter_district_options_and_more.py deleted file mode 100644 index 220c039..0000000 --- a/apps/region/migrations/0002_alter_businessarea_options_alter_district_options_and_more.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-29 11:07 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('region', '0001_initial'), - ] - - operations = [ - migrations.AlterModelOptions( - name='businessarea', - options={'ordering': ['district_id', 'sort_order', 'name'], 'verbose_name': '商圈', 'verbose_name_plural': '商圈'}, - ), - migrations.AlterModelOptions( - name='district', - options={'ordering': ['city', 'sort_order', 'name'], 'verbose_name': '城区', 'verbose_name_plural': '城区'}, - ), - migrations.AlterModelOptions( - name='metroline', - options={'ordering': ['city', 'sort_order', 'name'], 'verbose_name': '地铁线路', 'verbose_name_plural': '地铁线路'}, - ), - migrations.AlterModelOptions( - name='metrostation', - options={'ordering': ['metro_line_id', 'sort_order'], 'verbose_name': '地铁站点', 'verbose_name_plural': '地铁站点'}, - ), - migrations.AlterModelOptions( - name='school', - options={'ordering': ['name'], 'verbose_name': '学校', 'verbose_name_plural': '学校'}, - ), - ] diff --git a/apps/region/migrations/0003_alter_businessarea_district_and_more.py b/apps/region/migrations/0003_alter_businessarea_district_and_more.py deleted file mode 100644 index cb0b820..0000000 --- a/apps/region/migrations/0003_alter_businessarea_district_and_more.py +++ /dev/null @@ -1,154 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-30 01:46 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('region', '0002_alter_businessarea_options_alter_district_options_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='businessarea', - name='district', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='business_areas', to='region.district', verbose_name='所属城区'), - ), - migrations.AlterField( - model_name='businessarea', - name='is_active', - field=models.BooleanField(default=True, help_text='False=已停用,不在筛选项中展示', verbose_name='是否启用'), - ), - migrations.AlterField( - model_name='businessarea', - name='latitude', - field=models.DecimalField(blank=True, decimal_places=7, max_digits=10, null=True, verbose_name='商圈中心纬度'), - ), - migrations.AlterField( - model_name='businessarea', - name='longitude', - field=models.DecimalField(blank=True, decimal_places=7, max_digits=10, null=True, verbose_name='商圈中心经度'), - ), - migrations.AlterField( - model_name='businessarea', - name='name', - field=models.CharField(max_length=100, verbose_name='商圈名称'), - ), - migrations.AlterField( - model_name='businessarea', - name='sort_order', - field=models.IntegerField(default=0, verbose_name='排序'), - ), - migrations.AlterField( - model_name='district', - name='city', - field=models.CharField(help_text='支持多城市扩展,如「上海」「北京」', max_length=50, verbose_name='所属城市'), - ), - migrations.AlterField( - model_name='district', - name='is_active', - field=models.BooleanField(default=True, help_text='False=已停用,不在筛选项中展示', verbose_name='是否启用'), - ), - migrations.AlterField( - model_name='district', - name='name', - field=models.CharField(help_text='如「静安区」', max_length=50, verbose_name='行政区名称'), - ), - migrations.AlterField( - model_name='district', - name='short_name', - field=models.CharField(blank=True, default='', help_text='如「静安」', max_length=20, verbose_name='简称'), - ), - migrations.AlterField( - model_name='district', - name='sort_order', - field=models.IntegerField(default=0, help_text='列表展示排序', verbose_name='排序'), - ), - migrations.AlterField( - model_name='metroline', - name='city', - field=models.CharField(max_length=50, verbose_name='所属城市'), - ), - migrations.AlterField( - model_name='metroline', - name='color', - field=models.CharField(blank=True, default='', help_text='HEX 色值,如 #E3002B', max_length=7, verbose_name='线路颜色'), - ), - migrations.AlterField( - model_name='metroline', - name='is_active', - field=models.BooleanField(default=True, help_text='False=已停用,不在筛选项中展示', verbose_name='是否启用'), - ), - migrations.AlterField( - model_name='metroline', - name='name', - field=models.CharField(help_text='如「1号线」', max_length=50, verbose_name='线路名称'), - ), - migrations.AlterField( - model_name='metroline', - name='sort_order', - field=models.IntegerField(default=0, verbose_name='排序'), - ), - migrations.AlterField( - model_name='metrostation', - name='is_active', - field=models.BooleanField(default=True, help_text='False=已停用,不在筛选项中展示', verbose_name='是否启用'), - ), - migrations.AlterField( - model_name='metrostation', - name='latitude', - field=models.DecimalField(blank=True, decimal_places=7, max_digits=10, null=True, verbose_name='站点纬度'), - ), - migrations.AlterField( - model_name='metrostation', - name='longitude', - field=models.DecimalField(blank=True, decimal_places=7, max_digits=10, null=True, verbose_name='站点经度'), - ), - migrations.AlterField( - model_name='metrostation', - name='metro_line', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stations', to='region.metroline', verbose_name='所属线路'), - ), - migrations.AlterField( - model_name='metrostation', - name='name', - field=models.CharField(max_length=50, verbose_name='站名'), - ), - migrations.AlterField( - model_name='metrostation', - name='sort_order', - field=models.IntegerField(default=0, verbose_name='沿线排序'), - ), - migrations.AlterField( - model_name='school', - name='district', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='schools', to='region.district', verbose_name='所属城区'), - ), - migrations.AlterField( - model_name='school', - name='is_active', - field=models.BooleanField(default=True, help_text='False=已停用,不在筛选项中展示', verbose_name='是否启用'), - ), - migrations.AlterField( - model_name='school', - name='level', - field=models.CharField(blank=True, choices=[('normal', '普通'), ('key', '重点'), ('top', '名校')], default='', help_text='normal=普通 / key=重点 / top=名校', max_length=20, verbose_name='学校等级'), - ), - migrations.AlterField( - model_name='school', - name='name', - field=models.CharField(max_length=100, verbose_name='学校名称'), - ), - migrations.AlterField( - model_name='school', - name='nature', - field=models.CharField(blank=True, choices=[('public', '公立'), ('private', '私立'), ('international', '国际')], default='', help_text='public=公立 / private=私立 / international=国际学校', max_length=20, verbose_name='学校性质'), - ), - migrations.AlterField( - model_name='school', - name='type', - field=models.CharField(blank=True, choices=[('primary', '小学'), ('middle', '初中'), ('high', '高中'), ('k9', '九年一贯制'), ('k12', '十二年一贯制')], default='', help_text='primary=小学 / middle=初中 / high=高中 / k9=九年一贯制 / k12=十二年一贯制', max_length=20, verbose_name='学校类型'), - ), - ] diff --git a/apps/region/migrations/__init__.py b/apps/region/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/region/models/__init__.py b/apps/region/models/__init__.py deleted file mode 100644 index 2067f08..0000000 --- a/apps/region/models/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -from apps.region.models.region import ( - BusinessArea, - District, - MetroLine, - MetroStation, - School, -) - -__all__ = [ - "BusinessArea", - "District", - "MetroLine", - "MetroStation", - "School", -] diff --git a/apps/region/models/region.py b/apps/region/models/region.py deleted file mode 100644 index 0550013..0000000 --- a/apps/region/models/region.py +++ /dev/null @@ -1,257 +0,0 @@ -from django.db import models - -from core.enums import SchoolLevel, SchoolNature, SchoolType -from core.models.base import TimeStampedModel - - -class District(TimeStampedModel): - city = models.CharField( - max_length=50, - verbose_name="所属城市", - help_text="支持多城市扩展,如「上海」「北京」", - ) - name = models.CharField( - max_length=50, - verbose_name="行政区名称", - help_text="如「静安区」", - ) - short_name = models.CharField( - max_length=20, - blank=True, - default="", - verbose_name="简称", - help_text="如「静安」", - ) - sort_order = models.IntegerField( - default=0, - verbose_name="排序", - help_text="列表展示排序", - ) - is_active = models.BooleanField( - default=True, - verbose_name="是否启用", - help_text="False=已停用,不在筛选项中展示", - ) - - class Meta: - db_table = "districts" - verbose_name = "城区" - verbose_name_plural = "城区" - constraints = [ - models.UniqueConstraint( - fields=["city", "name"], - condition=models.Q(is_active=True), - name="uq_districts_city_name", - ), - ] - ordering = ["city", "sort_order", "name"] - - def __str__(self) -> str: - return f"{self.city} / {self.name}" - - -class BusinessArea(TimeStampedModel): - district = models.ForeignKey( - "region.District", - on_delete=models.PROTECT, - related_name="business_areas", - verbose_name="所属城区", - ) - name = models.CharField( - max_length=100, - verbose_name="商圈名称", - ) - sort_order = models.IntegerField( - default=0, - verbose_name="排序", - ) - latitude = models.DecimalField( - max_digits=10, - decimal_places=7, - null=True, - blank=True, - verbose_name="商圈中心纬度", - ) - longitude = models.DecimalField( - max_digits=10, - decimal_places=7, - null=True, - blank=True, - verbose_name="商圈中心经度", - ) - is_active = models.BooleanField( - default=True, - verbose_name="是否启用", - help_text="False=已停用,不在筛选项中展示", - ) - - class Meta: - db_table = "business_areas" - verbose_name = "商圈" - verbose_name_plural = "商圈" - constraints = [ - models.UniqueConstraint( - fields=["district", "name"], - name="uq_business_areas_name", - ), - ] - indexes = [ - models.Index( - fields=["district"], - name="idx_business_areas_district", - condition=models.Q(is_active=True), - ), - ] - ordering = ["district_id", "sort_order", "name"] - - def __str__(self) -> str: - return self.name - - -class MetroLine(TimeStampedModel): - city = models.CharField( - max_length=50, - verbose_name="所属城市", - ) - name = models.CharField( - max_length=50, - verbose_name="线路名称", - help_text="如「1号线」", - ) - color = models.CharField( - max_length=7, - blank=True, - default="", - verbose_name="线路颜色", - help_text="HEX 色值,如 #E3002B", - ) - sort_order = models.IntegerField( - default=0, - verbose_name="排序", - ) - is_active = models.BooleanField( - default=True, - verbose_name="是否启用", - help_text="False=已停用,不在筛选项中展示", - ) - - class Meta: - db_table = "metro_lines" - verbose_name = "地铁线路" - verbose_name_plural = "地铁线路" - ordering = ["city", "sort_order", "name"] - - def __str__(self) -> str: - return f"{self.city} {self.name}" - - -class MetroStation(TimeStampedModel): - metro_line = models.ForeignKey( - "region.MetroLine", - on_delete=models.CASCADE, - related_name="stations", - verbose_name="所属线路", - ) - name = models.CharField( - max_length=50, - verbose_name="站名", - ) - latitude = models.DecimalField( - max_digits=10, - decimal_places=7, - null=True, - blank=True, - verbose_name="站点纬度", - ) - longitude = models.DecimalField( - max_digits=10, - decimal_places=7, - null=True, - blank=True, - verbose_name="站点经度", - ) - sort_order = models.IntegerField( - default=0, - verbose_name="沿线排序", - ) - is_active = models.BooleanField( - default=True, - verbose_name="是否启用", - help_text="False=已停用,不在筛选项中展示", - ) - - class Meta: - db_table = "metro_stations" - verbose_name = "地铁站点" - verbose_name_plural = "地铁站点" - indexes = [ - models.Index( - fields=["metro_line"], - name="idx_metro_stations_line", - condition=models.Q(is_active=True), - ), - ] - ordering = ["metro_line_id", "sort_order"] - - def __str__(self) -> str: - return self.name - - -class School(TimeStampedModel): - district = models.ForeignKey( - "region.District", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="schools", - verbose_name="所属城区", - ) - name = models.CharField( - max_length=100, - verbose_name="学校名称", - ) - type = models.CharField( - max_length=20, - blank=True, - default="", - choices=SchoolType.choices, - verbose_name="学校类型", - help_text="primary=小学 / middle=初中 / high=高中 / k9=九年一贯制 / k12=十二年一贯制", - ) - nature = models.CharField( - max_length=20, - blank=True, - default="", - choices=SchoolNature.choices, - verbose_name="学校性质", - help_text="public=公立 / private=私立 / international=国际学校", - ) - level = models.CharField( - max_length=20, - blank=True, - default="", - choices=SchoolLevel.choices, - verbose_name="学校等级", - help_text="normal=普通 / key=重点 / top=名校", - ) - is_active = models.BooleanField( - default=True, - verbose_name="是否启用", - help_text="False=已停用,不在筛选项中展示", - ) - - class Meta: - db_table = "schools" - verbose_name = "学校" - verbose_name_plural = "学校" - indexes = [ - models.Index( - fields=["district"], - name="idx_schools_district", - condition=models.Q(is_active=True), - ), - ] - ordering = ["name"] - - def __str__(self) -> str: - return self.name diff --git a/apps/region/serializers.py b/apps/region/serializers.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/region/services/__init__.py b/apps/region/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/region/tasks.py b/apps/region/tasks.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/region/templates/region/.gitkeep b/apps/region/templates/region/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/apps/region/tests/__init__.py b/apps/region/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/region/urls.py b/apps/region/urls.py deleted file mode 100644 index abaf4ad..0000000 --- a/apps/region/urls.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.urls import path - -app_name = "region" - -urlpatterns: list = [] diff --git a/apps/region/views.py b/apps/region/views.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/release/__init__.py b/apps/release/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/release/admin.py b/apps/release/admin.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/release/apps.py b/apps/release/apps.py deleted file mode 100644 index 4a37600..0000000 --- a/apps/release/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class ReleaseConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.release" - label = "release" diff --git a/apps/release/migrations/__init__.py b/apps/release/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/release/models/__init__.py b/apps/release/models/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/release/serializers.py b/apps/release/serializers.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/release/tests/__init__.py b/apps/release/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/release/urls.py b/apps/release/urls.py deleted file mode 100644 index a49f6e7..0000000 --- a/apps/release/urls.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.urls import path - -app_name = "release" - -urlpatterns: list = [] diff --git a/apps/release/views.py b/apps/release/views.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/setting/__init__.py b/apps/setting/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/setting/admin.py b/apps/setting/admin.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/setting/apps.py b/apps/setting/apps.py deleted file mode 100644 index bfc243c..0000000 --- a/apps/setting/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class SettingConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.setting" - label = "setting" diff --git a/apps/setting/migrations/0001_initial.py b/apps/setting/migrations/0001_initial.py deleted file mode 100644 index 8071ee2..0000000 --- a/apps/setting/migrations/0001_initial.py +++ /dev/null @@ -1,114 +0,0 @@ -# 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'), - ), - ] diff --git a/apps/setting/migrations/0002_alter_fieldrequirementrule_options_and_more.py b/apps/setting/migrations/0002_alter_fieldrequirementrule_options_and_more.py deleted file mode 100644 index 2fcf2f1..0000000 --- a/apps/setting/migrations/0002_alter_fieldrequirementrule_options_and_more.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-29 11:07 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('setting', '0001_initial'), - ] - - operations = [ - migrations.AlterModelOptions( - name='fieldrequirementrule', - options={'verbose_name': '字段必填规则', 'verbose_name_plural': '字段必填规则'}, - ), - migrations.AlterModelOptions( - name='lookupgroup', - options={'verbose_name': '查找组', 'verbose_name_plural': '查找组'}, - ), - migrations.AlterModelOptions( - name='lookupitem', - options={'verbose_name': '查找项', 'verbose_name_plural': '查找项'}, - ), - migrations.AlterModelOptions( - name='tenantsetting', - options={'verbose_name': '租户设置', 'verbose_name_plural': '租户设置'}, - ), - ] diff --git a/apps/setting/migrations/0003_alter_fieldrequirementrule_entity_type_and_more.py b/apps/setting/migrations/0003_alter_fieldrequirementrule_entity_type_and_more.py deleted file mode 100644 index 20dce68..0000000 --- a/apps/setting/migrations/0003_alter_fieldrequirementrule_entity_type_and_more.py +++ /dev/null @@ -1,160 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-30 01:46 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('org', '0003_alter_orgunit_address_city_and_more'), - ('setting', '0002_alter_fieldrequirementrule_options_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='fieldrequirementrule', - name='entity_type', - field=models.CharField(choices=[('residential', '住宅'), ('villa', '别墅'), ('commercial_residential', '商住'), ('shop', '商铺'), ('office', '写字楼'), ('other', '其他')], help_text='与 property.property_type 值域完全一致:residential/villa/commercial_residential/shop/office/other', max_length=50, verbose_name='实体类型'), - ), - migrations.AlterField( - model_name='fieldrequirementrule', - name='field_key', - field=models.CharField(help_text='如 orientation / decoration / floor / building_area', max_length=50, verbose_name='字段 key'), - ), - migrations.AlterField( - model_name='fieldrequirementrule', - name='module', - field=models.CharField(choices=[('property', '房源'), ('client', '客源')], help_text='property / client,MVP 仅 property', max_length=20, verbose_name='模块'), - ), - migrations.AlterField( - model_name='fieldrequirementrule', - name='requirement', - field=models.CharField(choices=[('required', '必填'), ('optional', '选填'), ('hidden', '隐藏')], help_text='required=必填 / optional=选填 / hidden=隐藏', max_length=10, verbose_name='规则'), - ), - migrations.AlterField( - model_name='fieldrequirementrule', - name='trade_status', - field=models.CharField(choices=[('sale', '出售'), ('rent', '出租'), ('sale_rent', '租售'), ('*', '全部')], help_text='sale=出售 / rent=出租 / sale_rent=租售 / *=全部(fallback 通配)', max_length=20, verbose_name='交易状态'), - ), - migrations.AlterField( - model_name='fieldrequirementrule', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='更新时间'), - ), - migrations.AlterField( - 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', verbose_name='最后修改人'), - ), - migrations.AlterField( - model_name='lookupgroup', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'), - ), - migrations.AlterField( - model_name='lookupgroup', - name='description', - field=models.TextField(blank=True, default='', help_text='前端 tooltip 使用', verbose_name='分组说明'), - ), - migrations.AlterField( - model_name='lookupgroup', - name='key', - field=models.CharField(help_text='如 source / follow_purpose', max_length=100, verbose_name='分组 key'), - ), - migrations.AlterField( - model_name='lookupgroup', - name='label_zh', - field=models.CharField(help_text='界面显示名称,如「客源来源」', max_length=50, verbose_name='分组中文名'), - ), - migrations.AlterField( - model_name='lookupgroup', - name='module', - field=models.CharField(help_text='client / property', max_length=50, verbose_name='所属模块'), - ), - migrations.AlterField( - model_name='lookupgroup', - name='sort_order', - field=models.SmallIntegerField(default=0, verbose_name='排序'), - ), - migrations.AlterField( - model_name='lookupgroup', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='更新时间'), - ), - migrations.AlterField( - model_name='lookupitem', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'), - ), - migrations.AlterField( - model_name='lookupitem', - name='created_by', - field=models.ForeignKey(blank=True, help_text='系统预制时为 NULL', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_lookup_items', to='org.staff', verbose_name='创建人'), - ), - migrations.AlterField( - model_name='lookupitem', - name='group', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='setting.lookupgroup', verbose_name='所属分组'), - ), - migrations.AlterField( - model_name='lookupitem', - name='is_active', - field=models.BooleanField(default=True, help_text='False 后录入下拉不展示,历史记录保留并展示「(已停用)」后缀', verbose_name='是否启用'), - ), - migrations.AlterField( - model_name='lookupitem', - name='is_system', - field=models.BooleanField(default=False, help_text='True=系统预制不可物理删除,仅可停用', verbose_name='是否系统预制'), - ), - migrations.AlterField( - model_name='lookupitem', - name='label_zh', - field=models.CharField(help_text='如「上门」', max_length=50, verbose_name='显示文本'), - ), - migrations.AlterField( - model_name='lookupitem', - name='sort_order', - field=models.SmallIntegerField(default=0, verbose_name='排序'), - ), - migrations.AlterField( - model_name='lookupitem', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='更新时间'), - ), - migrations.AlterField( - model_name='lookupitem', - name='value', - field=models.CharField(help_text='英文 snake_case,如 door_to_door;写入后只读', max_length=100, verbose_name='存储值'), - ), - migrations.AlterField( - model_name='tenantsetting', - name='category', - field=models.CharField(help_text='client / property / showroom', max_length=50, verbose_name='配置分类'), - ), - migrations.AlterField( - model_name='tenantsetting', - name='key', - field=models.CharField(help_text='如 duplicate_check_scope', max_length=100, verbose_name='配置 key'), - ), - migrations.AlterField( - model_name='tenantsetting', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='更新时间'), - ), - migrations.AlterField( - model_name='tenantsetting', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tenant_settings', to='org.staff', verbose_name='最后修改人'), - ), - migrations.AlterField( - model_name='tenantsetting', - name='value', - field=models.JSONField(help_text='JSONB,统一格式 {"v": }', verbose_name='配置值'), - ), - migrations.AlterField( - model_name='tenantsetting', - name='value_type', - field=models.CharField(choices=[('bool', '布尔'), ('int', '整数'), ('string', '字符串'), ('enum', '枚举')], help_text='bool / int / string / enum,用于前端渲染控件', max_length=20, verbose_name='值类型'), - ), - ] diff --git a/apps/setting/migrations/__init__.py b/apps/setting/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/setting/models/__init__.py b/apps/setting/models/__init__.py deleted file mode 100644 index e177abb..0000000 --- a/apps/setting/models/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from .lookup import LookupGroup, LookupItem -from .setting import FieldRequirementRule, TenantSetting - -__all__ = [ - "LookupGroup", - "LookupItem", - "TenantSetting", - "FieldRequirementRule", -] diff --git a/apps/setting/models/lookup.py b/apps/setting/models/lookup.py deleted file mode 100644 index 259ae5c..0000000 --- a/apps/setting/models/lookup.py +++ /dev/null @@ -1,115 +0,0 @@ -from django.db import models - -from core.models.base import UUIDPrimaryKeyModel - - -class LookupGroup(UUIDPrimaryKeyModel): - module = models.CharField( - max_length=50, - verbose_name="所属模块", - help_text="client / property", - ) - key = models.CharField( - max_length=100, - verbose_name="分组 key", - help_text="如 source / follow_purpose", - ) - label_zh = models.CharField( - max_length=50, - verbose_name="分组中文名", - help_text="界面显示名称,如「客源来源」", - ) - description = models.TextField( - blank=True, - default="", - verbose_name="分组说明", - help_text="前端 tooltip 使用", - ) - sort_order = models.SmallIntegerField( - default=0, - verbose_name="排序", - ) - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name="创建时间", - ) - updated_at = models.DateTimeField( - auto_now=True, - verbose_name="更新时间", - ) - - class Meta: - db_table = "lookup_groups" - verbose_name = "查找组" - verbose_name_plural = "查找组" - 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", - verbose_name="所属分组", - ) - value = models.CharField( - max_length=100, - verbose_name="存储值", - help_text="英文 snake_case,如 door_to_door;写入后只读", - ) - label_zh = models.CharField( - max_length=50, - verbose_name="显示文本", - help_text="如「上门」", - ) - is_system = models.BooleanField( - default=False, - verbose_name="是否系统预制", - help_text="True=系统预制不可物理删除,仅可停用", - ) - is_active = models.BooleanField( - default=True, - verbose_name="是否启用", - help_text="False 后录入下拉不展示,历史记录保留并展示「(已停用)」后缀", - ) - sort_order = models.SmallIntegerField( - default=0, - verbose_name="排序", - ) - created_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="created_lookup_items", - verbose_name="创建人", - help_text="系统预制时为 NULL", - ) - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name="创建时间", - ) - updated_at = models.DateTimeField( - auto_now=True, - verbose_name="更新时间", - ) - - class Meta: - db_table = "lookup_items" - verbose_name = "查找项" - verbose_name_plural = "查找项" - 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", - ), - ] diff --git a/apps/setting/models/setting.py b/apps/setting/models/setting.py deleted file mode 100644 index ff11669..0000000 --- a/apps/setting/models/setting.py +++ /dev/null @@ -1,125 +0,0 @@ -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, - verbose_name="配置分类", - help_text="client / property / showroom", - ) - key = models.CharField( - max_length=100, - verbose_name="配置 key", - help_text="如 duplicate_check_scope", - ) - value = models.JSONField( - verbose_name="配置值", - help_text='JSONB,统一格式 {"v": }', - ) - value_type = models.CharField( - max_length=20, - choices=SettingValueType.choices, - verbose_name="值类型", - help_text="bool / int / string / enum,用于前端渲染控件", - ) - updated_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="updated_tenant_settings", - verbose_name="最后修改人", - ) - updated_at = models.DateTimeField( - auto_now=True, - verbose_name="更新时间", - ) - - class Meta: - db_table = "tenant_settings" - verbose_name = "租户设置" - verbose_name_plural = "租户设置" - 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, - verbose_name="模块", - help_text="property / client,MVP 仅 property", - ) - entity_type = models.CharField( - max_length=50, - choices=FieldRuleEntityType.choices, - verbose_name="实体类型", - help_text="与 property.property_type 值域完全一致:residential/villa/commercial_residential/shop/office/other", - ) - trade_status = models.CharField( - max_length=20, - choices=TRADE_STATUS_CHOICES, - verbose_name="交易状态", - help_text="sale=出售 / rent=出租 / sale_rent=租售 / *=全部(fallback 通配)", - ) - field_key = models.CharField( - max_length=50, - verbose_name="字段 key", - help_text="如 orientation / decoration / floor / building_area", - ) - requirement = models.CharField( - max_length=10, - choices=FieldRuleRequirement.choices, - verbose_name="规则", - help_text="required=必填 / optional=选填 / hidden=隐藏", - ) - updated_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="updated_field_rules", - verbose_name="最后修改人", - ) - updated_at = models.DateTimeField( - auto_now=True, - verbose_name="更新时间", - ) - - class Meta: - db_table = "field_requirement_rules" - verbose_name = "字段必填规则" - verbose_name_plural = "字段必填规则" - 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", - ), - ] diff --git a/apps/setting/serializers.py b/apps/setting/serializers.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/setting/services/__init__.py b/apps/setting/services/__init__.py deleted file mode 100644 index 1f1fa4f..0000000 --- a/apps/setting/services/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from apps.setting.services.seed_default_lookups import seed_default_lookups - -__all__ = ["seed_default_lookups"] diff --git a/apps/setting/services/seed_default_lookups.py b/apps/setting/services/seed_default_lookups.py deleted file mode 100644 index 8ff4260..0000000 --- a/apps/setting/services/seed_default_lookups.py +++ /dev/null @@ -1,113 +0,0 @@ -import logging - -logger = logging.getLogger(__name__) - -_GROUPS = [ - {"module": "client", "key": "source", "label_zh": "客源来源", "description": "客源从何处获取,用于来源渠道分析", "sort_order": 1}, - {"module": "client", "key": "follow_purpose", "label_zh": "跟进目的", "description": "客源跟进时选择的目的分类", "sort_order": 2}, - {"module": "property", "key": "source", "label_zh": "房源来源", "description": "房源从何处获取", "sort_order": 3}, -] - -_ITEMS = { - ("client", "source"): [ - {"value": "store_reception", "label_zh": "门店接待", "sort_order": 1}, - {"value": "old_client_referral", "label_zh": "老客户转介绍", "sort_order": 2}, - {"value": "stationed_dispatch", "label_zh": "驻守派单", "sort_order": 3}, - {"value": "walk_in", "label_zh": "上门", "sort_order": 4}, - {"value": "online_58", "label_zh": "网络-58同城", "sort_order": 5}, - {"value": "online_anjuke", "label_zh": "网络-安居客", "sort_order": 6}, - {"value": "wechat", "label_zh": "微信", "sort_order": 7}, - {"value": "friend_referral", "label_zh": "朋友介绍", "sort_order": 8}, - ], - ("client", "follow_purpose"): [ - {"value": "callback", "label_zh": "回拨", "sort_order": 1}, - {"value": "push_property", "label_zh": "推房", "sort_order": 2}, - {"value": "showing", "label_zh": "带看", "sort_order": 3}, - {"value": "maintain", "label_zh": "维护", "sort_order": 4}, - {"value": "other", "label_zh": "其他", "sort_order": 5}, - ], - ("property", "source"): [ - {"value": "proactive_development", "label_zh": "主动开发", "sort_order": 1}, - {"value": "owner_walk_in", "label_zh": "业主上门", "sort_order": 2}, - {"value": "old_client_referral", "label_zh": "老客户转介绍", "sort_order": 3}, - {"value": "online_inquiry", "label_zh": "网络来电", "sort_order": 4}, - ], -} - -_TENANT_SETTINGS = [ - {"category": "client", "key": "duplicate_check_scope", "value": {"v": "self"}, "value_type": "enum"}, -] - -_FIELD_RULES = [ - {"module": "property", "entity_type": "residential", "trade_status": "sale", "field_key": "orientation", "requirement": "optional"}, - {"module": "property", "entity_type": "residential", "trade_status": "sale", "field_key": "decoration", "requirement": "optional"}, - {"module": "property", "entity_type": "residential", "trade_status": "sale", "field_key": "floor", "requirement": "optional"}, - {"module": "property", "entity_type": "residential", "trade_status": "sale", "field_key": "building_area", "requirement": "required"}, - {"module": "property", "entity_type": "residential", "trade_status": "sale", "field_key": "inner_area", "requirement": "optional"}, - {"module": "property", "entity_type": "residential", "trade_status": "sale", "field_key": "room_layout", "requirement": "required"}, - {"module": "property", "entity_type": "residential", "trade_status": "rent", "field_key": "decoration", "requirement": "optional"}, - {"module": "property", "entity_type": "residential", "trade_status": "rent", "field_key": "floor", "requirement": "optional"}, - {"module": "property", "entity_type": "residential", "trade_status": "rent", "field_key": "building_area", "requirement": "required"}, - {"module": "property", "entity_type": "residential", "trade_status": "rent", "field_key": "room_layout", "requirement": "required"}, -] - - -def seed_default_lookups(schema_name: str) -> None: - from django.apps import apps - - LookupGroup = apps.get_model("fonrey_setting", "LookupGroup") - LookupItem = apps.get_model("fonrey_setting", "LookupItem") - TenantSetting = apps.get_model("fonrey_setting", "TenantSetting") - FieldRequirementRule = apps.get_model("fonrey_setting", "FieldRequirementRule") - - group_map = {} - for gd in _GROUPS: - group, _ = LookupGroup.objects.get_or_create( - module=gd["module"], - key=gd["key"], - defaults={ - "label_zh": gd["label_zh"], - "description": gd["description"], - "sort_order": gd["sort_order"], - }, - ) - group_map[(gd["module"], gd["key"])] = group - - item_objects = [] - for (module, key), items in _ITEMS.items(): - group = group_map[(module, key)] - for item in items: - item_objects.append( - LookupItem( - group=group, - value=item["value"], - label_zh=item["label_zh"], - is_system=True, - is_active=True, - sort_order=item["sort_order"], - ) - ) - LookupItem.objects.bulk_create(item_objects, ignore_conflicts=True) - - ts_objects = [ - TenantSetting( - category=ts["category"], - key=ts["key"], - value=ts["value"], - value_type=ts["value_type"], - ) - for ts in _TENANT_SETTINGS - ] - TenantSetting.objects.bulk_create(ts_objects, ignore_conflicts=True) - - rule_objects = [ - FieldRequirementRule( - module=r["module"], - entity_type=r["entity_type"], - trade_status=r["trade_status"], - field_key=r["field_key"], - requirement=r["requirement"], - ) - for r in _FIELD_RULES - ] - FieldRequirementRule.objects.bulk_create(rule_objects, ignore_conflicts=True) diff --git a/apps/setting/tasks.py b/apps/setting/tasks.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/setting/templates/setting/.gitkeep b/apps/setting/templates/setting/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/apps/setting/tests/__init__.py b/apps/setting/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/setting/urls.py b/apps/setting/urls.py deleted file mode 100644 index 69d71a1..0000000 --- a/apps/setting/urls.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.urls import path - -app_name = "setting" - -urlpatterns: list = [] diff --git a/apps/setting/views.py b/apps/setting/views.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/tenant/__init__.py b/apps/tenant/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/tenant/admin.py b/apps/tenant/admin.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/tenant/apps.py b/apps/tenant/apps.py deleted file mode 100644 index 11fc974..0000000 --- a/apps/tenant/apps.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.apps import AppConfig - - -class TenantConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.tenant" - label = "tenant" - - def ready(self): - from apps.tenant import signals # noqa: F401 - - signals._register() diff --git a/apps/tenant/migrations/0001_initial.py b/apps/tenant/migrations/0001_initial.py deleted file mode 100644 index 7828f0e..0000000 --- a/apps/tenant/migrations/0001_initial.py +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-29 11:07 - -from django.db import migrations, models -import django.db.models.deletion -import django_tenants.postgresql_backend.base - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Tenant', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('schema_name', models.CharField(db_index=True, max_length=63, unique=True, validators=[django_tenants.postgresql_backend.base._check_schema_name])), - ('name', models.CharField(max_length=255)), - ('created_on', models.DateField(auto_now_add=True)), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='Domain', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('domain', models.CharField(db_index=True, max_length=253, unique=True)), - ('is_primary', models.BooleanField(db_index=True, default=True)), - ('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='domains', to='tenant.tenant')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/apps/tenant/migrations/0002_alter_domain_options_alter_tenant_options_and_more.py b/apps/tenant/migrations/0002_alter_domain_options_alter_tenant_options_and_more.py deleted file mode 100644 index 2d87951..0000000 --- a/apps/tenant/migrations/0002_alter_domain_options_alter_tenant_options_and_more.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 4.2.16 on 2026-04-30 01:46 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('tenant', '0001_initial'), - ] - - operations = [ - migrations.AlterModelOptions( - name='domain', - options={'verbose_name': '域名', 'verbose_name_plural': '域名'}, - ), - migrations.AlterModelOptions( - name='tenant', - options={'verbose_name': '租户', 'verbose_name_plural': '租户'}, - ), - migrations.AlterField( - model_name='tenant', - name='created_on', - field=models.DateField(auto_now_add=True, verbose_name='创建日期'), - ), - migrations.AlterField( - model_name='tenant', - name='name', - field=models.CharField(help_text='租户公司名称', max_length=255, verbose_name='公司名称'), - ), - ] diff --git a/apps/tenant/migrations/__init__.py b/apps/tenant/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/tenant/models.py b/apps/tenant/models.py deleted file mode 100644 index 082da1b..0000000 --- a/apps/tenant/models.py +++ /dev/null @@ -1,27 +0,0 @@ -from django.db import models -from django_tenants.models import DomainMixin, TenantMixin - - -class Tenant(TenantMixin): - name = models.CharField( - max_length=255, - verbose_name="公司名称", - help_text="租户公司名称", - ) - created_on = models.DateField( - auto_now_add=True, - verbose_name="创建日期", - ) - - auto_create_schema = True - - class Meta: - verbose_name = "租户" - verbose_name_plural = "租户" - - -class Domain(DomainMixin): - - class Meta: - verbose_name = "域名" - verbose_name_plural = "域名" diff --git a/apps/tenant/signals.py b/apps/tenant/signals.py deleted file mode 100644 index 4f5d9b9..0000000 --- a/apps/tenant/signals.py +++ /dev/null @@ -1,36 +0,0 @@ -import logging - -from django.db.models.signals import post_save -from django.dispatch import receiver - -logger = logging.getLogger(__name__) - - -def _get_tenant_model(): - from django.apps import apps - return apps.get_model("tenant", "Tenant") - - -def _register(): - Tenant = _get_tenant_model() - - @receiver(post_save, sender=Tenant) - def on_tenant_created(sender, instance, created, **kwargs): - if not created: - return - if instance.schema_name == "public": - return - - from django_tenants.utils import schema_context - - from apps.permission.services import seed_default_roles - from apps.setting.services import seed_default_lookups - - try: - with schema_context(instance.schema_name): - seed_default_roles(instance.schema_name) - seed_default_lookups(instance.schema_name) - except Exception: - logger.exception( - "Failed to seed defaults for tenant %s", instance.schema_name - ) diff --git a/apps/tenant/tests/__init__.py b/apps/tenant/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/config/__init__.py b/config/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/config/asgi.py b/config/asgi.py deleted file mode 100644 index bc33ab2..0000000 --- a/config/asgi.py +++ /dev/null @@ -1,7 +0,0 @@ -import os - -from django.core.asgi import get_asgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.development") - -application = get_asgi_application() diff --git a/config/settings/__init__.py b/config/settings/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/config/settings/base.py b/config/settings/base.py deleted file mode 100644 index 04e6fe1..0000000 --- a/config/settings/base.py +++ /dev/null @@ -1,193 +0,0 @@ -"""Fonrey base Django settings. Secrets via env (python-decouple). ASGI + django-tenants.""" -from pathlib import Path - -from decouple import Csv, config as env - -BASE_DIR = Path(__file__).resolve().parent.parent.parent - -SECRET_KEY = env("SECRET_KEY") -DEBUG = env("DEBUG", default=False, cast=bool) -ALLOWED_HOSTS = env("ALLOWED_HOSTS", default="localhost,127.0.0.1", cast=Csv()) - -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" - -SHARED_APPS = [ - "django_tenants", # MUST be first per django-tenants - "apps.tenant", - "apps.release", - "apps.permission_def", - "shared", - "django.contrib.contenttypes", - "django.contrib.auth", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", - "django_celery_beat", - "django_celery_results", - "rest_framework", - "drf_spectacular", - "core", - "django_htmx", - "django_extensions", -] - -TENANT_APPS = [ - "apps.account", - "apps.permission", - "apps.org", - "apps.region", - "apps.complex", - "apps.property", - "apps.client", - "apps.setting", -] - -INSTALLED_APPS = list(SHARED_APPS) + [a for a in TENANT_APPS if a not in SHARED_APPS] - -TENANT_MODEL = "tenant.Tenant" -TENANT_DOMAIN_MODEL = "tenant.Domain" - -MIDDLEWARE = [ - "django_tenants.middleware.main.TenantMainMiddleware", # MUST be first - "django.middleware.security.SecurityMiddleware", - "whitenoise.middleware.WhiteNoiseMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", - "django_htmx.middleware.HtmxMiddleware", - "core.middleware.audit.AuditMiddleware", -] - -ROOT_URLCONF = "config.urls" -PUBLIC_SCHEMA_URLCONF = "config.urls_public" - -DATABASES = { - "default": { - "ENGINE": "django_tenants.postgresql_backend", - "NAME": env("DB_NAME"), - "USER": env("DB_USER"), - "PASSWORD": env("DB_PASSWORD"), - "HOST": env("DB_HOST", default="localhost"), - "PORT": env("DB_PORT", default="5432"), - "CONN_MAX_AGE": 60, - # Connection pooling lives at PgBouncer; no non-standard DSN keys here. - } -} - -DATABASE_ROUTERS = ["django_tenants.routers.TenantSyncRouter"] - -CACHES = { - "default": { - "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": env("REDIS_URL", default="redis://127.0.0.1:6379/0"), - "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"}, - "KEY_PREFIX": "fonrey", - } -} -SESSION_ENGINE = "django.contrib.sessions.backends.cache" -SESSION_CACHE_ALIAS = "default" - -CELERY_BROKER_URL = env("CELERY_BROKER_URL", default="redis://127.0.0.1:6379/1") -CELERY_RESULT_BACKEND = "django-db" -CELERY_TASK_ALWAYS_EAGER = False -CELERY_TASK_TIME_LIMIT = 300 -CELERY_TASK_SOFT_TIME_LIMIT = 270 -CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" -CELERY_ACCEPT_CONTENT = ["json"] -CELERY_TASK_SERIALIZER = "json" -CELERY_RESULT_SERIALIZER = "json" -CELERY_TIMEZONE = "Asia/Shanghai" - -DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" -AWS_S3_ENDPOINT_URL = env("R2_ENDPOINT_URL", default="") -AWS_ACCESS_KEY_ID = env("R2_ACCESS_KEY_ID", default="") -AWS_SECRET_ACCESS_KEY = env("R2_SECRET_ACCESS_KEY", default="") -AWS_STORAGE_BUCKET_NAME = env("R2_BUCKET_NAME", default="media") -AWS_S3_CUSTOM_DOMAIN = env("R2_CUSTOM_DOMAIN", default="") or None -AWS_DEFAULT_ACL = "private" -AWS_S3_SIGNATURE_VERSION = "s3v4" -AWS_S3_FILE_OVERWRITE = False - -ASGI_APPLICATION = "config.asgi.application" -WSGI_APPLICATION = "config.wsgi.application" - -AUTH_USER_MODEL = "account.UserAccount" - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [BASE_DIR / "templates"], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - ] - }, - } -] - -HTMX_GLOBAL_CSRF = True - -STATIC_URL = "/static/" -STATIC_ROOT = BASE_DIR / "staticfiles" -STATICFILES_DIRS = [BASE_DIR / "static"] -MEDIA_URL = "/media/" -MEDIA_ROOT = BASE_DIR / "media" - -LANGUAGE_CODE = "zh-hans" -TIME_ZONE = "Asia/Shanghai" -USE_I18N = True -USE_TZ = True -LOCALE_PATHS = [BASE_DIR / "locale"] - -SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") -SESSION_COOKIE_HTTPONLY = True -SESSION_COOKIE_SAMESITE = "Lax" -# CSRF_COOKIE_HTTPONLY MUST be False so HTMX can read the token from JS (spec §3.2). -CSRF_COOKIE_HTTPONLY = False -X_FRAME_OPTIONS = "DENY" - -REST_FRAMEWORK = { - "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", - "DEFAULT_AUTHENTICATION_CLASSES": [ - "rest_framework.authentication.SessionAuthentication", - ], - "DEFAULT_RENDERER_CLASSES": [ - "rest_framework.renderers.JSONRenderer", - ], -} - -SPECTACULAR_SETTINGS = { - "TITLE": "Fonrey API", - "DESCRIPTION": "Fonrey 房产经纪管理系统 OpenAPI 3.1", - "VERSION": "1.0.0", - "SERVE_INCLUDE_SCHEMA": False, - "COMPONENT_SPLIT_REQUEST": True, - "ENUM_GENERATE_CHOICE_DESCRIPTION": True, -} - -LOGGING = { - "version": 1, - "disable_existing_loggers": False, - "formatters": { - "verbose": { - "format": "[{asctime}] {levelname} {name} ({process:d}) {message}", - "style": "{", - }, - }, - "handlers": { - "console": { - "class": "logging.StreamHandler", - "formatter": "verbose", - } - }, - "root": {"handlers": ["console"], "level": "INFO"}, -} - -PHONE_ENCRYPTION_KEY = env("PHONE_ENCRYPTION_KEY", default="") diff --git a/config/settings/development.py b/config/settings/development.py deleted file mode 100644 index c32de63..0000000 --- a/config/settings/development.py +++ /dev/null @@ -1,7 +0,0 @@ -from .base import * # noqa: F401,F403 -from .base import INSTALLED_APPS, MIDDLEWARE - -INSTALLED_APPS = list(INSTALLED_APPS) + ["debug_toolbar"] -MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware", *MIDDLEWARE] - -INTERNAL_IPS = ["127.0.0.1"] diff --git a/config/settings/production.py b/config/settings/production.py deleted file mode 100644 index 0631477..0000000 --- a/config/settings/production.py +++ /dev/null @@ -1,27 +0,0 @@ -import sentry_sdk -from sentry_sdk.integrations.celery import CeleryIntegration -from sentry_sdk.integrations.django import DjangoIntegration - -from .base import * # noqa: F401,F403 -from .base import env - -DEBUG = False - -SECURE_SSL_REDIRECT = True -SESSION_COOKIE_SECURE = True -CSRF_COOKIE_SECURE = True -SECURE_HSTS_SECONDS = 31536000 -SECURE_HSTS_INCLUDE_SUBDOMAINS = True -SECURE_HSTS_PRELOAD = True -SECURE_CONTENT_TYPE_NOSNIFF = True -SECURE_REFERRER_POLICY = "same-origin" - -_sentry_dsn = env("SENTRY_DSN", default="") -if _sentry_dsn: - sentry_sdk.init( - dsn=_sentry_dsn, - integrations=[DjangoIntegration(), CeleryIntegration()], - traces_sample_rate=0.1, - send_default_pii=False, - environment=env("SENTRY_ENV", default="production"), - ) diff --git a/config/settings/testing.py b/config/settings/testing.py deleted file mode 100644 index 6c3bce4..0000000 --- a/config/settings/testing.py +++ /dev/null @@ -1,18 +0,0 @@ -from .base import * # noqa: F401,F403 - -DEBUG = False - -CELERY_TASK_ALWAYS_EAGER = True -CELERY_TASK_EAGER_PROPAGATES = True - -CACHES = { - "default": { - "BACKEND": "django.core.cache.backends.locmem.LocMemCache", - "LOCATION": "fonrey-test", - } -} - -# Use synchronous in-memory session backend during tests; avoids Redis dependency. -SESSION_ENGINE = "django.contrib.sessions.backends.db" - -PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] diff --git a/config/urls.py b/config/urls.py deleted file mode 100644 index ab46257..0000000 --- a/config/urls.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.urls import path - -urlpatterns: list[path] = [] diff --git a/config/urls_public.py b/config/urls_public.py deleted file mode 100644 index 782501a..0000000 --- a/config/urls_public.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.urls import include, path -from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView - -urlpatterns = [ - path("api/client/", include("apps.release.urls")), - path("api/schema/", SpectacularAPIView.as_view(), name="schema"), - path("api/docs/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"), -] diff --git a/config/wsgi.py b/config/wsgi.py deleted file mode 100644 index 3eb3a0c..0000000 --- a/config/wsgi.py +++ /dev/null @@ -1,7 +0,0 @@ -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.development") - -application = get_wsgi_application() diff --git a/core/__init__.py b/core/__init__.py deleted file mode 100644 index 2da8e69..0000000 --- a/core/__init__.py +++ /dev/null @@ -1 +0,0 @@ -default_app_config = "core.apps.CoreConfig" diff --git a/core/apps.py b/core/apps.py deleted file mode 100644 index 4eaba6d..0000000 --- a/core/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class CoreConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "core" - verbose_name = "核心工具" diff --git a/core/cache.py b/core/cache.py deleted file mode 100644 index 3a71698..0000000 --- a/core/cache.py +++ /dev/null @@ -1,17 +0,0 @@ -from django.core.cache import cache - - -def get_redis_key(tenant_schema: str, module: str, key: str) -> str: - return f"{tenant_schema}:{module}:{key}" - - -def cache_get(tenant_schema: str, module: str, key: str, default=None): - return cache.get(get_redis_key(tenant_schema, module, key), default) - - -def cache_set(tenant_schema: str, module: str, key: str, value, timeout: int = 300) -> None: - cache.set(get_redis_key(tenant_schema, module, key), value, timeout) - - -def cache_delete(tenant_schema: str, module: str, key: str) -> None: - cache.delete(get_redis_key(tenant_schema, module, key)) diff --git a/core/encryption.py b/core/encryption.py deleted file mode 100644 index f4ae497..0000000 --- a/core/encryption.py +++ /dev/null @@ -1,57 +0,0 @@ -"""PII encryption per AGENTS.md §4.4: AES-256-GCM (NOT Fernet).""" -import base64 -import hashlib -import os - -from django.conf import settings -from cryptography.hazmat.primitives.ciphers.aead import AESGCM - - -class PhoneEncryption: - """Encrypt phone numbers with AES-256-GCM, index via SHA-256 hash, display masked. - - Storage: phone_encrypted = base64(nonce || ciphertext || tag) - Index: phone_hash = sha256(plaintext) — used for equality lookups - Display: 138****1234 - """ - - NONCE_LEN = 12 - - @staticmethod - def _key() -> bytes: - key_b64 = settings.PHONE_ENCRYPTION_KEY - if not key_b64: - raise RuntimeError( - "PHONE_ENCRYPTION_KEY is not configured. " - "Generate with: python -c 'import secrets,base64; print(base64.b64encode(secrets.token_bytes(32)).decode())'" - ) - key = base64.b64decode(key_b64) - if len(key) != 32: - raise RuntimeError("PHONE_ENCRYPTION_KEY must decode to exactly 32 bytes (AES-256).") - return key - - @classmethod - def encrypt(cls, phone: str) -> str: - if phone is None: - raise ValueError("phone cannot be None") - aesgcm = AESGCM(cls._key()) - nonce = os.urandom(cls.NONCE_LEN) - ct = aesgcm.encrypt(nonce, phone.encode("utf-8"), None) - return base64.b64encode(nonce + ct).decode("ascii") - - @classmethod - def decrypt(cls, ciphertext_b64: str) -> str: - raw = base64.b64decode(ciphertext_b64) - nonce, ct = raw[: cls.NONCE_LEN], raw[cls.NONCE_LEN :] - aesgcm = AESGCM(cls._key()) - return aesgcm.decrypt(nonce, ct, None).decode("utf-8") - - @staticmethod - def hash(phone: str) -> str: - return hashlib.sha256(phone.encode("utf-8")).hexdigest() - - @staticmethod - def mask(phone: str) -> str: - if not phone or len(phone) < 7: - return "***" - return phone[:3] + "****" + phone[-4:] diff --git a/core/enums.py b/core/enums.py deleted file mode 100644 index c2bae77..0000000 --- a/core/enums.py +++ /dev/null @@ -1,818 +0,0 @@ -"""Python mirror of DATA_MODEL/ENUMS.md v2.2. All values are lower_snake_case. - -Authority: ENUMS.md is the single source of truth. When ENUMS.md changes, -update this file in the same commit. All models/serializers MUST import -choices from here; never hardcode enum strings. -""" -from django.db import models - - -# ────────────────────────────────────────────────────────────── -# 2. Public / 平台级 固定枚举 -# ────────────────────────────────────────────────────────────── - -class TenantPlan(models.TextChoices): - BASIC = "basic", "基础版" - PROFESSIONAL = "professional", "专业版" - ENTERPRISE = "enterprise", "企业版" - - -class TenantStatus(models.TextChoices): - CREATING = "creating", "创建中" - ACTIVE = "active", "正常" - SUSPENDED = "suspended", "已挂起" - PENDING_DELETE = "pending_delete", "待删除" - DELETED = "deleted", "已删除" - FAILED = "failed", "创建/初始化失败" - - -class TenantSuspendedReason(models.TextChoices): - OVERDUE = "overdue", "欠费" - VIOLATION = "violation", "违规" - REQUESTED = "requested", "客户申请" - OTHER = "other", "其他" - - -class PlatformAdminRole(models.TextChoices): - SUPER_ADMIN = "super_admin", "超级管理员" - OPS_OPERATOR = "ops_operator", "运营管理员" - READ_ONLY_AUDITOR = "read_only_auditor", "只读审计员" - - -class PlatformAuditResult(models.TextChoices): - SUCCESS = "success", "成功" - FAILED = "failed", "失败" - - -class BackupScheduleFrequency(models.TextChoices): - HOURLY = "hourly", "每小时" - DAILY = "daily", "每日" - WEEKLY = "weekly", "每周" - - -class BackupStorageTarget(models.TextChoices): - LOCAL = "local", "本地存储" - S3 = "s3", "Amazon S3" - R2 = "r2", "Cloudflare R2" - GCS = "gcs", "Google Cloud Storage" - - -class BackupTriggerType(models.TextChoices): - AUTO = "auto", "自动触发" - MANUAL = "manual", "手动触发" - PRE_UPGRADE = "pre_upgrade", "升级前触发" - PRE_RESTORE = "pre_restore", "恢复前触发" - - -class BackupRecordStatus(models.TextChoices): - PENDING = "pending", "待执行" - IN_PROGRESS = "in_progress", "执行中" - SUCCESS = "success", "成功" - FAILED = "failed", "失败" - - -class ExportTaskFormat(models.TextChoices): - CSV = "csv", "CSV" - JSON = "json", "JSON" - SQL_DUMP = "sql_dump", "SQL 导出" - - -class ExportTaskStatus(models.TextChoices): - PENDING = "pending", "待执行" - IN_PROGRESS = "in_progress", "执行中" - SUCCESS = "success", "成功" - FAILED = "failed", "失败" - - -class UpgradeEventType(models.TextChoices): - UPGRADE = "upgrade", "升级" - ROLLBACK = "rollback", "回滚" - - -class UpgradeType(models.TextChoices): - APP = "app", "A类-应用升级" - SCHEMA = "schema", "B类-数据库结构升级" - FEATURE = "feature", "C类-功能开关升级" - - -class UpgradeStrategy(models.TextChoices): - FULL = "full", "全量发布" - CANARY = "canary", "灰度发布" - - -class UpgradeEventStatus(models.TextChoices): - DRAFT = "draft", "草稿" - PRE_CHECK = "pre_check", "预检查" - PRE_BACKUP = "pre_backup", "预备份" - BATCH_RUNNING = "batch_running", "批次执行中" - BATCH_DONE = "batch_done", "批次完成" - HALTED = "halted", "已暂停" - SUCCEEDED = "succeeded", "已成功" - FAILED = "failed", "失败" - ROLLBACK_RUNNING = "rollback_running", "回滚中" - ROLLED_BACK = "rolled_back", "已回滚" - - -class UpgradeFailurePolicy(models.TextChoices): - HALT_BATCH = "halt_batch", "失败即停止批次" - CONTINUE = "continue", "失败继续" - - -class ClientReleasePlatform(models.TextChoices): - WIN32 = "win32", "Windows 客户端" - - -class ClientReleaseArch(models.TextChoices): - X64 = "x64", "x64 架构" - ARM64 = "arm64", "ARM64 架构" - - -class ClientReleaseType(models.TextChoices): - NORMAL = "normal", "普通更新" - FORCE = "force", "强制更新" - - -class ClientReleaseStatus(models.TextChoices): - DRAFT = "draft", "草稿" - PUBLISHED = "published", "已发布" - ARCHIVED = "archived", "已归档" - - -# ────────────────────────────────────────────────────────────── -# 3.1 login / account -# ────────────────────────────────────────────────────────────── - -class UserAccountStatus(models.TextChoices): - ACTIVE = "active", "启用" - DISABLED = "disabled", "停用" - LOCKED = "locked", "锁定" - - -class LoginFailureReason(models.TextChoices): - WRONG_PASSWORD = "wrong_password", "用户名或密码错误" - WRONG_CAPTCHA = "wrong_captcha", "验证码错误" - ACCOUNT_LOCKED = "account_locked", "账号锁定" - ACCOUNT_DISABLED = "account_disabled", "账号停用" - TENANT_NOT_FOUND = "tenant_not_found", "租户不存在" - - -# ────────────────────────────────────────────────────────────── -# 3.2 org -# ────────────────────────────────────────────────────────────── - -class OrgUnitType(models.TextChoices): - COMPANY = "company", "公司" - DIVISION = "division", "事业部" - REGION = "region", "大区" - AREA = "area", "区域" - DISTRICT = "district", "片区" - STORE = "store", "门店" - GROUP = "group", "店组" - FUNCTIONAL = "functional", "职能部门" - - -class OrgUnitAttribute(models.TextChoices): - DIRECT = "direct", "直营" - FRANCHISE = "franchise", "加盟" - - -class StaffRole(models.TextChoices): - AGENT = "agent", "经纪人" - STORE_MANAGER = "store_manager", "店长" - AREA_MANAGER = "area_manager", "区域经理" - ADMIN = "admin", "系统管理员" - OPERATOR = "operator", "运营/行政" - SYSTEM = "system", "系统账号" - - -class StaffStatus(models.TextChoices): - ACTIVE = "active", "在职" - PROBATION = "probation", "试用" - RESIGNED = "resigned", "离职" - FROZEN = "frozen", "冻结" - - -class StaffGender(models.TextChoices): - MALE = "male", "男" - FEMALE = "female", "女" - UNKNOWN = "unknown", "未知" - - -class StaffIdType(models.TextChoices): - ID_CARD = "id_card", "身份证" - PASSPORT = "passport", "护照" - OTHER = "other", "其他" - - -class StaffTransferType(models.TextChoices): - ONBOARD = "onboard", "入职" - TRANSFER = "transfer", "调动" - RESIGN = "resign", "离职" - REJOIN = "rejoin", "复职" - SUPERVISOR_CHANGE = "supervisor_change", "上级变更" - ROLE_CHANGE = "role_change", "角色变更" - FREEZE = "freeze", "冻结账号" - UNFREEZE = "unfreeze", "恢复账号" - - -class StaffAccountPlatform(models.TextChoices): - FONREY = "fonrey", "房睿主账号" - ANJUKE_58 = "58anjuke", "58安居客" - CNREIC = "cnreic", "中国网络经纪人" - WECHAT_MP = "wechat_mp", "微信公众号" - - -# ────────────────────────────────────────────────────────────── -# 3.3 permission -# ────────────────────────────────────────────────────────────── - -class PermissionModule(models.TextChoices): - HOME = "home", "首页" - PROPERTY = "property", "房源" - NEW_HOUSE = "new_house", "新房" - CLIENT = "client", "客源" - TRANSACTION = "transaction", "交易" - DATA = "data", "数据" - MARKETING = "marketing", "营销" - HR = "hr", "人事OA" - CONTRACT = "contract", "合同" - TRINET = "trinet", "三网" - SYSTEM = "system", "系统" - MOBILE = "mobile", "移动端" - SMART_STORE = "smart_store", "智能门店" - RECHARGE = "recharge", "在线充值" - - -class PermissionValueType(models.TextChoices): - BOOLEAN = "boolean", "开关型" - SCOPE = "scope", "范围型" - INTEGER = "integer", "数值型" - - -class PermissionRoleCategory(models.TextChoices): - AGENT = "agent", "置业顾问" - STORE_MANAGER = "store_manager", "店管" - DIRECTOR = "director", "总经" - OPERATOR = "operator", "运营/行政" - CUSTOM = "custom", "自定义" - - -class PermissionScopeLevel(models.TextChoices): - NONE = "none", "无" - SELF = "self", "本人" - GROUP = "group", "本组" - STORE = "store", "本门店" - AREA = "area", "本区域" - REGION = "region", "本大区" - COMPANY = "company", "全公司" - - -class PermissionOverrideMode(models.TextChoices): - REPLACE = "replace", "覆盖" - RESTRICT = "restrict", "限制" - GRANT = "grant", "授予" - - -class PermissionDataScopeType(models.TextChoices): - SELF = "self", "本人" - GROUP = "group", "本组" - STORE = "store", "本门店" - AREA = "area", "本区域" - REGION = "region", "本大区" - COMPANY = "company", "全公司" - CUSTOM_UNIT = "custom_unit", "自定义组织单元" - - -class PermissionChangeTargetType(models.TextChoices): - ROLE = "role", "角色" - ROLE_PERMISSION = "role_permission", "角色权限" - STAFF_ROLE = "staff_role", "员工角色" - STAFF_OVERRIDE = "staff_override", "员工权限覆盖" - STAFF_SCOPE = "staff_scope", "员工数据范围" - - -class PermissionChangeAction(models.TextChoices): - CREATE = "create", "创建" - UPDATE = "update", "更新" - DELETE = "delete", "删除" - ASSIGN = "assign", "分配" - REVOKE = "revoke", "撤销" - - -# ────────────────────────────────────────────────────────────── -# 3.4 complex -# ────────────────────────────────────────────────────────────── - -class SchoolType(models.TextChoices): - PRIMARY = "primary", "小学" - MIDDLE = "middle", "初中" - HIGH = "high", "高中" - K9 = "k9", "九年一贯制" - K12 = "k12", "十二年一贯制" - - -class SchoolNature(models.TextChoices): - PUBLIC = "public", "公立" - PRIVATE = "private", "私立" - INTERNATIONAL = "international", "国际" - - -class SchoolLevel(models.TextChoices): - NORMAL = "normal", "普通" - KEY = "key", "重点" - TOP = "top", "名校" - - -class ComplexBuildingType(models.TextChoices): - SLAB = "slab", "板楼" - TOWER = "tower", "塔楼" - SLAB_TOWER = "slab_tower", "板塔结合" - - -class ComplexWaterType(models.TextChoices): - CIVIL = "civil", "民水" - COMMERCIAL = "commercial", "商水" - - -class ComplexElectricityType(models.TextChoices): - CIVIL = "civil", "民电" - COMMERCIAL = "commercial", "商电" - - -class SchoolZoneType(models.TextChoices): - GUARANTEED = "guaranteed", "对口" - REFERENCE = "reference", "参考" - LOTTERY = "lottery", "摇号" - - -class ComplexPhotoCategory(models.TextChoices): - COMPLEX = "complex", "楼盘图" - LAYOUT = "layout", "户型图" - VR = "vr", "VR图" - 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 -# ────────────────────────────────────────────────────────────── - -class PropertyType(models.TextChoices): - RESIDENTIAL = "residential", "住宅" - VILLA = "villa", "别墅" - COMMERCIAL_RESIDENTIAL = "commercial_residential", "商住" - SHOP = "shop", "商铺" - OFFICE = "office", "写字楼" - OTHER = "other", "其他" - - -class PropertyStatus(models.TextChoices): - FOR_SALE = "for_sale", "出售" - FOR_RENT = "for_rent", "出租" - FOR_SALE_RENT = "for_sale_rent", "租售" - SUSPENDED = "suspended", "暂缓" - SOLD_ELSEWHERE = "sold_elsewhere", "他售" - RENTED_ELSEWHERE = "rented_elsewhere", "他租" - SOLD = "sold", "成交" - UNLISTED = "unlisted", "未挂牌" - - -class PropertyAttribute(models.TextChoices): - PUBLIC = "public", "公盘" - PRIVATE = "private", "私盘" - SPECIAL = "special", "特盘" - SEALED = "sealed", "封盘" - - -class PropertyOrientation(models.TextChoices): - EAST = "east", "东" - SOUTH = "south", "南" - WEST = "west", "西" - NORTH = "north", "北" - SOUTHEAST = "southeast", "东南" - NORTHEAST = "northeast", "东北" - EAST_WEST = "east_west", "东西" - SOUTH_NORTH = "south_north", "南北" - NORTHWEST = "northwest", "西北" - SOUTHWEST = "southwest", "西南" - - -class PropertyDecoration(models.TextChoices): - ROUGH = "rough", "毛坯" - PLAIN = "plain", "清水" - SIMPLE = "simple", "简装" - MEDIUM = "medium", "中装" - FINE = "fine", "精装" - LUXURY = "luxury", "豪装" - - -class PropertyHouseStatus(models.TextChoices): - OWNER_OCCUPIED = "owner_occupied", "业主自住" - VACANT = "vacant", "空置" - TENANT_OCCUPIED = "tenant_occupied", "租客在住" - UNKNOWN = "unknown", "未知" - - -class PropertyViewingTime(models.TextChoices): - ANYTIME = "anytime", "随时看房" - BY_APPOINTMENT = "by_appointment", "预约看房" - INCONVENIENT = "inconvenient", "不便看房" - - -class PropertyGrade(models.TextChoices): - A = "a", "A(急迫)" - B = "b", "B(较强)" - C = "c", "C(一般)" - D = "d", "D(较弱)" - - -class PropertyShopLocation(models.TextChoices): - STREET = "street", "临街商铺" - MALL = "mall", "商场" - RESIDENTIAL = "residential", "住宅底商" - GROUND_FLOOR = "ground_floor", "底层" - COMPLEX = "complex", "综合体" - - -class PropertyOwnershipNature(models.TextChoices): - COMMERCIAL = "commercial", "商品房" - REFORM_HOUSING = "reform_housing", "房改房" - COLLECTIVE = "collective", "集资房" - ECONOMIC = "economic", "经济适用房" - - -class PropertyPaymentMethod(models.TextChoices): - FULL = "full", "全款" - MORTGAGE = "mortgage", "按揭" - INSTALLMENT = "installment", "分期" - ADVANCE = "advance", "垫资" - - -class PropertyTaxIncluded(models.TextChoices): - EACH_PARTY = "each_party", "各付" - NET = "net", "净到手" - INCLUSIVE = "inclusive", "包税" - - -class PropertyContactGender(models.TextChoices): - MALE = "male", "先生" - FEMALE = "female", "女士" - - -class PropertyContactIdentity(models.TextChoices): - OWNER = "owner", "业主" - CONTACT = "contact", "联系人" - SUBLETTER = "subletter", "转租人" - TENANT = "tenant", "租客" - AGENT = "agent", "代理人" - CORPORATE = "corporate", "企业法人" - - -class PropertyListingType(models.TextChoices): - FOR_SALE = "for_sale", "出售挂牌" - FOR_RENT = "for_rent", "出租挂牌" - - -class PropertyListingHistoryStatus(models.TextChoices): - ACTIVE = "active", "生效中" - ENDED = "ended", "已结束" - - -class PropertyFollowLogType(models.TextChoices): - WRITTEN = "written", "手写跟进" - MODIFIED = "modified", "修改跟进" - SENSITIVE_OP = "sensitive_op", "敏感操作" - SENSITIVE_VIEW = "sensitive_view", "敏感查看" - OTHER = "other", "其他" - SYSTEM = "system", "系统" - - -class PropertyFollowAiTag(models.TextChoices): - AI_FOR_SALE = "ai_for_sale", "AI判断可售" - AI_NOT_FOR_SALE = "ai_not_for_sale", "AI判断不可售" - - -class PropertyFollowAttachmentFileType(models.TextChoices): - BMP = "bmp", "BMP" - JPG = "jpg", "JPG" - PNG = "png", "PNG" - SVG = "svg", "SVG" - GIF = "gif", "GIF" - - -class PropertyKeyType(models.TextChoices): - MECHANICAL = "mechanical", "机械钥匙" - PASSWORD = "password", "密码钥匙" - - -class PropertyCommissionOwnerType(models.TextChoices): - OWNER = "owner", "产权人本人" - AUTHORIZED_THIRD = "authorized_third", "授权第三方" - - -class PropertyCommissionStatus(models.TextChoices): - ACTIVE = "active", "有效" - EXPIRED = "expired", "过期" - CANCELLED = "cancelled", "取消" - - -class PropertyCommissionAttachmentCategory(models.TextChoices): - ID_CARD = "id_card", "身份证件" - PROPERTY_CERT = "property_cert", "产权证明" - COMMISSION_LETTER = "commission_letter", "委托书" - OTHER = "other", "其他" - - -class PropertyFieldSurveyStatus(models.TextChoices): - DRAFT = "draft", "草稿" - SUBMITTED = "submitted", "已提交" - - -class PropertySurveyPhotoCategory(models.TextChoices): - LAYOUT = "layout", "户型图" - LIVING_ROOM = "living_room", "客厅" - DINING_ROOM = "dining_room", "餐厅" - BEDROOM = "bedroom", "卧室" - BATHROOM = "bathroom", "卫生间" - KITCHEN = "kitchen", "厨房" - ENTRANCE = "entrance", "入户" - BALCONY = "balcony", "阳台" - STUDY = "study", "书房" - INDOOR_OTHER = "indoor_other", "室内其他" - OUTDOOR = "outdoor", "室外" - - -class PropertyPhotoCategory(models.TextChoices): - COVER = "cover", "封面" - ENTRANCE = "entrance", "入户" - LIVING_ROOM = "living_room", "客厅" - DINING_ROOM = "dining_room", "餐厅" - BEDROOM = "bedroom", "卧室" - BATHROOM = "bathroom", "卫生间" - KITCHEN = "kitchen", "厨房" - BALCONY = "balcony", "阳台" - STUDY = "study", "书房" - INDOOR_OTHER = "indoor_other", "室内其他" - OUTDOOR = "outdoor", "室外" - PANORAMA = "panorama", "全景" - - -class PropertyAttachmentCategory(models.TextChoices): - ID_CARD = "id_card", "身份证件" - PROPERTY_CERT = "property_cert", "产权证明" - COMMISSION_LETTER = "commission_letter", "委托书" - OTHER = "other", "其他" - - -class PropertyNumberHolderApprovalStatus(models.TextChoices): - PENDING = "pending", "待审批" - APPROVED = "approved", "已通过" - REJECTED = "rejected", "已驳回" - - -# ────────────────────────────────────────────────────────────── -# 3.6 client -# ────────────────────────────────────────────────────────────── - -class ClientType(models.TextChoices): - PRIVATE = "private", "私客" - PUBLIC = "public", "公客" - TRANSACTED = "transacted", "成交客" - - -class ClientStatus(models.TextChoices): - BUYING = "buying", "求购" - RENTING = "renting", "求租" - BUY_OR_RENT = "buy_or_rent", "租购" - SUSPENDED = "suspended", "暂缓" - BOUGHT = "bought", "已购" - RENTED_DONE = "rented_done", "已租" - PUBLIC = "public", "公客" - INVALID = "invalid", "无效" - - -class ClientGrade(models.TextChoices): - A = "A", "A(急迫)" - B = "B", "B(较强)" - C = "C", "C(一般)" - D = "D", "D(较弱)" - E = "E", "E(暂不关注)" - - -class ClientPropertyUsage(models.TextChoices): - RESIDENTIAL = "residential", "住宅" - VILLA = "villa", "别墅" - COMMERCIAL_RESIDENTIAL = "commercial_residential", "商住" - SHOP = "shop", "商铺" - OFFICE = "office", "写字楼" - OTHER = "other", "其他" - - -class ClientBuyingPurpose(models.TextChoices): - RIGID = "rigid", "刚需" - INVESTMENT = "investment", "投资" - SCHOOL_DISTRICT = "school_district", "学区" - UPGRADE = "upgrade", "改善" - COMMERCIAL = "commercial", "商用" - OTHER = "other", "其他" - - -class ClientPaymentMethod(models.TextChoices): - FULL = "full", "全额" - MORTGAGE = "mortgage", "商业贷款" - MORTGAGE_FUND = "mortgage_fund", "商贷+公积金" - FUND = "fund", "公积金" - - -class ClientPropertiesOwned(models.TextChoices): - NONE = "none", "无" - LOCAL_NONE = "local_none", "本地无/外地有" - LOCAL_HAS = "local_has", "本地有" - - -class ClientIdType(models.TextChoices): - ID_CARD = "id_card", "身份证" - PASSPORT = "passport", "护照" - HK_MACAO = "hk_macao", "港澳通行证" - OTHER = "other", "其他" - - -class ClientTransferToPublicType(models.TextChoices): - MANUAL = "manual", "手动转公" - AUTO = "auto", "自动转公" - MARKETING_JUMP = "marketing_jump", "营销客跳公" - RESOURCE_PUBLIC = "resource_public", "资料客素公" - - -class ClientInvalidReason(models.TextChoices): - INVALID_PHONE = "invalid_phone", "号码无效" - PEER_AGENT = "peer_agent", "同行" - AD = "ad", "广告推销" - NO_INTENT = "no_intent", "无意向" - OTHER = "other", "其他" - - -class ClientTransactedType(models.TextChoices): - BOUGHT = "bought", "我购" - RENTED = "rented", "我租" - - -class ClientTransactedPropertyType(models.TextChoices): - SECOND_HAND = "second_hand", "二手" - NEW_HOUSE = "new_house", "新房" - - -class ClientActivityLevel(models.TextChoices): - NEW_MATCHED = "new_matched", "新配对" - ACTIVE_7D = "active_7d", "7日活跃" - ACTIVE_30D = "active_30d", "30日活跃" - ACTIVE_90D = "active_90d", "90日活跃" - EXPIRING = "expiring", "即将过期" - FROZEN = "frozen", "暂缓中" - INVALID = "invalid", "无效" - - -class ClientContactGender(models.TextChoices): - MALE = "male", "先生" - FEMALE = "female", "女士" - - -class ClientRequirementType(models.TextChoices): - SECOND_HAND = "second_hand", "二手" - NEW_HOUSE = "new_house", "新房" - RENTAL = "rental", "租房" - - -class ClientFloorPreference(models.TextChoices): - NO_FIRST = "no_first", "不要一楼" - LOW = "low", "低楼层" - MID = "mid", "中楼层" - HIGH = "high", "高楼层" - NO_TOP = "no_top", "不要顶楼" - - -class ClientOrientation(models.TextChoices): - EAST = "east", "东" - SOUTH = "south", "南" - WEST = "west", "西" - NORTH = "north", "北" - - -class ClientDecoration(models.TextChoices): - ROUGH = "rough", "毛坯" - PLAIN = "plain", "清水" - SIMPLE = "simple", "简装" - MEDIUM = "medium", "中装" - FINE = "fine", "精装" - LUXURY = "luxury", "豪装" - - -class ClientBuildingAgeRange(models.TextChoices): - WITHIN_5Y = "within_5y", "5年内" - Y5_10 = "5_10y", "5-10年" - Y10_15 = "10_15y", "10-15年" - Y15_20 = "15_20y", "15-20年" - OVER_20Y = "over_20y", "20年以上" - - -class ClientFollowLogType(models.TextChoices): - WRITTEN = "written", "写入跟进" - MODIFIED = "modified", "修改跟进" - SENSITIVE_VIEW = "sensitive_view", "敏感查看" - OTHER = "other", "其他" - SYSTEM = "system", "系统" - - -class ClientViewingType(models.TextChoices): - APPOINTMENT = "appointment", "预约" - VIEWING = "viewing", "带看" - REVISIT = "revisit", "复看" - EMPTY = "empty", "空看" - - -class ClientViewingIntent(models.TextChoices): - INTERESTED = "interested", "感兴趣" - NOT_INTERESTED = "not_interested", "不感兴趣" - NEGOTIATING = "negotiating", "谈判中" - CANCELLED = "cancelled", "取消" - - -class ClientPropertyMatchSource(models.TextChoices): - RECORDED = "recorded", "录客配房" - SYSTEM = "system", "系统配房" - - -class ClientPropertyMatchGroup(models.TextChoices): - QUALITY_LAYOUT = "quality_layout", "优质户型" - PRICE_REDUCED = "price_reduced", "降价" - HOT = "hot", "热门" - NEWLY_LISTED = "newly_listed", "新上" - - -class ClientPropertyMatchStatus(models.TextChoices): - SUGGESTED = "suggested", "待推送" - SHARED = "shared", "已分享" - REJECTED = "rejected", "已反馈不合适" - VIEWED = "viewed", "客户已查看" - - -class ClientStatusLogChangeType(models.TextChoices): - STATUS_CHANGE = "status_change", "改状态" - GRADE_CHANGE = "grade_change", "改等级" - TO_PUBLIC = "to_public", "转公客" - TO_TRANSACTED = "to_transacted", "转成交" - TO_INVALID = "to_invalid", "转无效" - OWNER_CHANGE = "owner_change", "改归属人" - SOURCE_CHANGE = "source_change", "改来源" - MERGE = "merge", "合并客源" - - -# ────────────────────────────────────────────────────────────── -# 3.7 setting -# ────────────────────────────────────────────────────────────── - -class SettingValueType(models.TextChoices): - BOOL = "bool", "布尔" - INT = "int", "整数" - STRING = "string", "字符串" - ENUM = "enum", "枚举" - - -class FieldRuleModule(models.TextChoices): - PROPERTY = "property", "房源" - CLIENT = "client", "客源" - - -class FieldRuleEntityType(models.TextChoices): - RESIDENTIAL = "residential", "住宅" - VILLA = "villa", "别墅" - COMMERCIAL_RESIDENTIAL = "commercial_residential", "商住" - SHOP = "shop", "商铺" - OFFICE = "office", "写字楼" - OTHER = "other", "其他" - - -class FieldRuleTradeStatus(models.TextChoices): - SALE = "sale", "出售" - RENT = "rent", "出租" - SALE_RENT = "sale_rent", "租售" - ALL = "all", "全部" - - -class FieldRuleRequirement(models.TextChoices): - REQUIRED = "required", "必填" - OPTIONAL = "optional", "选填" - HIDDEN = "hidden", "隐藏" diff --git a/core/htmx.py b/core/htmx.py deleted file mode 100644 index 4be94dc..0000000 --- a/core/htmx.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.http import HttpResponse -from django.template.loader import render_to_string - - -def htmx_response(request, template: str, context: dict | None = None, status: int = 200) -> HttpResponse: - html = render_to_string(template, context or {}, request=request) - return HttpResponse(html, status=status) - - -def htmx_trigger(response: HttpResponse, event: str, detail: dict | None = None) -> HttpResponse: - import json - - payload = response.headers.get("HX-Trigger") - triggers = json.loads(payload) if payload else {} - triggers[event] = detail or {} - response.headers["HX-Trigger"] = json.dumps(triggers) - return response - - -def htmx_redirect(url: str) -> HttpResponse: - response = HttpResponse(status=204) - response.headers["HX-Redirect"] = url - return response diff --git a/core/middleware/__init__.py b/core/middleware/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/middleware/audit.py b/core/middleware/audit.py deleted file mode 100644 index db87144..0000000 --- a/core/middleware/audit.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.utils.deprecation import MiddlewareMixin - - -class AuditMiddleware(MiddlewareMixin): - def process_request(self, request): - request.audit_actor = getattr(request, "user", None) - return None diff --git a/core/models/__init__.py b/core/models/__init__.py deleted file mode 100644 index dcf3cdc..0000000 --- a/core/models/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -from core.models.base import ( - ActiveManager, - AuditedModel, - SoftDeleteModel, - TimeStampedModel, - UUIDPrimaryKeyModel, -) - -__all__ = [ - "ActiveManager", - "AuditedModel", - "SoftDeleteModel", - "TimeStampedModel", - "UUIDPrimaryKeyModel", -] diff --git a/core/models/base.py b/core/models/base.py deleted file mode 100644 index 0f52552..0000000 --- a/core/models/base.py +++ /dev/null @@ -1,71 +0,0 @@ -import uuid - -from django.db import models -from django.utils import timezone - - -class UUIDPrimaryKeyModel(models.Model): - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - - class Meta: - abstract = True - - -class TimeStampedModel(UUIDPrimaryKeyModel): - created_at = models.DateTimeField(auto_now_add=True, db_index=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - abstract = True - ordering = ["-created_at"] - - -class ActiveManager(models.Manager): - def get_queryset(self): - return super().get_queryset().filter(deleted_at__isnull=True) - - -class SoftDeleteModel(TimeStampedModel): - deleted_at = models.DateTimeField(null=True, blank=True, db_index=True) - - objects = ActiveManager() - all_objects = models.Manager() - - def delete(self, using=None, keep_parents=False): - self.deleted_at = timezone.now() - self.save(update_fields=["deleted_at"]) - - def hard_delete(self): - super().delete() - - def restore(self): - self.deleted_at = None - self.save(update_fields=["deleted_at"]) - - @property - def is_deleted(self): - return self.deleted_at is not None - - class Meta: - abstract = True - - -class AuditedModel(SoftDeleteModel): - created_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="%(app_label)s_%(class)s_created", - db_index=True, - ) - updated_by = models.ForeignKey( - "org.Staff", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="%(app_label)s_%(class)s_updated", - ) - - class Meta: - abstract = True diff --git a/core/templatetags/__init__.py b/core/templatetags/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/templatetags/heroicons.py b/core/templatetags/heroicons.py deleted file mode 100644 index 2884e00..0000000 --- a/core/templatetags/heroicons.py +++ /dev/null @@ -1,12 +0,0 @@ -from django import template -from django.utils.safestring import mark_safe - -register = template.Library() - - -@register.simple_tag -def heroicon(name: str, variant: str = "outline", css_class: str = "w-5 h-5") -> str: - return mark_safe( - f'' - ) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml deleted file mode 100644 index 9033e28..0000000 --- a/docker-compose.prod.yml +++ /dev/null @@ -1,57 +0,0 @@ -services: - web: - build: . - command: gunicorn config.asgi:application -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 --workers 4 - env_file: .env - depends_on: - - db - - redis - networks: - - fonrey_net - - db: - image: postgres:16-alpine - env_file: .env - environment: - POSTGRES_DB: ${DB_NAME} - POSTGRES_USER: ${DB_USER} - POSTGRES_PASSWORD: ${DB_PASSWORD} - volumes: - - fonrey_db_data:/var/lib/postgresql/data - networks: - - fonrey_net - - redis: - image: redis:7-alpine - volumes: - - fonrey_redis_data:/data - networks: - - fonrey_net - - celery: - build: . - command: celery -A config worker -l info --concurrency 4 - env_file: .env - depends_on: - - db - - redis - networks: - - fonrey_net - - celery-beat: - build: . - command: celery -A config beat -l info - env_file: .env - depends_on: - - db - - redis - networks: - - fonrey_net - -volumes: - fonrey_db_data: - fonrey_redis_data: - -networks: - fonrey_net: - driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 9d3f46d..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,78 +0,0 @@ -services: - web: - build: . - command: uvicorn config.asgi:application --host 0.0.0.0 --port 8000 --reload - ports: - - "8001:8000" - env_file: .env - volumes: - - .:/app - depends_on: - - db - - redis - networks: - - fonrey_net - - db: - image: postgres:16-alpine - ports: - - "5432:5432" - env_file: .env - environment: - POSTGRES_DB: ${DB_NAME} - POSTGRES_USER: ${DB_USER} - POSTGRES_PASSWORD: ${DB_PASSWORD} - volumes: - - fonrey_db_data:/var/lib/postgresql/data - networks: - - fonrey_net - - redis: - image: redis:7-alpine - ports: - - "6379:6379" - volumes: - - fonrey_redis_data:/data - networks: - - fonrey_net - - celery: - build: . - command: celery -A config worker -l info - env_file: .env - volumes: - - .:/app - depends_on: - - db - - redis - networks: - - fonrey_net - - celery-beat: - build: . - command: celery -A config beat -l info - env_file: .env - volumes: - - .:/app - depends_on: - - db - - redis - networks: - - fonrey_net - - tailwind: - image: node:20-alpine - working_dir: /app - command: sh -c "npm install && npm run watch" - volumes: - - .:/app - networks: - - fonrey_net - -volumes: - fonrey_db_data: - fonrey_redis_data: - -networks: - fonrey_net: - driver: bridge diff --git a/manage.py b/manage.py deleted file mode 100644 index a13257f..0000000 --- a/manage.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" -import os -import sys - - -def main() -> None: - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.development") - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH? Did you forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == "__main__": - main() diff --git a/package.json b/package.json deleted file mode 100644 index 7ac8260..0000000 --- a/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "fonrey-frontend", - "version": "1.0.0", - "private": true, - "scripts": { - "build": "tailwindcss -i ./static/css/main.css -o ./static/css/output.css --minify", - "watch": "tailwindcss -i ./static/css/main.css -o ./static/css/output.css --watch" - }, - "devDependencies": { - "tailwindcss": "^3.4.0" - } -} diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 366c720..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,20 +0,0 @@ -[tool.ruff] -line-length = 100 -target-version = "py312" - -[tool.ruff.lint] -select = ["E", "F", "I", "N", "W", "UP"] -ignore = ["E501"] - -[tool.black] -line-length = 100 -target-version = ["py312"] - -[tool.isort] -profile = "black" -line_length = 100 - -[tool.pytest.ini_options] -DJANGO_SETTINGS_MODULE = "config.settings.testing" -python_files = ["test_*.py", "*_test.py"] -addopts = "--reuse-db --cov=apps --cov=core --cov-report=term-missing -n auto" diff --git a/requirements/base.txt b/requirements/base.txt deleted file mode 100644 index 0f80b6a..0000000 --- a/requirements/base.txt +++ /dev/null @@ -1,20 +0,0 @@ -Django==4.2.16 -django-tenants==3.7.0 -psycopg[binary]>=3.1.18,<3.3 -django-redis==5.4.0 -celery==5.4.0 -django-celery-beat==2.7.0 -django-celery-results==2.5.1 -django-storages[s3]==1.14.4 -boto3==1.35.0 -cryptography==43.0.0 -whitenoise==6.8.2 -gunicorn==23.0.0 -uvicorn[standard]==0.32.0 -sentry-sdk[django]==2.18.0 -python-decouple==3.8 -Pillow==11.0.0 -djangorestframework==3.15.2 -drf-spectacular==0.27.2 -django-htmx==1.21.0 -django-extensions==3.2.3 diff --git a/requirements/development.txt b/requirements/development.txt deleted file mode 100644 index e26673a..0000000 --- a/requirements/development.txt +++ /dev/null @@ -1,13 +0,0 @@ --r base.txt -ruff==0.7.0 -black==24.10.0 -pytest-django==4.9.0 -factory-boy==3.3.1 -pytest-mock==3.14.0 -responses==0.25.3 -pytest-cov==5.0.0 -pytest-xdist==3.6.1 -django-debug-toolbar==4.4.6 -schemathesis==3.36.0 -pytest-playwright==0.6.2 -playwright==1.49.1 diff --git a/requirements/production.txt b/requirements/production.txt deleted file mode 100644 index a3e81b8..0000000 --- a/requirements/production.txt +++ /dev/null @@ -1 +0,0 @@ --r base.txt diff --git a/shared/__init__.py b/shared/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/shared/apps.py b/shared/apps.py deleted file mode 100644 index 5bfd568..0000000 --- a/shared/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class SharedConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "shared" diff --git a/static/css/main.css b/static/css/main.css deleted file mode 100644 index b5c61c9..0000000 --- a/static/css/main.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/static/js/main.js b/static/js/main.js deleted file mode 100644 index 2893674..0000000 --- a/static/js/main.js +++ /dev/null @@ -1,32 +0,0 @@ -document.body.addEventListener("htmx:afterRequest", function (event) { - var trigger = event.detail.xhr.getResponseHeader("HX-Trigger"); - if (!trigger) return; - - try { - var payload = JSON.parse(trigger); - var toast = payload["fonrey:toast"]; - if (!toast) return; - - var container = document.getElementById("toast-container"); - if (!container) return; - - var node = document.createElement("div"); - node.className = - "bg-white border border-neutral-200 rounded-lg shadow-xs px-4 py-3 min-w-[280px]"; - node.setAttribute("data-toast-type", toast.type || "info"); - node.textContent = toast.message || ""; - container.appendChild(node); - - setTimeout(function () { - node.remove(); - }, 4000); - } catch (e) { - } -}); - -document.body.addEventListener("htmx:configRequest", function (event) { - var meta = document.querySelector('meta[name="csrf-token"]'); - if (meta) { - event.detail.headers["X-CSRFToken"] = meta.getAttribute("content"); - } -}); diff --git a/static/vendor/.gitkeep b/static/vendor/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tailwind.config.js b/tailwind.config.js deleted file mode 100644 index 255f3a3..0000000 --- a/tailwind.config.js +++ /dev/null @@ -1,61 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: [ - "./templates/**/*.html", - "./apps/**/templates/**/*.html", - "./static/js/**/*.js", - ], - theme: { - extend: { - colors: { - primary: { - 50: "#F0FDFA", - 100: "#CCFBF1", - 200: "#99F6E4", - 300: "#5EEAD4", - 400: "#2DD4BF", - 500: "#14B8A6", - 600: "#0F766E", - 700: "#115E59", - 800: "#134E4A", - }, - neutral: { - 50: "#F8FAFC", - 100: "#F1F5F9", - 200: "#E2E8F0", - 300: "#CBD5E1", - 400: "#94A3B8", - 500: "#64748B", - 600: "#475569", - 700: "#334155", - 800: "#1E293B", - 900: "#0F172A", - }, - success: { 600: "#16A34A" }, - warning: { 600: "#D97706" }, - danger: { 600: "#DC2626" }, - info: { 600: "#2563EB" }, - }, - fontFamily: { - sans: ["Inter", "PingFang SC", "Microsoft YaHei", "sans-serif"], - }, - zIndex: { - 60: "60", - 70: "70", - }, - boxShadow: { - xs: "0 1px 2px 0 rgb(0 0 0 / 0.05)", - }, - keyframes: { - "slide-in-right": { - "0%": { transform: "translateX(100%)", opacity: "0" }, - "100%": { transform: "translateX(0)", opacity: "1" }, - }, - }, - animation: { - "slide-in-right": "slide-in-right 0.2s ease-out", - }, - }, - }, - plugins: [], -}; diff --git a/templates/base.html b/templates/base.html deleted file mode 100644 index e324496..0000000 --- a/templates/base.html +++ /dev/null @@ -1,25 +0,0 @@ -{% load static %} - - - - - - - {% block title %}Fonrey{% endblock %} - - - - {% block extra_head %}{% endblock %} - - - - - - - {% block content %}{% endblock %} - -
- - {% block extra_js %}{% endblock %} - - diff --git a/templates/components/empty-state.html b/templates/components/empty-state.html deleted file mode 100644 index 9cf6ddb..0000000 --- a/templates/components/empty-state.html +++ /dev/null @@ -1,3 +0,0 @@ -
-

{% block empty_message %}暂无数据{% endblock %}

-
diff --git a/templates/components/modal.html b/templates/components/modal.html deleted file mode 100644 index 3f5e5e7..0000000 --- a/templates/components/modal.html +++ /dev/null @@ -1,9 +0,0 @@ -
-
- {% block modal_body %}{% endblock %} -
-
diff --git a/templates/components/pagination.html b/templates/components/pagination.html deleted file mode 100644 index 85a3e0e..0000000 --- a/templates/components/pagination.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/templates/components/sidebar.html b/templates/components/sidebar.html deleted file mode 100644 index 26f763d..0000000 --- a/templates/components/sidebar.html +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/templates/components/toast.html b/templates/components/toast.html deleted file mode 100644 index 8bcbe3a..0000000 --- a/templates/components/toast.html +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/templates/components/topbar.html b/templates/components/topbar.html deleted file mode 100644 index 7d60a0a..0000000 --- a/templates/components/topbar.html +++ /dev/null @@ -1,15 +0,0 @@ -
-
- Fonrey -
- - - -
- - - -
-
diff --git a/templates/errors/403.html b/templates/errors/403.html deleted file mode 100644 index f9be568..0000000 --- a/templates/errors/403.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "base.html" %} -{% block title %}403 - 无权限{% endblock %} -{% block content %} -
-
-

403

-

您没有访问该资源的权限

-
-
-{% endblock %} diff --git a/templates/errors/404.html b/templates/errors/404.html deleted file mode 100644 index 57ea23f..0000000 --- a/templates/errors/404.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "base.html" %} -{% block title %}404 - 页面未找到{% endblock %} -{% block content %} -
-
-

404

-

页面未找到

-
-
-{% endblock %} diff --git a/templates/errors/500.html b/templates/errors/500.html deleted file mode 100644 index 5cb6edc..0000000 --- a/templates/errors/500.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "base.html" %} -{% block title %}500 - 服务器错误{% endblock %} -{% block content %} -
-
-

500

-

服务器内部错误

-
-
-{% endblock %} diff --git a/templates/layouts/app.html b/templates/layouts/app.html deleted file mode 100644 index 10dd435..0000000 --- a/templates/layouts/app.html +++ /dev/null @@ -1,38 +0,0 @@ -{% extends "base.html" %} - -{% block body_class %}bg-neutral-50 text-neutral-900{% endblock %} - -{% block content %} -
- {% include "components/topbar.html" %} - -
- {% include "components/sidebar.html" %} - -
- {% block main %}{% endblock %} -
-
-
- - - -{% endblock %} diff --git a/templates/layouts/auth.html b/templates/layouts/auth.html deleted file mode 100644 index 6d3beb4..0000000 --- a/templates/layouts/auth.html +++ /dev/null @@ -1,9 +0,0 @@ -{% extends "base.html" %} - -{% block body_class %}bg-neutral-50 min-h-screen flex items-center justify-center{% endblock %} - -{% block content %} -
- {% block auth_content %}{% endblock %} -
-{% endblock %} diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 0482f14..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,9 +0,0 @@ -import pytest -from django_tenants.test.client import TenantClient -from django_tenants.utils import schema_context - - -@pytest.fixture -def tenant_client(db, tenant): - with schema_context(tenant.schema_name): - yield TenantClient(tenant) diff --git a/tests/e2e/.gitkeep b/tests/e2e/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/e2e/__init__.py b/tests/e2e/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/client/.gitkeep b/tests/integration/client/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/client/__init__.py b/tests/integration/client/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/property/.gitkeep b/tests/integration/property/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/property/__init__.py b/tests/integration/property/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/release/__init__.py b/tests/integration/release/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/release/test_client_update_api.py b/tests/integration/release/test_client_update_api.py deleted file mode 100644 index cbe5d51..0000000 --- a/tests/integration/release/test_client_update_api.py +++ /dev/null @@ -1,6 +0,0 @@ -import pytest - - -@pytest.mark.skip(reason="skeleton - implement after release endpoints are wired") -def test_client_update_api_contract(): - pass