框架参考

Constant、ErrorCode 与 I18n 约定

遵守常量、错误码和语言资源约定,业务号段以全局登记为准。

浏览全部手册
本页目录
文档来源与 Markdown 原文

权威正文:ineed-core/docs/core/constant-errorcode-i18n-guide.md。网站按工作区快照同步,原文中的历史日期和验证范围保留。

开始编码前核对同版本源码;跨仓文件引用可在源码定位目录查找。

下载 Markdown 原文 ↓

文档定位

本文档定义业务模块中的常量、模块内错误码和国际化资源的统一规则,包含:

  • 框架通用错误码与业务模块私有错误码的编号边界
  • 业务模块 {Object}ConstantUrlsResourceErrorCode 的落点
  • 应用错误码权威登记入口(避免不同模块重复占用)
  • i18n 资源文件的格式、顺序和 key 约定

如果需要直接复制代码骨架,使用 module-constant-i18n-template.md


一、框架通用错误码(根级 ErrorCode)

总体规则

  • 框架通用错误码使用负数数字字符串,定义在 ineed-core-base/src/main/java/com/ineed/base/exception/ErrorCode.java
  • 业务模块私有错误码使用 6 位正整数数字字符串
  • 新增业务错误码必须按模块分段分配,不允许随意占号
  • 错误文案占位符统一使用 MessageFormat 风格:{0}{1}{2}

框架保留区间

区间 类型 说明
-1 ~ -99 系统通用错误 未知错误、不支持操作、非法操作、配置缺失、任务运行中
-100 ~ -199 数据通用错误 数据重复、不存在、已删除、被占用、版本冲突、固定/锁定/保留/同步数据限制、树结构限制
-200 ~ -299 请求/参数错误 参数为空、参数取值错误、参数超过限制等非 Bean Validation 场景
-300 ~ -399 数据库/基础设施错误 SQL 执行、文件处理、网关调用等底层或外部基础设施错误
-400 ~ -499 租户/上下文错误 租户无效、上下文缺失、上下文不匹配等
-500 ~ -599 验证/校验错误 Bean Validation、领域校验、验证码错误
-800 ~ -999 认证/会话/授权错误 登录、账号状态、会话、密码、凭证、访问授权等通用安全错误

子段划分:

-800 ~ -999 内部:

区间 类型
-800 ~ -849 登录 / 账号 / 会话
-850 ~ -879 密码 / 凭证
-880 ~ -899 授权 / 访问控制

-500 ~ -599 内部:

区间 类型
-500 ~ -549 Bean Validation / 参数校验
-550 ~ -559 验证码

框架通用错误边界

以下场景应继续复用根级 ErrorCode

  • 数据不存在、重复、被使用、版本冲突
  • 参数为空、取值错误、Bean Validation 失败
  • 租户上下文无效、未授权访问
  • 账号密码错误、账号锁定/禁用/过期

以下场景应放入模块私有错误码:

  • 某个业务对象的特定规则
  • 某个业务流程的状态机限制
  • 某个模块私有的授权/密码治理/第三方集成错误

二、应用模块错误码的权威来源

应用私有错误码的格式、范围、状态和新增流程以工作区 design/standards/error-code-registry.md 为唯一来源。Core 只维护框架通用错误机制与使用约定。

原本文中的 BBNNNN 分段规则和业务块号表已退出维护,不能用于新代码。实际业务域的编号方式不同,不根据旧模块名或示例数字推导可用号段。

新增错误时:先判断是否能复用通用错误 → 查全局登记与真实模块常量 → 按登记流程分配 → 同步常量、语言资源与测试。已发布错误不随意改号或复用。

三、业务登记与迁移边界

本页不保留应用模块号段副本。UK/FK 约束继续使用 Core 通用错误与模块约束 i18n key,不为每个数据库约束创建私有错误码。

模板中的 <REGISTERED_ERROR_1> 是占位符,必须替换为登记表确认过的真实号码。遇到历史代码与登记表不一致时,核实兼容策略并在所属模块修正,不为适配旧代码恢复另一套全局登记。


四、Constant 分层边界

CoreConstant(框架级)

