# Constant、ErrorCode 与 I18n 约定

## 文档定位

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

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

如果需要直接复制代码骨架，使用 [module-constant-i18n-template.md](../project-development/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](../../../../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 前缀
- 通用状态位（`YESNO`、`DataStatus`、`AccountStatus`）
- 通用动作路径（`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` 中：

```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>";
  }
}
```

默认允许的嵌套类：`Urls`、`Resource`、`ErrorCode`。按需增加：`CacheKey`、`Topic`、`HeaderKey`、`PropertyKey`。

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

### `Urls` 约定

只定义模块的稳定资源路径：
- 优先复用上级常量组合路径
- 通用动作后缀复用 `CoreConstant.Urls`
- 不重复声明框架已稳定的动作路径（`/enable`、`/disable`、`/order/bottom` 等）

### `Resource` 约定

集中表达资源标识和资源展示名：
- `KEY_*` 使用小写英文和点分层，如 `authorization.user`
- `KEY_*` 子资源沿用点分层，如 `platform.tenant.config`
- `NAME_*` 放业务展示名，用于 `getResourceName()`、OpenAPI 摘要和权限资源展示
- 不把按钮文案放进 `Resource`
- 不重复定义通用动作资源名（`create`、`update`、`delete` 等）

---

## 五、模块内 ErrorCode 落点

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

抛出示例：

```java
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 只作为兼容回退

示例：

```properties
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` 中，业务代码只引用常量，不在 `Manager`、`Service`、`Repository` 或 `Mapper` 中散落字符串字面量。

固定使用一个内部类：

```java
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，直接输出对应约束文案：

```java
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_VERSION`、`USER_ACCOUNT`
- FK 常量命名按“引用关系 + 方向”表达，例如 `ROLE_USED_BY_ROLE_PERMISSION`
- 常量值必须与 `messages*.properties` 和 `message.multiproperties` 中的 key 完全一致
- 常量值必须以数据库真实表名开头，末段必须使用数据库真实约束名，不得改成字段名、Java 属性名或业务别名
- 数据库约束错误继续使用框架通用错误码，例如 `ErrorCode.DATA_DUPLICATE`、`ErrorCode.DATA_IN_USE`；不要为了每个 UK/FK 新增模块私有错误码

---

## 六、i18n 资源文件约定

### 目录结构

```text
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` 分组标注：`Errors`、`Common`、`Entity - Xxx`。

### i18n Key 约定

**错误码文案**：key 直接使用错误码本身
```properties
<REGISTERED_ERROR_1>=用户已经分配到该分组，不允许重复分配
```

**对象与字段文案**：使用 `{basePackage}.{Entity}` 格式
```properties
com.ineed.config.dictionary.DictionaryDefinition=字典定义
com.ineed.config.dictionary.DictionaryDefinition.applicationKey=应用标识
```

- `{basePackage}` 是业务基础包，不包含 `contract.ao`、`jpa.entity`、`jdbc.entity`、`mybatisflex.entity`、`r2dbc.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 前缀

**数据库约束文案**：
```properties
{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-Language` → `OperateInfoRequestResolver` 解析 → 写入 `OperateInfo.locale` → `WebMvcOperateInfoFilter`/`WebFluxOperateInfoFilter` 分别存入 ThreadLocal / Reactor Context。

### 各层调用场景

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

### 同步代码（WebMVC / Service / Manager）

```java
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）

```java
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` 且不需要从上下文获取时使用：

```java
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：

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

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

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

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

### 服务层与 Manager 层使用

`I18nHandler` 和 `ReactiveI18nHandler` 都放在 `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. **没有请求上下文**（定时任务、消息消费者、工具类）：直接传 `Locale` 给 `I18nResolver` 或 `I18nExceptionHandler.invokeException(exception, locale)`

> 最后更新：2026-05-17
