TestHub智能测试管理平台框架介绍

一、项目介绍

基于 Python + Django + DRF + Vue3 + MySQL + Celery + Redis + Playwright + Airtest + Allure

基于 Django REST Framework + Vue3 前后端分离架构搭建的 TestHub 智能测试管理平台,

把测试工作中分散的工具整合到统一平台:在线编写用例、用例评审、接口测试、

Web UI 自动化、Android APP 自动化、AI 需求分析与用例生成、测试报告集成。

平台覆盖从需求文档到测试报告的全链路:上传 PDF/Word/TXT 需求文档 →

调用大模型解析业务需求 → 批量生成测试用例 → 进入评审流程 →

关联到接口/UI/APP 自动化执行 → 生成 Allure 报告,实现测试资产的一站式管理。技术栈明细

二、项目结构说明

python 复制代码
testhub_platform/                    # 项目根目录
├── backend/                         # Django 项目配置包
│   ├── __init__.py                  # PyMySQL 伪装为 MySQLdb 的兼容入口
│   ├── settings.py                  # 全局配置(数据库/JWT/DRF/Celery/日志)
│   ├── urls.py                      # 根路由:统一按 /api/<模块>/ 分发
│   ├── wsgi.py                      # 生产 HTTP 入口(gunicorn 加载)
│   ├── asgi.py                      # WebSocket 入口(Daphne 加载)
│   ├── celery.py                    # Celery 应用初始化
│   └── middleware.py                # 自定义中间件(CSRF 控制)
├── apps/                            # 15 个业务应用(模块化拆分)
│   ├── users/                       # 自定义用户模型 + JWT 登录/登出
│   ├── projects/                    # 项目、成员角色、环境配置
│   ├── testcases/                   # 测试用例、步骤、附件、评论
│   ├── testsuites/                  # 测试套件
│   ├── executions/                  # 测试计划、执行、结果
│   ├── reports/                     # 测试报告
│   ├── reviews/                     # 用例评审流程、模板、检查项
│   ├── versions/                    # 版本管理
│   ├── assistant/                   # Dify AI 助手
│   ├── requirement_analysis/        # AI 需求分析 + 用例生成 + AI 模型配置
│   ├── api_testing/                 # 接口测试(HTTP/WebSocket)
│   ├── ui_automation/               # Web UI 自动化(Selenium/Playwright)
│   ├── app_automation/              # Android APP 自动化(Airtest)
│   ├── core/                        # 公共能力:变量解析/定位策略/定时调度
│   └── data_factory/                # 数据工厂:造数、编码、加密、JSON 工具
├── frontend/                        # Vue3 前端
│   ├── src/
│   │   ├── views/                   # 84 个页面(按模块分目录)
│   │   ├── api/                     # 接口封装,按模块拆分
│   │   ├── stores/                  # Pinia 状态管理(user / app)
│   │   ├── router/                  # Vue Router 路由与登录守卫
│   │   ├── utils/api.js             # axios 实例:JWT 注入 + 自动刷新
│   │   └── layout/                  # 整体布局
│   ├── vite.config.js               # Vite 配置(端口 3000,代理 /api → 8000)
│   └── package.json
├── docs/                            # 20 篇功能与排查文档
├── allure/                          # Allure 命令行工具
├── media/                           # 上传文件与报告产物
├── logs/                            # 运行日志
├── manage.py                        # 开发期命令入口
├── requirements.txt                 # 依赖清单
├── .env                             # 环境变量(数据库/密钥/Redis)
└── start-testhub.ps1                # 一键启动脚本(数据库/后端/前端/Redis)

业务应用内部结构:

python 复制代码
apps/api_testing/
├── models.py            # 数据模型
├── serializers.py       # 校验 + 序列化
├── views.py             # 业务逻辑
├── urls.py              # 路由
├── admin.py             # 后台注册
├── request_utils.py     # 请求组装与响应提取
├── utils.py             # 断言执行
├── variable_resolver.py # 变量解析(委托 core)
└── tasks.py             # Celery 异步任务

三、核心代码

1.程序主入口与请求链路

Django四个入口:

|-------------------|----------------------------------|
| 入口 | 用途 |
| manage.py | 开发期命令(runserver/ migrate/ shell) |
| backend/wsgi.py | 生产 HTTP(gunicorn 加载) |
| backend/asgi.py | WebSocket(Daphne 加载) |
| backend/celery.py | 异步任务 |

