feat: 添加用户登录功能、添加注册功能、注册审批功能,参考:注册审批功能测试指南.md、驱动代码模块增加插入其他项目代码功能,驱动代码模块增加插入其他项目代码功能,优化测试报告模块前端布局和美化
- 实现超级用户管理系统用户,可进行增删改查,其他用户只可查看本人的数据,不可进行增删改
- 将Django后台管理中的用户配置相关功能迁移到系统中进行适配、可配置禁用、是否管理员、是否超级用户
- 增加用户管理的分组字段、可直接进行分组操作,实现用户管理即可通过分组配置实现各项目的访问权限
- 实现登录页面注册
- 实现管理员首页点击注册用户审批可实现通过不通过
- 实现未审核用户的登录提示与审核未通过的用户登录提示
- 实现一键插入其他本人可访问项目的脚本代码
14 files added
13 files modified
4166 ■■■■■ changed files
测试组/Test_platform/Interface_automation/backend/DEPLOY.md 186 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/add_status_field.py 275 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/migrations/0003_myuser_status.py 22 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/models.py 13 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/serializers.py 170 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/urls.py 20 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/views.py 236 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/backend/settings.py 2 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/backend/utils/permissions.py 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/check_users.py 223 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/scripts/README.md 231 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/scripts/__init__.py patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/scripts/collect_static.py 58 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/scripts/create_admin_user.py 158 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/scripts/create_groups.py 110 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/scripts/deploy_init.py 258 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/scripts/run_migrations.py 43 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/api/user.js 37 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/home/components/Header.vue 303 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/login/Login.vue 307 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/project/ProjectList.vue 29 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/user/UserManagement.vue 687 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/user/UserManagementDrawer.vue 591 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/restful/api.js 55 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/store/mutations.js 4 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/store/state.js 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/注册审批功能测试指南.md 144 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/DEPLOY.md
New file
@@ -0,0 +1,186 @@
# 首次部署说明
## 快速开始(推荐方式)
使用更新后的脚本一键完成所有初始化:
```bash
cd backend
python add_status_field.py --init
```
这将自动完成:
1. 检查数据库连接
2. 执行数据库迁移(makemigrations + migrate)
3. 创建管理员用户(admin/admin123)
4. 创建默认用户组
5. 收集静态文件
## 使用 Django 标准命令
如果您更喜欢使用 Django 的标准命令,也可以按以下步骤执行:
```bash
cd backend
# 1. 生成迁移文件
python manage.py makemigrations
# 2. 应用迁移
python manage.py migrate
# 3. 创建管理员用户(交互式)
python manage.py createsuperuser
```
## 脚本功能说明
### add_status_field.py - 首次部署初始化脚本
**完整初始化:**
```bash
python add_status_field.py --init
```
**分步执行:**
```bash
# 仅执行数据库迁移
python add_status_field.py --migrate
# 仅创建管理员用户
python add_status_field.py --create-admin
# 仅创建用户组
python add_status_field.py --create-groups
# 仅收集静态文件
python add_status_field.py --collect-static
```
### check_users.py - 用户管理脚本
**列出所有用户:**
```bash
python check_users.py --list
```
**创建管理员用户:**
```bash
python check_users.py --create-admin
```
**创建测试用户:**
```bash
python check_users.py --create-test
```
**创建用户组:**
```bash
python check_users.py --create-groups
```
**列出所有用户组:**
```bash
python check_users.py --list-groups
```
**一键初始化用户数据:**
```bash
python check_users.py --init
```
## 默认账号信息
**管理员账号:**
- 用户名: `admin`
- 密码: `admin123`
⚠️ **重要提示:** 请在首次登录后立即修改默认密码!
**测试账号:**
- `testuser` / `test123` - 测试用户
- `developer` / `dev123` - 开发人员
## 部署前准备
1. **配置数据库连接**
编辑 `backend/conf/env.py` 文件:
```python
DATABASE_NAME = 'your_database_name'
DATABASE_USER = 'your_database_user'
DATABASE_PASSWORD = 'your_database_password'
DATABASE_HOST = 'localhost'
DATABASE_PORT = '3306'
```
2. **创建数据库**
在 MySQL 中执行:
```sql
CREATE DATABASE your_database_name CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
3. **安装依赖**
```bash
cd backend
pip install -r requirements.txt
```
## 启动服务
**开发环境:**
```bash
cd backend
python manage.py runserver
```
**生产环境:**
```bash
cd backend
gunicorn backend.wsgi:application -c gunicorn_conf.py
```
## 数据库表结构
### lunaruser 应用
- `lunaruser_myuser` - 用户表(包含 phone、show_hosts、name、status 字段)
### lunarlink 应用
- `project` - 项目信息表
- `config` - 环境信息表
- `api` - 接口信息表
- `case` - 用例信息表
- `case_step` - 用例步骤表
- `variables` - 全局变量表
- `debugtalk` - 驱动代码表
- `report` - 测试报告表
- `report_detail` - 测试报告详情表
- `relation` - 树形结构关系表
- `visit` - 访问日志表
- `login_log` - 登录日志表
## 常见问题
### 1. 数据库连接失败
检查 `conf/env.py` 中的数据库配置是否正确,确保数据库已创建且用户有足够的权限。
### 2. 迁移失败
确保数据库表不存在或为空。如果之前有迁移历史,可能需要删除 `migrations` 目录下的迁移文件(保留 `__init__.py`)。
### 3. 静态文件收集失败
确保 `STATIC_ROOT` 目录有写入权限。
### 4. 用户已存在
脚本会自动检测用户是否已存在,如果存在则跳过创建。
## 技术支持
如有问题,请查看项目文档或联系技术支持团队。
测试组/Test_platform/Interface_automation/backend/add_status_field.py
New file
@@ -0,0 +1,275 @@
#!/usr/bin/env python
"""
首次部署初始化脚本
该脚本用于首次部署系统时执行所有必要的初始化操作,包括:
1. 检查数据库连接
2. 执行数据库迁移
3. 创建管理员用户
4. 创建默认用户组
5. 收集静态文件
使用方法:
    python add_status_field.py --init
或者使用 Django 标准命令:
    python manage.py makemigrations
    python manage.py migrate
    python manage.py createsuperuser
"""
import os
import sys
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
django.setup()
from django.core.management import call_command
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
def check_database_connection():
    """
    检查数据库连接
    """
    print("=" * 60)
    print("检查数据库连接...")
    print("=" * 60)
    try:
        from django.db import connection
        with connection.cursor() as cursor:
            cursor.execute("SELECT 1")
            print("✓ 数据库连接正常")
            return True
    except Exception as e:
        print(f"✗ 数据库连接失败: {e}")
        print("\n请检查 conf/env.py 中的数据库配置")
        return False
def run_migrations():
    """
    执行数据库迁移
    """
    print("\n" + "=" * 60)
    print("执行数据库迁移...")
    print("=" * 60)
    try:
        print("\n[1/2] 生成迁移文件...")
        call_command('makemigrations', verbosity=1)
        print("✓ 迁移文件生成成功")
        print("\n[2/2] 应用迁移...")
        call_command('migrate', verbosity=1)
        print("✓ 数据库迁移成功")
        return True
    except Exception as e:
        print(f"✗ 数据库迁移失败: {e}")
        import traceback
        traceback.print_exc()
        return False
def create_admin_user(username='admin', password='admin123', email='admin@example.com', name='系统管理员'):
    """
    创建管理员用户
    """
    print("\n" + "=" * 60)
    print("创建管理员用户...")
    print("=" * 60)
    User = get_user_model()
    try:
        user = User.objects.get(username=username)
        print(f"管理员用户 '{username}' 已存在,跳过创建")
        print(f"  用户名: {user.username}")
        print(f"  姓名: {user.name}")
        print(f"  邮箱: {user.email}")
        return True
    except User.DoesNotExist:
        try:
            user = User.objects.create_superuser(
                username=username,
                email=email,
                password=password
            )
            user.name = name
            user.phone = None
            user.show_hosts = False
            user.status = 'approved'
            user.save()
            print("✓ 管理员用户创建成功")
            print(f"  用户名: {username}")
            print(f"  密码: {password}")
            print(f"  邮箱: {email}")
            print(f"  姓名: {name}")
            print("\n⚠️  重要提示: 请在首次登录后修改默认密码!")
            return True
        except Exception as e:
            print(f"✗ 创建管理员用户失败: {e}")
            import traceback
            traceback.print_exc()
            return False
def create_default_groups():
    """
    创建默认用户组
    """
    print("\n" + "=" * 60)
    print("创建默认用户组...")
    print("=" * 60)
    groups = [
        {'name': '管理员组', 'description': '拥有所有权限的管理员组'},
        {'name': '开发人员组', 'description': '开发人员组'},
        {'name': '测试人员组', 'description': '测试人员组'},
        {'name': '只读用户组', 'description': '只读用户组'},
    ]
    created_count = 0
    for group_info in groups:
        try:
            Group.objects.get(name=group_info['name'])
            print(f"  用户组 '{group_info['name']}' 已存在")
        except Group.DoesNotExist:
            try:
                Group.objects.create(name=group_info['name'])
                print(f"  ✓ 创建用户组: {group_info['name']}")
                created_count += 1
            except Exception as e:
                print(f"  ✗ 创建用户组 '{group_info['name']}' 失败: {e}")
    print(f"\n✓ 成功创建 {created_count} 个用户组")
    return True
def collect_static_files():
    """
    收集静态文件
    """
    print("\n" + "=" * 60)
    print("收集静态文件...")
    print("=" * 60)
    try:
        call_command('collectstatic', '--noinput', verbosity=1)
        print("✓ 静态文件收集成功")
        return True
    except Exception as e:
        print(f"✗ 静态文件收集失败: {e}")
        import traceback
        traceback.print_exc()
        return False
def display_summary():
    """
    显示部署摘要
    """
    print("\n" + "=" * 60)
    print("部署摘要")
    print("=" * 60)
    User = get_user_model()
    print(f"\n用户数量: {User.objects.count()}")
    print(f"用户组数量: {Group.objects.count()}")
    print("\n默认管理员账号:")
    print("  用户名: admin")
    print("  密码: admin123")
    print("\n⚠️  请在首次登录后立即修改默认密码!")
def main():
    """
    主函数
    """
    import argparse
    parser = argparse.ArgumentParser(description='首次部署初始化脚本')
    parser.add_argument('--init', action='store_true', help='执行完整的初始化流程')
    parser.add_argument('--migrate', action='store_true', help='仅执行数据库迁移')
    parser.add_argument('--create-admin', action='store_true', help='仅创建管理员用户')
    parser.add_argument('--create-groups', action='store_true', help='仅创建用户组')
    parser.add_argument('--collect-static', action='store_true', help='仅收集静态文件')
    args = parser.parse_args()
    if args.init:
        print("\n")
        print("╔" + "=" * 58 + "╗")
        print("║" + " " * 10 + "接口自动化平台 - 首次部署初始化" + " " * 13 + "║")
        print("╚" + "=" * 58 + "╝")
        print()
        steps = [
            ("检查数据库连接", check_database_connection),
            ("执行数据库迁移", run_migrations),
            ("创建管理员用户", create_admin_user),
            ("创建默认用户组", create_default_groups),
            ("收集静态文件", collect_static_files),
        ]
        failed_steps = []
        for step_name, step_func in steps:
            if not step_func():
                failed_steps.append(step_name)
                print(f"\n✗ '{step_name}' 失败,终止部署")
                break
        if not failed_steps:
            display_summary()
            print("\n" + "=" * 60)
            print("✓ 部署初始化完成!")
            print("=" * 60)
            print("\n系统已准备就绪,可以启动服务:")
            print("  python manage.py runserver")
            print()
            return 0
        else:
            print("\n" + "=" * 60)
            print("✗ 部署初始化失败!")
            print("=" * 60)
            print(f"\n失败的步骤: {', '.join(failed_steps)}")
            return 1
    elif args.migrate:
        return 0 if run_migrations() else 1
    elif args.create_admin:
        return 0 if create_admin_user() else 1
    elif args.create_groups:
        return 0 if create_default_groups() else 1
    elif args.collect_static:
        return 0 if collect_static_files() else 1
    else:
        parser.print_help()
        print("\n使用示例:")
        print("  python add_status_field.py --init          # 完整初始化")
        print("  python add_status_field.py --migrate       # 仅迁移数据库")
        print("  python add_status_field.py --create-admin   # 仅创建管理员")
        print("\n或使用 Django 标准命令:")
        print("  python manage.py makemigrations")
        print("  python manage.py migrate")
        print("  python manage.py createsuperuser")
        return 0
if __name__ == '__main__':
    sys.exit(main())
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/migrations/0003_myuser_status.py
New file
@@ -0,0 +1,22 @@
from django.db import migrations, models
class Migration(migrations.Migration):
    dependencies = [
        ('lunaruser', '0002_myuser_name'),
    ]
    operations = [
        migrations.AddField(
            model_name='myuser',
            name='status',
            field=models.CharField(
                choices=[('pending', '待审核'), ('approved', '已通过'), ('rejected', '未通过')],
                default='approved',
                help_text='用户状态',
                max_length=20,
                verbose_name='状态'
            ),
        ),
    ]
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/models.py
@@ -2,7 +2,6 @@
from django.contrib.auth.models import AbstractUser
# Create your models here.
class MyUser(AbstractUser):
    """
    使用AbstractUser可以对User进行扩展使用,添加用户自定义的属性
@@ -27,6 +26,18 @@
        null=True,
        help_text="姓名",
    )
    STATUS_CHOICES = (
        ('pending', '待审核'),
        ('approved', '已通过'),
        ('rejected', '未通过'),
    )
    status = models.CharField(
        verbose_name="状态",
        max_length=20,
        choices=STATUS_CHOICES,
        default='approved',
        help_text="用户状态",
    )
    class Meta(AbstractUser.Meta):
        pass
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/serializers.py
@@ -10,6 +10,7 @@
from rest_framework import serializers
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from lunarlink.models import LoginLog
@@ -67,3 +68,172 @@
        if attrs['new_password'] != attrs['confirm_password']:
            raise serializers.ValidationError("两次输入的新密码不一致")
        return attrs
class UserCreateSerializer(serializers.ModelSerializer):
    """
    创建用户序列化
    """
    password = serializers.CharField(required=True, min_length=6, max_length=128, write_only=True)
    confirm_password = serializers.CharField(required=True, max_length=128, write_only=True)
    groups = serializers.PrimaryKeyRelatedField(
        many=True,
        queryset=Group.objects.all(),
        required=False
    )
    class Meta:
        model = User
        fields = ['id', 'username', 'password', 'confirm_password', 'name', 'phone', 'is_staff', 'is_superuser', 'is_active', 'groups', 'show_hosts']
    def validate(self, attrs):
        if attrs['password'] != attrs['confirm_password']:
            raise serializers.ValidationError("两次输入的密码不一致")
        return attrs
    def create(self, validated_data):
        validated_data.pop('confirm_password')
        password = validated_data.pop('password')
        groups = validated_data.pop('groups', None)
        user = User(**validated_data)
        user.set_password(password)
        user.save()
        if groups:
            user.groups.set(groups)
        return user
class UserUpdateSerializer(serializers.ModelSerializer):
    """
    更新用户序列化
    """
    groups = serializers.PrimaryKeyRelatedField(
        many=True,
        queryset=Group.objects.all(),
        required=False
    )
    class Meta:
        model = User
        fields = ['id', 'username', 'name', 'phone', 'is_staff', 'is_superuser', 'is_active', 'groups', 'show_hosts']
    def update(self, instance, validated_data):
        groups = validated_data.pop('groups', None)
        for attr, value in validated_data.items():
            setattr(instance, attr, value)
        instance.save()
        if groups is not None:
            instance.groups.set(groups)
        return instance
class UserDetailSerializer(serializers.ModelSerializer):
    """
    用户详情序列化
    """
    groups = serializers.SerializerMethodField()
    accessible_projects = serializers.SerializerMethodField()
    status_display = serializers.CharField(source='get_status_display', read_only=True)
    class Meta:
        model = User
        fields = ['id', 'username', 'name', 'phone', 'is_staff', 'is_superuser', 'is_active', 'groups', 'show_hosts', 'date_joined', 'last_login', 'accessible_projects', 'status', 'status_display']
    def get_groups(self, obj):
        return [g.name for g in obj.groups.all()]
    def get_accessible_projects(self, obj):
        from lunarlink.models import Project
        user_groups = obj.groups.all()
        return list(Project.objects.filter(groups__in=user_groups).distinct().values_list("name", flat=True))
class UserRegisterSerializer(serializers.ModelSerializer):
    """
    用户注册序列化
    """
    password = serializers.CharField(required=True, min_length=6, max_length=128, write_only=True)
    confirm_password = serializers.CharField(required=True, max_length=128, write_only=True)
    groups = serializers.PrimaryKeyRelatedField(
        many=True,
        queryset=Group.objects.all(),
        required=False
    )
    class Meta:
        model = User
        fields = ['username', 'password', 'confirm_password', 'name', 'phone', 'groups']
        extra_kwargs = {
            'username': {'validators': []},
            'phone': {'validators': []}
        }
    def validate_username(self, value):
        existing_user = User.objects.filter(username=value).first()
        if existing_user and existing_user.status != 'rejected':
            raise serializers.ValidationError("用户名已存在")
        return value
    def validate_phone(self, value):
        if value:
            existing_user = User.objects.filter(phone=value).first()
            if existing_user and existing_user.status != 'rejected':
                raise serializers.ValidationError("手机号已存在")
        return value
    def validate(self, attrs):
        if attrs['password'] != attrs['confirm_password']:
            raise serializers.ValidationError("两次输入的密码不一致")
        return attrs
    def create(self, validated_data):
        validated_data.pop('confirm_password')
        password = validated_data.pop('password')
        groups = validated_data.pop('groups', None)
        username = validated_data['username']
        phone = validated_data.get('phone')
        existing_user = User.objects.filter(username=username).first()
        if existing_user and existing_user.status == 'rejected':
            user = existing_user
            user.name = validated_data.get('name', user.name)
            user.phone = phone
            user.set_password(password)
            user.status = 'pending'
            user.is_active = False
            user.save()
            if groups:
                user.groups.set(groups)
            return user
        if phone:
            existing_phone_user = User.objects.filter(phone=phone).first()
            if existing_phone_user and existing_phone_user.status == 'rejected':
                user = existing_phone_user
                user.username = username
                user.name = validated_data.get('name', user.name)
                user.set_password(password)
                user.status = 'pending'
                user.is_active = False
                user.save()
                if groups:
                    user.groups.set(groups)
                return user
        user = User(**validated_data)
        user.set_password(password)
        user.status = 'pending'
        user.is_active = False
        user.save()
        if groups:
            user.groups.set(groups)
        return user
class UserApprovalSerializer(serializers.Serializer):
    """
    用户审批序列化
    """
    action = serializers.ChoiceField(choices=['approve', 'reject'])
    reason = serializers.CharField(required=False, allow_blank=True, max_length=500)
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/urls.py
@@ -1,22 +1,20 @@
# -*- coding: utf-8 -*-
"""
@File    : urls.py
@Time    : 2023/1/13 16:09
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 接口路径
"""
from django.urls import path
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from lunaruser import views
router = DefaultRouter()
router.register(r'management', views.UserManagementViewSet, basename='user-management')
router.register(r'approval', views.UserApprovalViewSet, basename='user-approval')
urlpatterns = [
    path("login", views.LoginView.as_view()),
    path("register", views.RegisterView.as_view()),
    path("list", views.UserView.as_view()),
    path(
        "login_log",
        views.LoginLogView.as_view({"get": "list"}),
    ),
    path('change_password/', views.ChangePasswordView.as_view(), name='change_password'),
    path('groups', views.GroupListView.as_view(), name='group-list'),
    path('', include(router.urls)),
]
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/views.py
@@ -4,11 +4,15 @@
from rest_framework_jwt.settings import api_settings
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.viewsets import GenericViewSet
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from rest_framework.decorators import action
from drf_yasg.utils import swagger_auto_schema
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.filters import SearchFilter
from backend.utils.auth import MyJWTAuthentication
from backend.utils.permissions import CustomIsAdminUser
import logging
from backend.utils.request_util import save_login_log
from lunarlink.models import LoginLog
@@ -43,16 +47,33 @@
        except KeyError:
            return Response(response.KEY_MISS)
        user = authenticate(username=username, password=password)
        if user is None:
        try:
            user = User.objects.get(username=username)
        except User.DoesNotExist:
            return Response(response.LOGIN_FAILED)
        # 0后面还需要优化定义明确
        if not user.check_password(password):
            return Response(response.LOGIN_FAILED)
        if hasattr(user, 'status') and user.status == 'pending':
            return Response({
                "success": False,
                "msg": "你提交的注册还在审批中,暂时无法登录,可联系管理员确认!"
            }, status=status.HTTP_403_FORBIDDEN)
        if hasattr(user, 'status') and user.status == 'rejected':
            return Response({
                "success": False,
                "msg": "你的注册申请未通过审核,无法登录!"
            }, status=status.HTTP_403_FORBIDDEN)
        if user.is_active == 0:
            return Response(response.USER_BLOCKED)
        # JWT token creation
        from django.utils import timezone
        user.last_login = timezone.now()
        user.save(update_fields=['last_login'])
        jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
        jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
@@ -65,6 +86,7 @@
                "user": user.username,
                "name": user.name,
                "is_superuser": user.is_superuser,
                "is_staff": user.is_staff,
                "show_hosts": user.show_hosts,
                "token": token,
            }
@@ -144,3 +166,205 @@
                "success": False,
                "msg": str(e)
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class UserManagementViewSet(ModelViewSet):
    """
    用户管理视图集
    超级用户可以管理所有用户,普通用户只能查看自己的信息
    """
    authentication_classes = [MyJWTAuthentication]
    permission_classes = [IsAuthenticated]
    pagination_class = None
    filter_backends = [SearchFilter]
    search_fields = ['username', 'name']
    def get_queryset(self):
        if self.request.user.is_superuser:
            return User.objects.filter(status='approved').prefetch_related('groups')
        else:
            return User.objects.filter(id=self.request.user.id).prefetch_related('groups')
    def get_serializer_class(self):
        if self.action == 'create':
            return serializers.UserCreateSerializer
        elif self.action in ['update', 'partial_update']:
            return serializers.UserUpdateSerializer
        else:
            return serializers.UserDetailSerializer
    def get_permissions(self):
        if self.action in ['create', 'update', 'partial_update', 'destroy']:
            self.permission_classes = [IsAuthenticated, CustomIsAdminUser]
        return super().get_permissions()
    def create(self, request, *args, **kwargs):
        try:
            return super().create(request, *args, **kwargs)
        except Exception as e:
            import logging
            logger = logging.getLogger(__name__)
            logger.error(f"创建用户失败: {str(e)}", exc_info=True)
            return Response({
                "success": False,
                "msg": f"创建用户失败: {str(e)}"
            }, status=status.HTTP_400_BAD_REQUEST)
    def update(self, request, *args, **kwargs):
        try:
            return super().update(request, *args, **kwargs)
        except Exception as e:
            import logging
            logger = logging.getLogger(__name__)
            logger.error(f"更新用户失败: {str(e)}", exc_info=True)
            return Response({
                "success": False,
                "msg": f"更新用户失败: {str(e)}"
            }, status=status.HTTP_400_BAD_REQUEST)
    def perform_create(self, serializer):
        serializer.save()
    def perform_update(self, serializer):
        serializer.save()
    def perform_destroy(self, instance):
        instance.delete()
class RegisterView(APIView):
    """
    用户注册视图
    """
    authentication_classes = ()
    permission_classes = ()
    @swagger_auto_schema(request_body=serializers.UserRegisterSerializer)
    def post(self, request):
        """
        用户注册
        {
            username: str
            password: str
            confirm_password: str
            name: str
            phone: str
            groups: list (optional)
        }
        """
        serializer = serializers.UserRegisterSerializer(data=request.data)
        if not serializer.is_valid():
            return Response({
                "success": False,
                "msg": serializer.errors
            }, status=status.HTTP_400_BAD_REQUEST)
        try:
            user = serializer.save()
            return Response({
                "success": True,
                "msg": "你的注册申请已提交,待管理员审核中!",
                "data": {
                    "id": user.id,
                    "username": user.username
                }
            }, status=status.HTTP_201_CREATED)
        except Exception as e:
            import logging
            logger = logging.getLogger(__name__)
            logger.error(f"注册失败: {str(e)}", exc_info=True)
            return Response({
                "success": False,
                "msg": f"注册失败: {str(e)}"
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class UserApprovalViewSet(ModelViewSet):
    """
    用户审批视图集
    """
    authentication_classes = [MyJWTAuthentication]
    permission_classes = [IsAuthenticated]
    pagination_class = None
    filter_backends = [SearchFilter]
    search_fields = ['username', 'name']
    def get_queryset(self):
        if self.request.user.is_superuser or self.request.user.is_staff:
            return User.objects.filter(status='pending').prefetch_related('groups')
        else:
            return User.objects.none()
    def get_serializer_class(self):
        return serializers.UserDetailSerializer
    def list(self, request, *args, **kwargs):
        queryset = self.get_queryset()
        serializer = self.get_serializer(queryset, many=True)
        return Response({
            "success": True,
            "results": serializer.data
        })
    @swagger_auto_schema(request_body=serializers.UserApprovalSerializer)
    @action(detail=True, methods=['post'])
    def approve(self, request, pk=None):
        """
        审批用户
        {
            action: 'approve' | 'reject'
            reason: str (optional)
        }
        """
        user_id = pk
        try:
            user = User.objects.get(id=user_id)
        except User.DoesNotExist:
            return Response({
                "success": False,
                "msg": "用户不存在"
            }, status=status.HTTP_404_NOT_FOUND)
        serializer = serializers.UserApprovalSerializer(data=request.data)
        if not serializer.is_valid():
            return Response({
                "success": False,
                "msg": serializer.errors
            }, status=status.HTTP_400_BAD_REQUEST)
        action = serializer.validated_data['action']
        if action == 'approve':
            user.status = 'approved'
            user.is_active = True
            user.save()
            return Response({
                "success": True,
                "msg": "用户审批已通过"
            })
        elif action == 'reject':
            user.status = 'rejected'
            user.save()
            return Response({
                "success": True,
                "msg": "用户审批未通过"
            })
class GroupListView(APIView):
    """
    获取分组列表(无需认证)
    用于注册时选择分组
    """
    authentication_classes = ()
    permission_classes = ()
    def get(self, request):
        """
        获取所有分组列表
        """
        from django.contrib.auth.models import Group
        groups = Group.objects.all()
        group_list = [{"id": group.id, "name": group.name} for group in groups]
        return Response(group_list)
测试组/Test_platform/Interface_automation/backend/backend/settings.py
@@ -158,6 +158,8 @@
    "DEFAULT_PERMISSION_CLASSES": (
        "rest_framework.permissions.IsAuthenticated",  # 有经过身份认证确定用户身份才能访问
    ),
    "DATETIME_FORMAT": "%Y-%m-%d %H:%M:%S",  # 日期时间格式
    "DATE_FORMAT": "%Y-%m-%d",  # 日期格式
}
# ================================================= #
测试组/Test_platform/Interface_automation/backend/backend/utils/permissions.py
@@ -62,3 +62,6 @@
class CustomIsAdminUser(IsAdminUser):
    message = "您没有执行此操作的权限"
    def has_permission(self, request, view):
        return request.user.is_superuser
测试组/Test_platform/Interface_automation/backend/check_users.py
New file
@@ -0,0 +1,223 @@
import os
import sys
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
django.setup()
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
User = get_user_model()
def list_users():
    """
    列出所有用户
    """
    print("=" * 60)
    print("系统用户列表:")
    print("=" * 60)
    users = User.objects.all()
    if not users:
        print("暂无用户")
    else:
        for user in users:
            print(f"\n用户名: {user.username}")
            print(f"  姓名: {user.name}")
            print(f"  邮箱: {user.email}")
            print(f"  手机号: {user.phone}")
            print(f"  状态: {user.status}")
            print(f"  是否管理员: {'是' if user.is_superuser else '否'}")
            print(f"  是否员工: {'是' if user.is_staff else '否'}")
            print(f"  是否激活: {'是' if user.is_active else '否'}")
            print(f"  所属用户组: {', '.join([g.name for g in user.groups.all()]) or '无'}")
def create_admin_user(username='admin', password='admin123', email='admin@example.com', name='系统管理员'):
    """
    创建管理员用户
    """
    try:
        user = User.objects.get(username=username)
        print(f"\n用户 '{username}' 已存在,跳过创建")
        return user
    except User.DoesNotExist:
        try:
            user = User.objects.create_superuser(
                username=username,
                email=email,
                password=password
            )
            user.name = name
            user.phone = None
            user.show_hosts = False
            user.status = 'approved'
            user.save()
            print(f"\n✓ 成功创建管理员用户: {username}")
            print(f"  - 姓名: {name}")
            print(f"  - 邮箱: {email}")
            print(f"  - 密码: {password}")
            print(f"  - 状态: {user.status}")
            return user
        except Exception as e:
            print(f"\n✗ 创建用户失败: {e}")
            return None
def create_test_users():
    """
    创建测试用户
    """
    test_users = [
        {
            'username': 'testuser',
            'password': 'test123',
            'email': 'test@example.com',
            'name': '测试用户',
            'is_staff': False,
            'is_superuser': False
        },
        {
            'username': 'developer',
            'password': 'dev123',
            'email': 'dev@example.com',
            'name': '开发人员',
            'is_staff': True,
            'is_superuser': False
        }
    ]
    created_count = 0
    print("\n" + "=" * 60)
    print("创建测试用户...")
    print("=" * 60)
    for user_data in test_users:
        try:
            user = User.objects.get(username=user_data['username'])
            print(f"用户 '{user_data['username']}' 已存在,跳过创建")
            continue
        except User.DoesNotExist:
            try:
                user = User.objects.create_user(
                    username=user_data['username'],
                    email=user_data['email'],
                    password=user_data['password']
                )
                user.name = user_data['name']
                user.is_staff = user_data['is_staff']
                user.is_superuser = user_data['is_superuser']
                user.phone = None
                user.show_hosts = False
                user.status = 'approved'
                user.save()
                print(f"✓ 成功创建用户: {user_data['username']} ({user_data['name']})")
                created_count += 1
            except Exception as e:
                print(f"✗ 创建用户 '{user_data['username']}' 失败: {e}")
    return created_count
def list_groups():
    """
    列出所有用户组
    """
    print("\n" + "=" * 60)
    print("系统用户组列表:")
    print("=" * 60)
    groups = Group.objects.all()
    if not groups:
        print("暂无用户组")
    else:
        for group in groups:
            print(f"\n组名: {group.name}")
            print(f"  权限数量: {group.permissions.count()}")
            print(f"  用户数量: {group.user_set.count()}")
            if group.user_set.exists():
                print(f"  成员: {', '.join([u.username for u in group.user_set.all()])}")
def create_default_groups():
    """
    创建默认用户组
    """
    groups_config = [
        {'name': '管理员组', 'description': '拥有所有权限的管理员组'},
        {'name': '开发人员组', 'description': '开发人员组'},
        {'name': '测试人员组', 'description': '测试人员组'},
        {'name': '只读用户组', 'description': '只读用户组'},
    ]
    created_count = 0
    print("\n" + "=" * 60)
    print("创建默认用户组...")
    print("=" * 60)
    for group_config in groups_config:
        try:
            Group.objects.get(name=group_config['name'])
            print(f"用户组 '{group_config['name']}' 已存在")
        except Group.DoesNotExist:
            try:
                Group.objects.create(name=group_config['name'])
                print(f"✓ 创建用户组: {group_config['name']}")
                created_count += 1
            except Exception as e:
                print(f"✗ 创建用户组 '{group_config['name']}' 失败: {e}")
    return created_count
if __name__ == '__main__':
    import argparse
    parser = argparse.ArgumentParser(description='用户管理脚本')
    parser.add_argument('--list', action='store_true', help='列出所有用户')
    parser.add_argument('--create-admin', action='store_true', help='创建管理员用户')
    parser.add_argument('--create-test', action='store_true', help='创建测试用户')
    parser.add_argument('--create-groups', action='store_true', help='创建默认用户组')
    parser.add_argument('--list-groups', action='store_true', help='列出所有用户组')
    parser.add_argument('--init', action='store_true', help='初始化所有数据(创建管理员、测试用户和用户组)')
    args = parser.parse_args()
    if args.list:
        list_users()
    elif args.create_admin:
        create_admin_user()
    elif args.create_test:
        create_test_users()
    elif args.create_groups:
        create_default_groups()
    elif args.list_groups:
        list_groups()
    elif args.init:
        print("=" * 60)
        print("初始化用户数据...")
        print("=" * 60)
        admin = create_admin_user()
        if admin:
            test_count = create_test_users()
            group_count = create_default_groups()
            list_users()
            list_groups()
            print("\n" + "=" * 60)
            print("初始化完成!")
            print("=" * 60)
            print(f"✓ 创建了 {test_count} 个测试用户")
            print(f"✓ 创建了 {group_count} 个用户组")
            print("\n默认管理员账号:")
            print("  用户名: admin")
            print("  密码: admin123")
            print("\n⚠️  请在首次登录后修改默认密码!")
    else:
        parser.print_help()
测试组/Test_platform/Interface_automation/backend/scripts/README.md
New file
@@ -0,0 +1,231 @@
# 首次部署脚本使用说明
本目录包含接口自动化平台首次部署时所需的初始化脚本。
## 脚本列表
### 1. deploy_init.py - 首次部署初始化脚本(推荐)
一键执行所有初始化操作,包括:
- 检查数据库连接
- 执行数据库迁移
- 创建管理员用户
- 创建默认用户组
- 收集静态文件
**使用方法:**
```bash
cd backend
python scripts/deploy_init.py
```
**默认管理员账号:**
- 用户名: `admin`
- 密码: `admin123`
⚠️ **重要提示:** 请在首次登录后立即修改默认密码!
---
### 2. run_migrations.py - 数据库迁移脚本
单独执行数据库迁移操作。
**使用方法:**
```bash
cd backend
python scripts/run_migrations.py
```
---
### 3. create_admin_user.py - 创建管理员用户脚本
创建管理员用户和测试用户。
**使用方法:**
```bash
cd backend
python scripts/create_admin_user.py
```
**默认创建的用户:**
- `admin` - 系统管理员(超级用户)
- `testuser` - 测试用户(普通用户)
- `developer` - 开发人员(员工用户)
**自定义管理员账号:**
修改脚本中的 `create_admin_user()` 函数参数:
```python
create_admin_user(
    username='your_username',
    password='your_password',
    email='your_email@example.com',
    name='your_name'
)
```
---
### 4. create_groups.py - 创建用户组脚本
创建默认用户组并分配权限。
**使用方法:**
```bash
cd backend
python scripts/create_groups.py
```
**默认创建的用户组:**
- `管理员组` - 拥有所有权限
- `开发人员组` - 可以查看、添加和修改数据
- `测试人员组` - 可以查看和添加数据
- `只读用户组` - 只能查看数据
---
### 5. collect_static.py - 收集静态文件脚本
收集所有静态文件到 STATIC_ROOT 目录。
**使用方法:**
```bash
cd backend
python scripts/collect_static.py
```
---
## 部署前准备
### 1. 配置数据库连接
编辑 `backend/conf/env.py` 文件,配置数据库连接信息:
```python
DATABASE_NAME = 'your_database_name'
DATABASE_USER = 'your_database_user'
DATABASE_PASSWORD = 'your_database_password'
DATABASE_HOST = 'localhost'
DATABASE_PORT = '3306'
```
### 2. 创建数据库
在 MySQL 中创建数据库:
```sql
CREATE DATABASE your_database_name CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
### 3. 安装依赖
```bash
cd backend
pip install -r requirements.txt
```
---
## 完整部署流程
### 方式一:使用一键部署脚本(推荐)
```bash
cd backend
python scripts/deploy_init.py
```
### 方式二:分步执行
```bash
cd backend
# 1. 执行数据库迁移
python scripts/run_migrations.py
# 2. 创建管理员用户
python scripts/create_admin_user.py
# 3. 创建用户组
python scripts/create_groups.py
# 4. 收集静态文件
python scripts/collect_static.py
```
---
## 启动服务
### 开发环境
```bash
cd backend
python manage.py runserver
```
### 生产环境
```bash
cd backend
gunicorn backend.wsgi:application -c gunicorn_conf.py
```
---
## 常见问题
### 1. 数据库连接失败
检查 `conf/env.py` 中的数据库配置是否正确,确保数据库已创建且用户有足够的权限。
### 2. 迁移失败
确保数据库表不存在或为空。如果之前有迁移历史,可能需要删除 `migrations` 目录下的迁移文件(保留 `__init__.py`)。
### 3. 静态文件收集失败
确保 `STATIC_ROOT` 目录有写入权限。
### 4. 用户已存在
脚本会自动检测用户是否已存在,如果存在则跳过创建。
---
## 数据库表结构说明
### lunaruser 应用
- `lunaruser_myuser` - 用户表(继承自 Django AbstractUser)
### lunarlink 应用
- `project` - 项目信息表
- `config` - 环境信息表
- `api` - 接口信息表
- `case` - 用例信息表
- `case_step` - 用例步骤表
- `variables` - 全局变量表
- `debugtalk` - 驱动代码表
- `report` - 测试报告表
- `report_detail` - 测试报告详情表
- `relation` - 树形结构关系表
- `visit` - 访问日志表
- `login_log` - 登录日志表
---
## 安全建议
1. **修改默认密码:** 首次登录后立即修改管理员密码
2. **使用强密码:** 密码长度至少 8 位,包含大小写字母、数字和特殊字符
3. **限制访问:** 在生产环境中配置防火墙规则
4. **启用 HTTPS:** 生产环境必须使用 HTTPS
5. **定期备份:** 定期备份数据库和重要文件
---
## 技术支持
如有问题,请查看项目文档或联系技术支持团队。
测试组/Test_platform/Interface_automation/backend/scripts/__init__.py
测试组/Test_platform/Interface_automation/backend/scripts/collect_static.py
New file
@@ -0,0 +1,58 @@
import os
import sys
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
django.setup()
from django.core.management import call_command
def collect_static():
    """
    收集静态文件
    """
    print("=" * 60)
    print("开始收集静态文件...")
    print("=" * 60)
    try:
        call_command('collectstatic', '--noinput', verbosity=2)
        print("\n✓ 静态文件收集完成!")
        return True
    except Exception as e:
        print(f"\n✗ 静态文件收集失败: {e}")
        import traceback
        traceback.print_exc()
        return False
