From f22786d7465141b9ebc9e9ff279b8790cc44fc97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 15 Sep 2026 14:25:14 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8F=90=E5=8F=96=E5=85=B1?= =?UTF-8?q?=E4=BA=AB=E4=BF=A1=E5=B0=81=E6=A8=A1=E5=9D=97=20EnvelopeMixin/o?= =?UTF-8?q?k?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit auth.py 的 _EnvelopeMixin/_ok 提取到 tracker/api/envelope.py(去下划线前缀), auth.py 改为 import 使用;collect.py 的 finalize_response 含 ACAO 回显逻辑 保留在自身视图,仅注释指向未来收敛。 Co-Authored-By: Claude Code --- server/tracker/api/auth.py | 41 ++++++++-------------------------- server/tracker/api/collect.py | 3 +++ server/tracker/api/envelope.py | 25 +++++++++++++++++++++ 3 files changed, 37 insertions(+), 32 deletions(-) create mode 100644 server/tracker/api/envelope.py diff --git a/server/tracker/api/auth.py b/server/tracker/api/auth.py index 2d3786d..05def93 100644 --- a/server/tracker/api/auth.py +++ b/server/tracker/api/auth.py @@ -5,36 +5,13 @@ from rest_framework.response import Response from rest_framework.permissions import AllowAny, IsAdminUser from rest_framework.throttling import ScopedRateThrottle from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView +from tracker.api.envelope import EnvelopeMixin, ok # 对齐 Django UnicodeUsernameValidator(\w 默认 Unicode)——create_user 不跑校验器,入口自防 _USERNAME_RE = re.compile(r"[\w.@+-]+") -def _ok(data, message="ok"): - return Response({"code": 200, "data": data, "message": message}) - - -class _EnvelopeMixin: - """错误信封统一收口(Task7 同款裁剪,无 ACAO——管理台与 API 同源,跨域由 nginx 层面不存在)。 - - simplejwt 认证失败 401 / 权限 403 / 限流 429 等由 DRF 异常处理器产出的响应 - body 是 {detail},管理台前端(Task17)按 {code,data,message} 信封对接—— - 在此统一转换,视图自身的 _ok/4xx 响应不受影响(只动含 detail 的 dict)。 - """ - - def finalize_response(self, request, response, *args, **kwargs): - response = super().finalize_response(request, response, *args, **kwargs) - if response.status_code >= 400 and isinstance(response.data, dict): - if "detail" in response.data: - response.data = {"code": response.status_code, "data": None, - "message": response.data["detail"]} - elif "code" not in response.data: # simplejwt 序列化器字段错误 {"password": [...]} 等 - response.data = {"code": response.status_code, "data": response.data, - "message": "invalid_input"} - return response - - -class LoginView(_EnvelopeMixin, TokenObtainPairView): +class LoginView(EnvelopeMixin, TokenObtainPairView): """登录。JWT 取舍(M1 决策):access 12h / refresh 24h、无黑名单轮换—— token 泄露唯一应急是换 SECRET_KEY(全体登出),README-deploy 写明。""" throttle_classes = [ScopedRateThrottle] @@ -43,27 +20,27 @@ class LoginView(_EnvelopeMixin, TokenObtainPairView): def post(self, request, *args, **kwargs): resp = super().post(request, *args, **kwargs) # 失败由 simplejwt 返回 401(信封由 mixin 补) if resp.status_code == 200: - return _ok({"access": resp.data["access"], "refresh": resp.data["refresh"]}) + return ok({"access": resp.data["access"], "refresh": resp.data["refresh"]}) return resp -class RefreshView(_EnvelopeMixin, TokenRefreshView): +class RefreshView(EnvelopeMixin, TokenRefreshView): permission_classes = [AllowAny] # TokenRefreshView 自带 AllowAny,显式声明表意 def post(self, request, *args, **kwargs): resp = super().post(request, *args, **kwargs) if resp.status_code == 200: # 成功同样走信封——与 LoginView 对齐,前端只需一种解析路径 - return _ok({"access": resp.data["access"]}) + return ok({"access": resp.data["access"]}) return resp -class MeView(_EnvelopeMixin, APIView): +class MeView(EnvelopeMixin, APIView): def get(self, request): - return _ok({"username": request.user.username, + return ok({"username": request.user.username, "is_admin": request.user.is_staff}) -class UserCreateView(_EnvelopeMixin, APIView): +class UserCreateView(EnvelopeMixin, APIView): permission_classes = [IsAdminUser] # is_staff def post(self, request): @@ -80,4 +57,4 @@ class UserCreateView(_EnvelopeMixin, APIView): if User.objects.filter(username=username).exists(): return Response({"code": 409, "data": None, "message": "username_exists"}, status=409) User.objects.create_user(username=username, password=password, is_staff=False) - return _ok({"username": username}, "created") + return ok({"username": username}, "created") diff --git a/server/tracker/api/collect.py b/server/tracker/api/collect.py index aa55bd9..51099e3 100644 --- a/server/tracker/api/collect.py +++ b/server/tracker/api/collect.py @@ -79,6 +79,9 @@ class CollectBatchView(APIView): def finalize_response(self, request, response, *args, **kwargs): """统一收口:ACAO 回显 + 错误信封。 + 错误信封部分与 tracker.api.envelope.EnvelopeMixin 同构——collect 端含 ACAO 回显 + (浏览器 SDK 跨域需读错误体),暂保留在自身视图,未来收敛至共享模块。 + 429/ParseError 等由 DRF 异常处理器产出的响应(无 ACAO、body 是 {detail})在这里补齐—— 否则浏览器 SDK 跨域读不到错误体会误判网络错误而重试,限流时反而打爆限流器。 dispatch 对 initial() 抛出的 Throttled/ParseError 同样走 finalize_response,此钩子全覆盖。 diff --git a/server/tracker/api/envelope.py b/server/tracker/api/envelope.py new file mode 100644 index 0000000..569058c --- /dev/null +++ b/server/tracker/api/envelope.py @@ -0,0 +1,25 @@ +from rest_framework.response import Response + + +def ok(data, message="ok"): + return Response({"code": 200, "data": data, "message": message}) + + +class EnvelopeMixin: + """错误信封统一收口(Task7 同款裁剪,无 ACAO——管理台与 API 同源,跨域由 nginx 层面不存在)。 + + simplejwt 认证失败 401 / 权限 403 / 限流 429 等由 DRF 异常处理器产出的响应 + body 是 {detail},管理台前端(Task17)按 {code,data,message} 信封对接—— + 在此统一转换,视图自身的 ok/4xx 响应不受影响(只动含 detail 的 dict)。 + """ + + def finalize_response(self, request, response, *args, **kwargs): + response = super().finalize_response(request, response, *args, **kwargs) + if response.status_code >= 400 and isinstance(response.data, dict): + if "detail" in response.data: + response.data = {"code": response.status_code, "data": None, + "message": response.data["detail"]} + elif "code" not in response.data: # simplejwt 序列化器字段错误 {"password": [...]} 等 + response.data = {"code": response.status_code, "data": response.data, + "message": "invalid_input"} + return response