代码如下:manage.py

python 复制代码
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys


def main():
    """Run administrative tasks."""
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)


if __name__ == '__main__':
    main()

代码说明

设置配置模块

python 复制代码
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
  • 告诉 Django 到哪个模块读取配置。
  • 用 setdefault 而不是直接赋值,是为了允许环境变量覆盖,生产环境可以通过外部变量切换不同 settings,不用改代码。

Django 命令行框架

python 复制代码
execute_from_command_line(sys.argv)
  • 把命令行参数交给 Django 解析并执行对应命令
  • 万能入口:runserver、migrate、createsuperuser、shell全部走这里,不只是"启动服务器"

判断__name__ == 'main:

  • 保证该文件被其他模块 import 时不会自动执行命令行逻辑

启动链路

python 复制代码
manage.py:9   设置 DJANGO_SETTINGS_MODULE='backend.settings'
manage.py:13  execute_from_command_line(sys.argv)
      ↓
backend/settings.py   读取配置
   ├ MIDDLEWARE(CORS / CSRF / Session / 认证)
   ├ ROOT_URLCONF = 'backend.urls'      ← 真正的路由中枢
   ├ AUTH_USER_MODEL = 'users.User'     ← 自定义用户模型
   └ CELERY_BROKER_URL ← 读 .env 的 REDIS_URL
      ↓
backend/urls.py       按路径分发到各模块
      ↓
apps/<模块>/urls.py → views.py → serializers.py → models.py → MySQL

manage.py 只是启动器,真正决定应用行为的是 settings.py(配置)和 urls.py(路由)

路由分发:backend/urls.py

python 复制代码
urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
    path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
    path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),

    path('api/auth/', include('apps.users.urls')),
    path('api/projects/', include('apps.projects.urls')),
    path('api/testcases/', include('apps.testcases.urls')),
    path('api/requirement-analysis/', include('apps.requirement_analysis.urls')),
    path('api/ui-automation/', include('apps.ui_automation.urls')),
    path('api/app-automation/', include('apps.app_automation.urls')),
    path('api/data-factory/', include('apps.data_factory.urls')),
    ...
]
  • 所有接口统一以 /api/为前缀,根据 URL 查找对应代码目录。

2. 统一变量解析引擎(platforms 核心亮点)

接口测试和 UI 自动化都需要动态参数:随机手机号、时间戳、加密串等。

平台用一套 ${function_name(args)} 语法统一解决。

代码如下:apps/core/variable_resolver.py

python 复制代码
# 工具名 → (工具类别, 真实实现函数)
TOOL_FUNCTIONS = {
    # ---------- 随机工具 ----------
    'random_int': ('random', RandomTools.random_int),
    'random_uuid': ('random', RandomTools.random_uuid),
    'random_date': ('random', RandomTools.random_date),
    # ---------- 测试数据工具 ----------
    'random_phone': ('test_data', TestDataTools.generate_chinese_phone),
    'generate_id_card': ('test_data', TestDataTools.generate_id_card),
    # ---------- 编码工具 ----------
    'base64_encode': ('encoding', EncodingTools.base64_encode),
    'generate_qrcode': ('encoding', EncodingTools.generate_qrcode),
    # ---------- 加密工具 ----------
    'md5': ('encryption', EncryptionTools.md5_hash),
    'sha256_hash': ('encryption', EncryptionTools.sha256_hash),
    # ---------- 时间日期函数(本地实现)----------
    'timestamp': ('time', '_time_timestamp'),
    'date': ('time', '_time_date'),
    ...
}


