feat(common): 建立公共归属与日志基础能力
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
"""JobRadar 公共基础能力。"""
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
"""公共应用配置。"""
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CommonConfig(AppConfig):
|
||||||
|
"""注册不拥有独立业务数据的公共能力。"""
|
||||||
|
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "common"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
"""可由入口层稳定映射的领域异常。"""
|
||||||
|
|
||||||
|
|
||||||
|
class DomainError(Exception):
|
||||||
|
"""所有可预期业务错误的基类。"""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidStateTransition(DomainError):
|
||||||
|
"""运行状态不允许执行目标转换。"""
|
||||||
|
|
||||||
|
|
||||||
|
class DuplicateOperation(DomainError):
|
||||||
|
"""同一幂等操作已经执行或正在执行。"""
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionDenied(DomainError):
|
||||||
|
"""操作者无权执行领域命令。"""
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user