This commit is contained in:
27942
2026-02-26 01:27:35 +08:00
parent 5e4e987313
commit 83c8f72cc7
11 changed files with 102 additions and 79 deletions

View File

@@ -10,8 +10,8 @@ BOSS 账号 API需要登录
"""
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from server.core.response import api_success, api_error
from server.models import BossAccount
from server.serializers import BossAccountSerializer, AccountBindSerializer
from server.core.worker_manager import worker_manager
@@ -41,7 +41,7 @@ def account_list(request):
qs = BossAccount.objects.all().order_by("-updated_at")
if worker_id:
qs = qs.filter(worker_id=worker_id)
return Response([_enrich(a) for a in qs])
return api_success([_enrich(a) for a in qs])
# POST: 添加账号
ser = AccountBindSerializer(data=request.data)
@@ -58,7 +58,7 @@ def account_list(request):
"is_logged_in": False,
},
)
return Response(_enrich(account), status=status.HTTP_201_CREATED)
return api_success(_enrich(account), http_status=status.HTTP_201_CREATED)
@api_view(["GET", "DELETE"])
@@ -70,11 +70,11 @@ def account_detail(request, account_id):
try:
account = BossAccount.objects.get(pk=account_id)
except BossAccount.DoesNotExist:
return Response({"detail": "账号不存在"}, status=status.HTTP_404_NOT_FOUND)
return api_error(status.HTTP_404_NOT_FOUND, "账号不存在")
if request.method == "GET":
return Response(_enrich(account))
return api_success(_enrich(account))
# DELETE
account.delete()
return Response({"message": "账号已删除"})
return api_success(msg="账号已删除")

View File

@@ -7,9 +7,9 @@ import uuid
from rest_framework import status
from rest_framework.decorators import api_view, authentication_classes, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from server import config
from server.core.response import api_success, api_error
from server.models import AuthToken
from server.serializers import LoginSerializer
@@ -32,7 +32,7 @@ def login(request):
password = ser.validated_data["password"]
if username != config.ADMIN_USERNAME or password != config.ADMIN_PASSWORD:
return Response({"detail": "用户名或密码错误"}, status=status.HTTP_401_UNAUTHORIZED)
return api_error(status.HTTP_401_UNAUTHORIZED, "用户名或密码错误")
token = uuid.uuid4().hex
AuthToken.objects.update_or_create(
@@ -40,7 +40,7 @@ def login(request):
defaults={"token": token},
)
return Response({"token": token})
return api_success({"token": token})
@api_view(["GET"])
@@ -55,18 +55,16 @@ def get_info(request):
token = auth_header.replace("Bearer ", "").strip() if auth_header else ""
if not token:
return Response({"detail": "未提供认证令牌"}, status=status.HTTP_401_UNAUTHORIZED)
return api_error(status.HTTP_401_UNAUTHORIZED, "未提供认证令牌")
try:
record = AuthToken.objects.get(token=token)
except AuthToken.DoesNotExist:
return Response({"detail": "令牌无效或已过期"}, status=status.HTTP_401_UNAUTHORIZED)
return api_error(status.HTTP_401_UNAUTHORIZED, "令牌无效或已过期")
return Response({
"data": {
"account_id": record.id,
"username": record.username,
"token": record.token,
"created_at": record.created_at,
},
return api_success({
"account_id": record.id,
"username": record.username,
"token": record.token,
"created_at": record.created_at,
})

View File

@@ -12,8 +12,8 @@ from django.db.models import Q
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from server.core.response import api_success, api_error
from server.models import ContactRecord
from server.serializers import ContactRecordSerializer
@@ -44,7 +44,7 @@ def contact_list(request):
end = start + page_size
records = qs[start:end]
return Response({
return api_success({
"total": total,
"page": page,
"page_size": page_size,
@@ -54,7 +54,7 @@ def contact_list(request):
ser = ContactRecordSerializer(data=request.data)
ser.is_valid(raise_exception=True)
ser.save()
return Response(ser.data, status=status.HTTP_201_CREATED)
return api_success(ser.data, http_status=status.HTTP_201_CREATED)
@api_view(["GET"])
@@ -65,10 +65,7 @@ def contact_query(request):
"""
contact = (request.query_params.get("contact") or "").strip()
if not contact:
return Response(
{"detail": "请提供 contact 参数(电话号码或微信号)"},
status=status.HTTP_400_BAD_REQUEST,
)
return api_error(status.HTTP_400_BAD_REQUEST, "请提供 contact 参数(电话号码或微信号)")
page = int(request.query_params.get("page", 1))
page_size = int(request.query_params.get("page_size", 20))
@@ -78,7 +75,7 @@ def contact_query(request):
end = start + page_size
records = qs[start:end]
return Response({
return api_success({
"total": total,
"page": page,
"page_size": page_size,
@@ -92,16 +89,16 @@ def contact_detail(request, pk):
try:
obj = ContactRecord.objects.get(pk=pk)
except ContactRecord.DoesNotExist:
return Response({"detail": "联系记录不存在"}, status=status.HTTP_404_NOT_FOUND)
return api_error(status.HTTP_404_NOT_FOUND, "联系记录不存在")
if request.method == "GET":
return Response(ContactRecordSerializer(obj).data)
return api_success(ContactRecordSerializer(obj).data)
if request.method == "PUT":
ser = ContactRecordSerializer(obj, data=request.data, partial=True)
ser.is_valid(raise_exception=True)
ser.save()
return Response(ser.data)
return api_success(ser.data)
obj.delete()
return Response({"message": "联系记录已删除"})
return api_success(msg="联系记录已删除")

View File

@@ -9,8 +9,8 @@
"""
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from server.core.response import api_success, api_error
from server.models import FilterConfig
from server.serializers import FilterConfigSerializer
@@ -19,12 +19,12 @@ from server.serializers import FilterConfigSerializer
def filter_list(request):
if request.method == "GET":
qs = FilterConfig.objects.all().order_by("-updated_at")
return Response(FilterConfigSerializer(qs, many=True).data)
return api_success(FilterConfigSerializer(qs, many=True).data)
ser = FilterConfigSerializer(data=request.data)
ser.is_valid(raise_exception=True)
ser.save()
return Response(ser.data, status=status.HTTP_201_CREATED)
return api_success(ser.data, http_status=status.HTTP_201_CREATED)
@api_view(["GET", "PUT", "DELETE"])
@@ -32,16 +32,16 @@ def filter_detail(request, pk):
try:
obj = FilterConfig.objects.get(pk=pk)
except FilterConfig.DoesNotExist:
return Response({"detail": "筛选配置不存在"}, status=status.HTTP_404_NOT_FOUND)
return api_error(status.HTTP_404_NOT_FOUND, "筛选配置不存在")
if request.method == "GET":
return Response(FilterConfigSerializer(obj).data)
return api_success(FilterConfigSerializer(obj).data)
if request.method == "PUT":
ser = FilterConfigSerializer(obj, data=request.data, partial=True)
ser.is_valid(raise_exception=True)
ser.save()
return Response(ser.data)
return api_success(ser.data)
obj.delete()
return Response({"message": "筛选配置已删除"})
return api_success(msg="筛选配置已删除")

View File

@@ -9,8 +9,8 @@
"""
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from server.core.response import api_success, api_error
from server.models import ChatScript
from server.serializers import ChatScriptSerializer
@@ -25,12 +25,12 @@ def script_list(request):
qs = qs.filter(position=position)
if script_type:
qs = qs.filter(script_type=script_type)
return Response(ChatScriptSerializer(qs, many=True).data)
return api_success(ChatScriptSerializer(qs, many=True).data)
ser = ChatScriptSerializer(data=request.data)
ser.is_valid(raise_exception=True)
ser.save()
return Response(ser.data, status=status.HTTP_201_CREATED)
return api_success(ser.data, http_status=status.HTTP_201_CREATED)
@api_view(["GET", "PUT", "DELETE"])
@@ -38,16 +38,16 @@ def script_detail(request, pk):
try:
obj = ChatScript.objects.get(pk=pk)
except ChatScript.DoesNotExist:
return Response({"detail": "话术不存在"}, status=status.HTTP_404_NOT_FOUND)
return api_error(status.HTTP_404_NOT_FOUND, "话术不存在")
if request.method == "GET":
return Response(ChatScriptSerializer(obj).data)
return api_success(ChatScriptSerializer(obj).data)
if request.method == "PUT":
ser = ChatScriptSerializer(obj, data=request.data, partial=True)
ser.is_valid(raise_exception=True)
ser.save()
return Response(ser.data)
return api_success(ser.data)
obj.delete()
return Response({"message": "话术已删除"})
return api_success(msg="话术已删除")

View File

@@ -7,8 +7,8 @@
"""
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from server.core.response import api_success, api_error
from server.models import SystemConfig
from server.serializers import SystemConfigSerializer
@@ -17,11 +17,11 @@ from server.serializers import SystemConfigSerializer
def settings_list(request):
if request.method == "GET":
qs = SystemConfig.objects.all().order_by("key")
return Response(SystemConfigSerializer(qs, many=True).data)
return api_success(SystemConfigSerializer(qs, many=True).data)
# POST: 批量更新,请求体格式 {"key1": "value1", "key2": "value2", ...}
if not isinstance(request.data, dict):
return Response({"detail": "请传入 key-value 对象"}, status=status.HTTP_400_BAD_REQUEST)
return api_error(status.HTTP_400_BAD_REQUEST, "请传入 key-value 对象")
results = []
for key, value in request.data.items():
@@ -30,7 +30,7 @@ def settings_list(request):
defaults={"value": str(value)},
)
results.append(SystemConfigSerializer(obj).data)
return Response(results)
return api_success(results)
@api_view(["GET"])
@@ -38,5 +38,5 @@ def settings_detail(request, key):
try:
obj = SystemConfig.objects.get(key=key)
except SystemConfig.DoesNotExist:
return Response({"detail": f"配置项 '{key}' 不存在"}, status=status.HTTP_404_NOT_FOUND)
return Response(SystemConfigSerializer(obj).data)
return api_error(status.HTTP_404_NOT_FOUND, f"配置项 '{key}' 不存在")
return api_success(SystemConfigSerializer(obj).data)

View File

@@ -9,8 +9,8 @@ from datetime import timedelta
from django.db.models import Count, Q
from django.utils import timezone
from rest_framework.decorators import api_view
from rest_framework.response import Response
from server.core.response import api_success
from server.models import ContactRecord, BossAccount, TaskLog
from server.core.worker_manager import worker_manager
@@ -55,7 +55,7 @@ def stats_overview(request):
reply_rate = round(total_replied / max(total_contacts, 1) * 100, 1)
wechat_rate = round(total_wechat / max(total_replied, 1) * 100, 1)
return Response({
return api_success({
"period": period,
"contacts": {
"total": total_contacts,
@@ -105,4 +105,4 @@ def stats_daily(request):
"reply_rate": round(replied / max(total, 1) * 100, 1),
})
return Response(daily_data)
return api_success(daily_data)

View File

@@ -9,9 +9,9 @@ import logging
from asgiref.sync import async_to_sync
from rest_framework import status as http_status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from common.protocol import TaskStatus, TaskType
from server.core.response import api_success, api_error
from server.models import BossAccount, TaskCreate
from server.serializers import TaskCreateSerializer, TaskOutSerializer
from server.core.worker_manager import worker_manager
@@ -49,7 +49,7 @@ def task_list(request):
limit = int(request.query_params.get("limit", 50))
task_status = TaskStatus(st) if st else None
tasks = task_dispatcher.list_tasks(worker_id=wid, status=task_status, limit=limit)
return Response([_task_to_dict(t) for t in tasks])
return api_success([_task_to_dict(t) for t in tasks])
# POST: 提交新任务
data = request.data.copy()
@@ -70,16 +70,16 @@ def task_list(request):
if not target_worker_id and req.account_name:
target_worker_id = worker_manager.find_worker_by_account(req.account_name)
if not target_worker_id:
return Response(
{"detail": f"未找到拥有浏览器 '{req.account_name}' 的在线 Worker"},
status=http_status.HTTP_404_NOT_FOUND,
return api_error(
http_status.HTTP_404_NOT_FOUND,
f"未找到拥有浏览器 '{req.account_name}' 的在线 Worker",
)
if not target_worker_id:
return Response({"detail": "请指定 worker_id 或 account_name"}, status=http_status.HTTP_400_BAD_REQUEST)
return api_error(http_status.HTTP_400_BAD_REQUEST, "请指定 worker_id 或 account_name")
if not worker_manager.is_online(target_worker_id):
return Response({"detail": f"Worker {target_worker_id} 不在线"}, status=http_status.HTTP_503_SERVICE_UNAVAILABLE)
return api_error(http_status.HTTP_503_SERVICE_UNAVAILABLE, f"Worker {target_worker_id} 不在线")
req.worker_id = target_worker_id
task = task_dispatcher.create_task(req)
@@ -88,11 +88,11 @@ def task_list(request):
if not send_fn:
task.status = TaskStatus.FAILED
task.error = "Worker WebSocket 连接不存在"
return Response({"detail": "Worker WebSocket 连接不存在"}, status=http_status.HTTP_503_SERVICE_UNAVAILABLE)
return api_error(http_status.HTTP_503_SERVICE_UNAVAILABLE, "Worker WebSocket 连接不存在")
success = async_to_sync(task_dispatcher.dispatch)(task, send_fn)
if not success:
return Response({"detail": f"任务派发失败: {task.error}"}, status=http_status.HTTP_503_SERVICE_UNAVAILABLE)
return api_error(http_status.HTTP_503_SERVICE_UNAVAILABLE, f"任务派发失败: {task.error}")
worker_manager.set_current_task(target_worker_id, task.task_id)
@@ -109,7 +109,7 @@ def task_list(request):
except Exception as e:
logger.warning("关联账号任务状态失败: %s", e)
return Response(_task_to_dict(task), status=http_status.HTTP_201_CREATED)
return api_success(_task_to_dict(task), http_status=http_status.HTTP_201_CREATED)
@api_view(["GET"])
@@ -117,5 +117,5 @@ def task_detail(request, task_id):
"""查询指定任务的状态和结果。"""
task = task_dispatcher.get_task(task_id)
if not task:
return Response({"detail": f"任务 {task_id} 不存在"}, status=http_status.HTTP_404_NOT_FOUND)
return Response(_task_to_dict(task))
return api_error(http_status.HTTP_404_NOT_FOUND, f"任务 {task_id} 不存在")
return api_success(_task_to_dict(task))

View File

@@ -5,9 +5,8 @@ Worker 查询 API需要登录
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from server.serializers import WorkerOutSerializer
from server.core.response import api_success, api_error
from server.core.worker_manager import worker_manager
@@ -16,7 +15,7 @@ from server.core.worker_manager import worker_manager
def health_check(request):
"""健康检查。"""
online = len([w for w in worker_manager.get_all_workers() if w.online])
return Response({"status": "ok", "workers_online": online})
return api_success({"status": "ok", "workers_online": online})
def _worker_to_dict(w) -> dict:
@@ -33,7 +32,7 @@ def _worker_to_dict(w) -> dict:
def worker_list(request):
"""获取所有已注册的 Worker含在线状态与浏览器列表"""
workers = worker_manager.get_all_workers()
return Response([_worker_to_dict(w) for w in workers])
return api_success([_worker_to_dict(w) for w in workers])
@api_view(["GET"])
@@ -41,5 +40,5 @@ def worker_detail(request, worker_id):
"""获取指定 Worker 的详情。"""
w = worker_manager.get_worker(worker_id)
if not w:
return Response({"detail": f"Worker {worker_id} 不存在"}, status=status.HTTP_404_NOT_FOUND)
return Response(_worker_to_dict(w))
return api_error(status.HTTP_404_NOT_FOUND, f"Worker {worker_id} 不存在")
return api_success(_worker_to_dict(w))

View File

@@ -1,22 +1,22 @@
# -*- coding: utf-8 -*-
"""
DRF 自定义异常处理器:统一错误响应格式,并确保返回正确 HTTP 状态码
DRF 自定义异常处理器:统一错误响应格式为 code、data、msg
"""
from rest_framework.views import exception_handler
from rest_framework.response import Response
from rest_framework.exceptions import APIException
def custom_exception_handler(exc, context):
response = exception_handler(exc, context)
if response is not None:
# 确保使用异常自带的 HTTP 状态码(如 401 登录失效、403 无权限)
if isinstance(exc, APIException) and getattr(exc, "status_code", None):
response.status_code = exc.status_code
# 统一格式:{"detail": "...", "code": 状态码}
code = response.status_code
if isinstance(response.data, list):
response.data = {"detail": response.data[0] if response.data else str(exc)}
elif isinstance(response.data, dict) and "detail" not in response.data:
response.data = {"detail": str(response.data)}
response.data["code"] = response.status_code
msg = response.data[0] if response.data else str(exc)
elif isinstance(response.data, dict) and "detail" in response.data:
msg = response.data["detail"]
else:
msg = str(response.data) if response.data else str(exc)
response.data = {"code": code, "data": None, "msg": msg}
return response

29
server/core/response.py Normal file
View File

@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
"""
统一 API 响应格式code、data、msg。
"""
from rest_framework.response import Response
from rest_framework import status
def api_response(code: int = 0, data=None, msg: str = "success", http_status: int = None):
"""
统一接口响应格式。
- code: 0 表示成功,非 0 表示错误(通常与 HTTP 状态码对应)
- data: 业务数据
- msg: 提示信息
"""
resp = {"code": code, "data": data, "msg": msg}
if http_status is None:
http_status = status.HTTP_200_OK if code == 0 else min(code, 599)
return Response(resp, status=http_status)
def api_success(data=None, msg: str = "success", http_status: int = status.HTTP_200_OK):
"""成功响应。"""
return api_response(code=0, data=data, msg=msg, http_status=http_status)
def api_error(code: int, msg: str, data=None):
"""错误响应。"""
return api_response(code=code, data=data, msg=msg, http_status=code)