PayrollManagementSystem/backend/app/core/logging.py
liuwenxin fbf044f240 fix(system): code-review 15 项修复(预览基数/维度校验/脱敏穿透/并发版本号等)
- 信息员预览基数改用本人销售额(原误用折算毛利润额,金额算错且口径自相矛盾)
- 产品类别入参与维度值加存在性校验(拼错静默回退默认阈值 30% 会算错轨交工资)
- submit 校验顺序前置查库(未注册 param_code 由 500 修正为 400)
- 金额拒绝 NaN/Infinity(比较抛 InvalidOperation、计算抛溢出均穿透为 500)
- version_no 并发冲突翻译为 400(对齐 user/role service 的 IntegrityError 处理)
- 旧值留痕改取条目生效日时点的值(多未来批次下留痕链不再错位)
- 脱敏红线:phone/mobile/id_card 键名整值抹除 + 手机号正则兼容分隔符写法
- 前端:撤回按钮与导出文件名改本地日期(UTC/本地混算)、维度行内精确「改」入口、
  detailOf 补 catch、抽屉分页 size 生效、blob 错误读出真实 message
- e2e 密码移除硬编码 fallback(缺失即报错退出);按钮文案同步「改」
- 全量回归:后端 406 + 集成 104 + 前端 type-check/build + e2e 7 步 全绿

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-13 02:28:20 +08:00

95 lines
3.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""日志配置。
规范要求(PROJECT_STANDARDS 第 8 节):生产 INFO 级别、按天+大小切分、
保留 30 天、禁止输出密码/手机号/身份证/Token。
Windows 默认编码为 cp936,日志文件 handler 必须显式指定 utf-8,
否则中文日志会变成乱码。
"""
import logging
import re
import sys
from logging.handlers import TimedRotatingFileHandler
from app.core.config import BACKEND_DIR
LOG_DIR = BACKEND_DIR / "logs"
# 文本脱敏规则。日志是最容易泄漏敏感信息的地方,且多半来自异常堆栈里的
# 请求体字符串,无法靠"调用方注意"来保证,所以在这里统一兜底。
_SENSITIVE_RULES: tuple[tuple[re.Pattern[str], str], ...] = (
# 含 password / passwd / pwd / secret / token 的键名,或 app_key / access_key 之类的短名
(
re.compile(
r'((?:\w*(?:password|passwd|pwd|secret|token)\w*|app_?key|access_?key)["\']?\s*[:=]\s*["\']?)([^"\'\s,}&]+)',
re.IGNORECASE,
),
r"\1***",
),
# Authorization: Bearer xxx
(re.compile(r"(Bearer\s+)[A-Za-z0-9\-._~+/]+=*"), r"\1***"),
# 手机号:138****8888(兼容带空格/横线分隔的写法,如 138 0013 8000)
(re.compile(r"\b(1[3-9]\d)[-\s]?(\d{4})[-\s]?(\d{4})\b"), r"\1****\3"),
# 18 位身份证:保留前 6 位与后 4 位
(re.compile(r"\b(\d{6})\d{8}(\d{3}[\dXx])\b"), r"\1********\2"),
)
def scrub(text: str) -> str:
"""对文本按规则脱敏。导出供单测直接验证。"""
for pattern, replacement in _SENSITIVE_RULES:
text = pattern.sub(replacement, text)
return text
class SensitiveFormatter(logging.Formatter):
"""脱敏格式化器。
脱敏必须在父类完成 %-格式化**之后**进行,而不是在 filter 阶段改写 ``record.msg``。
后者会把 ``"登录失败 password=%s"`` 里的占位符一并抹成 ``password=***``,
随后 logging 内部执行 ``msg % args`` 时因参数无处安放抛 TypeError,
导致整条日志丢失 —— 这比不脱敏更危险。
"""
def format(self, record: logging.LogRecord) -> str:
return scrub(super().format(record))
def setup_logging(level: str = "INFO", log_to_file: bool = True) -> None:
"""初始化根日志配置。
重复调用是安全的:先清空已有 handler,避免 uvicorn --reload
或测试中重复挂载导致日志重复输出。
:param level: 日志级别
:param log_to_file: 是否写文件;测试环境可关闭以避免产生日志文件
"""
root = logging.getLogger()
root.setLevel(level)
for handler in list(root.handlers):
root.removeHandler(handler)
formatter = SensitiveFormatter(
fmt="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
# 控制台:Windows 终端默认 cp936,中文日志可能报编码错误,显式重配为 utf-8
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8") # type: ignore[union-attr]
console = logging.StreamHandler(sys.stdout)
console.setFormatter(formatter)
root.addHandler(console)
if log_to_file:
LOG_DIR.mkdir(parents=True, exist_ok=True)
file_handler = TimedRotatingFileHandler(
filename=LOG_DIR / "app.log",
when="midnight",
backupCount=30, # 规范要求保留 30 天
encoding="utf-8",
)
file_handler.setFormatter(formatter)
root.addHandler(file_handler)