Files
gaokao-volunteer-system/admin/errors/registry.py

250 lines
8.7 KiB
Python
Raw Normal View History

2026-06-13 14:49:58 +08:00
"""i18n 消息注册表 (T9.2 — 用户友好提示).
每个错误码对应:
- message : 给用户看的中文短句 ( 30 , 一句话讲清楚发生了什么)
- suggestion : 给用户的可执行建议 ( 50 , 具体到下一步动作)
- severity : 'info' / 'warn' / 'error' (驱动前端图标与重试策略, T9.3 接入)
- retryable : True 表示前端/SDK 可安全重试 (T9.4 装饰器用)
未来扩展:
- 引入 en-US 资源包时, MESSAGES 改为按 locale 索引的 dict:
MESSAGES = {"zh-CN": {...}, "en-US": {...}}
- CI 校验脚本应扫描 codes.py 的常量是否都已注册, 防止散落字符串
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Dict, Literal, Mapping
from admin.errors.codes import (
AUTH_ACCOUNT_DISABLED,
AUTH_INSUFFICIENT_PERMISSION,
AUTH_INVALID_CREDENTIALS,
AUTH_TOKEN_EXPIRED,
AUTH_TOKEN_INVALID,
BIZ_ORDER_INVALID_STATUS,
BIZ_ORDER_NOT_FOUND,
BIZ_ORDER_RETENTION_NOT_EXPIRED,
2026-06-13 14:49:58 +08:00
BIZ_RATE_LIMITED,
DATA_NOT_FOUND,
DATA_PERSIST_FAILED,
DATA_VALIDATION_FAILED,
FALLBACK_CODE,
SYS_CONFIG_MISSING,
SYS_INTERNAL_ERROR,
SYS_RESOURCE_EXHAUSTED,
THIRD_PARTY_TIMEOUT,
THIRD_PARTY_UPSTREAM_ERROR,
)
Severity = Literal["info", "warn", "error"]
@dataclass(frozen=True)
class Message:
"""用户可见的错误消息 (i18n 资源包的最小单元)."""
code: str
message: str
suggestion: str
severity: Severity
retryable: bool
def to_dict(self) -> dict:
return {
"code": self.code,
"message": self.message,
"suggestion": self.suggestion,
"severity": self.severity,
"retryable": self.retryable,
}
class MessageNotFoundError(KeyError):
"""注册表中找不到对应码点 (本应被兜底拦截,仅测试用)."""
# ---------------- zh-CN 资源包 ----------------
#
# 编写准则:
# - message: 陈述句, 不带感叹号; 第二人称避免 ('您'/'请' 可用)
# - suggestion: 祈使句开头, 给出 1-2 步可执行动作; 不承诺能解决
# - 不暴露内部实现细节 (DB 名/字段名/IP), 但允许给文件路径/链接
# - 同一段号内的码点, 文案语气保持一致
MESSAGES_ZH_CN: Mapping[str, Message] = {
# 01 段 — 用户域
str(AUTH_INVALID_CREDENTIALS): Message(
code=str(AUTH_INVALID_CREDENTIALS),
message="用户名或密码不正确",
suggestion="请检查大小写和输入法,确认无误后重新登录。忘记密码请联系管理员重置。",
severity="warn",
retryable=False,
),
str(AUTH_TOKEN_EXPIRED): Message(
code=str(AUTH_TOKEN_EXPIRED),
message="登录状态已过期",
suggestion="请重新登录后再继续操作。系统默认会话有效期 5 分钟。",
severity="warn",
retryable=False,
),
str(AUTH_TOKEN_INVALID): Message(
code=str(AUTH_TOKEN_INVALID),
message="登录凭证无效",
suggestion="请重新登录。如反复出现,请清除浏览器 Cookie 后重试。",
severity="warn",
retryable=False,
),
str(AUTH_INSUFFICIENT_PERMISSION): Message(
code=str(AUTH_INSUFFICIENT_PERMISSION),
message="当前账号无访问权限",
suggestion="请使用具备相应权限的账号登录,或联系管理员开通权限。",
severity="warn",
retryable=False,
),
str(AUTH_ACCOUNT_DISABLED): Message(
code=str(AUTH_ACCOUNT_DISABLED),
message="账号已被停用",
suggestion="请联系管理员确认账号状态,启用后再登录。",
severity="warn",
retryable=False,
),
# 02 段 — 业务域
str(BIZ_ORDER_NOT_FOUND): Message(
code=str(BIZ_ORDER_NOT_FOUND),
message="未找到该订单",
suggestion="请检查订单号是否正确,或在订单列表中通过筛选条件重新查询。",
severity="warn",
retryable=False,
),
str(BIZ_ORDER_RETENTION_NOT_EXPIRED): Message(
code=str(BIZ_ORDER_RETENTION_NOT_EXPIRED),
message="订单仍在数据保留期内",
suggestion="请等待保留期(服务完成后 180 天)结束,或走支付争议/法务豁免审批流程后再处理。",
severity="warn",
retryable=False,
),
2026-06-13 14:49:58 +08:00
str(BIZ_ORDER_INVALID_STATUS): Message(
code=str(BIZ_ORDER_INVALID_STATUS),
message="订单当前状态不支持该操作",
suggestion="请刷新订单详情查看最新状态,或前往订单列表查看状态流转。",
severity="warn",
retryable=False,
),
str(BIZ_RATE_LIMITED): Message(
code=str(BIZ_RATE_LIMITED),
message="请求过于频繁",
suggestion="请稍候 30 秒后再试。如持续触发,可联系管理员调整配额。",
severity="warn",
retryable=True,
),
# 03 段 — 数据域
str(DATA_VALIDATION_FAILED): Message(
code=str(DATA_VALIDATION_FAILED),
message="请求数据未通过校验",
suggestion="请检查必填字段和格式。鼠标悬停字段标签可查看填写要求。",
severity="warn",
retryable=False,
),
str(DATA_NOT_FOUND): Message(
code=str(DATA_NOT_FOUND),
message="未找到对应的数据",
suggestion="该记录可能已被删除或尚未创建,请刷新页面或返回列表确认。",
severity="warn",
retryable=False,
),
str(DATA_PERSIST_FAILED): Message(
code=str(DATA_PERSIST_FAILED),
message="数据保存失败",
suggestion="请稍后重试。如反复失败,请联系技术支持并保留错误码。",
severity="error",
retryable=True,
),
# 04 段 — 第三方域
str(THIRD_PARTY_UPSTREAM_ERROR): Message(
code=str(THIRD_PARTY_UPSTREAM_ERROR),
message="外部服务暂时不可用",
suggestion="我们正在同步上游状态,请稍后重试;如紧急可联系客服。",
severity="error",
retryable=True,
),
str(THIRD_PARTY_TIMEOUT): Message(
code=str(THIRD_PARTY_TIMEOUT),
message="外部服务响应超时",
suggestion="请稍后重试,或在网络稳定的环境下再次尝试。",
severity="error",
retryable=True,
),
# 05 段 — 系统域
str(SYS_INTERNAL_ERROR): Message(
code=str(SYS_INTERNAL_ERROR),
message="系统内部异常",
suggestion="请稍后重试。如反复出现,请联系技术支持并提供错误码。",
severity="error",
retryable=True,
),
str(SYS_CONFIG_MISSING): Message(
code=str(SYS_CONFIG_MISSING),
message="系统配置缺失",
suggestion="请联系运维人员检查服务配置,确认环境变量已正确注入。",
severity="error",
retryable=False,
),
str(SYS_RESOURCE_EXHAUSTED): Message(
code=str(SYS_RESOURCE_EXHAUSTED),
message="系统资源不足",
suggestion="当前访问量过高,请稍后再试;如紧急可联系运维扩容。",
severity="error",
retryable=True,
),
}
# 兜底文案 (任何未注册码点都返回这一份, 永远不会让用户看到空白)
_FALLBACK_MESSAGE = Message(
code=str(FALLBACK_CODE),
message="服务暂时无法处理请求",
suggestion="请稍后重试,或联系技术支持并提供错误码以便排查。",
severity="error",
retryable=True,
)
def get_message(code: str, locale: str = "zh-CN") -> Message:
"""根据业务码查找本地化消息.
行为契约:
- 已注册码点 返回 MESSAGES_ZH_CN 中的 Message
- 未注册但格式合法的码点 返回 _FALLBACK_MESSAGE ( code 字段被替换为入参,
便于排查是哪类未注册)
- 格式非法码点 返回 _FALLBACK_MESSAGE (code 字段保留原值以保留上下文)
当前实现只支持 zh-CN; 后续多 locale 时改为 MESSAGES[locale].get(code, fallback).
"""
if locale != "zh-CN":
# 当前仅支持 zh-CN, 其他 locale 走兜底
return _FALLBACK_MESSAGE
msg = MESSAGES_ZH_CN.get(code)
if msg is not None:
return msg
# 未注册: 保留原 code 用于排查, 文案用兜底
return Message(
code=code,
message=_FALLBACK_MESSAGE.message,
suggestion=_FALLBACK_MESSAGE.suggestion,
severity=_FALLBACK_MESSAGE.severity,
retryable=_FALLBACK_MESSAGE.retryable,
)
def is_registered(code: str) -> bool:
"""检查码点是否已注册 (CI 校验脚本与单元测试用)."""
return code in MESSAGES_ZH_CN
def registered_codes() -> Dict[str, Message]:
"""返回当前已注册的所有码点 (测试与文档生成用)."""
return dict(MESSAGES_ZH_CN)