feat: scaffold Django multi-tenant project with 5 of 9 apps
Phase 1 scaffolding: config/, core/, base models, AES-256-GCM phone encryption, enums mirror apps.tenant: Tenant + Domain (django-tenants) apps.org: 11 models (OrgUnit hierarchy, Staff, audit logs) apps.account: 4 models (UserAccount as AUTH_USER_MODEL, login/password tracking) apps.permission: 7 models (RBAC + overrides + datascope + append-only changelog) apps.region: 5 models (District, BusinessArea, MetroLine, MetroStation, School) All migrations generated, manage.py check passes
This commit is contained in:
0
apps/__init__.py
Normal file
0
apps/__init__.py
Normal file
0
apps/account/__init__.py
Normal file
0
apps/account/__init__.py
Normal file
7
apps/account/apps.py
Normal file
7
apps/account/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.account"
|
||||
label = "account"
|
||||
81
apps/account/migrations/0001_initial.py
Normal file
81
apps/account/migrations/0001_initial.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# 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')],
|
||||
},
|
||||
),
|
||||
]
|
||||
50
apps/account/migrations/0002_initial.py
Normal file
50
apps/account/migrations/0002_initial.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
0
apps/account/migrations/__init__.py
Normal file
0
apps/account/migrations/__init__.py
Normal file
15
apps/account/models/__init__.py
Normal file
15
apps/account/models/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from apps.account.models.account import (
|
||||
LoginAttempt,
|
||||
PasswordHistory,
|
||||
PasswordResetToken,
|
||||
UserAccount,
|
||||
UserAccountManager,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LoginAttempt",
|
||||
"PasswordHistory",
|
||||
"PasswordResetToken",
|
||||
"UserAccount",
|
||||
"UserAccountManager",
|
||||
]
|
||||
151
apps/account/models/account.py
Normal file
151
apps/account/models/account.py
Normal file
@@ -0,0 +1,151 @@
|
||||
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)
|
||||
email = models.EmailField(null=True, blank=True)
|
||||
phone_enc = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="AES-256-GCM ciphertext of phone (core.encryption.PhoneEncryption).",
|
||||
)
|
||||
phone_hash = models.CharField(max_length=64, null=True, blank=True)
|
||||
staff = models.OneToOneField(
|
||||
"org.Staff",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="account",
|
||||
)
|
||||
is_tenant_admin = models.BooleanField(default=False)
|
||||
status = models.CharField(
|
||||
max_length=10,
|
||||
choices=UserAccountStatus.choices,
|
||||
default=UserAccountStatus.ACTIVE,
|
||||
)
|
||||
is_initial_password = models.BooleanField(default=True)
|
||||
locked_until = models.DateTimeField(null=True, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_by = models.ForeignKey(
|
||||
"self",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="created_accounts",
|
||||
)
|
||||
|
||||
USERNAME_FIELD = "username"
|
||||
REQUIRED_FIELDS: list = []
|
||||
|
||||
objects = UserAccountManager()
|
||||
|
||||
class Meta:
|
||||
db_table = "user_accounts"
|
||||
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)
|
||||
ip_address = models.GenericIPAddressField()
|
||||
user_agent = models.TextField(null=True, blank=True)
|
||||
success = models.BooleanField()
|
||||
failure_reason = models.CharField(
|
||||
max_length=30,
|
||||
null=True,
|
||||
blank=True,
|
||||
choices=LoginFailureReason.choices,
|
||||
)
|
||||
attempted_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
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",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class PasswordResetToken(models.Model):
|
||||
user = models.ForeignKey(
|
||||
"account.UserAccount",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="reset_tokens",
|
||||
)
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
db_table = "password_reset_tokens"
|
||||
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",
|
||||
)
|
||||
password_hash = models.CharField(max_length=128)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "password_histories"
|
||||
ordering = ["-created_at"]
|
||||
indexes = [
|
||||
models.Index(fields=["user", "-created_at"], name="idx_pw_histories_user"),
|
||||
]
|
||||
0
apps/account/serializers.py
Normal file
0
apps/account/serializers.py
Normal file
0
apps/account/services/__init__.py
Normal file
0
apps/account/services/__init__.py
Normal file
0
apps/account/tasks.py
Normal file
0
apps/account/tasks.py
Normal file
0
apps/account/templates/account/.gitkeep
Normal file
0
apps/account/templates/account/.gitkeep
Normal file
0
apps/account/tests/__init__.py
Normal file
0
apps/account/tests/__init__.py
Normal file
5
apps/account/urls.py
Normal file
5
apps/account/urls.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.urls import path
|
||||
|
||||
app_name = "account"
|
||||
|
||||
urlpatterns: list = []
|
||||
0
apps/account/views.py
Normal file
0
apps/account/views.py
Normal file
0
apps/client/__init__.py
Normal file
0
apps/client/__init__.py
Normal file
7
apps/client/apps.py
Normal file
7
apps/client/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ClientConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.client"
|
||||
label = "fonrey_client"
|
||||
0
apps/client/migrations/__init__.py
Normal file
0
apps/client/migrations/__init__.py
Normal file
0
apps/client/models/__init__.py
Normal file
0
apps/client/models/__init__.py
Normal file
0
apps/complex/__init__.py
Normal file
0
apps/complex/__init__.py
Normal file
7
apps/complex/apps.py
Normal file
7
apps/complex/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ComplexConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.complex"
|
||||
label = "fonrey_complex"
|
||||
0
apps/complex/migrations/__init__.py
Normal file
0
apps/complex/migrations/__init__.py
Normal file
0
apps/complex/models/__init__.py
Normal file
0
apps/complex/models/__init__.py
Normal file
0
apps/org/__init__.py
Normal file
0
apps/org/__init__.py
Normal file
7
apps/org/apps.py
Normal file
7
apps/org/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class OrgConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.org"
|
||||
label = "org"
|
||||
300
apps/org/migrations/0001_initial.py
Normal file
300
apps/org/migrations/0001_initial.py
Normal file
@@ -0,0 +1,300 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
0
apps/org/migrations/__init__.py
Normal file
0
apps/org/migrations/__init__.py
Normal file
26
apps/org/models/__init__.py
Normal file
26
apps/org/models/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
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",
|
||||
]
|
||||
57
apps/org/models/org_unit.py
Normal file
57
apps/org/models/org_unit.py
Normal file
@@ -0,0 +1,57 @@
|
||||
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)
|
||||
type = models.CharField(max_length=20, choices=OrgUnitType.choices)
|
||||
parent = models.ForeignKey(
|
||||
"self",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.RESTRICT,
|
||||
related_name="children",
|
||||
db_index=True,
|
||||
)
|
||||
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(
|
||||
max_length=10,
|
||||
choices=OrgUnitAttribute.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
address_city = models.CharField(max_length=50, blank=True, default="")
|
||||
address_district = models.CharField(max_length=50, blank=True, default="")
|
||||
address_detail = models.CharField(max_length=200, blank=True, default="")
|
||||
latitude = models.DecimalField(max_digits=10, decimal_places=7, null=True, blank=True)
|
||||
longitude = models.DecimalField(max_digits=10, decimal_places=7, null=True, blank=True)
|
||||
manager = models.ForeignKey(
|
||||
"org.Staff",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="managed_org_units",
|
||||
)
|
||||
established_at = models.DateField(null=True, blank=True)
|
||||
phone = models.CharField(max_length=30, blank=True, default="")
|
||||
ext_start = models.IntegerField(null=True, blank=True)
|
||||
ext_end = models.IntegerField(null=True, blank=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "org_units"
|
||||
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})"
|
||||
138
apps/org/models/staff.py
Normal file
138
apps/org/models/staff.py
Normal file
@@ -0,0 +1,138 @@
|
||||
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,
|
||||
)
|
||||
user = models.OneToOneField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="staff_profile",
|
||||
)
|
||||
name = models.CharField(max_length=50)
|
||||
nickname = models.CharField(max_length=50, blank=True, default="")
|
||||
employee_no = models.CharField(max_length=30, null=True, blank=True, unique=True)
|
||||
role = models.CharField(max_length=30, choices=StaffRole.choices)
|
||||
job_title = models.CharField(max_length=100, blank=True, default="")
|
||||
job_category = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
default="",
|
||||
help_text="Job classification (e.g. '置业顾问' = agent qualification flag).",
|
||||
)
|
||||
job_level = models.SmallIntegerField(null=True, blank=True)
|
||||
supervisor = models.ForeignKey(
|
||||
"self",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="subordinates",
|
||||
)
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=StaffStatus.choices,
|
||||
default=StaffStatus.ACTIVE,
|
||||
)
|
||||
phone_enc = models.BinaryField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="AES-256-GCM encrypted phone (DATA_MODEL_ORG §3.2).",
|
||||
)
|
||||
phone_hash = models.CharField(max_length=64, null=True, blank=True, db_index=True)
|
||||
phone_hide = models.BooleanField(default=False)
|
||||
email = models.EmailField(max_length=255, blank=True, default="")
|
||||
extension = models.CharField(max_length=20, blank=True, default="")
|
||||
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(null=True, blank=True)
|
||||
rejoined_at = models.DateField(null=True, blank=True)
|
||||
resigned_at = models.DateField(null=True, blank=True)
|
||||
joined_count = models.SmallIntegerField(default=1)
|
||||
industry_exp_years = models.SmallIntegerField(null=True, blank=True)
|
||||
mentor = models.ForeignKey(
|
||||
"self",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="mentees",
|
||||
)
|
||||
business_type = models.CharField(max_length=50, blank=True, default="")
|
||||
bank_name = models.CharField(max_length=100, blank=True, default="")
|
||||
bank_account = models.CharField(max_length=50, blank=True, default="")
|
||||
partner_no = models.CharField(max_length=50, blank=True, default="")
|
||||
recruit_by = models.ForeignKey(
|
||||
"self",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="recruited_staff",
|
||||
)
|
||||
recruit_source = models.CharField(max_length=50, blank=True, default="")
|
||||
referrer = models.ForeignKey(
|
||||
"self",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="referred_staff",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "staff"
|
||||
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,
|
||||
)
|
||||
gender = models.CharField(max_length=10, choices=StaffGender.choices, blank=True, default="")
|
||||
id_type = models.CharField(max_length=20, choices=StaffIdType.choices, blank=True, default="")
|
||||
id_number_enc = models.BinaryField(null=True, blank=True)
|
||||
id_number_hash = models.CharField(max_length=64, null=True, blank=True, db_index=True)
|
||||
id_verified = models.BooleanField(default=False)
|
||||
id_verified_at = models.DateTimeField(null=True, blank=True)
|
||||
birthdate = models.DateField(null=True, blank=True)
|
||||
native_place = models.CharField(max_length=100, blank=True, default="")
|
||||
domicile_type = models.CharField(max_length=20, blank=True, default="")
|
||||
marital_status = models.CharField(max_length=20, blank=True, default="")
|
||||
political_status = models.CharField(max_length=20, blank=True, default="")
|
||||
has_children = models.BooleanField(null=True, blank=True)
|
||||
education_level = models.CharField(max_length=20, blank=True, default="")
|
||||
ethnicity = models.CharField(max_length=20, blank=True, default="")
|
||||
domicile_address = models.CharField(max_length=200, blank=True, default="")
|
||||
residence_address = models.CharField(max_length=200, blank=True, default="")
|
||||
work_start_date = models.DateField(null=True, blank=True)
|
||||
emergency_contact = models.CharField(max_length=50, blank=True, default="")
|
||||
emergency_phone_enc = models.BinaryField(null=True, blank=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
updated_by = models.ForeignKey(
|
||||
"org.Staff",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="updated_personal_info",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "staff_personal_info"
|
||||
164
apps/org/models/staff_logs.py
Normal file
164
apps/org/models/staff_logs.py
Normal file
@@ -0,0 +1,164 @@
|
||||
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",
|
||||
)
|
||||
transfer_type = models.CharField(max_length=30, choices=StaffTransferType.choices)
|
||||
old_value = models.JSONField(null=True, blank=True)
|
||||
new_value = models.JSONField(null=True, blank=True)
|
||||
transfer_date = models.DateField()
|
||||
remarks = models.CharField(max_length=50, blank=True, default="")
|
||||
operator = models.ForeignKey(
|
||||
"org.Staff",
|
||||
on_delete=models.RESTRICT,
|
||||
related_name="operated_transfers",
|
||||
)
|
||||
operated_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
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"),
|
||||
]
|
||||
|
||||
|
||||
class StaffRewardPunish(SoftDeleteModel):
|
||||
staff = models.ForeignKey(
|
||||
"org.Staff",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="reward_punish_records",
|
||||
)
|
||||
rp_date = models.DateField()
|
||||
category = models.CharField(
|
||||
max_length=50,
|
||||
help_text="Configurable lookup_items domain: org.reward_punish_category.",
|
||||
)
|
||||
name = models.CharField(max_length=100)
|
||||
remarks = models.TextField(blank=True, default="")
|
||||
created_by = models.ForeignKey(
|
||||
"org.Staff",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="created_reward_punish",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "staff_reward_punish"
|
||||
|
||||
|
||||
class StaffAccount(TimeStampedModel):
|
||||
staff = models.ForeignKey(
|
||||
"org.Staff",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="external_accounts",
|
||||
)
|
||||
platform = models.CharField(max_length=30, choices=StaffAccountPlatform.choices)
|
||||
account_no = models.CharField(max_length=100, blank=True, default="")
|
||||
is_real_name_match = models.BooleanField(null=True, blank=True)
|
||||
is_bound = models.BooleanField(default=False)
|
||||
bound_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "staff_accounts"
|
||||
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",
|
||||
)
|
||||
company = models.CharField(max_length=200)
|
||||
job_title = models.CharField(max_length=100, blank=True, default="")
|
||||
start_date = models.DateField(null=True, blank=True)
|
||||
end_date = models.DateField(null=True, blank=True)
|
||||
reason = models.CharField(max_length=200, blank=True, default="")
|
||||
reference_name = models.CharField(max_length=50, blank=True, default="")
|
||||
reference_phone = models.CharField(max_length=30, blank=True, default="")
|
||||
|
||||
class Meta:
|
||||
db_table = "staff_work_experiences"
|
||||
|
||||
|
||||
class StaffEducation(TimeStampedModel):
|
||||
staff = models.ForeignKey(
|
||||
"org.Staff",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="educations",
|
||||
)
|
||||
stage = models.CharField(max_length=30, blank=True, default="")
|
||||
school = models.CharField(max_length=200)
|
||||
major = models.CharField(max_length=100, blank=True, default="")
|
||||
start_date = models.DateField(null=True, blank=True)
|
||||
end_date = models.DateField(null=True, blank=True)
|
||||
enrollment_status = models.CharField(max_length=30, blank=True, default="")
|
||||
degree = models.CharField(max_length=30, blank=True, default="")
|
||||
|
||||
class Meta:
|
||||
db_table = "staff_educations"
|
||||
|
||||
|
||||
class StaffTraining(TimeStampedModel):
|
||||
staff = models.ForeignKey(
|
||||
"org.Staff",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="trainings",
|
||||
)
|
||||
training_name = models.CharField(max_length=200)
|
||||
training_date = models.DateField(null=True, blank=True)
|
||||
certificate = models.CharField(max_length=200, blank=True, default="")
|
||||
|
||||
class Meta:
|
||||
db_table = "staff_trainings"
|
||||
|
||||
|
||||
class StaffFamilyMember(TimeStampedModel):
|
||||
staff = models.ForeignKey(
|
||||
"org.Staff",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="family_members",
|
||||
)
|
||||
relation = models.CharField(max_length=30)
|
||||
name = models.CharField(max_length=50)
|
||||
birthdate = models.DateField(null=True, blank=True)
|
||||
occupation = models.CharField(max_length=100, blank=True, default="")
|
||||
work_unit = models.CharField(max_length=200, blank=True, default="")
|
||||
phone_enc = models.BinaryField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "staff_family_members"
|
||||
|
||||
|
||||
class StaffRemark(SoftDeleteModel):
|
||||
staff = models.ForeignKey(
|
||||
"org.Staff",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="remarks",
|
||||
)
|
||||
content = models.TextField()
|
||||
created_by = models.ForeignKey(
|
||||
"org.Staff",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="created_remarks",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "staff_remarks"
|
||||
0
apps/org/serializers.py
Normal file
0
apps/org/serializers.py
Normal file
0
apps/org/services/__init__.py
Normal file
0
apps/org/services/__init__.py
Normal file
0
apps/org/tasks.py
Normal file
0
apps/org/tasks.py
Normal file
0
apps/org/templates/org/.gitkeep
Normal file
0
apps/org/templates/org/.gitkeep
Normal file
0
apps/org/tests/__init__.py
Normal file
0
apps/org/tests/__init__.py
Normal file
5
apps/org/urls.py
Normal file
5
apps/org/urls.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.urls import path
|
||||
|
||||
app_name = "org"
|
||||
|
||||
urlpatterns: list = []
|
||||
0
apps/org/views.py
Normal file
0
apps/org/views.py
Normal file
0
apps/permission/__init__.py
Normal file
0
apps/permission/__init__.py
Normal file
7
apps/permission/apps.py
Normal file
7
apps/permission/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PermissionConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.permission"
|
||||
label = "fonrey_permission"
|
||||
249
apps/permission/migrations/0001_initial.py
Normal file
249
apps/permission/migrations/0001_initial.py
Normal file
@@ -0,0 +1,249 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
0
apps/permission/migrations/__init__.py
Normal file
0
apps/permission/migrations/__init__.py
Normal file
18
apps/permission/models/__init__.py
Normal file
18
apps/permission/models/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from apps.permission.models.permission_def import PermissionDef
|
||||
from apps.permission.models.role import Role, RolePermission
|
||||
from apps.permission.models.staff_perm import (
|
||||
PermissionChangeLog,
|
||||
StaffDataScope,
|
||||
StaffPermissionOverride,
|
||||
StaffRole,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"PermissionChangeLog",
|
||||
"PermissionDef",
|
||||
"Role",
|
||||
"RolePermission",
|
||||
"StaffDataScope",
|
||||
"StaffPermissionOverride",
|
||||
"StaffRole",
|
||||
]
|
||||
46
apps/permission/models/permission_def.py
Normal file
46
apps/permission/models/permission_def.py
Normal file
@@ -0,0 +1,46 @@
|
||||
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)
|
||||
module = models.CharField(max_length=50, choices=PermissionModule.choices)
|
||||
sub_module = models.CharField(max_length=50, blank=True, default="")
|
||||
group_name = models.CharField(max_length=100)
|
||||
name = models.CharField(max_length=200)
|
||||
description = models.TextField(blank=True, default="")
|
||||
value_type = models.CharField(max_length=20, choices=PermissionValueType.choices)
|
||||
scope_choices = models.JSONField(default=list, blank=True)
|
||||
integer_min = models.IntegerField(null=True, blank=True)
|
||||
integer_max = models.IntegerField(null=True, blank=True)
|
||||
default_value = models.JSONField(default=dict)
|
||||
max_allowed_categories = ArrayField(
|
||||
models.CharField(max_length=50),
|
||||
default=list,
|
||||
blank=True,
|
||||
)
|
||||
sort_order = models.PositiveIntegerField(default=0)
|
||||
is_active = models.BooleanField(default=True)
|
||||
is_deprecated = models.BooleanField(default=False)
|
||||
version = models.PositiveIntegerField(default=1)
|
||||
|
||||
class Meta:
|
||||
db_table = "permission_defs"
|
||||
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})"
|
||||
91
apps/permission/models/role.py
Normal file
91
apps/permission/models/role.py
Normal file
@@ -0,0 +1,91 @@
|
||||
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)
|
||||
category = models.CharField(max_length=30, choices=PermissionRoleCategory.choices)
|
||||
description = models.TextField(blank=True, default="")
|
||||
template_role = models.ForeignKey(
|
||||
"fonrey_permission.Role",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="derived_roles",
|
||||
)
|
||||
is_system_builtin = models.BooleanField(default=False)
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_by = models.ForeignKey(
|
||||
"org.Staff",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="permission_roles_created",
|
||||
)
|
||||
updated_by = models.ForeignKey(
|
||||
"org.Staff",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="permission_roles_updated",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "roles"
|
||||
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",
|
||||
)
|
||||
permission_def = models.ForeignKey(
|
||||
"fonrey_permission.PermissionDef",
|
||||
on_delete=models.PROTECT,
|
||||
related_name="role_assignments",
|
||||
)
|
||||
value = models.JSONField()
|
||||
updated_by = models.ForeignKey(
|
||||
"org.Staff",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="role_permissions_updated",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "role_permissions"
|
||||
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}"
|
||||
200
apps/permission/models/staff_perm.py
Normal file
200
apps/permission/models/staff_perm.py
Normal file
@@ -0,0 +1,200 @@
|
||||
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",
|
||||
)
|
||||
role = models.ForeignKey(
|
||||
"fonrey_permission.Role",
|
||||
on_delete=models.PROTECT,
|
||||
related_name="staff_links",
|
||||
)
|
||||
is_primary = models.BooleanField(default=False)
|
||||
assigned_at = models.DateTimeField(auto_now_add=True)
|
||||
assigned_by = models.ForeignKey(
|
||||
"org.Staff",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="staff_role_assignments_made",
|
||||
)
|
||||
valid_from = models.DateField(null=True, blank=True)
|
||||
valid_until = models.DateField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "staff_roles"
|
||||
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",
|
||||
)
|
||||
permission_def = models.ForeignKey(
|
||||
"fonrey_permission.PermissionDef",
|
||||
on_delete=models.PROTECT,
|
||||
related_name="staff_overrides",
|
||||
)
|
||||
value = models.JSONField()
|
||||
override_mode = models.CharField(
|
||||
max_length=10,
|
||||
choices=PermissionOverrideMode.choices,
|
||||
default=PermissionOverrideMode.REPLACE,
|
||||
)
|
||||
reason = models.TextField(blank=True, default="")
|
||||
modified_by = models.ForeignKey(
|
||||
"org.Staff",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="staff_overrides_modified",
|
||||
)
|
||||
modified_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "staff_permission_overrides"
|
||||
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",
|
||||
)
|
||||
scope_type = models.CharField(
|
||||
max_length=20,
|
||||
choices=PermissionDataScopeType.choices,
|
||||
)
|
||||
org_unit = models.ForeignKey(
|
||||
"org.OrgUnit",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="data_scope_grants",
|
||||
)
|
||||
is_readable = models.BooleanField(default=True)
|
||||
is_writable = models.BooleanField(default=False)
|
||||
granted_by = models.ForeignKey(
|
||||
"org.Staff",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="data_scopes_granted",
|
||||
)
|
||||
granted_at = models.DateTimeField(auto_now_add=True)
|
||||
expires_at = models.DateTimeField(null=True, blank=True)
|
||||
reason = models.TextField(blank=True, default="")
|
||||
|
||||
class Meta:
|
||||
db_table = "staff_data_scopes"
|
||||
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,
|
||||
)
|
||||
target_id = models.UUIDField()
|
||||
staff = models.ForeignKey(
|
||||
"org.Staff",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="permission_change_logs_affecting",
|
||||
)
|
||||
role = models.ForeignKey(
|
||||
"fonrey_permission.Role",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="change_logs",
|
||||
)
|
||||
permission_code = models.CharField(max_length=150, blank=True, default="")
|
||||
action = models.CharField(max_length=20, choices=PermissionChangeAction.choices)
|
||||
old_value = models.JSONField(null=True, blank=True)
|
||||
new_value = models.JSONField(null=True, blank=True)
|
||||
operator = models.ForeignKey(
|
||||
"org.Staff",
|
||||
on_delete=models.PROTECT,
|
||||
related_name="permission_changes_operated",
|
||||
)
|
||||
operator_ip = models.GenericIPAddressField(null=True, blank=True)
|
||||
user_agent = models.TextField(blank=True, default="")
|
||||
reason = models.TextField(blank=True, default="")
|
||||
operated_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "permission_change_logs"
|
||||
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.")
|
||||
0
apps/permission/serializers.py
Normal file
0
apps/permission/serializers.py
Normal file
0
apps/permission/services/__init__.py
Normal file
0
apps/permission/services/__init__.py
Normal file
0
apps/permission/tasks.py
Normal file
0
apps/permission/tasks.py
Normal file
0
apps/permission/templates/permission/.gitkeep
Normal file
0
apps/permission/templates/permission/.gitkeep
Normal file
0
apps/permission/tests/__init__.py
Normal file
0
apps/permission/tests/__init__.py
Normal file
5
apps/permission/urls.py
Normal file
5
apps/permission/urls.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.urls import path
|
||||
|
||||
app_name = "permission"
|
||||
|
||||
urlpatterns: list = []
|
||||
0
apps/permission/views.py
Normal file
0
apps/permission/views.py
Normal file
0
apps/property/__init__.py
Normal file
0
apps/property/__init__.py
Normal file
7
apps/property/apps.py
Normal file
7
apps/property/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PropertyConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.property"
|
||||
label = "fonrey_property"
|
||||
0
apps/property/migrations/__init__.py
Normal file
0
apps/property/migrations/__init__.py
Normal file
0
apps/property/models/__init__.py
Normal file
0
apps/property/models/__init__.py
Normal file
0
apps/region/__init__.py
Normal file
0
apps/region/__init__.py
Normal file
7
apps/region/apps.py
Normal file
7
apps/region/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class RegionConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.region"
|
||||
label = "region"
|
||||
128
apps/region/migrations/0001_initial.py
Normal file
128
apps/region/migrations/0001_initial.py
Normal file
@@ -0,0 +1,128 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
0
apps/region/migrations/__init__.py
Normal file
0
apps/region/migrations/__init__.py
Normal file
15
apps/region/models/__init__.py
Normal file
15
apps/region/models/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from apps.region.models.region import (
|
||||
BusinessArea,
|
||||
District,
|
||||
MetroLine,
|
||||
MetroStation,
|
||||
School,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"BusinessArea",
|
||||
"District",
|
||||
"MetroLine",
|
||||
"MetroStation",
|
||||
"School",
|
||||
]
|
||||
145
apps/region/models/region.py
Normal file
145
apps/region/models/region.py
Normal file
@@ -0,0 +1,145 @@
|
||||
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)
|
||||
name = models.CharField(max_length=50)
|
||||
short_name = models.CharField(max_length=20, blank=True, default="")
|
||||
sort_order = models.IntegerField(default=0)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "districts"
|
||||
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",
|
||||
)
|
||||
name = models.CharField(max_length=100)
|
||||
sort_order = models.IntegerField(default=0)
|
||||
latitude = models.DecimalField(max_digits=10, decimal_places=7, null=True, blank=True)
|
||||
longitude = models.DecimalField(max_digits=10, decimal_places=7, null=True, blank=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "business_areas"
|
||||
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)
|
||||
name = models.CharField(max_length=50)
|
||||
color = models.CharField(max_length=7, blank=True, default="")
|
||||
sort_order = models.IntegerField(default=0)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "metro_lines"
|
||||
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",
|
||||
)
|
||||
name = models.CharField(max_length=50)
|
||||
latitude = models.DecimalField(max_digits=10, decimal_places=7, null=True, blank=True)
|
||||
longitude = models.DecimalField(max_digits=10, decimal_places=7, null=True, blank=True)
|
||||
sort_order = models.IntegerField(default=0)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "metro_stations"
|
||||
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",
|
||||
)
|
||||
name = models.CharField(max_length=100)
|
||||
type = models.CharField(
|
||||
max_length=20,
|
||||
blank=True,
|
||||
default="",
|
||||
choices=SchoolType.choices,
|
||||
)
|
||||
nature = models.CharField(
|
||||
max_length=20,
|
||||
blank=True,
|
||||
default="",
|
||||
choices=SchoolNature.choices,
|
||||
)
|
||||
level = models.CharField(
|
||||
max_length=20,
|
||||
blank=True,
|
||||
default="",
|
||||
choices=SchoolLevel.choices,
|
||||
)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "schools"
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["district"],
|
||||
name="idx_schools_district",
|
||||
condition=models.Q(is_active=True),
|
||||
),
|
||||
]
|
||||
ordering = ["name"]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
0
apps/region/serializers.py
Normal file
0
apps/region/serializers.py
Normal file
0
apps/region/services/__init__.py
Normal file
0
apps/region/services/__init__.py
Normal file
0
apps/region/tasks.py
Normal file
0
apps/region/tasks.py
Normal file
0
apps/region/templates/region/.gitkeep
Normal file
0
apps/region/templates/region/.gitkeep
Normal file
0
apps/region/tests/__init__.py
Normal file
0
apps/region/tests/__init__.py
Normal file
5
apps/region/urls.py
Normal file
5
apps/region/urls.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.urls import path
|
||||
|
||||
app_name = "region"
|
||||
|
||||
urlpatterns: list = []
|
||||
0
apps/region/views.py
Normal file
0
apps/region/views.py
Normal file
0
apps/release/__init__.py
Normal file
0
apps/release/__init__.py
Normal file
7
apps/release/apps.py
Normal file
7
apps/release/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ReleaseConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.release"
|
||||
label = "release"
|
||||
0
apps/release/migrations/__init__.py
Normal file
0
apps/release/migrations/__init__.py
Normal file
0
apps/release/models/__init__.py
Normal file
0
apps/release/models/__init__.py
Normal file
0
apps/release/serializers.py
Normal file
0
apps/release/serializers.py
Normal file
5
apps/release/urls.py
Normal file
5
apps/release/urls.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.urls import path
|
||||
|
||||
app_name = "release"
|
||||
|
||||
urlpatterns: list = []
|
||||
0
apps/release/views.py
Normal file
0
apps/release/views.py
Normal file
0
apps/setting/__init__.py
Normal file
0
apps/setting/__init__.py
Normal file
7
apps/setting/apps.py
Normal file
7
apps/setting/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SettingConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.setting"
|
||||
label = "setting"
|
||||
0
apps/setting/migrations/__init__.py
Normal file
0
apps/setting/migrations/__init__.py
Normal file
0
apps/setting/models/__init__.py
Normal file
0
apps/setting/models/__init__.py
Normal file
0
apps/tenant/__init__.py
Normal file
0
apps/tenant/__init__.py
Normal file
7
apps/tenant/apps.py
Normal file
7
apps/tenant/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TenantConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.tenant"
|
||||
label = "tenant"
|
||||
0
apps/tenant/migrations/__init__.py
Normal file
0
apps/tenant/migrations/__init__.py
Normal file
13
apps/tenant/models.py
Normal file
13
apps/tenant/models.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from django.db import models
|
||||
from django_tenants.models import DomainMixin, TenantMixin
|
||||
|
||||
|
||||
class Tenant(TenantMixin):
|
||||
name = models.CharField(max_length=255)
|
||||
created_on = models.DateField(auto_now_add=True)
|
||||
|
||||
auto_create_schema = True
|
||||
|
||||
|
||||
class Domain(DomainMixin):
|
||||
pass
|
||||
Reference in New Issue
Block a user