ineed-core-base/src/main/java/com/ineed/base/CoreConstant.java 只承载跨模块稳定契约:

  • 根级 URL / WS 前缀
  • 通用状态位(YESNODataStatusAccountStatus
  • 通用动作路径(CoreConstant.Urls
  • 通用动作资源标识(CoreConstant.ResourceKey
  • HTTP Header Key、上下文 Key

约束:不放具体业务对象的模块路径、私有资源标识、私有错误码。

CoreDataConstant(数据层)

ineed-core-data-base/src/main/java/com/ineed/core/data/CoreDataConstant.java 承载跨数据栈共享的数据层常量:

  • 通用列名 Columns
  • 通用属性名 Fields
  • 通用数据层 SpEL Spel

约束:只放 JPA / JDBC / MyBatis-Flex / R2DBC 共享契约,不塞入业务域私有字段。

业务模块 {Object}Constant

放在 core 层根包下的 {Object}Constant.java 中:

public final class UserConstant {
  private UserConstant() { super(); }

  public static final class Urls {
    public static final String USER = AuthorizationCoreConstant.Urls.AUTHORIZATION + "/user";
  }

  public static final class Resource {
    public static final String KEY_USER = "authorization.user";
    public static final String NAME_USER = "用户";
  }

  public static final class ErrorCode {
    public static final String USER_GROUP_ALREADY_ASSIGNED = "<REGISTERED_ERROR_1>";
  }
}

默认允许的嵌套类:UrlsResourceErrorCode。按需增加:CacheKeyTopicHeaderKeyPropertyKey

不放:临时变量、日志文案、SQL 片段、展示文案、magic value。

Urls 约定

只定义模块的稳定资源路径:

  • 优先复用上级常量组合路径
  • 通用动作后缀复用 CoreConstant.Urls
  • 不重复声明框架已稳定的动作路径(/enable/disable/order/bottom 等)

Resource 约定

集中表达资源标识和资源展示名:

  • KEY_* 使用小写英文和点分层,如 authorization.user
  • KEY_* 子资源沿用点分层,如 platform.tenant.config
  • NAME_* 放业务展示名,用于 getResourceName()、OpenAPI 摘要和权限资源展示
  • 不把按钮文案放进 Resource
  • 不重复定义通用动作资源名(createupdatedelete 等)

五、模块内 ErrorCode 落点

同一 {Object}Constant.ErrorCode 中的编号必须落在当前模块预留块号内,不混入根级负数编号。

抛出示例:

throw StatefulException.of(UserConstant.ErrorCode.USER_GROUP_ALREADY_ASSIGNED);
throw StatefulException.of(UserConstant.ErrorCode.BUILTIN_USER_ROLE_CANNOT_REMOVE, roleName);

与数据库约束消息的关系

唯一索引、外键等约束不建议为每个约束新增错误码:

  • 错误码使用框架通用的数据重复或数据库约束错误,不为每个数据库约束新增模块私有错误码
  • 唯一约束文案通过 {tableName}.uk.{dbConstraintName} 映射
  • 外键约束必须区分引用目标不存在、引用目标正在被使用两类语义
  • 历史 constraint.* key 只作为兼容回退

示例:

isec_permission.uk.uk_isec_permission_key=权限标识已经存在
isec_role_permission.fk.not-found.fk_role_permission_role=角色不存在,不能建立角色权限关系
isec_role_permission.fk.in-use.fk_role_permission_role=角色数据被角色权限关系使用,不能删除或更新

约束 i18n key 常量化

唯一约束和外键约束的 i18n key 必须沉淀到模块 {Object}Constant 中,业务代码只引用常量,不在 ManagerServiceRepositoryMapper 中散落字符串字面量。

固定使用一个内部类:

public static final class I18nKey {
  public static final String USER_ACCOUNT = "isec_user.uk.uk_isec_user_account";
  public static final String USER_GROUP_USED_BY_USER =
      "isec_user_group.fk.in-use.fk_isec_user_group_user_id";

  private I18nKey() {
    super();
  }
}

使用示例。重复数据业务校验传实体类名和字段名,由 core 统一转换对象与字段文案;外键被占用类主动校验可以传 FK i18n key,直接输出对应约束文案:

throw StatefulException.of(ErrorCode.DATA_DUPLICATE, this.getEntityClass().getName(), "account");
throw StatefulException.of(ErrorCode.DATA_IN_USE, UserConstant.I18nKey.USER_GROUP_USED_BY_USER);

约束:

  • I18nKey 只放代码需要主动引用的模块内 i18n key,当前主要包括唯一约束文案 key 和外键约束文案 key
  • UK 常量命名按业务含义表达唯一索引覆盖的字段,例如 APP_VERSIONUSER_ACCOUNT
  • FK 常量命名按“引用关系 + 方向”表达,例如 ROLE_USED_BY_ROLE_PERMISSION
  • 常量值必须与 messages*.propertiesmessage.multiproperties 中的 key 完全一致
  • 常量值必须以数据库真实表名开头,末段必须使用数据库真实约束名,不得改成字段名、Java 属性名或业务别名
  • 数据库约束错误继续使用框架通用错误码,例如 ErrorCode.DATA_DUPLICATEErrorCode.DATA_IN_USE;不要为了每个 UK/FK 新增模块私有错误码

六、i18n 资源文件约定

目录结构

src/main/resources/
  i18n/ineed/{module-segments}/
    message.multiproperties
    messages.properties
    messages_en.properties
    messages_zh_TW.properties
  • message.multiproperties 是编辑源,多语言并排维护
  • 运行时实际加载的是 messages*.properties
  • I18nConfiguration 自动扫描 classpath*:i18n/**/messages.properties

生成顺序(固定)

  1. Errors
  2. Common
  3. Entity name
  4. Entity fields
  5. Entity unique constraints
  6. Entity foreign-key constraints

message.multiproperties 中也必须使用同样顺序,通过 Comment 分组标注:ErrorsCommonEntity - Xxx

i18n Key 约定

错误码文案:key 直接使用错误码本身

<REGISTERED_ERROR_1>=用户已经分配到该分组,不允许重复分配

对象与字段文案:使用 {basePackage}.{Entity} 格式

com.ineed.config.dictionary.DictionaryDefinition=字典定义
com.ineed.config.dictionary.DictionaryDefinition.applicationKey=应用标识
  • {basePackage} 是业务基础包,不包含 contract.aojpa.entityjdbc.entitymybatisflex.entityr2dbc.entity 等实现分层片段
  • {Entity} 使用实体类简单名,保持 UpperCamelCase
  • {fieldName} 直接使用 Java 字段名
  • 对象和字段 key 必须跨 AO / Query / VO / Entity 复用
  • 新生成代码不得使用持久化实现类全名 key,例如 com.ineed.security.authorization.accesscontrol.jdbc.entity.Role.rolePurpose
  • applicationKey 仍用于 META-INF/ineed/modules/*.json、权限 permissionKey、菜单资源标识等业务标识,不作为对象/字段 i18n key 前缀

数据库约束文案

{tableName}.uk.{dbConstraintName}=唯一约束文案
{tableName}.fk.not-found.{dbConstraintName}=引用目标不存在时的外键文案
{tableName}.fk.in-use.{dbConstraintName}=引用目标正在被使用时的外键文案
  • uk 后缀、fk.not-found / fk.in-use 后缀末段都必须使用数据库返回的真实约束名
  • {tableName} 必须使用数据库真实表名
  • 外键 key 中的 {tableName} 必须是引用方表,也就是 FK 所在表;不要使用被引用目标表
  • not-found 对应子表 INSERT / UPDATE 时引用的父表数据不存在,例如 MySQL Cannot add or update a child row
  • in-use 对应父表 DELETE / UPDATE 时仍被子表数据引用,例如 MySQL Cannot delete or update a parent row
  • SQLException 国际化解析会优先使用 SQL 错误中解析出的 {tableName} 精确查找;解析不到表名时才按约束名后缀兼容查找
  • 历史 {basePackage}.{Entity}.fk.{dbConstraintName} 只作为兼容回退,新模块必须生成表名格式的两类语义化 FK key
  • 不再新增根级 constraint.* key

占位符格式

统一使用 MessageFormat 风格:{0}{1}{2}。不再新增 %1%s{}

生成要求

创建新业务模块时,如果存在以下任一情况,应同时生成 {Object}Constant 与 i18n 资源:

  • 新建了模块私有控制器资源根路径
  • 新建了模块私有资源标识
  • 新增了模块私有业务错误码
  • 需要对象名、字段名、重复校验、校验失败的国际化文案

最少输出物:

  1. {Object}Constant.java
  2. message.multiproperties
  3. messages.properties
  4. messages_en.properties
  5. messages_zh_TW.properties

生成顺序:先写 {Object}Constant → 接 Controller 注解 → 再补模块内 ErrorCode → 再补 message.multiproperties → 再补 messages*.properties


七、历史兼容区间

i18n/ineed/error/v1/messages*.properties 作为历史兼容资源保留,状态为 legacy/frozen

  • 历史正数错误码可以继续保留
  • 新模块不要参考历史编号无序追加
  • 不要向 error/v1 继续追加新错误码

八、自检清单

  1. Urls 里没有重复定义框架通用动作后缀
  2. Resource 同时承载 KEY_* 稳定资源标识和 NAME_* 业务展示名
  3. 通用错误继续复用根级 ErrorCode
  4. 新增业务错误码不是负数,且是 6 位正整数
  5. 同一模块的新错误码落在同一个块号区间内
  6. 模块号位已登记到 design 全局错误码登记表,代码中的错误码落在登记区间内
  7. 模块私有业务错误码已配齐 message.multiproperties + 三份 messages*.properties
  8. 新增文案占位符统一使用 {0}{1} 风格
  9. 对象名、字段名 i18n key 使用 {basePackage}.{Entity} 格式,字段追加 .{fieldName};新代码不得使用持久化实现类全名 key
  10. 控制器没有直接写资源路径/资源标识字面量
  11. 所有出现在 messages*.properties 的模块私有 key 已同步到 message.multiproperties

九、I18n Java API 使用指南

架构分层

I18n 按调用栈分为三层,核心逻辑共享,locale 解析按同步/响应式分别处理:

ineed-core-base         I18nResolver       通用核心(需显式传入 Locale)
                        I18nExceptionHandler  异常国际化(需显式传入 Locale)

ineed-core-context      I18nHandler        同步包装(从 ThreadLocal 获取 locale)
                         └─ ContextHandler.getLocale() → OperateInfo.locale

ineed-core-context-     ReactiveI18nHandler 响应式包装(从 Reactor Context 获取 locale)
  reactive               └─ ReactiveContextHandler.getLocale() → OperateInfo.locale

Locale 来源链路:请求头 Content-LanguageOperateInfoRequestResolver 解析 → 写入 OperateInfo.localeWebMvcOperateInfoFilter/WebFluxOperateInfoFilter 分别存入 ThreadLocal / Reactor Context。

各层调用场景

调用方所在模块 使用类 返回类型
Controller / Service / Manager(同步) I18nHandler 直接返回值
Controller / Service / Manager(响应式) ReactiveI18nHandler Mono<T>
纯逻辑工具、不依赖上下文 I18nResolver(显式传 Locale) 直接返回值

同步代码(WebMVC / Service / Manager)

import com.ineed.base.context.I18nHandler;

// 获取国际化消息
String message = I18nHandler.getMessage("error.code.key");
String messageWithArgs = I18nHandler.getMessage("parameter.incorrect", "昵称");

// 获取属性名称文案
String fieldLabel = I18nHandler.getPropertyMessage("com.ineed.demo.contract.ao.SampleAO", "nickname");

// 获取校验错误消息
String validationMsg = I18nHandler.getValidationMessage(validationResult);

// 获取重复数据错误消息
String duplicateMsg = I18nHandler.getDuplicateMessage(entity, Set.of("account", "mobileNumber"));

// 处理异常,返回国际化的 ErrorMessage
ErrorMessage error = I18nHandler.handleException(exception);

响应式代码(WebFlux)

import com.ineed.base.context.ReactiveI18nHandler;

// 所有方法返回 Mono,在响应式链中安全使用
Mono<String> message = ReactiveI18nHandler.getMessage("error.code.key");

Mono<String> fieldLabel = ReactiveI18nHandler.getPropertyMessage(
    "com.ineed.demo.contract.ao.SampleAO", "nickname");

Mono<String> validationMsg = ReactiveI18nHandler.getValidationMessage(validationResult);

// 异常处理
ReactiveI18nHandler.handleException(exception)
    .flatMap(errorMessage -> ...);

直接使用 I18nResolver(不依赖上下文)

仅当已有显式 Locale 且不需要从上下文获取时使用:

import com.ineed.base.i18n.I18nResolver;

String message = I18nResolver.getMessage("key", locale);
String property = I18nResolver.getPropertyMessage(className, property, locale);
String duplicate = I18nResolver.getDuplicateMessage(object, fields, locale);
String validation = I18nResolver.getValidationMessage(result, locale);

异常处理

I18nExceptionHandler 也需要显式 Locale。优先使用 I18nHandler.handleException() / ReactiveI18nHandler.handleException(),它们会自动从上下文获取 locale:

// 同步
ErrorMessage error = I18nHandler.handleException(exception);

// 响应式
Mono<ErrorMessage> error = ReactiveI18nHandler.handleException(exception);

仅在已有显式 Locale 时直接调用:

ErrorMessage error = I18nExceptionHandler.invokeException(exception, locale);

服务层与 Manager 层使用

I18nHandlerReactiveI18nHandler 都放在 ineed-core-context / ineed-core-context-reactive 模块中,不被 web 层独占。Service 和 Manager 层可以直接依赖这两个 context 模块获取国际化消息,无需反向依赖 web 模块。

选择指引

  1. 同步链(WebMVC 请求线程、同步 Service/Manager):直接用 I18nHandler
  2. 响应式链(WebFlux 请求、Reactor 链内的 Service/Manager):用 ReactiveI18nHandler,返回 Mono<>
  3. 没有请求上下文(定时任务、消息消费者、工具类):直接传 LocaleI18nResolverI18nExceptionHandler.invokeException(exception, locale)

最后更新:2026-05-17

仍有疑问?按反馈清单整理复现信息 →