class VariableResolver:
    """统一变量解析器 - 使用数据工厂工具"""

    functions = TOOL_FUNCTIONS

    def __init__(self):
        # 类别 → 参数翻译器
        self.BUILDERS = {
            'random': self._build_random_kwargs,
            'test_data': self._build_test_data_kwargs,
            'string': self._build_string_kwargs,
            'encoding': self._build_encoding_kwargs,
            'encryption': self._build_encryption_kwargs,
            'crontab': self._build_crontab_kwargs,
        }

    def resolve(self, text):
        """解析文本中的动态函数占位符

        Args:
            text: 包含动态函数的文本,如 "Hello ${random_string(8)}"
        Returns:
            解析后的文本
        """
        if not isinstance(text, str):
            return text

        # 匹配 ${function_name(args)} 模式
        pattern = r'\$\{([^}]+)\}'

        def replace_func(match):
            expression = match.group(1)
            try:
                return str(self._evaluate_expression(expression))
            except Exception as e:
                if isinstance(e, UnicodeEncodeError):
                    return match.group(0)
                try:
                    print(f"[WARNING] Variable resolution failed: ${{{expression}}} - {str(e)}")
                except UnicodeEncodeError:
                    print(f"[WARNING] Variable resolution failed: ${{{expression}}}")
                return match.group(0)

        return re.sub(pattern, replace_func, text)

    def _evaluate_expression(self, expression):
        """评估单个表达式,如 "random_int(100, 200)" 或 "timestamp()" """
        match = re.match(r'(\w+)\((.*)\)', expression.strip())
        if not match:
            # 无参数函数
            func_name = expression.strip()
            args = []
        else:
            func_name = match.group(1)
            args_str = match.group(2)
            args = self._parse_args(args_str)

        # 只查一次表:函数名 → (调用类别, 实现函数)
        spec = TOOL_FUNCTIONS.get(func_name)
        if spec is None:
            return _Unresolved(func_name)

        kind, tool_func = spec
        tool_func = _load(tool_func)
        if kind == 'time':
            # 时间函数是本地实现,直接按 (func_name, args) 调用
            return tool_func(self, func_name, args)
        builder = self.BUILDERS.get(kind)
        if builder is None:
            return _Unresolved(func_name)
        return _run_tool(func_name, args, tool_func, builder)

方法说明:

单一登记表(注册表模式)