def create_superuser():
    """
    交互式创建超级用户
    """
    print("\n" + "=" * 60)
    print("创建超级用户 (可选)")
    print("=" * 60)
    print("如需创建超级用户,请按 Ctrl+C 取消,然后单独运行 create_admin_user.py")
    print()
    try:
        call_command('createsuperuser')
        return True
    except KeyboardInterrupt:
        print("\n跳过创建超级用户")
        return True
    except Exception as e:
        print(f"创建超级用户失败: {e}")
        return False
if __name__ == '__main__':
    success = collect_static()
    if success:
        create_superuser()
        sys.exit(0)
    else:
        sys.exit(1)
测试组/Test_platform/Interface_automation/backend/scripts/create_admin_user.py
New file
@@ -0,0 +1,158 @@
import os
import sys
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
django.setup()
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
User = get_user_model()
def create_admin_user(username='admin', password='admin123', email='admin@example.com', name='系统管理员'):
    """
    创建管理员用户
    Args:
        username: 用户名
        password: 密码
        email: 邮箱
        name: 姓名
    Returns:
        User: 创建的用户对象或已存在的用户对象
    """
    try:
        user = User.objects.get(username=username)
        print(f"用户 '{username}' 已存在,跳过创建")
        return user
    except User.DoesNotExist:
        try:
            user = User.objects.create_superuser(
                username=username,
                email=email,
                password=password
            )
            user.name = name
            user.phone = None
            user.show_hosts = False
            user.status = 'approved'
            user.save()
            print(f"✓ 成功创建管理员用户: {username}")
            print(f"  - 姓名: {name}")
            print(f"  - 邮箱: {email}")
            print(f"  - 状态: {user.status}")
            return user
        except ValidationError as e:
            print(f"✗ 创建用户失败,验证错误: {e}")
            return None
        except Exception as e:
            print(f"✗ 创建用户失败: {e}")
            import traceback
            traceback.print_exc()
            return None
