From f4a92699f00debeae7968e5bb54d33c8fba8d208 Mon Sep 17 00:00:00 2001 From: bruce Date: Wed, 9 Sep 2026 11:54:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(common):=20=E5=BB=BA=E7=AB=8B=E5=85=AC?= =?UTF-8?q?=E5=85=B1=E5=BD=92=E5=B1=9E=E4=B8=8E=E6=97=A5=E5=BF=97=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/__init__.py | 1 + common/apps.py | 10 ++++++++++ common/exceptions.py | 17 +++++++++++++++++ common/logging.py | 33 +++++++++++++++++++++++++++++++++ common/middleware.py | 27 +++++++++++++++++++++++++++ common/models.py | 28 ++++++++++++++++++++++++++++ 6 files changed, 116 insertions(+) create mode 100644 common/__init__.py create mode 100644 common/apps.py create mode 100644 common/exceptions.py create mode 100644 common/logging.py create mode 100644 common/middleware.py create mode 100644 common/models.py diff --git a/common/__init__.py b/common/__init__.py new file mode 100644 index 0000000..1e6663f --- /dev/null +++ b/common/__init__.py @@ -0,0 +1 @@ +"""JobRadar 公共基础能力。""" diff --git a/common/apps.py b/common/apps.py new file mode 100644 index 0000000..0a0eeb3 --- /dev/null +++ b/common/apps.py @@ -0,0 +1,10 @@ +"""公共应用配置。""" + +from django.apps import AppConfig + + +class CommonConfig(AppConfig): + """注册不拥有独立业务数据的公共能力。""" + + default_auto_field = "django.db.models.BigAutoField" + name = "common" diff --git a/common/exceptions.py b/common/exceptions.py new file mode 100644 index 0000000..fa52b1e --- /dev/null +++ b/common/exceptions.py @@ -0,0 +1,17 @@ +"""可由入口层稳定映射的领域异常。""" + + +class DomainError(Exception): + """所有可预期业务错误的基类。""" + + +class InvalidStateTransition(DomainError): + """运行状态不允许执行目标转换。""" + + +class DuplicateOperation(DomainError): + """同一幂等操作已经执行或正在执行。""" + + +class PermissionDenied(DomainError): + """操作者无权执行领域命令。""" diff --git a/common/logging.py b/common/logging.py new file mode 100644 index 0000000..9a6f701 --- /dev/null +++ b/common/logging.py @@ -0,0 +1,33 @@ +"""请求日志上下文和敏感摘要清理。""" + +import contextvars +import logging +from collections.abc import Mapping +from typing import Any + +request_id_context: contextvars.ContextVar[str] = contextvars.ContextVar( + "request_id", default="-" +) + +SENSITIVE_KEYS = {"api_key", "authorization", "cookie", "password", "secret", "token"} + + +class RequestContextFilter(logging.Filter): + """向每条日志补充请求关联标识,后台任务没有请求时使用短横线。""" + + def filter(self, record: logging.LogRecord) -> bool: + record.request_id = request_id_context.get() + return True + + +def sanitize_summary(value: Any) -> Any: + """递归遮蔽常见敏感键;调用方仍需限制摘要的业务范围和长度。""" + + if isinstance(value, Mapping): + return { + str(key): "***" if str(key).lower() in SENSITIVE_KEYS else sanitize_summary(item) + for key, item in value.items() + } + if isinstance(value, list): + return [sanitize_summary(item) for item in value] + return value diff --git a/common/middleware.py b/common/middleware.py new file mode 100644 index 0000000..db00658 --- /dev/null +++ b/common/middleware.py @@ -0,0 +1,27 @@ +"""请求级关联标识中间件。""" + +import re +import uuid + +from .logging import request_id_context + +REQUEST_ID_PATTERN = re.compile(r"^[A-Za-z0-9._-]{1,64}$") + + +class RequestContextMiddleware: + """复用合法入站标识或生成新标识,并确保上下文在请求结束后复位。""" + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + incoming = request.headers.get("X-Request-ID", "") + request_id = incoming if REQUEST_ID_PATTERN.fullmatch(incoming) else uuid.uuid4().hex + token = request_id_context.set(request_id) + request.request_id = request_id + try: + response = self.get_response(request) + response["X-Request-ID"] = request_id + return response + finally: + request_id_context.reset(token) diff --git a/common/models.py b/common/models.py new file mode 100644 index 0000000..cc5fac4 --- /dev/null +++ b/common/models.py @@ -0,0 +1,28 @@ +"""跨业务应用复用的抽象模型。""" + +from django.conf import settings +from django.db import models + + +class TimeStampedModel(models.Model): + """统一维护创建和更新时间,避免各业务模型重复声明。""" + + created_at = models.DateTimeField("创建时间", auto_now_add=True) + updated_at = models.DateTimeField("更新时间", auto_now=True) + + class Meta: + abstract = True + + +class UserOwnedModel(TimeStampedModel): + """为用户私有数据提供显式归属;审计数据存在时禁止删除用户。""" + + owner = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.PROTECT, + related_name="%(app_label)s_%(class)s_items", + verbose_name="所属用户", + ) + + class Meta: + abstract = True