python 复制代码
TOOL_FUNCTIONS = {
    'random_int': ('random', RandomTools.random_int),
    ...
}
  • 用字典隐射函数名映射到"类别+真是实现函数"
  • 新增函数只需注册一行,不改解析逻辑(对扩展开放、对修改封闭)
  • 所有实现最终都委托给 data_factory`的各类工具类

参数解析:

python 复制代码
match = re.match(r'(\w+)\((.*)\)', expression.strip())
  • (\w+)捕获函数名,\((.*)\) 捕获括号内全部参数串。
  • 匹配失败说明是无参函数,走 args = \[\]分支。

异常兜底

python 复制代码
except Exception as e:
    if isinstance(e, UnicodeEncodeError):
        return match.group(0)
    ...
    return match.group(0)
  • 解析失败时返回原始占位符而不是抛异常。
  • 单个变量写错不会导致整个请求失败,便于定位问题。
  • 对 UnicodeEncodeError 单独判断,是为了兼容 Windows GBK 控制台(打印中文/emoji 会报编码错误)

示例:

python 复制代码
# 接口测试的请求体配置
body:
  phone: "${random_phone()}"
  name: "${generate_chinese_name()}"
  sign: "${md5(123456)}"
  ts: "${timestamp()}"

3.接口测试:

代码如下:apps/api_testing/request_utils.py

python 复制代码
"""
接口测试公共工具模块。

集中此前散落在 views.py / utils.py 中重复拷贝的逻辑:
- 环境变量 {{var}} 替换(字符串 / 递归 dict)
- 动态函数 ${func()} 解析(委托 core.VariableResolver)
- 环境变量合并(GLOBAL + LOCAL,局部覆盖全局)
- 请求组装(headers / params / body,支持 json / raw / form-urlencoded / form-data)
- 响应提取(extractors:json_path / header / status_code / regex)→ 供接口关联使用
"""
import json
import re

from .variable_resolver import VariableResolver


# ---------------------------------------------------------------------------
# 1. 环境变量 {{var}} 替换
# ---------------------------------------------------------------------------
def replace_env_variables(text, variables):
    """把文本中的 {{key}} 占位符替换为环境变量值。

    变量值支持两种形态:普通标量,或前端变量对象 {currentValue, initialValue}。
    """
    if not isinstance(text, str):
        return text
    result = text
    for key, value in (variables or {}).items():
        if isinstance(value, dict):
            replacement = str(value.get('currentValue', '') or value.get('initialValue', ''))
        else:
            replacement = str(value) if value is not None else ''
        result = result.replace(f'{{{{{key}}}}}', replacement)
    return result


def replace_env_variables_in_obj(data, variables):
    """递归替换 dict / list / str 中的 {{var}}。"""
    if isinstance(data, dict):
        return {k: replace_env_variables_in_obj(v, variables) for k, v in data.items()}
    if isinstance(data, list):
        return [replace_env_variables_in_obj(item, variables) for item in data]
    if isinstance(data, str):
        return replace_env_variables(data, variables)
    return data


def resolve_dynamic_in_obj(data, resolver):
    """递归解析 dict / list / str 中的 ${func()} 动态函数。"""
    if isinstance(data, dict):
        return {k: resolve_dynamic_in_obj(v, resolver) for k, v in data.items()}
    if isinstance(data, list):
        return [resolve_dynamic_in_obj(item, resolver) for item in data]
    if isinstance(data, str):
        return resolver.resolve(data)
    return data

代码说明:

变量:

|-----------|-----------|--------------|
| 语法 | 语义 | 数据来源 |
| {{key}} | 环境变量静态替换 | 环境配置表(全局+局部) |
| ${func()} | 动态函数运行时生成 | 数据工厂工具 |

  • 分开的好处:环境变量是"配置"(如 {{base_url}}、{{token}}),动态函数是"生成"(如 ${random_phone()}),语义清晰、便于排查。如果混用一种语法,出问题时分不清是配置错了还是函数错了。

递归处理嵌套结构

python 复制代码
def replace_env_variables_in_obj(data, variables):
    if isinstance(data, dict):
        return {k: replace_env_variables_in_obj(v, variables) for k, v in data.items()}
    if isinstance(data, list):
        return [replace_env_variables_in_obj(item, variables) for item in data]
    if isinstance(data, str):
        return replace_env_variables(data, variables)
    return data
  • 请求体是多层嵌套的 JSON(如 `{"data": {"user": {"phone": "{{phone}}"}}}`),
    必须递归才能替换到最内层。
  • 三个分支覆盖 dict/ list / str,最后 return data 兜底数字、布尔等类型。
  • 递归遍历(对树形结构做递归映射):按节点类型分派,容器节点递归处理子节点,叶子节点就地处理,碰到非容器类型即终止。写法短且不会漏层级。

兼容两种变量值形态:

python 复制代码
if isinstance(value, dict):
    replacement = str(value.get('currentValue', '') or value.get('initialValue', ''))
else:
    replacement = str(value) if value is not None else ''
  • 前端传来的变量可能是**普通标量**,也可能是对象{currentValue, initialValue}(用于界面展示当前值与原值)。
  • 用 or 短路:currentValue 为空时回退到 initialValue。

4.断言执行引擎

代码如下:apps/api_testing/utils.py

python 复制代码
def execute_assertions(response, assertions):
    """执行断言验证"""
    results = []

    for assertion in assertions:
        result = {
            'name': assertion.get('name', '未命名断言'),
            'type': assertion.get('type'),
            'passed': False,
            'expected': assertion.get('expected'),
            'actual': None,
            'error': None
        }

        try:
            assertion_type = assertion.get('type')
            expected = assertion.get('expected')
            actual = None
            passed = False

            if assertion_type == 'status_code':
                actual = response.status_code
                passed = actual == expected

            elif assertion_type == 'response_time':
                # 响应时间断言在调用方处理
                actual = assertion.get('actual_time')
                passed = actual <= expected if actual else False

            elif assertion_type == 'contains':
                text = response.text or ''
                pattern = str(expected)
                actual = text[:200] + '...' if len(text) > 200 else text
                passed = pattern in str(text)

            elif assertion_type == 'json_path':
                json_path = assertion.get('json_path', '')
                expected_value = assertion.get('expected')
                actual = None
                passed = False

                try:
                    # 检查响应是否为JSON格式
                    content_type = response.headers.get('content-type', '').lower()
                    if 'application/json' not in content_type:
                        raise ValueError(f"响应不是JSON格式,Content-Type: {content_type}")

                    response_json = json.loads(response.text)

                    # 检查JSONPath表达式是否为空
                    if not json_path:
                        raise ValueError("JSON路径表达式不能为空")

                    from jsonpath_ng import parse
                    matches = parse(json_path).find(response_json)
                    actual = matches[0].value if matches else None
                    passed = str(actual) == str(expected_value)

                    # 确保actual值被正确设置到result中
                    result['actual'] = actual
                except json.JSONDecodeError as e:
                    actual = None

代码说明

统一的断言结果结构

python 复制代码
result = {
    'name': ..., 'type': ..., 'passed': False,
    'expected': ..., 'actual': None, 'error': None
}
  • 每条断言都产出结构一致的结果对象,这样前端可以直接渲染成表格(断言名/预期/实际/是否通过),不需要为每种断言类型写不同的展示逻辑。

六种断言类型:

|---------------|--------|---------------|------------------------------------------------|
| type | 界面显示名 | 校验内容 | 实现要点 |
| status_code | 状态码 | HTTP状态码 | 直接比较response.status_code |
| response_time | 响应时间 | 响应时间是否超时 | 耗时由调用方测量后传入 |
| contains | 包含文本 | 响应文本是否含子串 | 阶段到200字符避免报告过长 |
| json_path | JSON路径 | 按JSONPath取值比较 | 用jsonpath_ng解析 |
| header | 响应头 | 指定响应头的值 | response.headers.get(header_name) |
| equals | 完全匹配 | 响应全文与预期完全相等 | response.text.strip() == str(expected).strip() |

json_path比equals更常用。

5.JWT双Token+前端无感刷新

后端:Token策略配置(backend/settings.py)

python 复制代码
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),   # access_token 60分钟
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),      # refresh_token 7天
    'ROTATE_REFRESH_TOKENS': True,      # 刷新时轮换 refresh_token
    'BLACKLIST_AFTER_ROTATION': True,   # 旧的 refresh_token 加入黑名单
    'UPDATE_LAST_LOGIN': True,          # 更新最后登录时间

    'ALGORITHM': 'HS256',
    'SIGNING_KEY': SECRET_KEY,
    'AUTH_HEADER_TYPES': ('Bearer',),
    'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
    'USER_ID_FIELD': 'id',
    'USER_ID_CLAIM': 'user_id',
}

登录接口签发双Token(apps/users/views.py)

python 复制代码
@api_view(['POST'])
@permission_classes([permissions.AllowAny])
@csrf_exempt
def login_view(request):
    serializer = LoginSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    user = serializer.validated_data['user']

    # JWT Token (优先使用JWT)
    refresh = RefreshToken.for_user(user)
    access_token = str(refresh.access_token)
    refresh_token = str(refresh)

    return Response({
        'user': UserSerializer(user).data,
        'access': access_token,       # JWT access token
        'refresh': refresh_token,     # JWT refresh token
        'message': '登录成功'
    })

代码说明:

双token

|------|--------------|---------------|
| | Access Token | Refresh Token |
| 有效期 | 60分钟 | 7天 |
| 携带频率 | 每次业务请求都带 | 只在刷新时用 |

防重放:

python 复制代码
'ROTATE_REFRESH_TOKENS': True,      # 刷新时签发新 refresh
'BLACKLIST_AFTER_ROTATION': True,   # 旧 refresh 立即进黑名单
  • 每次刷新都换一对新 Token,旧的 refresh 立刻作废
  • 这样即使 refresh 被窃取,攻击者用过一次后原用户下次刷新就会失败,从而暴露异常

前端:请求排队无感刷新(frontend/src/utils/api.js)

python 复制代码
// 正在刷新的标志
let isRefreshing = false
// 等待刷新的请求队列
let failedQueue = []

// 处理队列中的请求
const processQueue = (error, token = null) => {
  failedQueue.forEach(prom => {
    if (error) {
      prom.reject(error)
    } else {
      prom.resolve(token)
    }
  })
  failedQueue = []
}

// 请求拦截器
api.interceptors.request.use(
  async (config) => {
    const userStore = useUserStore()

    // 检查是否是刷新token的请求
    if (config.url === '/auth/token/refresh/') {
      return config
    }

    // 如果有access token
    if (userStore.accessToken) {
      // 检查token是否即将过期(5分钟内)
      if (userStore.isTokenExpiringSoon && !userStore.isTokenExpired) {
        // 如果没有正在刷新,开始刷新
        if (!isRefreshing) {
          isRefreshing = true
          try {
            const newToken = await userStore.refreshAccessToken()
            processQueue(null, newToken)
            // 更新当前请求的token
            config.headers.Authorization = `Bearer ${newToken}`
          } catch (error) {
            processQueue(error, null)
            return Promise.reject(error)
          } finally {
            isRefreshing = false
          }
        } else {
          // 如果正在刷新,将请求加入队列
          return new Promise((resolve, reject) => {
            failedQueue.push({ resolve, reject })
          }).then(token => {
            config.headers.Authorization = `Bearer ${token}`
            return config
          })
        }
      }

      config.headers.Authorization = `Bearer ${userStore.accessToken}`
    }

    return config
  },
  (error) => Promise.reject(error)
)

用"标志位+队列"把并发的刷新请求串行化,保证同一时刻只有一个刷新

python 复制代码
let isRefreshing = false   // 是否正在刷新
let failedQueue = []       // 等待刷新的请求队列

|--------------|------------------|
| 变量 | 作用 |
| isRefreshing | 保证同一时刻只有一个刷新 |
| failedQueue | 挂起等待中的请求,刷新完统一唤醒 |

6.AI模型统一接入层

代码如下:apps/requirement_analysis/models.py

python 复制代码
class AIModelConfig(models.Model):
    """AI模型配置"""
    ROLE_CHOICES = [
        ('writer', '测试用例编写专家'),
        ('reviewer', '测试评审专家'),
        ('browser_use_text', 'Browser Use - 文本模式'),
        ('browser_use_vision', 'Browser Use - 视觉模式'),
    ]

    name = models.CharField(max_length=100, verbose_name='配置名称')
    model_type = models.CharField(max_length=50, choices=MODEL_TYPE_CHOICES,
                                  verbose_name='模型类型')
    role = models.CharField(max_length=20, choices=ROLE_CHOICES, verbose_name='角色')
    api_key = models.CharField(max_length=200, verbose_name='API Key', blank=True, null=True)
    base_url = models.URLField(verbose_name='API Base URL')
    model_name = models.CharField(max_length=100, verbose_name='模型名称')
    max_tokens = models.IntegerField(default=4096, verbose_name='最大Token数')
    temperature = models.FloatField(default=0.7, verbose_name='温度')
    is_active = models.BooleanField(default=True, verbose_name='是否启用')

    @classmethod
    def get_active_config(cls, model_type: str, role: str):
        return cls.objects.filter(
            model_type=model_type,
            role=role,
            is_active=True,
        ).first()

base_url归一化管理

python 复制代码
base_url = config.base_url.rstrip('/')

if not base_url.endswith('/chat/completions'):
    version_match = re.search(r'/v(\d+)/?$', base_url)
    if version_match:
        url = f"{base_url}/chat/completions"
    else:
        # 但对于某些API(如DeepSeek),base_url可能已经是 https://api.deepseek.com
        url = f"{base_url}/v1/chat/completions"
else:
    url = base_url

角色路由:按用途分类

python 复制代码
ROLE_CHOICES = [
    ('writer', '测试用例编写专家'),
    ('reviewer', '测试评审专家'),
    ...
]

API Key 的脱敏处理(apps/requirement_analysis/serializers.py)

python 复制代码
class AIModelConfigSerializer(serializers.ModelSerializer):
    api_key_masked = serializers.SerializerMethodField(read_only=True)

    class Meta:
        fields = ['id', 'name', 'model_type', 'role', 'api_key', 'api_key_masked',
                  'base_url', 'model_name', ...]
        extra_kwargs = {
            'api_key': {'write_only': True}  # API Key只用于写入,不在响应中返回
        }

    def get_api_key_masked(self, obj):
        if obj.api_key:
            if len(obj.api_key) > 7:
                return f"{obj.api_key[:3]}{'*' * (len(obj.api_key) - 7)}{obj.api_key[-4:]}"
            return '*' * len(obj.api_key)
相关推荐
瞬维AI6 小时前
RPA与AI智能体的跨平台自动化执行架构:从任务编排到异常处理
人工智能·自动化·rpa
冬奇Lab12 小时前
LLM 自动化测试系列(04):Web UI 自动化——Midscene 的视觉驱动脚本化
人工智能·测试
钱栈up12 小时前
全量 21 个失败、单跑全绿:泄漏进连接池的 SQL 变量
sql·单元测试·测试
冬奇Lab12 小时前
LLM 自动化测试系列(03):单元测试生成——TestGen-LLM 与 Qodo Cover
人工智能·单元测试·测试
ClouGence12 小时前
你 Vibe Coding 完的网站,不会直接上线了吧?
ai编程·测试·vibecoding
qq_426003966 天前
启动playwright录制codegen生成自动化测试脚本
python·自动化
其实防守也摸鱼6 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透