def create_test_users():
    """
    创建测试用户
    """
    test_users = [
        {
            'username': 'testuser',
            'password': 'test123',
            'email': 'test@example.com',
            'name': '测试用户',
            'is_staff': False,
            'is_superuser': False
        },
        {
            'username': 'developer',
            'password': 'dev123',
            'email': 'dev@example.com',
            'name': '开发人员',
            'is_staff': True,
            'is_superuser': False
        }
    ]
    created_count = 0
    for user_data in test_users:
        try:
            user = User.objects.get(username=user_data['username'])
            print(f"用户 '{user_data['username']}' 已存在,跳过创建")
            continue
        except User.DoesNotExist:
            try:
                user = User.objects.create_user(
                    username=user_data['username'],
                    email=user_data['email'],
                    password=user_data['password']
                )
                user.name = user_data['name']
                user.is_staff = user_data['is_staff']
                user.is_superuser = user_data['is_superuser']
                user.phone = None
                user.show_hosts = False
                user.status = 'approved'
                user.save()
                print(f"✓ 成功创建用户: {user_data['username']} ({user_data['name']})")
                created_count += 1
            except Exception as e:
                print(f"✗ 创建用户 '{user_data['username']}' 失败: {e}")
    return created_count
def list_all_users():
    """
    列出所有用户
    """
    print("\n" + "=" * 60)
    print("当前系统用户列表:")
    print("=" * 60)
    users = User.objects.all()
    if not users:
        print("暂无用户")
    else:
        for user in users:
            print(f"  - 用户名: {user.username}")
            print(f"    姓名: {user.name}")
            print(f"    邮箱: {user.email}")
            print(f"    手机号: {user.phone}")
            print(f"    状态: {user.status}")
            print(f"    是否管理员: {'是' if user.is_superuser else '否'}")
            print(f"    是否员工: {'是' if user.is_staff else '否'}")
            print(f"    是否激活: {'是' if user.is_active else '否'}")
            print()
if __name__ == '__main__':
    print("=" * 60)
    print("开始初始化用户数据...")
    print("=" * 60)
    admin = create_admin_user()
    if admin:
        print("\n创建测试用户...")
        test_count = create_test_users()
        print(f"\n✓ 成功创建 {test_count} 个测试用户")
        list_all_users()
        print("\n" + "=" * 60)
        print("用户初始化完成!")
        print("=" * 60)
        sys.exit(0)
    else:
        print("\n" + "=" * 60)
        print("用户初始化失败!")
        print("=" * 60)
        sys.exit(1)
测试组/Test_platform/Interface_automation/backend/scripts/create_groups.py
New file
@@ -0,0 +1,110 @@
import os
import sys
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
django.setup()
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from apps.lunarlink.models import Project, API, Case, Config, Variables, Report
def create_default_groups():
    """
    创建默认用户组
    """
    groups_config = [
        {
            'name': '管理员组',
            'permissions': ['all'],
            'description': '拥有所有权限的管理员组'
        },
        {
            'name': '开发人员组',
            'permissions': ['view', 'add', 'change'],
            'description': '开发人员组,可以查看、添加和修改数据'
        },
        {
            'name': '测试人员组',
            'permissions': ['view', 'add'],
            'description': '测试人员组,可以查看和添加数据'
        },
        {
            'name': '只读用户组',
            'permissions': ['view'],
            'description': '只读用户组,只能查看数据'
        }
    ]
    created_count = 0
    for group_config in groups_config:
        try:
            group = Group.objects.get(name=group_config['name'])
            print(f"用户组 '{group_config['name']}' 已存在,跳过创建")
            continue
        except Group.DoesNotExist:
            try:
                group = Group.objects.create(name=group_config['name'])
                print(f"✓ 成功创建用户组: {group_config['name']}")
                if 'all' in group_config['permissions']:
                    all_permissions = Permission.objects.all()
                    group.permissions.set(all_permissions)
                    print(f"  - 已添加所有权限")
                else:
                    for perm_type in group_config['permissions']:
                        for model in [Project, API, Case, Config, Variables, Report]:
                            content_type = ContentType.objects.get_for_model(model)
                            codename = f"{perm_type}_{model._meta.model_name}"
                            try:
                                perm = Permission.objects.get(
                                    content_type=content_type,
                                    codename=codename
                                )
                                group.permissions.add(perm)
                            except Permission.DoesNotExist:
                                pass
                    print(f"  - 已添加权限: {', '.join(group_config['permissions'])}")
                created_count += 1
            except Exception as e:
                print(f"✗ 创建用户组 '{group_config['name']}' 失败: {e}")
    return created_count
