You've already forked opc-backend
开发了多角色登录与鉴权接口:实现了普通用户、企业和管理员的登录分流,并支持Token验证。
开发了权限控制接口:实现了通过数据库分配菜单权限节点,控制接口访问安全。 开发了实名认证中心:实现了个人身份证信息与企业营业执照的提交与审核接口。 开发了任务与协作大厅核心业务:实现了任务的发布、接单、状态流转以及专家邀约接口。 配置了全局环境变量与数据库引擎:集成了 PostgreSQL 数据库、Redis 缓存与 MinIO 对象存储。
This commit is contained in:
0
users/__init__.py
Normal file
0
users/__init__.py
Normal file
3
users/admin.py
Normal file
3
users/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
users/apps.py
Normal file
6
users/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UsersConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'users'
|
||||
126
users/migrations/0001_initial.py
Normal file
126
users/migrations/0001_initial.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# Generated by Django 4.2.30 on 2026-04-25 09:46
|
||||
|
||||
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 = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
fields=[
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('username', models.CharField(max_length=64, unique=True)),
|
||||
('phone', models.CharField(blank=True, max_length=20, null=True, unique=True)),
|
||||
('email', models.EmailField(blank=True, max_length=128, null=True, unique=True)),
|
||||
('nickname', models.CharField(blank=True, max_length=64, null=True)),
|
||||
('avatar_url', models.CharField(blank=True, max_length=512, null=True)),
|
||||
('face_url', models.CharField(blank=True, max_length=512, null=True)),
|
||||
('wx_openid', models.CharField(blank=True, max_length=128, null=True, unique=True)),
|
||||
('wx_unionid', models.CharField(blank=True, max_length=128, null=True, unique=True)),
|
||||
('status', models.CharField(choices=[('ACTIVE', '正常'), ('INACTIVE', '未激活'), ('BANNED', '封禁')], default='ACTIVE', max_length=16)),
|
||||
('is_deleted', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('is_staff', models.BooleanField(default=False)),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'users',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Permission',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('code', models.CharField(max_length=128, unique=True)),
|
||||
('name', models.CharField(max_length=128)),
|
||||
('type', models.CharField(choices=[('MENU', '菜单'), ('BUTTON', '按钮'), ('API', '接口')], max_length=16)),
|
||||
('path', models.CharField(blank=True, max_length=256, null=True)),
|
||||
('method', models.CharField(blank=True, max_length=16, null=True)),
|
||||
('sort_order', models.IntegerField(default=0)),
|
||||
('icon', models.CharField(blank=True, max_length=64, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.permission')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'permissions',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Role',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('code', models.CharField(max_length=64, unique=True)),
|
||||
('name', models.CharField(max_length=64)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('is_system', models.BooleanField(default=False)),
|
||||
('sort_order', models.IntegerField(default=0)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'roles',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Enterprise',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('company_name', models.CharField(max_length=256)),
|
||||
('business_license', models.CharField(blank=True, max_length=512, null=True)),
|
||||
('contact_name', models.CharField(blank=True, max_length=64, null=True)),
|
||||
('contact_phone', models.CharField(blank=True, max_length=20, null=True)),
|
||||
('contact_email', models.CharField(blank=True, max_length=128, null=True)),
|
||||
('address', models.TextField(blank=True, null=True)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('is_deleted', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'enterprises',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserRole',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('granted_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='granted_roles', to=settings.AUTH_USER_MODEL)),
|
||||
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.role')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_roles', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'user_roles',
|
||||
'unique_together': {('user', 'role')},
|
||||
},
|
||||
),
|
||||
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)),
|
||||
('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.permission')),
|
||||
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='role_permissions', to='users.role')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'role_permissions',
|
||||
'unique_together': {('role', 'permission')},
|
||||
},
|
||||
),
|
||||
]
|
||||
18
users/migrations/0002_enterprise_credit_code.py
Normal file
18
users/migrations/0002_enterprise_credit_code.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.30 on 2026-04-25 10:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='enterprise',
|
||||
name='credit_code',
|
||||
field=models.CharField(blank=True, max_length=64, null=True),
|
||||
),
|
||||
]
|
||||
23
users/migrations/0003_user_bio_user_location.py
Normal file
23
users/migrations/0003_user_bio_user_location.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.2.30 on 2026-04-25 13:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0002_enterprise_credit_code'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='bio',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='location',
|
||||
field=models.CharField(blank=True, max_length=128, null=True),
|
||||
),
|
||||
]
|
||||
18
users/migrations/0004_alter_enterprise_credit_code.py
Normal file
18
users/migrations/0004_alter_enterprise_credit_code.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.30 on 2026-04-25 13:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0003_user_bio_user_location'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='enterprise',
|
||||
name='credit_code',
|
||||
field=models.CharField(blank=True, max_length=64, null=True, unique=True),
|
||||
),
|
||||
]
|
||||
30
users/migrations/0005_enterprisemember.py
Normal file
30
users/migrations/0005_enterprisemember.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 4.2.30 on 2026-04-25 14:48
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0004_alter_enterprise_credit_code'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='EnterpriseMember',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('role', models.CharField(choices=[('ADMIN', '管理员'), ('MEMBER', '普通成员'), ('GUEST', '外部观察员')], default='MEMBER', max_length=16)),
|
||||
('joined_at', models.DateTimeField(auto_now_add=True)),
|
||||
('enterprise', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='members', to='users.enterprise')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='enterprise_memberships', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'enterprise_members',
|
||||
'unique_together': {('enterprise', 'user')},
|
||||
},
|
||||
),
|
||||
]
|
||||
23
users/migrations/0006_user_completed_tasks_user_rating.py
Normal file
23
users/migrations/0006_user_completed_tasks_user_rating.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.2.30 on 2026-04-25 14:51
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0005_enterprisemember'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='completed_tasks',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='rating',
|
||||
field=models.DecimalField(decimal_places=2, default=5.0, max_digits=3),
|
||||
),
|
||||
]
|
||||
18
users/migrations/0007_enterprise_status.py
Normal file
18
users/migrations/0007_enterprise_status.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.30 on 2026-04-25 14:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0006_user_completed_tasks_user_rating'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='enterprise',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('PENDING', '待审核'), ('VERIFIED', '已认证'), ('REJECTED', '已拒绝')], default='PENDING', max_length=16),
|
||||
),
|
||||
]
|
||||
18
users/migrations/0008_enterprise_logo_url.py
Normal file
18
users/migrations/0008_enterprise_logo_url.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.30 on 2026-04-26 13:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0007_enterprise_status'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='enterprise',
|
||||
name='logo_url',
|
||||
field=models.CharField(blank=True, max_length=512, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.2.30 on 2026-04-27 17:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0008_enterprise_logo_url'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='is_recommended',
|
||||
field=models.BooleanField(default=False, help_text='管理员推荐'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='recommend_priority',
|
||||
field=models.IntegerField(default=0, help_text='推荐优先级, 越大越靠前'),
|
||||
),
|
||||
]
|
||||
18
users/migrations/0010_enterprise_landline.py
Normal file
18
users/migrations/0010_enterprise_landline.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.30 on 2026-04-27 18:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0009_user_is_recommended_user_recommend_priority'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='enterprise',
|
||||
name='landline',
|
||||
field=models.CharField(blank=True, help_text='座机号码', max_length=32, null=True),
|
||||
),
|
||||
]
|
||||
0
users/migrations/__init__.py
Normal file
0
users/migrations/__init__.py
Normal file
160
users/models.py
Normal file
160
users/models.py
Normal file
@@ -0,0 +1,160 @@
|
||||
import uuid
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
|
||||
|
||||
class UserStatus(models.TextChoices):
|
||||
ACTIVE = 'ACTIVE', '正常'
|
||||
INACTIVE = 'INACTIVE', '未激活'
|
||||
BANNED = 'BANNED', '封禁'
|
||||
|
||||
class CustomUserManager(BaseUserManager):
|
||||
def create_user(self, username, phone=None, password=None, **extra_fields):
|
||||
if not username:
|
||||
raise ValueError('The Username field must be set')
|
||||
user = self.model(username=username, phone=phone, **extra_fields)
|
||||
if password:
|
||||
user.set_password(password)
|
||||
user.save(using=self._db)
|
||||
return user
|
||||
|
||||
def create_superuser(self, username, phone=None, password=None, **extra_fields):
|
||||
extra_fields.setdefault('is_staff', True)
|
||||
extra_fields.setdefault('is_superuser', True)
|
||||
return self.create_user(username, phone, password, **extra_fields)
|
||||
|
||||
class User(AbstractBaseUser, PermissionsMixin):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
username = models.CharField(max_length=64, unique=True)
|
||||
phone = models.CharField(max_length=20, unique=True, null=True, blank=True)
|
||||
email = models.EmailField(max_length=128, unique=True, null=True, blank=True)
|
||||
nickname = models.CharField(max_length=64, null=True, blank=True)
|
||||
avatar_url = models.CharField(max_length=512, null=True, blank=True)
|
||||
face_url = models.CharField(max_length=512, null=True, blank=True)
|
||||
wx_openid = models.CharField(max_length=128, unique=True, null=True, blank=True)
|
||||
wx_unionid = models.CharField(max_length=128, unique=True, null=True, blank=True)
|
||||
bio = models.TextField(null=True, blank=True)
|
||||
location = models.CharField(max_length=128, null=True, blank=True)
|
||||
rating = models.DecimalField(max_digits=3, decimal_places=2, default=5.00)
|
||||
completed_tasks = models.IntegerField(default=0)
|
||||
|
||||
status = models.CharField(max_length=16, choices=UserStatus.choices, default=UserStatus.ACTIVE)
|
||||
|
||||
is_deleted = models.BooleanField(default=False)
|
||||
is_recommended = models.BooleanField(default=False, help_text='管理员推荐')
|
||||
recommend_priority = models.IntegerField(default=0, help_text='推荐优先级, 越大越靠前')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
is_active = models.BooleanField(default=True)
|
||||
is_staff = models.BooleanField(default=False)
|
||||
|
||||
objects = CustomUserManager()
|
||||
|
||||
@property
|
||||
def roles(self):
|
||||
return list(self.user_roles.values_list('role__code', flat=True))
|
||||
|
||||
USERNAME_FIELD = 'username'
|
||||
|
||||
REQUIRED_FIELDS = ['phone']
|
||||
|
||||
class Meta:
|
||||
db_table = 'users'
|
||||
|
||||
class Role(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
code = models.CharField(max_length=64, unique=True)
|
||||
name = models.CharField(max_length=64)
|
||||
description = models.TextField(null=True, blank=True)
|
||||
is_system = models.BooleanField(default=False)
|
||||
sort_order = models.IntegerField(default=0)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'roles'
|
||||
|
||||
class PermissionType(models.TextChoices):
|
||||
MENU = 'MENU', '菜单'
|
||||
BUTTON = 'BUTTON', '按钮'
|
||||
API = 'API', '接口'
|
||||
|
||||
class Permission(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
code = models.CharField(max_length=128, unique=True)
|
||||
name = models.CharField(max_length=128)
|
||||
type = models.CharField(max_length=16, choices=PermissionType.choices)
|
||||
path = models.CharField(max_length=256, null=True, blank=True)
|
||||
method = models.CharField(max_length=16, null=True, blank=True)
|
||||
parent = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True)
|
||||
sort_order = models.IntegerField(default=0)
|
||||
icon = models.CharField(max_length=64, null=True, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'permissions'
|
||||
|
||||
class UserRole(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_roles')
|
||||
role = models.ForeignKey(Role, on_delete=models.CASCADE)
|
||||
granted_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='granted_roles')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'user_roles'
|
||||
unique_together = ('user', 'role')
|
||||
|
||||
class RolePermission(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
role = models.ForeignKey(Role, on_delete=models.CASCADE, related_name='role_permissions')
|
||||
permission = models.ForeignKey(Permission, on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'role_permissions'
|
||||
unique_together = ('role', 'permission')
|
||||
|
||||
class Enterprise(models.Model):
|
||||
class EnterpriseStatus(models.TextChoices):
|
||||
PENDING = 'PENDING', '待审核'
|
||||
VERIFIED = 'VERIFIED', '已认证'
|
||||
REJECTED = 'REJECTED', '已拒绝'
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
company_name = models.CharField(max_length=256)
|
||||
credit_code = models.CharField(max_length=64, unique=True, null=True, blank=True)
|
||||
status = models.CharField(max_length=16, choices=EnterpriseStatus.choices, default=EnterpriseStatus.PENDING)
|
||||
|
||||
logo_url = models.CharField(max_length=512, null=True, blank=True)
|
||||
|
||||
business_license = models.CharField(max_length=512, null=True, blank=True)
|
||||
contact_name = models.CharField(max_length=64, null=True, blank=True)
|
||||
contact_phone = models.CharField(max_length=20, null=True, blank=True)
|
||||
landline = models.CharField(max_length=32, null=True, blank=True, help_text='座机号码')
|
||||
contact_email = models.CharField(max_length=128, null=True, blank=True)
|
||||
address = models.TextField(null=True, blank=True)
|
||||
description = models.TextField(null=True, blank=True)
|
||||
is_deleted = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'enterprises'
|
||||
|
||||
class EnterpriseMember(models.Model):
|
||||
class MemberRole(models.TextChoices):
|
||||
ADMIN = 'ADMIN', '管理员'
|
||||
MEMBER = 'MEMBER', '普通成员'
|
||||
GUEST = 'GUEST', '外部观察员'
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
enterprise = models.ForeignKey(Enterprise, on_delete=models.CASCADE, related_name='members')
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='enterprise_memberships')
|
||||
role = models.CharField(max_length=16, choices=MemberRole.choices, default=MemberRole.MEMBER)
|
||||
joined_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'enterprise_members'
|
||||
unique_together = ('enterprise', 'user')
|
||||
|
||||
31
users/permissions.py
Normal file
31
users/permissions.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from rest_framework import permissions
|
||||
from .models import RolePermission
|
||||
|
||||
class HasAPIPermission(permissions.BasePermission):
|
||||
"""
|
||||
Checks if the user has the specific API permission required by the view.
|
||||
The view must define `required_permission = 'api:something'` or a dictionary mapping methods to permissions:
|
||||
`required_permissions = {'GET': 'api:read', 'POST': 'api:write'}`
|
||||
"""
|
||||
def has_permission(self, request, view):
|
||||
if not request.user or not request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
if getattr(request.user, 'is_superuser', False):
|
||||
return True
|
||||
|
||||
method = request.method
|
||||
required_perm = None
|
||||
|
||||
if hasattr(view, 'required_permissions') and isinstance(view.required_permissions, dict):
|
||||
required_perm = view.required_permissions.get(method)
|
||||
elif hasattr(view, 'required_permission'):
|
||||
required_perm = view.required_permission
|
||||
|
||||
if not required_perm:
|
||||
# If no permission is required, default to IsAdminUser logic for safety,
|
||||
# or True if you want it open. Let's require staff status by default for admin views.
|
||||
return request.user.is_staff
|
||||
|
||||
user_perms = RolePermission.objects.filter(role__userrole__user=request.user).values_list('permission__code', flat=True)
|
||||
return required_perm in user_perms
|
||||
438
users/serializers.py
Normal file
438
users/serializers.py
Normal file
@@ -0,0 +1,438 @@
|
||||
from rest_framework import serializers
|
||||
from .models import User, Enterprise, Role, EnterpriseMember, Permission
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
roles = serializers.ReadOnlyField()
|
||||
permissions = serializers.SerializerMethodField()
|
||||
opc_certification = serializers.SerializerMethodField()
|
||||
enterprise_info = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'username', 'phone', 'email', 'nickname', 'avatar_url', 'face_url', 'bio', 'location', 'status', 'is_active', 'created_at', 'roles', 'permissions', 'is_staff', 'is_superuser', 'rating', 'completed_tasks', 'opc_certification', 'enterprise_info', 'is_recommended', 'recommend_priority']
|
||||
read_only_fields = ['id', 'status', 'is_active', 'created_at', 'roles', 'permissions', 'is_staff', 'is_superuser']
|
||||
|
||||
def get_enterprise_info(self, obj):
|
||||
if 'ENTERPRISE' in obj.roles:
|
||||
from .models import Enterprise, EnterpriseMember
|
||||
ent = Enterprise.objects.filter(user=obj).first()
|
||||
if ent:
|
||||
return {'company_name': ent.company_name, 'role': 'OWNER'}
|
||||
member = EnterpriseMember.objects.filter(user=obj).first()
|
||||
if member:
|
||||
return {'company_name': member.enterprise.company_name, 'role': member.role}
|
||||
return None
|
||||
|
||||
def get_opc_certification(self, obj):
|
||||
if 'OPC_USER' in obj.roles:
|
||||
from opc_cert.models import OpcCertification
|
||||
from opc_cert.serializers import OpcCertificationSerializer
|
||||
cert = OpcCertification.objects.filter(user=obj, status='APPROVED').first()
|
||||
if cert:
|
||||
return OpcCertificationSerializer(cert).data
|
||||
return None
|
||||
|
||||
def get_permissions(self, obj):
|
||||
if obj.is_superuser:
|
||||
return ['*']
|
||||
from .models import RolePermission
|
||||
perms = RolePermission.objects.filter(role__userrole__user=obj).values_list('permission__code', flat=True).distinct()
|
||||
return list(perms)
|
||||
|
||||
def validate(self, attrs):
|
||||
# Normalize empty strings to None to avoid unique constraint issues
|
||||
for field in ['phone', 'email']:
|
||||
if field in attrs and attrs[field] == '':
|
||||
attrs[field] = None
|
||||
return attrs
|
||||
|
||||
class RoleSerializer(serializers.ModelSerializer):
|
||||
permission_ids = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = '__all__'
|
||||
|
||||
def get_permission_ids(self, obj):
|
||||
return list(obj.role_permissions.values_list('permission_id', flat=True).distinct().values_list('permission_id', flat=True))
|
||||
|
||||
def to_representation(self, instance):
|
||||
data = super().to_representation(instance)
|
||||
# Ensure permission_ids are strings for frontend comparison
|
||||
data['permission_ids'] = [str(pid) for pid in data.get('permission_ids', [])]
|
||||
return data
|
||||
|
||||
class PermissionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Permission
|
||||
fields = '__all__'
|
||||
|
||||
class EnterpriseMemberSerializer(serializers.ModelSerializer):
|
||||
user_details = UserSerializer(source='user', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = EnterpriseMember
|
||||
fields = ['id', 'user', 'user_details', 'role', 'joined_at']
|
||||
read_only_fields = ['id', 'joined_at']
|
||||
|
||||
|
||||
|
||||
import re
|
||||
from django.core.validators import EmailValidator
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
class RegisterSerializer(serializers.ModelSerializer):
|
||||
password = serializers.CharField(write_only=True, min_length=6)
|
||||
|
||||
username = serializers.CharField(
|
||||
error_messages={
|
||||
'unique': '该用户名已被占用,请尝试其他名称'
|
||||
}
|
||||
)
|
||||
phone = serializers.CharField(
|
||||
required=False,
|
||||
allow_blank=True,
|
||||
error_messages={
|
||||
'unique': '该手机号已注册,请直接登录'
|
||||
}
|
||||
)
|
||||
email = serializers.EmailField(
|
||||
required=False,
|
||||
allow_blank=True,
|
||||
error_messages={
|
||||
'unique': '该邮箱已注册,请尝试其他邮箱'
|
||||
}
|
||||
)
|
||||
avatar_url = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
|
||||
model = User
|
||||
fields = ['username', 'phone', 'email', 'password', 'nickname', 'avatar_url']
|
||||
|
||||
def validate_phone(self, value):
|
||||
if value:
|
||||
if not re.match(r'^1[3-9]\d{9}$', value):
|
||||
raise ValidationError('请输入有效的11位手机号')
|
||||
if User.objects.filter(phone=value, is_deleted=False).exists():
|
||||
raise ValidationError('该手机号已注册,请直接登录')
|
||||
return value
|
||||
|
||||
def validate_email(self, value):
|
||||
if value:
|
||||
if User.objects.filter(email=value, is_deleted=False).exists():
|
||||
raise ValidationError('该邮箱已注册,请尝试其他邮箱')
|
||||
return value
|
||||
|
||||
def validate_username(self, value):
|
||||
if User.objects.filter(username=value, is_deleted=False).exists():
|
||||
raise ValidationError('该用户名已被占用,请尝试其他名称')
|
||||
return value
|
||||
|
||||
def create(self, validated_data):
|
||||
phone = validated_data.get('phone')
|
||||
if phone == '':
|
||||
phone = None
|
||||
email = validated_data.get('email')
|
||||
if email == '':
|
||||
email = None
|
||||
|
||||
user = User.objects.create_user(
|
||||
username=validated_data['username'],
|
||||
phone=phone,
|
||||
email=email,
|
||||
password=validated_data['password'],
|
||||
nickname=validated_data.get('nickname'),
|
||||
avatar_url=validated_data.get('avatar_url', '')
|
||||
)
|
||||
# Auto-assign USER role
|
||||
from .models import Role, UserRole
|
||||
user_role, _ = Role.objects.get_or_create(code='USER', defaults={'name': '普通用户'})
|
||||
UserRole.objects.get_or_create(user=user, role=user_role)
|
||||
return user
|
||||
|
||||
class AdminUserCreateSerializer(serializers.ModelSerializer):
|
||||
password = serializers.CharField(write_only=True, min_length=6)
|
||||
email = serializers.EmailField(required=False, allow_blank=True)
|
||||
username = serializers.CharField(
|
||||
error_messages={'unique': '该用户名已被占用'}
|
||||
)
|
||||
phone = serializers.CharField(
|
||||
required=False, allow_blank=True,
|
||||
error_messages={'unique': '该手机号已注册'}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username', 'phone', 'email', 'password', 'nickname']
|
||||
|
||||
def validate_phone(self, value):
|
||||
if value:
|
||||
if not re.match(r'^1[3-9]\d{9}$', value):
|
||||
raise ValidationError('请输入有效的11位手机号')
|
||||
if User.objects.filter(phone=value).exists():
|
||||
raise ValidationError('该手机号已注册')
|
||||
return value
|
||||
|
||||
def validate_email(self, value):
|
||||
if value:
|
||||
if User.objects.filter(email=value).exists():
|
||||
raise ValidationError('该邮箱已注册')
|
||||
return value
|
||||
|
||||
def create(self, validated_data):
|
||||
phone = validated_data.get('phone')
|
||||
if phone == '':
|
||||
phone = None
|
||||
|
||||
user = User.objects.create_user(
|
||||
username=validated_data['username'],
|
||||
phone=phone,
|
||||
email=validated_data.get('email'),
|
||||
password=validated_data['password'],
|
||||
nickname=validated_data.get('nickname')
|
||||
)
|
||||
# Auto-assign USER role
|
||||
from .models import Role, UserRole
|
||||
user_role, _ = Role.objects.get_or_create(code='USER', defaults={'name': '普通用户'})
|
||||
UserRole.objects.get_or_create(user=user, role=user_role)
|
||||
return user
|
||||
|
||||
|
||||
|
||||
class EnterpriseSerializer(serializers.ModelSerializer):
|
||||
user_details = UserSerializer(source='user', read_only=True)
|
||||
credit_code = serializers.CharField(
|
||||
error_messages={
|
||||
'unique': '该统一社会信用代码已注册,请核对信息'
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Enterprise
|
||||
fields = ['id', 'user', 'user_details', 'company_name', 'credit_code', 'business_license', 'contact_name', 'contact_phone', 'landline', 'contact_email', 'address', 'description', 'status', 'logo_url', 'created_at', 'updated_at']
|
||||
read_only_fields = ['id', 'user', 'user_details', 'status', 'created_at', 'updated_at']
|
||||
|
||||
|
||||
def validate_credit_code(self, value):
|
||||
if value:
|
||||
if not re.match(r'^[A-Z0-9]{18}$', value):
|
||||
raise ValidationError('请输入有效的18位统一社会信用代码')
|
||||
return value
|
||||
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
class EnterpriseRegisterSerializer(serializers.Serializer):
|
||||
email = serializers.EmailField()
|
||||
password = serializers.CharField(write_only=True, min_length=6)
|
||||
company_name = serializers.CharField(max_length=255)
|
||||
credit_code = serializers.CharField(max_length=18)
|
||||
business_license = serializers.CharField(required=False, allow_blank=True)
|
||||
logo_url = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
def validate_credit_code(self, value):
|
||||
if not re.match(r'^[A-Z0-9]{18}$', value):
|
||||
raise ValidationError('请输入有效的18位统一社会信用代码')
|
||||
if Enterprise.objects.filter(credit_code=value).exists():
|
||||
raise ValidationError('该社会信用代码已注册')
|
||||
return value
|
||||
|
||||
def validate_email(self, value):
|
||||
if User.objects.filter(email=value).exists():
|
||||
raise ValidationError('该邮箱已注册')
|
||||
return value
|
||||
|
||||
def create(self, validated_data):
|
||||
with transaction.atomic():
|
||||
# 1. Create User
|
||||
user = User.objects.create_user(
|
||||
username=validated_data['email'],
|
||||
email=validated_data['email'],
|
||||
password=validated_data['password'],
|
||||
nickname=validated_data['company_name'],
|
||||
avatar_url=validated_data.get('logo_url')
|
||||
)
|
||||
|
||||
# 2. Create Enterprise
|
||||
enterprise = Enterprise.objects.create(
|
||||
user=user,
|
||||
company_name=validated_data['company_name'],
|
||||
credit_code=validated_data['credit_code'],
|
||||
business_license=validated_data.get('business_license', ''),
|
||||
logo_url=validated_data.get('logo_url')
|
||||
)
|
||||
|
||||
# 3. Assign Role
|
||||
from .models import Role, UserRole
|
||||
role, created = Role.objects.get_or_create(code='ENTERPRISE', defaults={'name': '企业用户'})
|
||||
UserRole.objects.get_or_create(user=user, role=role)
|
||||
|
||||
return enterprise
|
||||
|
||||
class AdminEnterpriseCreateSerializer(serializers.ModelSerializer):
|
||||
email = serializers.EmailField(write_only=True)
|
||||
password = serializers.CharField(write_only=True, min_length=6)
|
||||
|
||||
class Meta:
|
||||
model = Enterprise
|
||||
fields = ['email', 'password', 'company_name', 'credit_code', 'business_license', 'contact_name', 'contact_phone', 'contact_email', 'address', 'description', 'logo_url']
|
||||
|
||||
def validate_credit_code(self, value):
|
||||
if value:
|
||||
if not re.match(r'^[A-Z0-9]{18}$', value):
|
||||
raise ValidationError('请输入有效的18位统一社会信用代码')
|
||||
if Enterprise.objects.filter(credit_code=value).exists():
|
||||
raise ValidationError('该社会信用代码已注册')
|
||||
return value
|
||||
|
||||
def validate_email(self, value):
|
||||
if User.objects.filter(email=value).exists():
|
||||
raise ValidationError('该邮箱已注册,无法创建企业账号')
|
||||
return value
|
||||
|
||||
def create(self, validated_data):
|
||||
email = validated_data.pop('email')
|
||||
password = validated_data.pop('password')
|
||||
|
||||
with transaction.atomic():
|
||||
user = User.objects.create_user(
|
||||
username=email,
|
||||
email=email,
|
||||
password=password,
|
||||
nickname=validated_data['company_name'],
|
||||
avatar_url=validated_data.get('logo_url')
|
||||
)
|
||||
|
||||
enterprise = Enterprise.objects.create(
|
||||
user=user,
|
||||
status='VERIFIED', # Admins create pre-verified enterprises
|
||||
**validated_data
|
||||
)
|
||||
|
||||
from .models import Role, UserRole
|
||||
role, _ = Role.objects.get_or_create(code='ENTERPRISE', defaults={'name': '企业用户'})
|
||||
UserRole.objects.get_or_create(user=user, role=role)
|
||||
|
||||
return enterprise
|
||||
|
||||
from django.contrib.auth import authenticate
|
||||
|
||||
class EnterpriseLoginSerializer(serializers.Serializer):
|
||||
login_type = serializers.ChoiceField(choices=['ADMIN', 'EMPLOYEE'], default='ADMIN')
|
||||
enterprise_email = serializers.EmailField()
|
||||
username = serializers.CharField(required=False, allow_blank=True)
|
||||
password = serializers.CharField(write_only=True)
|
||||
|
||||
def validate(self, data):
|
||||
login_type = data.get('login_type', 'ADMIN')
|
||||
enterprise_email = data.get('enterprise_email')
|
||||
username = data.get('username')
|
||||
password = data.get('password')
|
||||
|
||||
# 1. Find enterprise
|
||||
try:
|
||||
enterprise_user = User.objects.get(email=enterprise_email)
|
||||
enterprise = Enterprise.objects.get(user=enterprise_user)
|
||||
except (User.DoesNotExist, Enterprise.DoesNotExist):
|
||||
raise ValidationError({'detail': '企业邮箱未注册'})
|
||||
|
||||
if login_type == 'ADMIN':
|
||||
# 2. Find and authenticate the admin
|
||||
if not enterprise_user.is_active or enterprise_user.is_deleted:
|
||||
raise ValidationError({'detail': '您的账号已被禁用,可能因为违规操作或安全风险。如有疑问请联系平台管理员。'})
|
||||
if not enterprise_user.check_password(password):
|
||||
raise ValidationError({'detail': '管理员邮箱或密码错误'})
|
||||
|
||||
# Check if user is the enterprise owner
|
||||
user = enterprise_user
|
||||
|
||||
else:
|
||||
# EMPLOYEE login
|
||||
if not username:
|
||||
raise ValidationError({'detail': '员工账号不能为空'})
|
||||
|
||||
namespaced_username = f"{enterprise_email}_{username}"
|
||||
user = User.objects.filter(username=namespaced_username).first()
|
||||
|
||||
if not user:
|
||||
raise ValidationError({'detail': '员工账号未注册'})
|
||||
if not user.is_active or user.is_deleted:
|
||||
raise ValidationError({'detail': '您的账号已被禁用,可能因为违规操作或安全风险。如有疑问请联系平台管理员。'})
|
||||
if not user.check_password(password):
|
||||
raise ValidationError({'detail': '员工账号或密码错误'})
|
||||
|
||||
# Check if user is a member of the enterprise
|
||||
if not EnterpriseMember.objects.filter(enterprise=enterprise, user=user).exists():
|
||||
raise ValidationError({'detail': '该员工不属于此企业团队'})
|
||||
|
||||
data['user'] = user
|
||||
return data
|
||||
|
||||
class EnterpriseMemberAddSerializer(serializers.Serializer):
|
||||
username = serializers.CharField()
|
||||
password = serializers.CharField(write_only=True, min_length=6)
|
||||
nickname = serializers.CharField(required=False, allow_blank=True)
|
||||
role = serializers.ChoiceField(choices=EnterpriseMember.MemberRole.choices, default=EnterpriseMember.MemberRole.MEMBER)
|
||||
enterprise_id = serializers.UUIDField(required=False)
|
||||
|
||||
def _get_enterprise(self):
|
||||
request_user = self.context['request'].user
|
||||
enterprise_id = self.initial_data.get('enterprise_id')
|
||||
|
||||
if enterprise_id and (request_user.is_staff or request_user.is_superuser):
|
||||
try:
|
||||
return Enterprise.objects.get(id=enterprise_id)
|
||||
except Enterprise.DoesNotExist:
|
||||
raise ValidationError('指定的企业不存在')
|
||||
|
||||
try:
|
||||
return Enterprise.objects.get(user=request_user)
|
||||
except Enterprise.DoesNotExist:
|
||||
raise ValidationError('当前用户没有关联的企业主体')
|
||||
|
||||
def validate_username(self, value):
|
||||
enterprise = self._get_enterprise()
|
||||
|
||||
namespaced_username = f"{enterprise.user.email}_{value}"
|
||||
if User.objects.filter(username=namespaced_username).exists():
|
||||
raise ValidationError('该用户名已被占用')
|
||||
return value
|
||||
|
||||
def create(self, validated_data):
|
||||
enterprise = self._get_enterprise()
|
||||
|
||||
with transaction.atomic():
|
||||
namespaced_username = f"{enterprise.user.email}_{validated_data['username']}"
|
||||
user = User.objects.create_user(
|
||||
username=namespaced_username,
|
||||
password=validated_data['password'],
|
||||
nickname=validated_data.get('nickname', validated_data['username'])
|
||||
)
|
||||
|
||||
# Assign ENTERPRISE role so they can access enterprise portal
|
||||
from .models import Role, UserRole
|
||||
role, _ = Role.objects.get_or_create(code='ENTERPRISE', defaults={'name': '企业用户'})
|
||||
UserRole.objects.get_or_create(user=user, role=role)
|
||||
|
||||
member = EnterpriseMember.objects.create(
|
||||
enterprise=enterprise,
|
||||
user=user,
|
||||
role=validated_data.get('role', EnterpriseMember.MemberRole.MEMBER)
|
||||
)
|
||||
|
||||
return member
|
||||
|
||||
class ChangePasswordSerializer(serializers.Serializer):
|
||||
old_password = serializers.CharField(required=True)
|
||||
new_password = serializers.CharField(required=True, min_length=6)
|
||||
|
||||
class PasswordResetRequestSerializer(serializers.Serializer):
|
||||
email = serializers.EmailField(required=True)
|
||||
|
||||
class PasswordResetConfirmSerializer(serializers.Serializer):
|
||||
email = serializers.EmailField(required=True)
|
||||
verification_code = serializers.CharField(required=True, min_length=6, max_length=6)
|
||||
new_password = serializers.CharField(required=True, min_length=6)
|
||||
3
users/tests.py
Normal file
3
users/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
23
users/urls.py
Normal file
23
users/urls.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
|
||||
from .views import UserViewSet, RegisterView, EnterpriseViewSet, MyTokenObtainPairView, EnterpriseRegisterView, EnterpriseMemberViewSet, RoleViewSet, PermissionViewSet, EnterpriseLoginView
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register('users', UserViewSet, basename='user')
|
||||
router.register('enterprises', EnterpriseViewSet)
|
||||
router.register('enterprise-members', EnterpriseMemberViewSet, basename='enterprise-member')
|
||||
router.register('roles', RoleViewSet, basename='role')
|
||||
router.register('permissions', PermissionViewSet, basename='permission')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('auth/login/', MyTokenObtainPairView.as_view(), name='token_obtain_pair'),
|
||||
path('auth/enterprise/login/', EnterpriseLoginView.as_view(), name='auth_enterprise_login'),
|
||||
path('auth/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||
path('auth/register/', RegisterView.as_view(), name='auth_register'),
|
||||
path('auth/enterprise/register/', EnterpriseRegisterView.as_view(), name='auth_enterprise_register'),
|
||||
path('auth/me/', UserViewSet.as_view({'get': 'me'}), name='auth_me'),
|
||||
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
900
users/views.py
Normal file
900
users/views.py
Normal file
@@ -0,0 +1,900 @@
|
||||
from rest_framework import viewsets, generics, permissions, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework_simplejwt.views import TokenObtainPairView
|
||||
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.cache import cache
|
||||
import random
|
||||
from .serializers import (
|
||||
UserSerializer, RegisterSerializer, EnterpriseSerializer, EnterpriseRegisterSerializer,
|
||||
EnterpriseMemberSerializer, RoleSerializer, PermissionSerializer, EnterpriseLoginSerializer,
|
||||
EnterpriseMemberAddSerializer, AdminUserCreateSerializer, ChangePasswordSerializer,
|
||||
PasswordResetRequestSerializer, PasswordResetConfirmSerializer
|
||||
)
|
||||
from .models import Enterprise, EnterpriseMember, Role, Permission
|
||||
from .permissions import HasAPIPermission
|
||||
from rest_framework import permissions as drf_permissions
|
||||
|
||||
class EnterpriseLoginView(APIView):
|
||||
"""
|
||||
@author: xujl
|
||||
Api说明: 企业管理员专属登录通道接口。校验企业身份并返回携带对应权限的JWT Token。
|
||||
"""
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
serializer = EnterpriseLoginSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
user = serializer.validated_data['user']
|
||||
refresh = RefreshToken.for_user(user)
|
||||
return Response({
|
||||
'refresh': str(refresh),
|
||||
'access': str(refresh.access_token),
|
||||
'user': UserSerializer(user).data
|
||||
})
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
|
||||
def validate(self, attrs):
|
||||
username = attrs.get(self.username_field)
|
||||
password = attrs.get('password')
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.models import Q
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
from django.contrib.auth.models import update_last_login
|
||||
|
||||
User = get_user_model()
|
||||
user = User.objects.filter(Q(username=username) | Q(phone=username) | Q(email=username)).first()
|
||||
|
||||
if not user:
|
||||
raise AuthenticationFailed('账号未注册', code='not_registered')
|
||||
|
||||
if not user.is_active or user.is_deleted:
|
||||
raise AuthenticationFailed('您的账号已被禁用,可能因为违规操作或安全风险。如有疑问请联系平台管理员。', code='account_disabled')
|
||||
|
||||
if not user.check_password(password):
|
||||
raise AuthenticationFailed('密码错误,请检查后再试', code='wrong_password')
|
||||
|
||||
refresh = RefreshToken.for_user(user)
|
||||
update_last_login(None, user)
|
||||
|
||||
return {
|
||||
'refresh': str(refresh),
|
||||
'access': str(refresh.access_token),
|
||||
'user': UserSerializer(user).data
|
||||
}
|
||||
|
||||
class MyTokenObtainPairView(TokenObtainPairView):
|
||||
"""
|
||||
@author: xujl
|
||||
Api说明: 标准用户认证授权(JWT)接口视图。验证普通用户账号密码并下发访问凭证。
|
||||
"""
|
||||
serializer_class = MyTokenObtainPairSerializer
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
@author: xujl
|
||||
Api说明: 平台用户中心接口视图。用于用户的增删改查、信息完善、封禁及状态管理,以及管理员的全局管控。
|
||||
"""
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = [drf_permissions.IsAuthenticated]
|
||||
|
||||
# Custom attributes for HasAPIPermission check
|
||||
required_permissions = {
|
||||
'GET': 'api:users:read',
|
||||
'POST': 'api:users:write',
|
||||
'PUT': 'api:users:write',
|
||||
'PATCH': 'api:users:write',
|
||||
'DELETE': 'api:users:delete'
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
from django.db.models import Q
|
||||
from tasks.models import Task, TaskApplication, TaskStatus, ApplyStatus
|
||||
queryset = User.objects.filter(is_deleted=False)
|
||||
|
||||
# Non-admins can only see OPC users
|
||||
if not (self.request.user.is_staff or self.request.user.is_superuser):
|
||||
queryset = queryset.filter(user_roles__role__code='OPC_USER')
|
||||
|
||||
role = self.request.query_params.get('role')
|
||||
username = self.request.query_params.get('username')
|
||||
nickname = self.request.query_params.get('nickname')
|
||||
real_name = self.request.query_params.get('real_name')
|
||||
phone = self.request.query_params.get('phone')
|
||||
email = self.request.query_params.get('email')
|
||||
status_filter = self.request.query_params.get('status')
|
||||
idle = self.request.query_params.get('idle')
|
||||
|
||||
if role:
|
||||
queryset = queryset.filter(user_roles__role__code=role)
|
||||
if username:
|
||||
queryset = queryset.filter(username__icontains=username)
|
||||
if nickname:
|
||||
queryset = queryset.filter(nickname__icontains=nickname)
|
||||
if real_name:
|
||||
queryset = queryset.filter(opc_certifications__real_name__icontains=real_name)
|
||||
if phone:
|
||||
queryset = queryset.filter(phone__icontains=phone)
|
||||
if email:
|
||||
queryset = queryset.filter(email__icontains=email)
|
||||
|
||||
if status_filter == 'active':
|
||||
queryset = queryset.filter(is_active=True)
|
||||
elif status_filter == 'disabled':
|
||||
queryset = queryset.filter(is_active=False)
|
||||
|
||||
if idle == 'true':
|
||||
active_app_users = TaskApplication.objects.filter(
|
||||
status__in=[ApplyStatus.PENDING, ApplyStatus.APPROVED, ApplyStatus.DELIVERED]
|
||||
).values_list('applicant_id', flat=True)
|
||||
|
||||
active_task_publishers = Task.objects.filter(
|
||||
status__in=[TaskStatus.OPEN, TaskStatus.IN_PROGRESS]
|
||||
).values_list('publisher_id', flat=True)
|
||||
|
||||
queryset = queryset.exclude(id__in=active_app_users).exclude(id__in=active_task_publishers)
|
||||
|
||||
return queryset.order_by('-is_recommended', '-recommend_priority', '-created_at').distinct()
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action in ['me', 'update_profile', 'avatar', 'change_password', 'list', 'retrieve']:
|
||||
return [drf_permissions.IsAuthenticated()]
|
||||
if self.action in ['admin_create', 'toggle_status', 'reset_password', 'create', 'update', 'partial_update', 'destroy', 'update_opc_stats']:
|
||||
return [HasAPIPermission()]
|
||||
if self.action in ['request_password_reset', 'confirm_password_reset']:
|
||||
return [drf_permissions.AllowAny()]
|
||||
return super().get_permissions()
|
||||
|
||||
@action(detail=False, methods=['post'], permission_classes=[permissions.IsAdminUser])
|
||||
def admin_create(self, request):
|
||||
serializer = AdminUserCreateSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
user = serializer.save()
|
||||
user.set_password(serializer.validated_data['password'])
|
||||
user.save()
|
||||
return Response(UserSerializer(user).data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
partial = kwargs.pop('partial', False)
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance, data=request.data, partial=partial)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_update(serializer)
|
||||
|
||||
# Handle explicitly read-only fields for Admin
|
||||
if request.user.is_staff or request.user.is_superuser:
|
||||
needs_save = False
|
||||
for field in ['is_active', 'status', 'avatar_url', 'bio', 'location', 'face_url']:
|
||||
if field in request.data:
|
||||
setattr(instance, field, request.data[field])
|
||||
needs_save = True
|
||||
if needs_save:
|
||||
instance.save()
|
||||
|
||||
# Handle Role Assignments inside the big form
|
||||
if 'roles' in request.data:
|
||||
role_codes = request.data['roles']
|
||||
from .models import Role, UserRole
|
||||
UserRole.objects.filter(user=instance).delete()
|
||||
for code in role_codes:
|
||||
role = Role.objects.filter(code=code).first()
|
||||
if role:
|
||||
UserRole.objects.create(user=instance, role=role, granted_by=request.user)
|
||||
|
||||
# Handle enterprise_info updates
|
||||
enterprise_info = request.data.get('enterprise_info')
|
||||
if enterprise_info and 'ENTERPRISE' in instance.roles:
|
||||
from .models import Enterprise
|
||||
ent, created = Enterprise.objects.get_or_create(user=instance, defaults={'status': 'VERIFIED'})
|
||||
ent.company_name = enterprise_info.get('company_name', ent.company_name)
|
||||
ent.credit_code = enterprise_info.get('credit_code', ent.credit_code)
|
||||
ent.business_license = enterprise_info.get('business_license', ent.business_license)
|
||||
ent.logo_url = enterprise_info.get('logo_url', ent.logo_url)
|
||||
ent.contact_name = enterprise_info.get('contact_name', ent.contact_name)
|
||||
ent.contact_phone = enterprise_info.get('contact_phone', ent.contact_phone)
|
||||
ent.contact_email = enterprise_info.get('contact_email', ent.contact_email)
|
||||
ent.address = enterprise_info.get('address', ent.address)
|
||||
ent.description = enterprise_info.get('description', ent.description)
|
||||
ent.save()
|
||||
|
||||
# Handle opc_certification updates
|
||||
opc_certification = request.data.get('opc_certification')
|
||||
if opc_certification and 'OPC_USER' in instance.roles:
|
||||
from opc_cert.models import OpcCertification
|
||||
cert, created = OpcCertification.objects.get_or_create(user=instance, defaults={'status': 'APPROVED'})
|
||||
cert.real_name = opc_certification.get('real_name', cert.real_name)
|
||||
cert.id_card = opc_certification.get('id_card', cert.id_card)
|
||||
cert.experience = opc_certification.get('experience', cert.experience)
|
||||
cert.resume_url = opc_certification.get('resume_url', cert.resume_url)
|
||||
cert.attachments = opc_certification.get('attachments', cert.attachments)
|
||||
cert.skills = opc_certification.get('skills', cert.skills)
|
||||
cert.status = opc_certification.get('status', cert.status)
|
||||
if 'rating' in opc_certification:
|
||||
cert.rating = opc_certification.get('rating')
|
||||
cert.save()
|
||||
|
||||
if 'rating' in opc_certification:
|
||||
instance.rating = opc_certification.get('rating')
|
||||
instance.save()
|
||||
|
||||
if getattr(instance, '_prefetched_objects_cache', None):
|
||||
instance._prefetched_objects_cache = {}
|
||||
|
||||
return Response(self.get_serializer(instance).data)
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def change_password(self, request):
|
||||
serializer = ChangePasswordSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
user = request.user
|
||||
if not user.check_password(serializer.validated_data['old_password']):
|
||||
return Response({'old_password': ['原密码错误']}, status=status.HTTP_400_BAD_REQUEST)
|
||||
user.set_password(serializer.validated_data['new_password'])
|
||||
user.save()
|
||||
return Response({'status': '密码修改成功'})
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@action(detail=False, methods=['post'], permission_classes=[drf_permissions.AllowAny])
|
||||
def request_password_reset(self, request):
|
||||
serializer = PasswordResetRequestSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
email = serializer.validated_data['email']
|
||||
user = User.objects.filter(email=email).first()
|
||||
if user:
|
||||
# Mock sending code: Generate 6-digit code and store in cache
|
||||
code = ''.join(random.choices('0123456789', k=6))
|
||||
cache.set(f'pwd_reset_{email}', code, timeout=300) # 5 minutes
|
||||
# In production, send via Email/SMS. Here we return it or print it.
|
||||
print(f"--- MOCK EMAIL --- Password reset code for {email}: {code}")
|
||||
return Response({'status': '验证码已发送 (开发环境请查看控制台输出)'})
|
||||
else:
|
||||
# To prevent email enumeration, still return success
|
||||
return Response({'status': '验证码已发送 (开发环境请查看控制台输出)'})
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@action(detail=False, methods=['post'], permission_classes=[drf_permissions.AllowAny])
|
||||
def confirm_password_reset(self, request):
|
||||
serializer = PasswordResetConfirmSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
email = serializer.validated_data['email']
|
||||
code = serializer.validated_data['verification_code']
|
||||
cached_code = cache.get(f'pwd_reset_{email}')
|
||||
|
||||
if not cached_code or cached_code != code:
|
||||
return Response({'verification_code': ['验证码无效或已过期']}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
user = User.objects.filter(email=email).first()
|
||||
if user:
|
||||
user.set_password(serializer.validated_data['new_password'])
|
||||
user.save()
|
||||
cache.delete(f'pwd_reset_{email}')
|
||||
return Response({'status': '密码重置成功'})
|
||||
return Response({'detail': '用户不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def reset_password(self, request, pk=None):
|
||||
user = self.get_object()
|
||||
new_password = request.data.get('password', '123456')
|
||||
user.set_password(new_password)
|
||||
user.save()
|
||||
return Response({'status': '密码已重置'})
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def me(self, request):
|
||||
serializer = self.get_serializer(request.user)
|
||||
return Response(serializer.data)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
from tasks.models import Task, TaskApplication, TaskStatus, ApplyStatus
|
||||
from django.utils import timezone
|
||||
|
||||
force = self.request.query_params.get('force', '').lower() == 'true'
|
||||
|
||||
# Check active expert applications
|
||||
active_apps = TaskApplication.objects.filter(
|
||||
applicant=instance,
|
||||
status__in=[ApplyStatus.PENDING, ApplyStatus.APPROVED, ApplyStatus.DELIVERED]
|
||||
)
|
||||
|
||||
# Check active enterprise tasks
|
||||
active_tasks = Task.objects.filter(
|
||||
publisher=instance,
|
||||
status__in=[TaskStatus.OPEN, TaskStatus.IN_PROGRESS]
|
||||
)
|
||||
|
||||
has_blockers = active_apps.exists() or active_tasks.exists()
|
||||
|
||||
if has_blockers and not force:
|
||||
from rest_framework.exceptions import ValidationError
|
||||
blocker_details = []
|
||||
if active_apps.exists():
|
||||
blocker_details.append(f'专家接单 {active_apps.count()} 项')
|
||||
if active_tasks.exists():
|
||||
blocker_details.append(f'发布任务 {active_tasks.count()} 项')
|
||||
raise ValidationError(
|
||||
f'用户 {instance.username} 有正在进行中的任务({"、".join(blocker_details)}),无法直接删除。请先取消相关任务或强制删除。'
|
||||
)
|
||||
|
||||
if has_blockers and force:
|
||||
# Lock tasks instead of cancelling — mark as anomalous
|
||||
# For tasks where this user is the ONLY active participant, set CANCELLED
|
||||
# Otherwise keep IN_PROGRESS but lock the user's application
|
||||
for app in active_apps:
|
||||
app.status = ApplyStatus.REJECTED
|
||||
app.reject_reason = f'用户 {instance.nickname or instance.username} 账户已被管理员删除/锁定'
|
||||
app.save()
|
||||
|
||||
# Check if task still has other active participants
|
||||
other_apps = app.task.applications.filter(
|
||||
status__in=[ApplyStatus.APPROVED, ApplyStatus.DELIVERED]
|
||||
).exclude(applicant=instance)
|
||||
if not other_apps.exists() and app.task.status == TaskStatus.IN_PROGRESS:
|
||||
app.task.status = TaskStatus.CANCELLED
|
||||
app.task.cancel_reason = f'唯一执行专家 {instance.nickname or instance.username} 已被系统锁定,任务异常中止'
|
||||
app.task.cancelled_at = timezone.now()
|
||||
app.task.save()
|
||||
|
||||
active_tasks.update(
|
||||
status=TaskStatus.CANCELLED,
|
||||
cancelled_at=timezone.now(),
|
||||
cancel_reason=f'管理员强制删除用户 {instance.username},系统自动取消关联任务'
|
||||
)
|
||||
|
||||
# Check and cascade close enterprise
|
||||
enterprise = Enterprise.objects.filter(user=instance, is_deleted=False).first()
|
||||
if enterprise:
|
||||
# Close enterprise: soft delete + remove all members
|
||||
enterprise.is_deleted = True
|
||||
enterprise.status = 'REJECTED'
|
||||
enterprise.save()
|
||||
EnterpriseMember.objects.filter(enterprise=enterprise).delete()
|
||||
# Cancel enterprise's published tasks
|
||||
Task.objects.filter(
|
||||
enterprise=enterprise,
|
||||
status__in=[TaskStatus.OPEN, TaskStatus.IN_PROGRESS]
|
||||
).update(
|
||||
status=TaskStatus.CANCELLED,
|
||||
cancelled_at=timezone.now(),
|
||||
cancel_reason=f'企业 {enterprise.company_name} 已被清退,关联任务自动取消'
|
||||
)
|
||||
|
||||
instance.is_deleted = True
|
||||
instance.is_active = False
|
||||
# Free up unique fields for re-registration while preserving for audit
|
||||
deleted_suffix = f'__deleted_{instance.id}'
|
||||
if instance.phone:
|
||||
instance.phone = instance.phone + deleted_suffix
|
||||
if instance.email:
|
||||
instance.email = instance.email + deleted_suffix
|
||||
instance.username = instance.username + deleted_suffix
|
||||
instance.save()
|
||||
|
||||
@action(detail=False, methods=['post'], permission_classes=[permissions.IsAdminUser])
|
||||
def batch_delete(self, request):
|
||||
user_ids = request.data.get('user_ids', [])
|
||||
force = request.data.get('force', False)
|
||||
if not user_ids:
|
||||
return Response({'detail': '缺少 user_ids'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
users = User.objects.filter(id__in=user_ids)
|
||||
from tasks.models import Task, TaskApplication, TaskStatus, ApplyStatus
|
||||
from django.utils import timezone
|
||||
|
||||
blocked_users = []
|
||||
for user in users:
|
||||
active_apps = TaskApplication.objects.filter(
|
||||
applicant=user,
|
||||
status__in=[ApplyStatus.PENDING, ApplyStatus.APPROVED, ApplyStatus.DELIVERED]
|
||||
)
|
||||
active_tasks = Task.objects.filter(
|
||||
publisher=user,
|
||||
status__in=[TaskStatus.OPEN, TaskStatus.IN_PROGRESS]
|
||||
)
|
||||
if active_apps.exists() or active_tasks.exists():
|
||||
blocked_users.append({
|
||||
'id': str(user.id),
|
||||
'username': user.username,
|
||||
'nickname': user.nickname or user.username,
|
||||
'app_count': active_apps.count(),
|
||||
'task_count': active_tasks.count(),
|
||||
})
|
||||
|
||||
if blocked_users and not force:
|
||||
return Response({
|
||||
'detail': f'共 {len(blocked_users)} 个用户有正在进行中的任务,无法直接批量删除。',
|
||||
'blocked_users': blocked_users,
|
||||
'has_blockers': True,
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if blocked_users and force:
|
||||
for user in users:
|
||||
TaskApplication.objects.filter(
|
||||
applicant=user,
|
||||
status__in=[ApplyStatus.PENDING, ApplyStatus.APPROVED, ApplyStatus.DELIVERED]
|
||||
).update(status=ApplyStatus.WITHDRAWN)
|
||||
Task.objects.filter(
|
||||
publisher=user,
|
||||
status__in=[TaskStatus.OPEN, TaskStatus.IN_PROGRESS]
|
||||
).update(
|
||||
status=TaskStatus.CANCELLED,
|
||||
cancelled_at=timezone.now(),
|
||||
cancel_reason=f'管理员批量强制删除,系统自动取消关联任务'
|
||||
)
|
||||
|
||||
for user in users:
|
||||
user.is_deleted = True
|
||||
user.is_active = False
|
||||
deleted_suffix = f'__deleted_{user.id}'
|
||||
if user.phone:
|
||||
user.phone = user.phone + deleted_suffix
|
||||
if user.email:
|
||||
user.email = user.email + deleted_suffix
|
||||
user.username = user.username + deleted_suffix
|
||||
user.save()
|
||||
return Response({'status': '批量删除成功'})
|
||||
|
||||
@action(detail=True, methods=['post'], permission_classes=[permissions.IsAdminUser])
|
||||
def assign_roles(self, request, pk=None):
|
||||
user = self.get_object()
|
||||
role_codes = request.data.get('roles', [])
|
||||
from .models import Role, UserRole
|
||||
# 清除旧角色
|
||||
UserRole.objects.filter(user=user).delete()
|
||||
# 重新分配
|
||||
for code in role_codes:
|
||||
role = Role.objects.filter(code=code).first()
|
||||
if role:
|
||||
UserRole.objects.create(user=user, role=role, granted_by=request.user)
|
||||
return Response({'status': '角色分配成功'})
|
||||
|
||||
@action(detail=True, methods=['post'], permission_classes=[permissions.IsAdminUser])
|
||||
def update_rating(self, request, pk=None):
|
||||
user = self.get_object()
|
||||
rating = request.data.get('rating')
|
||||
if rating is not None:
|
||||
user.rating = rating
|
||||
user.save()
|
||||
return Response({'status': '评分已更新'})
|
||||
return Response({'detail': '缺少评分参数'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@action(detail=True, methods=['get'], permission_classes=[permissions.IsAdminUser])
|
||||
def active_tasks(self, request, pk=None):
|
||||
user = self.get_object()
|
||||
from tasks.models import Task, TaskApplication, TaskStatus, ApplyStatus
|
||||
|
||||
results = []
|
||||
|
||||
# Expert tasks
|
||||
expert_apps = TaskApplication.objects.filter(
|
||||
applicant=user,
|
||||
status__in=[ApplyStatus.PENDING, ApplyStatus.APPROVED, ApplyStatus.DELIVERED]
|
||||
)
|
||||
for app in expert_apps:
|
||||
results.append({
|
||||
'id': app.task.id,
|
||||
'title': app.task.title,
|
||||
'role': '专家接单',
|
||||
'status': app.status,
|
||||
'task_status': app.task.status,
|
||||
'type': 'application'
|
||||
})
|
||||
|
||||
# Publisher tasks
|
||||
pub_tasks = Task.objects.filter(
|
||||
publisher=user,
|
||||
status__in=[TaskStatus.OPEN, TaskStatus.IN_PROGRESS]
|
||||
)
|
||||
for t in pub_tasks:
|
||||
results.append({
|
||||
'id': t.id,
|
||||
'title': t.title,
|
||||
'role': '任务发布方',
|
||||
'status': t.status,
|
||||
'task_status': t.status,
|
||||
'type': 'task'
|
||||
})
|
||||
|
||||
return Response(results)
|
||||
|
||||
@action(detail=False, methods=['put'])
|
||||
def update_profile(self, request):
|
||||
user = request.user
|
||||
serializer = UserSerializer(user, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@action(detail=True, methods=['post'], permission_classes=[permissions.IsAdminUser])
|
||||
def toggle_status(self, request, pk=None):
|
||||
user = self.get_object()
|
||||
user.is_active = not user.is_active
|
||||
user.save()
|
||||
return Response({'status': '状态已更新', 'is_active': user.is_active})
|
||||
|
||||
@action(detail=True, methods=['put'], permission_classes=[permissions.IsAdminUser])
|
||||
def update_opc_stats(self, request, pk=None):
|
||||
user = self.get_object()
|
||||
|
||||
rating = request.data.get('rating')
|
||||
completed_tasks = request.data.get('completed_tasks')
|
||||
|
||||
if rating is not None:
|
||||
user.rating = rating
|
||||
if completed_tasks is not None:
|
||||
user.completed_tasks = completed_tasks
|
||||
|
||||
user.save()
|
||||
return Response(UserSerializer(user).data)
|
||||
|
||||
@action(detail=True, methods=['post'], permission_classes=[permissions.IsAdminUser])
|
||||
def toggle_recommend(self, request, pk=None):
|
||||
user = self.get_object()
|
||||
user.is_recommended = request.data.get('is_recommended', not user.is_recommended)
|
||||
user.recommend_priority = request.data.get('recommend_priority', user.recommend_priority)
|
||||
user.save(update_fields=['is_recommended', 'recommend_priority'])
|
||||
return Response({'is_recommended': user.is_recommended, 'recommend_priority': user.recommend_priority})
|
||||
|
||||
@action(detail=False, methods=['get'], permission_classes=[permissions.IsAdminUser])
|
||||
def deleted_users(self, request):
|
||||
"""List soft-deleted users for admin review/recovery."""
|
||||
deleted = User.objects.filter(is_deleted=True).order_by('-updated_at')
|
||||
page = self.paginate_queryset(deleted)
|
||||
if page is not None:
|
||||
serializer = UserSerializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
serializer = UserSerializer(deleted, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['post'], permission_classes=[permissions.IsAdminUser])
|
||||
def restore(self, request, pk=None):
|
||||
"""Restore a soft-deleted user by clearing the deletion suffix."""
|
||||
try:
|
||||
user = User.objects.get(id=pk, is_deleted=True)
|
||||
except User.DoesNotExist:
|
||||
return Response({'detail': '未找到已删除的用户'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
deleted_suffix = f'__deleted_{user.id}'
|
||||
|
||||
# Restore unique fields
|
||||
if user.username.endswith(deleted_suffix):
|
||||
original_username = user.username[:-len(deleted_suffix)]
|
||||
if User.objects.filter(username=original_username, is_deleted=False).exists():
|
||||
return Response({'detail': f'用户名 {original_username} 已被新用户占用,无法恢复'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
user.username = original_username
|
||||
|
||||
if user.phone and user.phone.endswith(deleted_suffix):
|
||||
original_phone = user.phone[:-len(deleted_suffix)]
|
||||
if User.objects.filter(phone=original_phone, is_deleted=False).exists():
|
||||
return Response({'detail': f'手机号 {original_phone} 已被新用户占用,无法恢复'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
user.phone = original_phone
|
||||
|
||||
if user.email and user.email.endswith(deleted_suffix):
|
||||
original_email = user.email[:-len(deleted_suffix)]
|
||||
if User.objects.filter(email=original_email, is_deleted=False).exists():
|
||||
return Response({'detail': f'邮箱 {original_email} 已被新用户占用,无法恢复'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
user.email = original_email
|
||||
|
||||
user.is_deleted = False
|
||||
user.is_active = True
|
||||
user.save()
|
||||
return Response({'status': f'用户 {user.username} 已成功恢复'})
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def avatar(self, request):
|
||||
# In a real app, handle file upload to MinIO
|
||||
# For now, we expect a URL or just mock it
|
||||
user = request.user
|
||||
avatar_url = request.data.get('avatar_url')
|
||||
if avatar_url:
|
||||
user.avatar_url = avatar_url
|
||||
user.save()
|
||||
return Response({'status': '头像已更新', 'avatar_url': avatar_url})
|
||||
return Response({'detail': '缺少 avatar_url'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class RegisterView(generics.CreateAPIView):
|
||||
"""
|
||||
@author: xujl
|
||||
Api说明: 平台普通用户注册接口视图。提供基础的账号密码/手机/邮箱注册流程。
|
||||
"""
|
||||
queryset = User.objects.all()
|
||||
permission_classes = [permissions.AllowAny]
|
||||
serializer_class = RegisterSerializer
|
||||
|
||||
class EnterpriseViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
@author: xujl
|
||||
Api说明: 企业资质及账号管理接口视图。企业用户提交工商资料认证、基本信息更新,管理员对企业入驻进行审核。
|
||||
"""
|
||||
queryset = Enterprise.objects.filter(is_deleted=False).order_by('-created_at')
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'create' and (self.request.user.is_staff or self.request.user.is_superuser):
|
||||
from .serializers import AdminEnterpriseCreateSerializer
|
||||
return AdminEnterpriseCreateSerializer
|
||||
from .serializers import EnterpriseSerializer
|
||||
return EnterpriseSerializer
|
||||
|
||||
required_permissions = {
|
||||
'GET': 'api:enterprises:read',
|
||||
'POST': 'api:enterprises:write',
|
||||
'PUT': 'api:enterprises:write',
|
||||
'PATCH': 'api:enterprises:write',
|
||||
'DELETE': 'api:enterprises:delete'
|
||||
}
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action in ['me', 'create', 'update', 'partial_update']:
|
||||
return [drf_permissions.IsAuthenticated()]
|
||||
if self.action in ['list', 'retrieve', 'verify_status']:
|
||||
return [HasAPIPermission()]
|
||||
return super().get_permissions()
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def me(self, request):
|
||||
try:
|
||||
# First check if user is the enterprise owner
|
||||
enterprise = Enterprise.objects.filter(user=request.user).first()
|
||||
role = 'ADMIN'
|
||||
|
||||
if not enterprise:
|
||||
# If not owner, check if user is a member
|
||||
member = EnterpriseMember.objects.filter(user=request.user).first()
|
||||
if member:
|
||||
enterprise = member.enterprise
|
||||
role = member.role
|
||||
else:
|
||||
return Response({'detail': '未找到企业信息'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
serializer = self.get_serializer(enterprise)
|
||||
data = serializer.data
|
||||
data['my_role'] = role
|
||||
return Response(data)
|
||||
except Exception as e:
|
||||
return Response({'detail': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
if self.request.user.is_staff or self.request.user.is_superuser:
|
||||
serializer.save()
|
||||
else:
|
||||
enterprise = serializer.save(user=self.request.user)
|
||||
# Auto assign ENTERPRISE role
|
||||
from .models import Role, UserRole
|
||||
role, created = Role.objects.get_or_create(code='ENTERPRISE', defaults={'name': '企业用户'})
|
||||
UserRole.objects.get_or_create(user=self.request.user, role=role)
|
||||
|
||||
def _check_admin_permission(self, request, enterprise):
|
||||
if request.user.is_superuser or request.user.is_staff:
|
||||
return True
|
||||
if request.user == enterprise.user:
|
||||
return True
|
||||
member = EnterpriseMember.objects.filter(enterprise=enterprise, user=request.user).first()
|
||||
return member and member.role == 'ADMIN'
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
enterprise = self.get_object()
|
||||
if not self._check_admin_permission(request, enterprise):
|
||||
return Response({'detail': '只有企业管理员可以编辑企业数据'}, status=status.HTTP_403_FORBIDDEN)
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
enterprise = self.get_object()
|
||||
if not self._check_admin_permission(request, enterprise):
|
||||
return Response({'detail': '只有企业管理员可以编辑企业数据'}, status=status.HTTP_403_FORBIDDEN)
|
||||
return super().partial_update(request, *args, **kwargs)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
"""Cascade enterprise deletion: soft-delete, remove members, cancel active tasks, free email."""
|
||||
from tasks.models import Task, TaskStatus
|
||||
from django.utils import timezone
|
||||
|
||||
# Cancel all active tasks for this enterprise
|
||||
Task.objects.filter(
|
||||
enterprise=instance,
|
||||
status__in=[TaskStatus.OPEN, TaskStatus.IN_PROGRESS]
|
||||
).update(
|
||||
status=TaskStatus.CANCELLED,
|
||||
cancelled_at=timezone.now(),
|
||||
cancel_reason=f'企业 {instance.company_name} 已被清退,项目中止'
|
||||
)
|
||||
|
||||
# Remove all team members
|
||||
EnterpriseMember.objects.filter(enterprise=instance).delete()
|
||||
|
||||
# Free up the enterprise user's email for re-use (suffix pattern)
|
||||
owner = instance.user
|
||||
suffix = f'__deleted_{str(instance.id)[:8]}'
|
||||
if owner.email and '__deleted_' not in owner.email:
|
||||
owner.email = owner.email + suffix
|
||||
owner.save(update_fields=['email'])
|
||||
|
||||
# Free up credit_code
|
||||
if instance.credit_code and '__deleted_' not in instance.credit_code:
|
||||
instance.credit_code = instance.credit_code + suffix
|
||||
|
||||
# Soft delete enterprise
|
||||
instance.is_deleted = True
|
||||
instance.status = 'REJECTED'
|
||||
instance.save()
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def deleted_enterprises(self, request):
|
||||
"""List soft-deleted enterprises for the recycle bin."""
|
||||
deleted = Enterprise.objects.filter(is_deleted=True).order_by('-updated_at')
|
||||
serializer = self.get_serializer(deleted, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def restore(self, request, pk=None):
|
||||
"""Restore a soft-deleted enterprise."""
|
||||
enterprise = Enterprise.objects.filter(pk=pk, is_deleted=True).first()
|
||||
if not enterprise:
|
||||
return Response({'detail': '未找到已删除的企业'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Restore credit_code
|
||||
if enterprise.credit_code and '__deleted_' in enterprise.credit_code:
|
||||
original_code = enterprise.credit_code.split('__deleted_')[0]
|
||||
if Enterprise.objects.filter(credit_code=original_code, is_deleted=False).exists():
|
||||
return Response({'detail': f'统一社会信用代码 {original_code} 已被其他企业占用'}, status=status.HTTP_409_CONFLICT)
|
||||
enterprise.credit_code = original_code
|
||||
|
||||
# Restore owner email
|
||||
owner = enterprise.user
|
||||
if owner.email and '__deleted_' in owner.email:
|
||||
original_email = owner.email.split('__deleted_')[0]
|
||||
if User.objects.filter(email=original_email, is_deleted=False).exclude(pk=owner.pk).exists():
|
||||
return Response({'detail': f'邮箱 {original_email} 已被其他用户占用,无法恢复'}, status=status.HTTP_409_CONFLICT)
|
||||
owner.email = original_email
|
||||
owner.save(update_fields=['email'])
|
||||
|
||||
enterprise.is_deleted = False
|
||||
enterprise.status = 'VERIFIED'
|
||||
enterprise.save()
|
||||
return Response({'detail': '企业已恢复'})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def verify_status(self, request, pk=None):
|
||||
enterprise = self.get_object()
|
||||
status_val = request.data.get('status')
|
||||
if status_val not in [choice[0] for choice in Enterprise.EnterpriseStatus.choices]:
|
||||
return Response({'detail': '无效的状态'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
enterprise.status = status_val
|
||||
enterprise.save()
|
||||
return Response({'status': '企业审核状态已更新', 'enterprise_status': enterprise.status})
|
||||
|
||||
class EnterpriseRegisterView(generics.CreateAPIView):
|
||||
"""
|
||||
@author: xujl
|
||||
Api说明: 企业级账号入驻/注册接口视图。提交企业信用代码和法人代表等认证资料。
|
||||
"""
|
||||
serializer_class = EnterpriseRegisterSerializer
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response({'status': '企业注册成功'}, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
class EnterpriseMemberViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
@author: xujl
|
||||
Api说明: 企业内部成员管理接口视图。用于给企业内部关联多个员工账号并分配角色(管理员、普通成员)。
|
||||
"""
|
||||
serializer_class = EnterpriseMemberSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.user.is_staff or self.request.user.is_superuser:
|
||||
enterprise_id = self.request.query_params.get('enterprise')
|
||||
if enterprise_id:
|
||||
return EnterpriseMember.objects.filter(enterprise_id=enterprise_id).order_by('-joined_at')
|
||||
return EnterpriseMember.objects.all().order_by('-joined_at')
|
||||
|
||||
try:
|
||||
enterprise = Enterprise.objects.get(user=self.request.user)
|
||||
return EnterpriseMember.objects.filter(enterprise=enterprise).order_by('-joined_at')
|
||||
except Enterprise.DoesNotExist:
|
||||
return EnterpriseMember.objects.none()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
enterprise = Enterprise.objects.get(user=self.request.user)
|
||||
serializer.save(enterprise=enterprise)
|
||||
|
||||
@action(detail=False, methods=['post'], permission_classes=[permissions.IsAuthenticated])
|
||||
def add_member(self, request):
|
||||
serializer = EnterpriseMemberAddSerializer(data=request.data, context={'request': request})
|
||||
if serializer.is_valid():
|
||||
member = serializer.save()
|
||||
return Response(EnterpriseMemberSerializer(member).data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@action(detail=False, methods=['post'], permission_classes=[permissions.IsAdminUser])
|
||||
def batch_delete(self, request):
|
||||
member_ids = request.data.get('member_ids', [])
|
||||
if not member_ids:
|
||||
return Response({'detail': '缺少 member_ids'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
EnterpriseMember.objects.filter(id__in=member_ids).delete()
|
||||
return Response({'status': '批量删除成功'})
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def invite(self, request):
|
||||
email = request.data.get('email')
|
||||
role = request.data.get('role', 'MEMBER')
|
||||
|
||||
try:
|
||||
enterprise = Enterprise.objects.get(user=request.user)
|
||||
user = User.objects.get(email=email)
|
||||
|
||||
if EnterpriseMember.objects.filter(enterprise=enterprise, user=user).exists():
|
||||
return Response({'detail': '该用户已在团队中'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
member = EnterpriseMember.objects.create(
|
||||
enterprise=enterprise,
|
||||
user=user,
|
||||
role=role
|
||||
)
|
||||
return Response(EnterpriseMemberSerializer(member).data, status=status.HTTP_201_CREATED)
|
||||
except Enterprise.DoesNotExist:
|
||||
return Response({'detail': '未找到企业信息'}, status=status.HTTP_404_NOT_FOUND)
|
||||
except User.DoesNotExist:
|
||||
return Response({'detail': '该用户尚未注册平台,请先引导其注册'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
class RoleViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
@author: xujl
|
||||
Api说明: RBAC 角色管理接口视图。用于定义平台角色类别,以及为用户进行角色的授予和回收。
|
||||
"""
|
||||
queryset = Role.objects.all().order_by('sort_order', 'created_at')
|
||||
serializer_class = RoleSerializer
|
||||
pagination_class = None # Return all roles without pagination
|
||||
permission_classes = [HasAPIPermission]
|
||||
required_permissions = {
|
||||
'GET': 'menu:account:roles',
|
||||
'POST': 'menu:account:roles',
|
||||
'PUT': 'menu:account:roles',
|
||||
'DELETE': 'menu:account:roles'
|
||||
}
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def assign_permissions(self, request, pk=None):
|
||||
role = self.get_object()
|
||||
permission_ids = request.data.get('permissions', [])
|
||||
from .models import Permission, RolePermission
|
||||
RolePermission.objects.filter(role=role).delete()
|
||||
for pid in permission_ids:
|
||||
perm = Permission.objects.filter(id=pid).first()
|
||||
if perm:
|
||||
RolePermission.objects.create(role=role, permission=perm)
|
||||
return Response({'status': '权限分配成功'})
|
||||
|
||||
class PermissionViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
@author: xujl
|
||||
Api说明: RBAC 权限节点管理接口视图。用于查询系统的路由、菜单及按钮级别的权限树结构。
|
||||
"""
|
||||
queryset = Permission.objects.all().order_by('sort_order', 'created_at')
|
||||
serializer_class = PermissionSerializer
|
||||
pagination_class = None # Return all permissions without pagination
|
||||
permission_classes = [HasAPIPermission]
|
||||
required_permissions = {
|
||||
'GET': 'menu:account:permissions',
|
||||
'POST': 'menu:account:permissions',
|
||||
'PUT': 'menu:account:permissions',
|
||||
'DELETE': 'menu:account:permissions'
|
||||
}
|
||||
Reference in New Issue
Block a user