def list_all_groups():
    """
    列出所有用户组
    """
    print("\n" + "=" * 60)
    print("当前系统用户组列表:")
    print("=" * 60)
    groups = Group.objects.all()
    if not groups:
        print("暂无用户组")
    else:
        for group in groups:
            print(f"  - 组名: {group.name}")
            print(f"    权限数量: {group.permissions.count()}")
            print(f"    用户数量: {group.user_set.count()}")
            print()
if __name__ == '__main__':
    print("=" * 60)
    print("开始初始化用户组数据...")
    print("=" * 60)
    count = create_default_groups()
    print(f"\n✓ 成功创建 {count} 个用户组")
    list_all_groups()
    print("\n" + "=" * 60)
    print("用户组初始化完成!")
    print("=" * 60)
测试组/Test_platform/Interface_automation/backend/scripts/deploy_init.py
New file
@@ -0,0 +1,258 @@
#!/usr/bin/env python
"""
首次部署初始化脚本
该脚本用于首次部署系统时执行所有必要的初始化操作,包括:
1. 数据库迁移
2. 创建管理员用户
3. 创建默认用户组
4. 收集静态文件
使用方法:
    python scripts/deploy_init.py
注意事项:
    - 确保已配置好数据库连接信息 (conf/env.py)
    - 确保数据库已创建
    - 确保已安装所有依赖 (pip install -r requirements.txt)
"""
import os
import sys
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
django.setup()
from django.core.management import call_command
def check_database_connection():
    """
    检查数据库连接
    """
    print("=" * 60)
    print("检查数据库连接...")
    print("=" * 60)
    try:
        from django.db import connection
        with connection.cursor() as cursor:
            cursor.execute("SELECT 1")
            print("✓ 数据库连接正常")
            return True
    except Exception as e:
        print(f"✗ 数据库连接失败: {e}")
        print("\n请检查 conf/env.py 中的数据库配置:")
        print("  - DATABASE_NAME")
        print("  - DATABASE_USER")
        print("  - DATABASE_PASSWORD")
        print("  - DATABASE_HOST")
        print("  - DATABASE_PORT")
        return False
def run_migrations():
    """
    执行数据库迁移
    """
    print("\n" + "=" * 60)
    print("执行数据库迁移...")
    print("=" * 60)
    try:
        print("\n[1/2] 生成迁移文件...")
        call_command('makemigrations', verbosity=1)
        print("✓ 迁移文件生成成功")
        print("\n[2/2] 应用迁移...")
        call_command('migrate', verbosity=1)
        print("✓ 数据库迁移成功")
        return True
    except Exception as e:
        print(f"✗ 数据库迁移失败: {e}")
        import traceback
        traceback.print_exc()
        return False
def create_admin_user():
    """
    创建管理员用户
    """
    print("\n" + "=" * 60)
    print("创建管理员用户...")
    print("=" * 60)
    from django.contrib.auth import get_user_model
    User = get_user_model()
    default_username = 'admin'
    default_password = 'admin123'
    default_email = 'admin@example.com'
    default_name = '系统管理员'
    try:
        user = User.objects.get(username=default_username)
        print(f"管理员用户 '{default_username}' 已存在,跳过创建")
        print(f"  用户名: {user.username}")
        print(f"  姓名: {user.name}")
        print(f"  邮箱: {user.email}")
        return True
    except User.DoesNotExist:
        try:
            user = User.objects.create_superuser(
                username=default_username,
                email=default_email,
                password=default_password
            )
            user.name = default_name
            user.phone = None
            user.show_hosts = False
            user.status = 'approved'
            user.save()
            print("✓ 管理员用户创建成功")
            print(f"  用户名: {default_username}")
            print(f"  密码: {default_password}")
            print(f"  邮箱: {default_email}")
            print(f"  姓名: {default_name}")
            print("\n⚠️  重要提示: 请在首次登录后修改默认密码!")
            return True
        except Exception as e:
            print(f"✗ 创建管理员用户失败: {e}")
            import traceback
            traceback.print_exc()
            return False
def create_default_groups():
    """
    创建默认用户组
    """
    print("\n" + "=" * 60)
    print("创建默认用户组...")
    print("=" * 60)
    from django.contrib.auth.models import Group
    groups = [
        {'name': '管理员组', 'description': '拥有所有权限的管理员组'},
        {'name': '开发人员组', 'description': '开发人员组'},
        {'name': '测试人员组', 'description': '测试人员组'},
        {'name': '只读用户组', 'description': '只读用户组'},
    ]
    created_count = 0
    for group_info in groups:
        try:
            Group.objects.get(name=group_info['name'])
            print(f"  用户组 '{group_info['name']}' 已存在")
        except Group.DoesNotExist:
            try:
                Group.objects.create(name=group_info['name'])
                print(f"  ✓ 创建用户组: {group_info['name']}")
                created_count += 1
            except Exception as e:
                print(f"  ✗ 创建用户组 '{group_info['name']}' 失败: {e}")
    print(f"\n✓ 成功创建 {created_count} 个用户组")
    return True
def collect_static_files():
    """
    收集静态文件
    """
    print("\n" + "=" * 60)
    print("收集静态文件...")
    print("=" * 60)
    try:
        call_command('collectstatic', '--noinput', verbosity=1)
        print("✓ 静态文件收集成功")
        return True
    except Exception as e:
        print(f"✗ 静态文件收集失败: {e}")
        import traceback
        traceback.print_exc()
        return False
def display_summary():
    """
    显示部署摘要
    """
    print("\n" + "=" * 60)
    print("部署摘要")
    print("=" * 60)
    from django.contrib.auth import get_user_model
    from django.contrib.auth.models import Group
    from apps.lunarlink.models import Project
    User = get_user_model()
    print(f"\n用户数量: {User.objects.count()}")
    print(f"用户组数量: {Group.objects.count()}")
    print(f"项目数量: {Project.objects.count()}")
    print("\n默认管理员账号:")
    print("  用户名: admin")
    print("  密码: admin123")
    print("\n⚠️  请在首次登录后立即修改默认密码!")
def main():
    """
    主函数
    """
    print("\n")
    print("╔" + "=" * 58 + "╗")
    print("║" + " " * 10 + "接口自动化平台 - 首次部署初始化" + " " * 13 + "║")
    print("╚" + "=" * 58 + "╝")
    print()
    steps = [
        ("检查数据库连接", check_database_connection),
        ("执行数据库迁移", run_migrations),
        ("创建管理员用户", create_admin_user),
        ("创建默认用户组", create_default_groups),
        ("收集静态文件", collect_static_files),
    ]
    failed_steps = []
    for step_name, step_func in steps:
        if not step_func():
            failed_steps.append(step_name)
            print(f"\n✗ '{step_name}' 失败,终止部署")
            break
    if not failed_steps:
        display_summary()
        print("\n" + "=" * 60)
        print("✓ 部署初始化完成!")
        print("=" * 60)
        print("\n系统已准备就绪,可以启动服务:")
        print("  python manage.py runserver")
        print("\n或使用 gunicorn 启动:")
        print("  gunicorn backend.wsgi:application -c gunicorn_conf.py")
        print()
        return 0
    else:
        print("\n" + "=" * 60)
        print("✗ 部署初始化失败!")
        print("=" * 60)
        print(f"\n失败的步骤: {', '.join(failed_steps)}")
        print("\n请检查错误信息并修复后重试")
        print()
        return 1
if __name__ == '__main__':
    sys.exit(main())
测试组/Test_platform/Interface_automation/backend/scripts/run_migrations.py
New file
@@ -0,0 +1,43 @@
import os
import sys
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
django.setup()
from django.core.management import call_command
def run_migrations():
    """
    执行数据库迁移脚本
    用于首次部署时创建数据库表结构
    """
    print("=" * 60)
    print("开始执行数据库迁移...")
    print("=" * 60)
    try:
        print("\n[1/2] 执行 makemigrations...")
        call_command('makemigrations', verbosity=2)
        print("✓ makemigrations 执行成功")
        print("\n[2/2] 执行 migrate...")
        call_command('migrate', verbosity=2)
        print("✓ migrate 执行成功")
        print("\n" + "=" * 60)
        print("数据库迁移完成!")
        print("=" * 60)
        return True
    except Exception as e:
        print(f"\n✗ 数据库迁移失败: {e}")
        import traceback
        traceback.print_exc()
        return False
if __name__ == '__main__':
    success = run_migrations()
    sys.exit(0 if success else 1)
测试组/Test_platform/Interface_automation/frontend/src/api/user.js
@@ -10,11 +10,46 @@
      Authorization: store.token
    }
  }).then(res => {
    // 密码修改成功后清除token和用户信息
    store.token = null
    store.user = null
    store.name = null
    store.id = null
    return res.data
  })
}
export function register(data) {
  return axios({
    url: '/api/user/register',
    method: 'post',
    data
  }).then(res => {
    return res.data
  })
}
export function getUserApprovalList(params) {
  return axios({
    url: '/api/user/approval/',
    method: 'get',
    params,
    headers: {
      Authorization: store.token
    }
  }).then(res => {
    return res.data
  })
}
export function approveUser(userId, data) {
  return axios({
    url: `/api/user/approval/${userId}/approve/`,
    method: 'post',
    data,
    headers: {
      Authorization: store.token
    }
  }).then(res => {
    return res.data
  })
}
测试组/Test_platform/Interface_automation/frontend/src/pages/home/components/Header.vue
@@ -11,6 +11,13 @@
      }}</span>
    </div>
    <div class="right">
      <div class="approval-button-wrapper" v-if="$store.state.is_superuser || $store.state.is_staff">
        <el-badge :value="pendingCount" :hidden="pendingCount === 0" class="approval-badge">
          <el-button type="success" size="small" icon="el-icon-s-check" @click="handleApproval">
            注册用户审批
          </el-button>
        </el-badge>
      </div>
      <div style="float: right; color: #262626; margin-right: 35px;">
        <el-dropdown trigger="click" @command="handleCommand">
          <span class="el-dropdown-link">
@@ -18,7 +25,6 @@
            {{ $store.state.name }}
            <i class="el-icon-arrow-down el-icon--right"></i>
          </span>
          <el-dropdown-menu slot="dropdown">
            <el-dropdown-item command="changePassword">
              <i class="el-icon-edit"></i>修改密码
@@ -71,11 +77,81 @@
        <el-button type="primary" @click="submitPasswordChange">确 定</el-button>
      </span>
    </el-dialog>
    <el-drawer
      title="注册用户审批"
      :visible.sync="approvalDrawerVisible"
      direction="rtl"
      size="60%"
      :modal="false"
      :append-to-body="true"
    >
      <div class="approval-container">
        <el-table
          :data="approvalData"
          v-loading="approvalLoading"
          stripe
          style="width: 100%"
        >
          <el-table-column prop="username" label="用户名" width="120"></el-table-column>
          <el-table-column prop="name" label="姓名" width="120"></el-table-column>
          <el-table-column prop="phone" label="手机号" width="120"></el-table-column>
          <el-table-column label="所属分组" width="200">
            <template slot-scope="scope">
              <el-tag
                v-for="group in scope.row.groups"
                :key="group"
                size="mini"
                style="margin-right: 3px"
              >
                {{ group }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column label="状态" width="100">
            <template slot-scope="scope">
              <el-tag :type="getStatusType(scope.row.status)">
                {{ scope.row.status_display }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column label="注册时间" width="180">
            <template slot-scope="scope">
              {{ scope.row.date_joined | datetimeFormat }}
            </template>
          </el-table-column>
          <el-table-column label="操作" fixed="right" width="200">
            <template slot-scope="scope">
              <el-button
                type="success"
                size="mini"
                icon="el-icon-check"
                @click="handleApprove(scope.row, 'approve')"
              >
                通过
              </el-button>
              <el-button
                type="danger"
                size="mini"
                icon="el-icon-close"
                @click="handleApprove(scope.row, 'reject')"
              >
                不通过
              </el-button>
            </template>
          </el-table-column>
        </el-table>
        <div v-if="!approvalLoading && approvalData.length === 0" class="empty-tip">
          <el-empty description="暂无待审批用户"></el-empty>
        </div>
      </div>
    </el-drawer>
  </div>
</template>
<script>
import { changePassword } from '@/api/user'
import { changePassword, approveUser } from '@/api/user'
import { getUserApprovalList } from '@/api/user'
export default {
  data() {
@@ -89,6 +165,10 @@
    return {
      passwordDialogVisible: false,
      approvalDrawerVisible: false,
      approvalLoading: false,
      approvalData: [],
      pendingCount: 0,
      passwordForm: {
        old_password: '',
        new_password: '',
@@ -110,6 +190,25 @@
    }
  },
  methods: {
    addBadgeAnimation() {
      if (this.pendingCount > 0) {
        const badge = document.querySelector('.approval-badge .el-badge__content');
        if (badge) {
          badge.classList.add('badge-enter');
          setTimeout(() => {
            badge.classList.remove('badge-enter');
          }, 1000);
        }
      }
    },
    getStatusType(status) {
      const statusMap = {
        'pending': 'warning',
        'approved': 'success',
        'rejected': 'danger'
      };
      return statusMap[status] || 'info';
    },
    handleCommand(command) {
      if (command === 'changePassword') {
        // 重置表单数据和验证状态
@@ -128,10 +227,42 @@
        this.$store.commit("isLogin", null);
        this.setLocalValue("token", null);
        this.setLocalValue("is_superuser", false);
        this.setLocalValue("is_staff", false);
        this.setLocalValue("show_hosts", false);
        this.$store.commit("setProjectName", "");
        this.$router.push({ name: "Login" });
      }
    },
    handleApproval() {
      this.approvalDrawerVisible = true;
      this.getApprovalList();
    },
    getApprovalList() {
      this.approvalLoading = true;
      getUserApprovalList().then(resp => {
        this.approvalData = resp.results || resp;
        this.pendingCount = this.approvalData.length;
        this.approvalLoading = false;
        if (this.pendingCount > 0) {
          this.addBadgeAnimation();
        }
      }).catch(() => {
        this.approvalLoading = false;
      });
    },
    handleApprove(row, action) {
      const actionText = action === 'approve' ? '通过' : '不通过';
      this.$confirm(`确定要${actionText}该用户的注册申请吗?`, "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      }).then(() => {
        this.$api.approveUser(row.id, { action: action }).then(() => {
          this.$message.success(`用户审批已${actionText}`);
          this.pendingCount = Math.max(0, this.pendingCount - 1);
          this.getApprovalList();
        });
      }).catch(() => {});
    },
    submitPasswordChange() {
      this.$refs.passwordForm.validate(valid => {
@@ -166,6 +297,14 @@
            })
        }
      })
    }
  },
  mounted() {
    if (this.$store.state.is_superuser || this.$store.state.is_staff) {
      this.getApprovalList();
      setTimeout(() => {
        this.addBadgeAnimation();
      }, 500);
    }
  }
}
@@ -219,4 +358,164 @@
  color: #409EFF;
  padding: 0 15px;
}
.approval-container {
  padding: 20px;
}
.empty-tip {
  text-align: center;
  padding: 40px 0;
}
.approval-button-wrapper {
  float: right;
  color: #262626;
  margin-right: 15px;
  display: flex;
  align-items: center;
  height: 49px;
  position: relative;
}
/* 优化按钮本身 */
.approval-button-wrapper .el-button {
  position: relative;
  padding: 7px 12px;
  border-radius: 6px;
  font-weight: 500;
  transition: all 0.3s ease;
  box-shadow: 0 2px 8px rgba(103, 194, 58, 0.2);
}
.approval-button-wrapper .el-button:hover {
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(103, 194, 58, 0.3);
}
.approval-button-wrapper .el-button:active {
  transform: translateY(0);
}
/* 徽章优化 - 更自然的设计 */
.approval-badge {
  position: relative;
  display: inline-flex;
  align-items: center;
}
.approval-badge ::v-deep .el-badge__content {
  position: absolute !important;
  top: -8px !important;
  right: -8px !important;
  height: 20px;
  min-width: 20px;
  line-height: 20px;
  padding: 0 4px;
  font-size: 12px;
  font-weight: 600;
  border-radius: 10px;
  background: linear-gradient(135deg, #ff6b6b, #ff4757);
  color: white;
  border: 2px solid white;
  box-shadow: 0 3px 6px rgba(255, 107, 107, 0.3);
  z-index: 10;
  animation: badge-pulse 2s infinite;
}
/* 徽章脉冲动画 */
@keyframes badge-pulse {
  0% {
    box-shadow: 0 0 0 0 rgba(255, 107, 107, 0.4);
  }
  70% {
    box-shadow: 0 0 0 4px rgba(255, 107, 107, 0);
  }
  100% {
    box-shadow: 0 0 0 0 rgba(255, 107, 107, 0);
  }
}
/* 徽章入场动画 */
@keyframes badge-bounce {
  0% {
    transform: scale(0);
  }
  50% {
    transform: scale(1.2);
  }
  100% {
    transform: scale(1);
  }
}
.approval-badge ::v-deep .el-badge__content.badge-enter {
  animation: badge-bounce 0.5s ease;
}
/* 当徽章值为0时的隐藏效果 */
.approval-badge ::v-deep .el-badge__content.is-fixed.is-dot {
  top: -4px !important;
  right: -4px !important;
  height: 8px;
  width: 8px;
  min-width: 8px;
  border: 1px solid white;
  background: linear-gradient(135deg, #ffa502, #ff7f00);
}
/* 可选:添加图标徽章效果 */
.approval-button-wrapper .el-button i {
  position: relative;
  margin-right: 4px;
}
/* 添加按钮渐变效果 */
.approval-button-wrapper .el-button--success {
  background: linear-gradient(135deg, #85d27a, #67c23a);
  border: none;
}
.approval-button-wrapper .el-button--success:hover {
  background: linear-gradient(135deg, #7ac76f, #5da534);
}
/* 徽章数字特殊样式 */
.approval-badge ::v-deep .el-badge__content .el-badge__inner {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  letter-spacing: -0.5px;
}
/* 调整整体布局 */
.right {
  position: fixed;
  left: 300px;
  right: 0;
  top: 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding-right: 10px;
  gap: 10px; /* 增加元素间距 */
}
/* 用户下拉菜单样式优化 */
.el-dropdown-link {
  cursor: pointer;
  color: #409EFF;
  padding: 8px 16px;
  border-radius: 6px;
  transition: all 0.3s ease;
  display: inline-flex;
  align-items: center;
}
.el-dropdown-link:hover {
  background-color: rgba(64, 158, 255, 0.1);
}
.el-dropdown-link i {
  margin-right: 4px;
}
</style>
测试组/Test_platform/Interface_automation/frontend/src/pages/login/Login.vue
@@ -1,6 +1,5 @@
<template>
  <div class="login-container">
    <!-- 动态背景 -->
    <div class="background-animation">
      <div class="floating-shapes">
        <div class="shape shape-1"></div>
@@ -12,28 +11,20 @@
      <div class="gradient-overlay"></div>
    </div>
    <!-- 主登录区域 -->
    <div class="login-content">
      <!-- 左侧品牌展示区 -->
      <div class="brand-section">
        <div class="brand-logo-container">
          <div class="logo-icon">
            <svg viewBox="0 0 100 100" class="api-icon">
              <!-- 主API连接图标 -->
              <path d="M20,30 L50,10 L80,30 L80,70 L50,90 L20,70 Z" fill="rgba(0,212,255,0.1)" stroke="rgba(0,212,255,0.6)" stroke-width="2"/>
              <!-- 内部连接层 -->
              <path d="M30,40 L50,25 L70,40 L70,60 L50,75 L30,60 Z" fill="rgba(148,0,211,0.1)" stroke="rgba(148,0,211,0.6)" stroke-width="1.5"/>
              <!-- 核心数据点 -->
              <path d="M40,50 L50,40 L60,50 L60,55 L50,60 L40,55 Z" fill="rgba(255,255,255,0.2)" stroke="rgba(255,255,255,0.8)" stroke-width="1"/>
              <!-- 动态连接线 -->
              <line x1="35" y1="45" x2="45" y2="52" stroke="#00d4ff" stroke-width="1.5" stroke-dasharray="2,2"/>
              <line x1="55" y1="45" x2="65" y2="52" stroke="#9400d3" stroke-width="1.5" stroke-dasharray="2,2"/>
              <!-- 中心数据流点 -->
              <circle cx="50" cy="50" r="4" fill="#00d4ff">
                <animate attributeName="r" values="4;6;4" dur="2s" repeatCount="indefinite"/>
                <animate attributeName="opacity" values="1;0.7;1" dur="2s" repeatCount="indefinite"/>
              </circle>
              <!-- 外围数据点 -->
              <circle cx="35" cy="35" r="2" fill="#00d4ff">
                <animate attributeName="opacity" values="0.5;1;0.5" dur="1.5s" repeatCount="indefinite"/>
              </circle>
@@ -68,12 +59,11 @@
        </div>
      </div>
      <!-- 右侧登录表单 -->
      <div class="login-form-section">
        <div class="form-container">
          <div class="form-header">
            <h2>欢迎登录</h2>
            <p>请使用您的账号密码登录系统</p>
            <h2>{{ isLoginMode ? '欢迎登录' : '用户注册' }}</h2>
            <p>{{ isLoginMode ? '请使用您的账号密码登录系统' : '请填写以下信息完成注册' }}</p>
          </div>
          <el-form
@@ -81,7 +71,104 @@
            @submit.native.prevent="submitForm"
            class="login-form"
          >
            <div class="input-group">
            <div class="input-group" v-if="!isLoginMode">
              <label class="input-label">用户名</label>
              <el-input
                v-model="registerForm.username"
                placeholder="请输入用户名"
                prefix-icon="el-icon-user"
                class="modern-input"
                :class="{ 'input-error': registerErrors.username }"
                @blur="validateRegisterField('username')"
              />
              <transition name="slide-fade">
                <div v-if="registerErrors.username" class="error-msg">{{ registerErrors.username }}</div>
              </transition>
            </div>
            <div class="input-group" v-if="!isLoginMode">
              <label class="input-label">姓名</label>
              <el-input
                v-model="registerForm.name"
                placeholder="请输入姓名"
                prefix-icon="el-icon-edit"
                class="modern-input"
                :class="{ 'input-error': registerErrors.name }"
                @blur="validateRegisterField('name')"
              />
              <transition name="slide-fade">
                <div v-if="registerErrors.name" class="error-msg">{{ registerErrors.name }}</div>
              </transition>
            </div>
            <div class="input-group" v-if="!isLoginMode">
              <label class="input-label">手机号</label>
              <el-input
                v-model="registerForm.phone"
                placeholder="请输入手机号"
                prefix-icon="el-icon-phone"
                class="modern-input"
                :class="{ 'input-error': registerErrors.phone }"
                @blur="validateRegisterField('phone')"
              />
              <transition name="slide-fade">
                <div v-if="registerErrors.phone" class="error-msg">{{ registerErrors.phone }}</div>
              </transition>
            </div>
            <div class="input-group" v-if="!isLoginMode">
              <label class="input-label">所属分组(可选)</label>
              <el-select
                v-model="registerForm.groups"
                multiple
                placeholder="请选择分组"
                class="modern-input"
                style="width: 100%"
              >
                <el-option
                  v-for="group in groupList"
                  :key="group.id"
                  :label="group.name"
                  :value="group.id"
                ></el-option>
              </el-select>
            </div>
            <div class="input-group" v-if="!isLoginMode">
              <label class="input-label">密码</label>
              <el-input
                v-model="registerForm.password"
                type="password"
                placeholder="请输入密码"
                prefix-icon="el-icon-lock"
                show-password
                class="modern-input"
                :class="{ 'input-error': registerErrors.password }"
                @blur="validateRegisterField('password')"
              />
              <transition name="slide-fade">
                <div v-if="registerErrors.password" class="error-msg">{{ registerErrors.password }}</div>
              </transition>
            </div>
            <div class="input-group" v-if="!isLoginMode">
              <label class="input-label">确认密码</label>
              <el-input
                v-model="registerForm.confirm_password"
                type="password"
                placeholder="请再次输入密码"
                prefix-icon="el-icon-lock"
                show-password
                class="modern-input"
                :class="{ 'input-error': registerErrors.confirm_password }"
                @blur="validateRegisterField('confirm_password')"
              />
              <transition name="slide-fade">
                <div v-if="registerErrors.confirm_password" class="error-msg">{{ registerErrors.confirm_password }}</div>
              </transition>
            </div>
            <div class="input-group" v-if="isLoginMode">
              <label class="input-label">用户名</label>
              <el-input
                v-model="loginForm.username"
@@ -96,7 +183,7 @@
              </transition>
            </div>
            <div class="input-group">
            <div class="input-group" v-if="isLoginMode">
              <label class="input-label">密码</label>
              <el-input
                v-model="loginForm.password"
@@ -119,12 +206,13 @@
              :loading="isLoading"
              @click="submitForm"
            >
              {{ isLoading ? '登录中...' : '登录系统' }}
              {{ isLoading ? (isLoginMode ? '登录中...' : '注册中...') : (isLoginMode ? '登录系统' : '注册账号') }}
            </el-button>
          </el-form>
          <div class="form-footer">
            <p>还没有账号?<a href="#" class="register-link">联系管理员注册</a></p>
            <p v-if="isLoginMode">还没有账号?<a href="#" class="register-link" @click.prevent="switchMode">立即注册</a></p>
            <p v-else>已有账号?<a href="#" class="register-link" @click.prevent="switchMode">返回登录</a></p>
            <p class="copyright">© 2025 APITest Pro 智能接口自动化平台</p>
          </div>
        </div>
@@ -140,16 +228,63 @@
    data() {
        return {
            isLoading: false,
            isLoginMode: true,
            loginForm: {
                username: "",
                password: ""
            },
            registerForm: {
                username: "",
                name: "",
                phone: "",
                password: "",
                confirm_password: "",
                groups: []
            },
            usernameInvalid: "",
            passwordInvalid: ""
            passwordInvalid: "",
            registerErrors: {
                username: "",
                name: "",
                phone: "",
                password: "",
                confirm_password: ""
            },
            groupList: []
        };
    },
    methods: {
        switchMode() {
            this.isLoginMode = !this.isLoginMode;
            this.resetForms();
            if (!this.isLoginMode) {
                this.getGroupList();
            }
        },
        resetForms() {
            this.loginForm = {
                username: "",
                password: ""
            };
            this.registerForm = {
                username: "",
                name: "",
                phone: "",
                password: "",
                confirm_password: "",
                groups: []
            };
            this.usernameInvalid = "";
            this.passwordInvalid = "";
            this.registerErrors = {
                username: "",
                name: "",
                phone: "",
                password: "",
                confirm_password: ""
            };
        },
        validateUserName() {
            if (this.loginForm.username.replace(/(^\s*)/g, "") === "") {
                this.usernameInvalid = "用户名不能为空";
@@ -164,18 +299,83 @@
            }
            return true;
        },
        validateRegisterField(field) {
            const form = this.registerForm;
            let isValid = true;
            switch (field) {
                case 'username':
                    if (!form.username) {
                        this.registerErrors.username = "用户名不能为空";
                        isValid = false;
                    } else {
                        this.registerErrors.username = "";
                    }
                    break;
                case 'name':
                    if (!form.name) {
                        this.registerErrors.name = "姓名不能为空";
                        isValid = false;
                    } else {
                        this.registerErrors.name = "";
                    }
                    break;
                case 'phone':
                    if (!form.phone) {
                        this.registerErrors.phone = "手机号不能为空";
                        isValid = false;
                    } else if (!/^1[3-9]\d{9}$/.test(form.phone)) {
                        this.registerErrors.phone = "手机号格式不正确";
                        isValid = false;
                    } else {
                        this.registerErrors.phone = "";
                    }
                    break;
                case 'password':
                    if (!form.password) {
                        this.registerErrors.password = "密码不能为空";
                        isValid = false;
                    } else if (form.password.length < 6) {
                        this.registerErrors.password = "密码长度不能少于6位";
                        isValid = false;
                    } else {
                        this.registerErrors.password = "";
                    }
                    break;
                case 'confirm_password':
                    if (!form.confirm_password) {
                        this.registerErrors.confirm_password = "请再次输入密码";
                        isValid = false;
                    } else if (form.password !== form.confirm_password) {
                        this.registerErrors.confirm_password = "两次输入的密码不一致";
                        isValid = false;
                    } else {
                        this.registerErrors.confirm_password = "";
                    }
                    break;
            }
            return isValid;
        },
        validateRegisterForm() {
            let isValid = true;
            const fields = ['username', 'name', 'phone', 'password', 'confirm_password'];
            fields.forEach(field => {
                if (!this.validateRegisterField(field)) {
                    isValid = false;
                }
            });
            return isValid;
        },
        handleLoginSuccess(resp) {
            if (resp.success) {
                // 显示登录信息提示
                this.showLoginNotification(resp);
                // 原有路由跳转和存储逻辑
                this.$router.push({ name: "ProjectList" });
                this.$store.commit("isLogin", resp.token);
                this.$store.commit("setUser", resp.user);
                this.$store.commit("setName", resp.name);
                this.$store.commit("setId", resp.id);
                this.$store.commit("setIsSuperuser", resp.is_superuser);
                this.$store.commit("setIsStaff", resp.is_staff);
                this.$store.commit("setRouterName", "ProjectList");
                this.$store.commit("setShowHosts", resp.show_hosts);
@@ -184,6 +384,7 @@
                this.setLocalValue("name", resp.name);
                this.setLocalValue("id", resp.id);
                this.setLocalValue("is_superuser", resp.is_superuser);
                this.setLocalValue("is_staff", resp.is_staff);
                this.setLocalValue("routerName", "ProjectList");
                this.setLocalValue("show_hosts", resp.show_hosts);
            } else {
@@ -207,14 +408,68 @@
            });
        },
        submitForm() {
            if (this.validateUserName() && this.validatePassword()) {
                this.isLoading = true;
                this.$api.login(this.loginForm).then(resp => {
                    this.handleLoginSuccess(resp);
                    this.isLoading = false;
                });
            if (this.isLoginMode) {
                if (this.validateUserName() && this.validatePassword()) {
                    this.isLoading = true;
                    this.$api.login(this.loginForm).then(resp => {
                        this.handleLoginSuccess(resp);
                        this.isLoading = false;
                    }).catch(error => {
                        this.isLoading = false;
                        if (error.response && error.response.data && error.response.data.msg) {
                            this.$message.error({
                                message: error.response.data.msg,
                                duration: 2000,
                                center: true
                            });
                        }
                    });
                }
            } else {
                if (this.validateRegisterForm()) {
                    this.isLoading = true;
                    this.$api.register(this.registerForm).then(resp => {
                        this.$message.success(resp.msg);
                        this.isLoading = false;
                        this.switchMode();
                    }).catch(error => {
                        this.isLoading = false;
                        if (error.response && error.response.data && error.response.data.msg) {
                            this.$message.error({
                                message: error.response.data.msg,
                                duration: 2000,
                                center: true
                            });
                        } else if (error.response && error.response.data) {
                            const errors = error.response.data;
                            let errorMsg = '';
                            for (let key in errors) {
                                if (Array.isArray(errors[key])) {
                                    errorMsg += errors[key].join('、') + ';';
                                } else if (typeof errors[key] === 'string') {
                                    errorMsg += errors[key] + ';';
                                }
                            }
                            this.$message.error({
                                message: errorMsg || '注册失败,请检查输入信息',
                                duration: 3000,
                                center: true
                            });
                        }
                    });
                }
            }
        },
        getGroupList() {
            this.$api.getGroupList().then(resp => {
                this.groupList = resp;
            }).catch(() => {
                this.groupList = [];
            });
        }
    },
    mounted() {
        this.getGroupList();
    }
};
</script>
测试组/Test_platform/Interface_automation/frontend/src/pages/project/ProjectList.vue
@@ -24,6 +24,13 @@
                        >项目看板</el-button
                    >
                    <el-button
                        type="warning"
                        size="small"
                        icon="el-icon-user"
                        @click="handleUserManagement"
                        >用户管理</el-button
                    >
                    <el-button
                        type="info"
                        size="small"
                        icon="el-icon-arrow-left"
@@ -375,6 +382,18 @@
        >
            <ProjectDashBoard></ProjectDashBoard>
        </el-drawer>
        <el-drawer
            title="用户管理"
            :visible.sync="userManagementVisible"
            direction="rtl"
            size="80%"
            :before-close="handleUserManagementClose"
            :modal="false"
            class="user-management-drawer-wrapper"
        >
            <user-management-drawer v-if="userManagementVisible"></user-management-drawer>
        </el-drawer>
        <el-container>
            <el-main style="padding: 0; margin-left: 10px">
                <el-table
@@ -550,9 +569,10 @@
<script>
import ProjectDashBoard from "@/pages/project/ProjectDashBoard.vue";
import UserManagementDrawer from "@/pages/user/UserManagementDrawer.vue";
export default {
    name: "ProjectList",
    components: { ProjectDashBoard },
    components: { ProjectDashBoard, UserManagementDrawer },
    data() {
        return {
            recentEnabledProjects: [], // 存储有启动任务的项目
@@ -563,6 +583,7 @@
            task_count: 0,
            dialogVisible: false,
            dashBoardVisible: false,
            userManagementVisible: false,
            editVisible: false,
            hoveringSuccess: false,
            hoveringError: false,
@@ -675,6 +696,12 @@
            this.dialogVisible = true;
            this.resetProjectForm();
        },
        handleUserManagement() {
            this.userManagementVisible = true;
        },
        handleUserManagementClose() {
            this.userManagementVisible = false;
        },
        handleEdit(index, row) {
            this.editVisible = true;
            this.projectForm.name = row["name"];
测试组/Test_platform/Interface_automation/frontend/src/pages/user/UserManagement.vue
New file
@@ -0,0 +1,687 @@
<template>
    <el-container>
        <el-container>
            <el-header style="padding-top: 10px; margin-left: 10px; height: 50px">
                <div class="env__header">
                    <div class="env__header--item">
                        <el-input
                            style="width: 300px"
                            size="small"
                            placeholder="请输入用户名或姓名"
                            v-model="search"
                            clearable
                        >
                        </el-input>
                    </div>
                    <div class="env__header--item">
                        <el-button
                            plain
                            size="small"
                            icon="el-icon-refresh"
                            @click="resetSearch"
                            >重置</el-button
                        >
                    </div>
                    <div class="env__header--item" v-if="isSuperuser">
                        <el-button
                            type="primary"
                            size="small"
                            icon="el-icon-plus"
                            @click="handleAdd"
                            >新增用户</el-button
                        >
                    </div>
                    <div class="env__header--item" v-if="isSuperuser || isStaff">
                        <el-button
                            type="success"
                            size="small"
                            icon="el-icon-s-check"
                            @click="handleApproval"
                            >注册用户审批</el-button
                        >
                    </div>
                </div>
            </el-header>
            <el-dialog
                :title="dialogTitle"
                width="50%"
                :close-on-click-modal="false"
                :visible.sync="dialogVisible"
                @close="handleDialogClose"
            >
                <el-form
                    :inline="true"
                    label-position="right"
                    :model="userForm"
                    :rules="rules"
                    ref="userForm"
                    label-width="100px"
                >
                    <el-form-item label="用户名" prop="username" v-if="isCreate">
                        <el-input v-model="userForm.username" placeholder="请输入用户名"></el-input>
                    </el-form-item>
                    <el-form-item label="用户名" prop="username" v-if="!isCreate">
                        <el-input v-model="userForm.username" disabled></el-input>
                    </el-form-item>
                    <el-form-item label="姓名" prop="name">
                        <el-input v-model="userForm.name" placeholder="请输入姓名"></el-input>
                    </el-form-item>
                    <el-form-item label="手机号" prop="phone">
                        <el-input v-model="userForm.phone" placeholder="请输入手机号"></el-input>
                    </el-form-item>
                    <el-form-item label="密码" prop="password" v-if="isCreate">
                        <el-input
                            type="password"
                            v-model="userForm.password"
                            placeholder="请输入密码"
                        ></el-input>
                    </el-form-item>
                    <el-form-item label="确认密码" prop="confirm_password" v-if="isCreate">
                        <el-input
                            type="password"
                            v-model="userForm.confirm_password"
                            placeholder="请再次输入密码"
                        ></el-input>
                    </el-form-item>
                    <el-form-item label="是否激活" prop="is_active" v-if="isSuperuser">
                        <el-switch v-model="userForm.is_active"></el-switch>
                    </el-form-item>
                    <el-form-item label="是否管理员" prop="is_staff" v-if="isSuperuser">
                        <el-switch v-model="userForm.is_staff"></el-switch>
                    </el-form-item>
                    <el-form-item label="超级用户" prop="is_superuser" v-if="isSuperuser">
                        <el-switch v-model="userForm.is_superuser"></el-switch>
                    </el-form-item>
                    <el-form-item label="显示Hosts" prop="show_hosts" v-if="isSuperuser">
                        <el-switch v-model="userForm.show_hosts"></el-switch>
                    </el-form-item>
                    <el-form-item label="所属分组" prop="groups" v-if="isSuperuser">
                        <el-select
                            v-model="userForm.groups"
                            multiple
                            placeholder="请选择分组"
                            style="width: 100%"
                        >
                            <el-option
                                v-for="group in groupList"
                                :key="group.id"
                                :label="group.name"
                                :value="group.id"
                            ></el-option>
                        </el-select>
                    </el-form-item>
                </el-form>
                <span slot="footer" class="dialog-footer">
                    <el-button @click="dialogVisible = false">取 消</el-button>
                    <el-button type="primary" @click="handleSubmit">确 定</el-button>
                </span>
            </el-dialog>
            <el-dialog
                title="用户详情"
                width="50%"
                :close-on-click-modal="false"
                :visible.sync="detailDialogVisible"
            >
                <el-form
                    :inline="true"
                    label-position="right"
                    :model="userDetail"
                    label-width="100px"
                >
                    <el-form-item label="用户名">
                        <el-input v-model="userDetail.username" readonly></el-input>
                    </el-form-item>
                    <el-form-item label="姓名">
                        <el-input v-model="userDetail.name" readonly></el-input>
                    </el-form-item>
                    <el-form-item label="手机号">
                        <el-input v-model="userDetail.phone" readonly></el-input>
                    </el-form-item>
                    <el-form-item label="是否激活">
                        <el-tag :type="userDetail.is_active ? 'success' : 'danger'">
                            {{ userDetail.is_active ? '是' : '否' }}
                        </el-tag>
                    </el-form-item>
                    <el-form-item label="是否管理员">
                        <el-tag :type="userDetail.is_staff ? 'success' : 'info'">
                            {{ userDetail.is_staff ? '是' : '否' }}
                        </el-tag>
                    </el-form-item>
                    <el-form-item label="超级用户">
                        <el-tag :type="userDetail.is_superuser ? 'success' : 'info'">
                            {{ userDetail.is_superuser ? '是' : '否' }}
                        </el-tag>
                    </el-form-item>
                    <el-form-item label="显示Hosts">
                        <el-tag :type="userDetail.show_hosts ? 'success' : 'info'">
                            {{ userDetail.show_hosts ? '是' : '否' }}
                        </el-tag>
                    </el-form-item>
                    <el-form-item label="所属分组">
                        <el-tag
                            v-for="group in userDetail.groups"
                            :key="group"
                            style="margin-right: 5px"
                        >
                            {{ group }}
                        </el-tag>
                    </el-form-item>
                    <el-form-item label="可访问项目">
                        <el-tag
                            v-for="project in userDetail.accessible_projects"
                            :key="project"
                            style="margin-right: 5px"
                        >
                            {{ project }}
                        </el-tag>
                    </el-form-item>
                    <el-form-item label="注册时间">
                        <el-input
                            :value="userDetail.date_joined | datetimeFormat"
                            readonly
                        ></el-input>
                    </el-form-item>
                    <el-form-item label="最后登录">
                        <el-input
                            :value="userDetail.last_login ? (userDetail.last_login | datetimeFormat) : '从未登录'"
                            readonly
                        ></el-input>
                    </el-form-item>
                </el-form>
                <span slot="footer" class="dialog-footer">
                    <el-button @click="detailDialogVisible = false">关 闭</el-button>
                </span>
            </el-dialog>
            <el-drawer
                title="注册用户审批"
                :visible.sync="approvalDrawerVisible"
                direction="rtl"
                size="60%"
            >
                <div class="approval-container">
                    <el-table
                        :data="approvalData"
                        v-loading="approvalLoading"
                        stripe
                        style="width: 100%"
                    >
                        <el-table-column prop="username" label="用户名" width="120"></el-table-column>
                        <el-table-column prop="name" label="姓名" width="120"></el-table-column>
                        <el-table-column prop="phone" label="手机号" width="120"></el-table-column>
                        <el-table-column label="所属分组" width="200">
                            <template slot-scope="scope">
                                <el-tag
                                    v-for="group in scope.row.groups"
                                    :key="group"
                                    size="mini"
                                    style="margin-right: 3px"
                                >
                                    {{ group }}
                                </el-tag>
                            </template>
                        </el-table-column>
                        <el-table-column label="状态" width="100">
                            <template slot-scope="scope">
                                <el-tag :type="getStatusType(scope.row.status)">
                                    {{ scope.row.status_display }}
                                </el-tag>
                            </template>
                        </el-table-column>
                        <el-table-column label="注册时间" width="180">
                            <template slot-scope="scope">
                                {{ scope.row.date_joined | datetimeFormat }}
                            </template>
                        </el-table-column>
                        <el-table-column label="操作" fixed="right" width="200">
                            <template slot-scope="scope">
                                <el-button
                                    type="success"
                                    size="mini"
                                    icon="el-icon-check"
                                    @click="handleApprove(scope.row, 'approve')"
                                >
                                    通过
                                </el-button>
                                <el-button
                                    type="danger"
                                    size="mini"
                                    icon="el-icon-close"
                                    @click="handleApprove(scope.row, 'reject')"
                                >
                                    不通过
                                </el-button>
                            </template>
                        </el-table-column>
                    </el-table>
                    <div v-if="!approvalLoading && approvalData.length === 0" class="empty-tip">
                        <el-empty description="暂无待审批用户"></el-empty>
                    </div>
                </div>
            </el-drawer>
            <el-container>
                <el-main style="margin-left: 10px; margin-top: 10px">
                    <div class="user-body-table">
                        <el-table
                            highlight-current-row
                            stripe
                            :data="userData"
                            v-loading="isLoading"
                            height="calc(100%)"
                            @cell-mouse-enter="cellMouseEnter"
                            @cell-mouse-leave="cellMouseLeave"
                        >
                            <el-table-column label="用户名" width="120">
                                <template slot-scope="scope">
                                    <div
                                        :title="scope.row.username"
                                        class="table-column"
                                    >
                                        {{ scope.row.username }}
                                    </div>
                                </template>
                            </el-table-column>
                            <el-table-column label="姓名" width="120">
                                <template slot-scope="scope">
                                    <div
                                        :title="scope.row.name"
                                        class="table-column"
                                    >
                                        {{ scope.row.name }}
                                    </div>
                                </template>
                            </el-table-column>
                            <el-table-column label="手机号" width="120">
                                <template slot-scope="scope">
                                    <div
                                        :title="scope.row.phone"
                                        class="table-column"
                                    >
                                        {{ scope.row.phone }}
                                    </div>
                                </template>
                            </el-table-column>
                            <el-table-column label="是否激活" width="100">
                                <template slot-scope="scope">
                                    <el-tag :type="scope.row.is_active ? 'success' : 'danger'">
                                        {{ scope.row.is_active ? '是' : '否' }}
                                    </el-tag>
                                </template>
                            </el-table-column>
                            <el-table-column label="是否管理员" width="100">
                                <template slot-scope="scope">
                                    <el-tag :type="scope.row.is_staff ? 'success' : 'info'">
                                        {{ scope.row.is_staff ? '是' : '否' }}
                                    </el-tag>
                                </template>
                            </el-table-column>
                            <el-table-column label="超级用户" width="100">
                                <template slot-scope="scope">
                                    <el-tag :type="scope.row.is_superuser ? 'success' : 'info'">
                                        {{ scope.row.is_superuser ? '是' : '否' }}
                                    </el-tag>
                                </template>
                            </el-table-column>
                            <el-table-column label="所属分组" width="200">
                                <template slot-scope="scope">
                                    <div class="table-column">
                                        <el-tag
                                            v-for="group in scope.row.groups"
                                            :key="group"
                                            size="mini"
                                            style="margin-right: 3px"
                                        >
                                            {{ group }}
                                        </el-tag>
                                    </div>
                                </template>
                            </el-table-column>
                            <el-table-column label="注册时间" width="180">
                                <template slot-scope="scope">
                                    <div>
                                        {{ scope.row.date_joined | datetimeFormat }}
                                    </div>
                                </template>
                            </el-table-column>
                            <el-table-column label="操作" width="200" fixed="right">
                                <template slot-scope="scope">
                                    <el-row v-show="currentRow === scope.row">
                                        <el-button
                                            type="success"
                                            icon="el-icon-view"
                                            title="查看"
                                            circle
                                            size="mini"
                                            @click="handleView(scope.row)"
                                        ></el-button>
                                        <el-button
                                            type="primary"
                                            icon="el-icon-edit"
                                            title="编辑"
                                            circle
                                            size="mini"
                                            v-if="isSuperuser"
                                            @click="handleEdit(scope.row)"
                                        ></el-button>
                                        <el-button
                                            type="danger"
                                            icon="el-icon-delete"
                                            title="删除"
                                            circle
                                            size="mini"
                                            v-if="isSuperuser && scope.row.id !== currentUserId"
                                            @click="handleDelete(scope.row)"
                                        ></el-button>
                                    </el-row>
                                </template>
                            </el-table-column>
                        </el-table>
                    </div>
                </el-main>
            </el-container>
        </el-container>
    </el-container>
</template>
<script>
export default {
    name: "UserManagement",
    data() {
        var validateConfirmPassword = (rule, value, callback) => {
            if (value !== this.userForm.password) {
                callback(new Error("两次输入的密码不一致"));
            } else {
                callback();
            }
        };
        return {
            search: "",
            currentRow: "",
            isLoading: true,
            userData: [],
            groupList: [],
            dialogVisible: false,
            detailDialogVisible: false,
            approvalDrawerVisible: false,
            approvalLoading: false,
            approvalData: [],
            isCreate: false,
            isSuperuser: false,
            isStaff: false,
            currentUserId: null,
            userForm: {
                id: null,
                username: "",
                name: "",
                phone: "",
                password: "",
                confirm_password: "",
                is_active: true,
                is_staff: false,
                is_superuser: false,
                show_hosts: false,
                groups: []
            },
            userDetail: {
                username: "",
                name: "",
                phone: "",
                is_active: false,
                is_staff: false,
                is_superuser: false,
                show_hosts: false,
                groups: [],
                accessible_projects: [],
                date_joined: "",
                last_login: ""
            },
            rules: {
                username: [
                    { required: true, message: "请输入用户名", trigger: "blur" }
                ],
                name: [
                    { required: true, message: "请输入姓名", trigger: "blur" }
                ],
                password: [
                    { required: true, message: "请输入密码", trigger: "blur" },
                    { min: 6, message: "密码长度不能少于6位", trigger: "blur" }
                ],
                confirm_password: [
                    { required: true, message: "请再次输入密码", trigger: "blur" },
                    { validator: validateConfirmPassword, trigger: "blur" }
                ]
            }
        };
    },
    computed: {
        dialogTitle() {
            return this.isCreate ? "新增用户" : "编辑用户";
        }
    },
    methods: {
        cellMouseEnter(row) {
            this.currentRow = row;
        },
        cellMouseLeave() {
            this.currentRow = "";
        },
        handleAdd() {
            this.isCreate = true;
            this.dialogVisible = true;
            this.resetForm();
        },
        handleEdit(row) {
            this.isCreate = false;
            this.dialogVisible = true;
            this.userForm = {
                id: row.id,
                username: row.username,
                name: row.name,
                phone: row.phone,
                is_active: row.is_active,
                is_staff: row.is_staff,
                is_superuser: row.is_superuser,
                show_hosts: row.show_hosts,
                groups: []
            };
        },
        handleView(row) {
            this.getUserDetail(row.id);
        },
        handleDelete(row) {
            this.$confirm("确定要删除该用户吗?", "提示", {
                confirmButtonText: "确定",
                cancelButtonText: "取消",
                type: "warning"
            }).then(() => {
                this.$api.deleteUser(row.id).then(() => {
                    this.$message.success("删除成功");
                    this.getUserList();
                });
            }).catch(() => {});
        },
        handleSubmit() {
            this.$refs.userForm.validate(valid => {
                if (valid) {
                    if (this.isCreate) {
                        this.$api.createUser(this.userForm).then(() => {
                            this.$message.success("创建成功");
                            this.dialogVisible = false;
                            this.getUserList();
                        });
                    } else {
                        this.$api.updateUser(this.userForm.id, this.userForm).then(() => {
                            this.$message.success("更新成功");
                            this.dialogVisible = false;
                            this.getUserList();
                        });
                    }
                }
            });
        },
        handleDialogClose() {
            this.$refs.userForm.resetFields();
        },
        resetForm() {
            this.userForm = {
                id: null,
                username: "",
                name: "",
                phone: "",
                password: "",
                confirm_password: "",
                is_active: true,
                is_staff: false,
                is_superuser: false,
                show_hosts: false,
                groups: []
            };
        },
        getUserList() {
            this.isLoading = true;
            this.$api.getUserManagementList({ search: this.search }).then(resp => {
                this.userData = resp.results || resp;
                this.isLoading = false;
            });
        },
        getUserDetail(userId) {
            this.$api.getUserDetail(userId).then(resp => {
                this.userDetail = resp;
                this.detailDialogVisible = true;
            });
        },
        getGroupList() {
            this.$api.getGroupList().then(resp => {
                this.groupList = resp;
            });
        },
        resetSearch() {
            this.search = "";
            this.getUserList();
        },
        handleApproval() {
            this.approvalDrawerVisible = true;
            this.getApprovalList();
        },
        getApprovalList() {
            this.approvalLoading = true;
            this.$api.getUserApprovalList({ search: this.search }).then(resp => {
                this.approvalData = resp.results || resp;
                this.approvalLoading = false;
            }).catch(() => {
                this.approvalLoading = false;
            });
        },
        getStatusType(status) {
            const statusMap = {
                'pending': 'warning',
                'approved': 'success',
                'rejected': 'danger'
            };
            return statusMap[status] || 'info';
        },
        handleApprove(row, action) {
            const actionText = action === 'approve' ? '通过' : '不通过';
            this.$confirm(`确定要${actionText}该用户的注册申请吗?`, "提示", {
                confirmButtonText: "确定",
                cancelButtonText: "取消",
                type: "warning"
            }).then(() => {
                this.$api.approveUser(row.id, { action: action }).then(() => {
                    this.$message.success(`用户审批已${actionText}`);
                    this.getApprovalList();
                });
            }).catch(() => {});
        }
    },
    watch: {
        search() {
            this.getUserList();
        }
    },
    mounted() {
        this.isSuperuser = this.$store.state.is_superuser;
        this.isStaff = this.$store.state.is_staff;
        this.currentUserId = this.$store.state.id;
        this.getUserList();
        if (this.isSuperuser) {
            this.getGroupList();
        }
    }
};
</script>
<style scoped>
.env__header {
    display: flex;
    align-items: center;
    margin-left: -30px;
}
.env__header--item {
    display: flex;
    margin-left: 10px;
}
.table-column {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.user-body-table {
    position: fixed;
    bottom: 0;
    right: 0;
    left: 220px;
    top: 100px;
    margin-left: -10px;
    padding-bottom: 60px;
}
.approval-container {
    padding: 20px;
}
.empty-tip {
    text-align: center;
    padding: 40px 0;
}
</style>
测试组/Test_platform/Interface_automation/frontend/src/pages/user/UserManagementDrawer.vue
New file
@@ -0,0 +1,591 @@
<template>
    <div class="user-management-drawer">
        <div class="toolbar">
            <el-input
                style="width: 300px"
                size="small"
                placeholder="请输入用户名或姓名"
                v-model="search"
                clearable
            >
            </el-input>
            <el-button
                plain
                size="small"
                icon="el-icon-refresh"
                @click="resetSearch"
                >重置</el-button
            >
            <el-button
                v-if="isSuperuser"
                type="primary"
                size="small"
                icon="el-icon-plus"
                @click="handleAdd"
                >新增用户</el-button
            >
        </div>
        <el-dialog
            :title="dialogTitle"
            width="50%"
            :close-on-click-modal="false"
            :modal="false"
            :visible.sync="dialogVisible"
            @close="handleDialogClose"
            append-to-body
        >
            <el-form
                :inline="true"
                label-position="right"
                :model="userForm"
                :rules="rules"
                ref="userForm"
                label-width="100px"
            >
                <el-form-item label="用户名" prop="username" v-if="isCreate">
                    <el-input v-model="userForm.username" placeholder="请输入用户名"></el-input>
                </el-form-item>
                <el-form-item label="用户名" prop="username" v-if="!isCreate">
                    <el-input v-model="userForm.username" disabled></el-input>
                </el-form-item>
                <el-form-item label="姓名" prop="name">
                    <el-input v-model="userForm.name" placeholder="请输入姓名"></el-input>
                </el-form-item>
                <el-form-item label="手机号" prop="phone">
                    <el-input v-model="userForm.phone" placeholder="请输入手机号"></el-input>
                </el-form-item>
                <el-form-item label="密码" prop="password" v-if="isCreate">
                    <el-input
                        type="password"
                        v-model="userForm.password"
                        placeholder="请输入密码"
                    ></el-input>
                </el-form-item>
                <el-form-item label="确认密码" prop="confirm_password" v-if="isCreate">
                    <el-input
                        type="password"
                        v-model="userForm.confirm_password"
                        placeholder="请再次输入密码"
                    ></el-input>
                </el-form-item>
                <el-form-item label="是否激活" prop="is_active">
                    <el-switch v-model="userForm.is_active" :disabled="!isSuperuser"></el-switch>
                </el-form-item>
                <el-form-item label="是否管理员" prop="is_staff">
                    <el-switch v-model="userForm.is_staff" :disabled="!isSuperuser"></el-switch>
                </el-form-item>
                <el-form-item label="超级用户" prop="is_superuser">
                    <el-switch v-model="userForm.is_superuser" :disabled="!isSuperuser"></el-switch>
                </el-form-item>
                <el-form-item label="显示Hosts" prop="show_hosts">
                    <el-switch v-model="userForm.show_hosts" :disabled="!isSuperuser"></el-switch>
                </el-form-item>
                <el-form-item label="所属分组" prop="groups">
                    <el-select
                        v-model="userForm.groups"
                        multiple
                        placeholder="请选择分组"
                        style="width: 100%"
                        :disabled="!isSuperuser"
                    >
                        <el-option
                            v-for="group in groupList"
                            :key="group.id"
                            :label="group.name"
                            :value="group.id"
                        ></el-option>
                    </el-select>
                </el-form-item>
            </el-form>
            <span slot="footer" class="dialog-footer">
                <el-button @click="dialogVisible = false">取 消</el-button>
                <el-button type="primary" @click="handleSubmit">确 定</el-button>
            </span>
        </el-dialog>
        <el-dialog
            title="用户详情"
            width="50%"
            :close-on-click-modal="false"
            :modal="false"
            :visible.sync="detailDialogVisible"
            append-to-body
        >
            <el-form
                :inline="true"
                label-position="right"
                :model="userDetail"
                label-width="100px"
            >
                <el-form-item label="用户名">
                    <el-input v-model="userDetail.username" readonly></el-input>
                </el-form-item>
                <el-form-item label="姓名">
                    <el-input v-model="userDetail.name" readonly></el-input>
                </el-form-item>
                <el-form-item label="手机号">
                    <el-input v-model="userDetail.phone" readonly></el-input>
                </el-form-item>
                <el-form-item label="是否激活">
                    <el-tag :type="userDetail.is_active ? 'success' : 'danger'">
                        {{ userDetail.is_active ? '是' : '否' }}
                    </el-tag>
                </el-form-item>
                <el-form-item label="是否管理员">
                    <el-tag :type="userDetail.is_staff ? 'success' : 'info'">
                        {{ userDetail.is_staff ? '是' : '否' }}
                    </el-tag>
                </el-form-item>
                <el-form-item label="超级用户">
                    <el-tag :type="userDetail.is_superuser ? 'success' : 'info'">
                        {{ userDetail.is_superuser ? '是' : '否' }}
                    </el-tag>
                </el-form-item>
                <el-form-item label="显示Hosts">
                    <el-tag :type="userDetail.show_hosts ? 'success' : 'info'">
                        {{ userDetail.show_hosts ? '是' : '否' }}
                    </el-tag>
                </el-form-item>
                <el-form-item label="所属分组">
                    <el-tag
                        v-for="group in userDetail.groups"
                        :key="group"
                        style="margin-right: 5px"
                    >
                        {{ group }}
                    </el-tag>
                </el-form-item>
                <el-form-item label="可访问项目">
                    <el-tag
                        v-for="project in userDetail.accessible_projects"
                        :key="project"
                        style="margin-right: 5px"
                    >
                        {{ project }}
                    </el-tag>
                </el-form-item>
                <el-form-item label="注册时间">
                    <el-input
                        :value="userDetail.date_joined | datetimeFormat"
                        readonly
                    ></el-input>
                </el-form-item>
                <el-form-item label="最后登录">
                    <el-input
                        :value="formatLastLogin(userDetail.last_login)"
                        readonly
                    ></el-input>
                </el-form-item>
            </el-form>
            <span slot="footer" class="dialog-footer">
                <el-button @click="detailDialogVisible = false">关 闭</el-button>
            </span>
        </el-dialog>
        <div class="table-container">
            <el-table
                highlight-current-row
                stripe
                :data="userData"
                v-loading="isLoading"
                height="calc(100vh - 150px)"
                @cell-mouse-enter="cellMouseEnter"
                @cell-mouse-leave="cellMouseLeave"
            >
                <el-table-column label="用户名" width="120">
                    <template slot-scope="scope">
                        <div
                            :title="scope.row.username"
                            class="table-column"
                        >
                            {{ scope.row.username }}
                        </div>
                    </template>
                </el-table-column>
                <el-table-column label="姓名" width="120">
                    <template slot-scope="scope">
                        <div
                            :title="scope.row.name"
                            class="table-column"
                        >
                            {{ scope.row.name }}
                        </div>
                    </template>
                </el-table-column>
                <el-table-column label="手机号" width="120">
                    <template slot-scope="scope">
                        <div
                            :title="scope.row.phone"
                            class="table-column"
                        >
                            {{ scope.row.phone }}
                        </div>
                    </template>
                </el-table-column>
                <el-table-column label="是否激活" width="100">
                    <template slot-scope="scope">
                        <el-tag :type="scope.row.is_active ? 'success' : 'danger'">
                            {{ scope.row.is_active ? '是' : '否' }}
                        </el-tag>
                    </template>
                </el-table-column>
                <el-table-column label="是否管理员" width="100">
                    <template slot-scope="scope">
                        <el-tag :type="scope.row.is_staff ? 'success' : 'info'">
                            {{ scope.row.is_staff ? '是' : '否' }}
                        </el-tag>
                    </template>
                </el-table-column>
                <el-table-column label="超级用户" width="100">
                    <template slot-scope="scope">
                        <el-tag :type="scope.row.is_superuser ? 'success' : 'info'">
                            {{ scope.row.is_superuser ? '是' : '否' }}
                        </el-tag>
                    </template>
                </el-table-column>
                <el-table-column label="所属分组" width="200">
                    <template slot-scope="scope">
                        <div class="table-column">
                            <el-tag
                                v-for="group in scope.row.groups"
                                :key="group"
                                size="mini"
                                style="margin-right: 3px"
                            >
                                {{ group }}
                            </el-tag>
                        </div>
                    </template>
                </el-table-column>
                <el-table-column label="注册时间" width="180">
                    <template slot-scope="scope">
                        <div>
                            {{ scope.row.date_joined | datetimeFormat }}
                        </div>
                    </template>
                </el-table-column>
                <el-table-column label="操作" width="200" fixed="right">
                    <template slot-scope="scope">
                        <el-row v-show="currentRow === scope.row">
                            <el-button
                                type="success"
                                icon="el-icon-view"
                                title="查看"
                                circle
                                size="mini"
                                @click="handleView(scope.row)"
                            ></el-button>
                            <el-button
                                type="primary"
                                icon="el-icon-edit"
                                title="编辑"
                                circle
                                size="mini"
                                v-if="isSuperuser"
                                @click="handleEdit(scope.row)"
                            ></el-button>
                            <el-button
                                type="danger"
                                icon="el-icon-delete"
                                title="删除"
                                circle
                                size="mini"
                                v-if="isSuperuser && scope.row.id !== currentUserId"
                                @click="handleDelete(scope.row)"
                            ></el-button>
                        </el-row>
                    </template>
                </el-table-column>
            </el-table>
        </div>
    </div>
</template>
<script>
export default {
    name: "UserManagementDrawer",
    data() {
        var validateConfirmPassword = (rule, value, callback) => {
            if (value !== this.userForm.password) {
                callback(new Error("两次输入的密码不一致"));
            } else {
                callback();
            }
        };
        return {
            search: "",
            currentRow: "",
            isLoading: true,
            userData: [],
            groupList: [],
            dialogVisible: false,
            detailDialogVisible: false,
            isCreate: false,
            isSuperuser: false,
            currentUserId: null,
            userForm: {
                id: null,
                username: "",
                name: "",
                phone: "",
                password: "",
                confirm_password: "",
                is_active: true,
                is_staff: false,
                is_superuser: false,
                show_hosts: false,
                groups: []
            },
            userDetail: {
                username: "",
                name: "",
                phone: "",
                is_active: false,
                is_staff: false,
                is_superuser: false,
                show_hosts: false,
                groups: [],
                accessible_projects: [],
                date_joined: "",
                last_login: ""
            },
            rules: {
                username: [
                    { required: true, message: "请输入用户名", trigger: "blur" }
                ],
                name: [
                    { required: true, message: "请输入姓名", trigger: "blur" }
                ],
                password: [
                    { required: true, message: "请输入密码", trigger: "blur" },
                    { min: 6, message: "密码长度不能少于6位", trigger: "blur" }
                ],
                confirm_password: [
                    { required: true, message: "请再次输入密码", trigger: "blur" },
                    { validator: validateConfirmPassword, trigger: "blur" }
                ]
            }
        };
    },
    computed: {
        dialogTitle() {
            return this.isCreate ? "新增用户" : "编辑用户";
        }
    },
    methods: {
        cellMouseEnter(row) {
            this.currentRow = row;
        },
        cellMouseLeave() {
            this.currentRow = "";
        },
        handleAdd() {
            this.isCreate = true;
            this.dialogVisible = true;
            this.resetForm();
        },
        handleEdit(row) {
            this.isCreate = false;
            this.dialogVisible = true;
            const groupIds = this.getGroupIdsByName(row.groups || []);
            this.userForm = {
                id: row.id,
                username: row.username,
                name: row.name,
                phone: row.phone,
                is_active: row.is_active,
                is_staff: row.is_staff,
                is_superuser: row.is_superuser,
                show_hosts: row.show_hosts,
                groups: groupIds
            };
        },
        handleView(row) {
            this.getUserDetail(row.id);
        },
        handleDelete(row) {
            this.$confirm("确定要删除该用户吗?", "提示", {
                confirmButtonText: "确定",
                cancelButtonText: "取消",
                type: "warning"
            }).then(() => {
                this.$api.deleteUser(row.id).then(() => {
                    this.$message.success("删除成功");
                    this.getUserList();
                });
            }).catch(() => {});
        },
        handleSubmit() {
            this.$refs.userForm.validate(valid => {
                if (valid) {
                    console.log("提交表单数据:", this.userForm);
                    if (this.isCreate) {
                        this.$api.createUser(this.userForm).then(() => {
                            this.$message.success("创建成功");
                            this.dialogVisible = false;
                            this.getUserList();
                        }).catch(err => {
                            console.error("创建用户失败:", err);
                            console.error("错误详情:", err.response ? err.response.data : err.message);
                            this.$message.error("创建用户失败,请检查输入信息");
                        });
                    } else {
                        this.$api.updateUser(this.userForm.id, this.userForm).then(() => {
                            this.$message.success("更新成功");
                            this.dialogVisible = false;
                            this.getUserList();
                        }).catch(err => {
                            console.error("更新用户失败:", err);
                            console.error("错误详情:", err.response ? err.response.data : err.message);
                            this.$message.error("更新用户失败,请检查输入信息");
                        });
                    }
                } else {
                    this.$message.warning("请检查表单输入");
                }
            });
        },
        handleDialogClose() {
            this.$refs.userForm.resetFields();
        },
        resetForm() {
            this.userForm = {
                id: null,
                username: "",
                name: "",
                phone: "",
                password: "",
                confirm_password: "",
                is_active: true,
                is_staff: false,
                is_superuser: false,
                show_hosts: false,
                groups: []
            };
        },
        getUserList() {
            this.isLoading = true;
            console.log("搜索关键词:", this.search);
            this.$api.getUserManagementList({ search: this.search }).then(resp => {
                console.log("搜索结果:", resp);
                this.userData = resp.results || resp;
                this.isLoading = false;
            }).catch(err => {
                console.error("获取用户列表失败:", err);
                this.isLoading = false;
            });
        },
        getUserDetail(userId) {
            this.$api.getUserDetail(userId).then(resp => {
                this.userDetail = resp;
                this.detailDialogVisible = true;
            });
        },
        getGroupList() {
            this.$api.getGroupList().then(resp => {
                this.groupList = resp;
            });
        },
        resetSearch() {
            this.search = "";
            this.getUserList();
        },
        formatLastLogin(lastLogin) {
            if (!lastLogin) {
                return "从未登录";
            }
            return lastLogin;
        },
        getGroupIdsByName(groupNames) {
            if (!groupNames || groupNames.length === 0) {
                return [];
            }
            const groupMap = {};
            this.groupList.forEach(group => {
                groupMap[group.name] = group.id;
            });
            return groupNames.map(name => groupMap[name]).filter(id => id !== undefined);
        }
    },
    watch: {
        search() {
            this.getUserList();
        }
    },
    mounted() {
        this.isSuperuser = this.$store.state.is_superuser;
        this.currentUserId = this.$store.state.id;
        this.getUserList();
        if (this.isSuperuser) {
            this.getGroupList();
        }
    }
};
</script>
<style scoped>
.user-management-drawer {
    width: 100%;
    height: 100%;
    padding: 0;
}
.toolbar {
    display: flex;
    align-items: center;
    padding: 10px 20px;
    gap: 10px;
    background: #f5f5f5;
    border-bottom: 1px solid #e0e0e0;
}
.toolbar .el-input {
    margin-right: 10px;
}
.table-container {
    padding: 20px;
    height: calc(100vh - 150px);
    overflow: auto;
}
.table-column {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
</style>
测试组/Test_platform/Interface_automation/frontend/src/restful/api.js
@@ -10,15 +10,14 @@
axios.interceptors.request.use(
    function(config) {
        if (
            !config.url.startsWith("/api/user") ||
            config.url === "/api/user/list" ||
            config.url === "/api/user/login_log"
        ) {
            // 在请求拦截中,每次请求,都会加上一个Authorization头
        const noAuthUrls = [
            '/api/user/login',
            '/api/user/register',
            '/api/user/groups'
        ];
        if (!noAuthUrls.includes(config.url)) {
            config.headers.Authorization = store.token;
            // 取url地址的第四位作为projectId, 如果不存在,默认设置为0
            let projectId = window.location.pathname.split("/")[3];
            projectId = projectId ? projectId : 0;
            config.headers["Project"] = projectId;
@@ -58,12 +57,6 @@
                        duration: 2000
                    });
                }
                if (status === 403) {
                    Message.error({
                        message: error.response.data.detail,
                        duration: 2000
                    });
                }
            } catch (e) {
                Message.error({
                    message: "服务器连接超时, 请重试",
@@ -77,6 +70,10 @@
export const login = params => {
    return axios.post("/api/user/login", params).then(res => res.data);
};
export const register = params => {
    return axios.post("/api/user/register", params).then(res => res.data);
};
export const addProject = params => {
@@ -143,7 +140,7 @@
export const getGroupList = () => {
    return axios
        .get("/api/lunarlink/project/groups")
        .get("/api/user/groups")
        .then(res => res.data);
};
@@ -527,3 +524,31 @@
    }).then(res => res.data);
};
export const getUserManagementList = (params) => {
    return axios.get("/api/user/management/", { params }).then(res => res.data);
};
export const createUser = (data) => {
    return axios.post("/api/user/management/", data).then(res => res.data);
};
export const updateUser = (userId, data) => {
    return axios.patch(`/api/user/management/${userId}/`, data).then(res => res.data);
};
export const deleteUser = (userId) => {
    return axios.delete(`/api/user/management/${userId}/`).then(res => res.data);
};
export const getUserDetail = (userId) => {
    return axios.get(`/api/user/management/${userId}/`).then(res => res.data);
};
export const getUserApprovalList = (params) => {
    return axios.get("/api/user/approval/", { params }).then(res => res.data);
};
export const approveUser = (userId, data) => {
    return axios.post(`/api/user/approval/${userId}/approve/`, data).then(res => res.data);
};
测试组/Test_platform/Interface_automation/frontend/src/store/mutations.js
@@ -31,6 +31,10 @@
        state.is_superuser = value;
    },
    setIsStaff(state, value) {
        state.is_staff = value;
    },
    setShowHosts(state, value) {
        state.show_hosts = value;
    }
测试组/Test_platform/Interface_automation/frontend/src/store/state.js
@@ -5,6 +5,7 @@
    token: null,
    user: null,
    is_superuser: false,
    is_staff: false,
    show_hosts: false,
    duration: 2000,
    report_path: "/api/lunarlink/reports/",
测试组/Test_platform/Interface_automation/注册审批功能测试指南.md
New file
@@ -0,0 +1,144 @@
# 用户注册审批功能测试指南
## 功能概述
本系统实现了完整的用户注册审批流程,包括:
1. 用户自主注册功能
2. 管理员审批功能
3. 登录状态检查
4. 多管理员支持(一人审批即可)
## 数据库迁移
由于项目依赖问题,请手动执行以下SQL语句添加status字段:
```sql
ALTER TABLE lunaruser_myuser
ADD COLUMN status VARCHAR(20) DEFAULT 'approved'
COMMENT '用户状态';
```
或者运行以下Python脚本:
```bash
cd backend
python add_status_field.py
```
## 测试步骤
### 1. 用户注册测试
1. 访问登录页面
2. 点击"立即注册"链接
3. 填写注册表单:
   - 用户名(必填)
   - 姓名(必填)
   - 手机号(必填)
   - 密码(必填,至少6位)
   - 确认密码(必填)
   - 所属分组(可选)
4. 点击"注册账号"
5. 预期结果:显示"你的注册申请已提交,待管理员审核中!"
### 2. 管理员审批测试
1. 使用超级管理员账号登录
2. 进入用户管理页面
3. 点击"注册用户审批"按钮
4. 查看待审批用户列表
5. 对某个用户点击"通过"或"不通过"
6. 预期结果:
   - 通过后,用户状态变为"已通过",is_active变为true
   - 不通过后,用户状态变为"未通过"
### 3. 登录状态检查测试
#### 待审核状态登录
1. 使用待审核的用户名和密码登录
2. 预期结果:显示"你提交的注册还在审批中,暂时无法登录,可联系管理员确认!"
#### 未通过状态登录
1. 使用未通过的用户名和密码登录
2. 预期结果:显示"你的注册申请未通过审核,无法登录!"
#### 已通过状态登录
1. 使用已通过的用户名和密码登录
2. 预期结果:正常登录
## API接口说明
### 注册接口
- URL: `POST /api/user/register`
- 参数:
  ```json
  {
    "username": "testuser",
    "name": "测试用户",
    "phone": "13800138000",
    "password": "123456",
    "confirm_password": "123456",
    "groups": []
  }
  ```
- 响应:
  ```json
  {
    "success": true,
    "msg": "你的注册申请已提交,待管理员审核中!",
    "data": {
      "id": 1,
      "username": "testuser"
    }
  }
  ```
### 审批列表接口
- URL: `GET /api/user/approval/`
- 响应:
  ```json
  {
    "success": true,
    "results": [...]
  }
  ```
### 审批操作接口
- URL: `POST /api/user/approval/{user_id}/approve/`
- 参数:
  ```json
  {
    "action": "approve"
  }
  ```
- 响应:
  ```json
  {
    "success": true,
    "msg": "用户审批已通过"
  }
  ```
## 文件修改清单
### 后端文件
1. `backend/apps/lunaruser/models.py` - 添加status字段
2. `backend/apps/lunaruser/serializers.py` - 添加注册和审批序列化器
3. `backend/apps/lunaruser/views.py` - 添加注册和审批视图
4. `backend/apps/lunaruser/urls.py` - 添加注册和审批路由
5. `backend/apps/lunaruser/migrations/0003_myuser_status.py` - 数据库迁移文件
### 前端文件
1. `frontend/src/pages/login/Login.vue` - 添加注册表单
2. `frontend/src/pages/user/UserManagement.vue` - 添加审批抽屉
3. `frontend/src/restful/api.js` - 添加API接口
4. `frontend/src/api/user.js` - 添加API接口
## 注意事项
1. 所有现有用户的status默认为'approved',可以正常登录
2. 新注册用户的status默认为'pending',需要管理员审批
3. 审批通过后,status变为'approved',is_active变为true
4. 审批不通过后,status变为'rejected',is_active保持false
5. 多个超级管理员时,任何一人审批即可生效