# 模块 Constant 与 I18n 样板

## 文档定位

本文档提供一份可直接复制的模块级样板，用于新建业务模块时一次性补齐：

- `{Object}Constant`
- 控制器 `Urls` / `Resource` 接入
- 模块内 `ErrorCode`
- `messages.properties`
- `messages_en.properties`
- `messages_zh_TW.properties`
- `message.multiproperties`

如果你想先理解规则边界，先读 [constant-errorcode-i18n-guide.md](../core/constant-errorcode-i18n-guide.md)。

所有 `REGISTERED_ERROR_1/2` 及带尖括号的同名项都是模板占位符，必须从[应用错误码登记表](../../../../design/standards/error-code-registry.md)取得当前模块真实号码后统一替换。它们不能直接作为运行时错误码。XML `<Name>` 中使用不带尖括号的占位文本。样板用于表达结构；真实可编译例子见[示例索引](../examples/README.md)。

## 适用场景

适合以下情况：

- 新建一个业务对象模块
- 模块需要私有资源路径
- 模块需要私有资源标识
- 模块需要私有业务错误码
- 模块需要对象名、字段名、错误码的国际化文案

## 样板一：`{Object}Constant.java`

路径建议：

`{module}-core/src/main/java/com/ineed/{domain}/{object}/{Object}Constant.java`

示例：

```java
package com.ineed.security.authorization.user;

import com.ineed.security.authorization.core.AuthorizationCoreConstant;

public final class UserConstant {

  private UserConstant() {
    super();
  }

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

    private Urls() {
      super();
    }
  }

  public static final class Resource {
    public static final String KEY_USER = "authorization.user";
    public static final String NAME_USER = "用户";
    public static final String KEY_GROUP = "authorization.group";
    public static final String NAME_GROUP = "分组";

    private Resource() {
      super();
    }
  }

  public static final class I18nKey {
    public static final String USER_ACCOUNT = "isec_user.uk.uk_isec_user_account";
    public static final String GROUP_NAME = "isec_group.uk.uk_isec_group_name";
    public static final String USER_GROUP_USER_NOT_FOUND =
        "isec_user_group.fk.not-found.fk_isec_user_group_user_id";
    public static final String USER_USED_BY_USER_GROUP =
        "isec_user_group.fk.in-use.fk_isec_user_group_user_id";

    private I18nKey() {
      super();
    }
  }

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

    private ErrorCode() {
      super();
    }
  }
}
```

约束：

- `Urls` 只放模块私有资源根路径
- `Resource` 同时放稳定资源标识和业务展示名，资源标识使用 `KEY_*`，资源名称使用 `NAME_*`
- `I18nKey` 只放代码需要主动引用的模块内 i18n key，当前主要包括唯一约束和外键约束文案 key，常量值必须与三语 `messages*.properties` 保持一致
- `ErrorCode` 只放模块私有业务错误
- 通用动作路径继续复用 `CoreConstant.Urls`
- 通用错误继续复用根级 `ErrorCode`
- UK/FK 约束错误继续复用通用错误码，不为每个数据库约束新增模块私有错误码

## 样板二：Controller 接入

示例：

```java
@RestController
@RequestMapping(UserConstant.Urls.USER)
public class UserController implements
    ICrudController<UserAO, UserVO, UserService>,
    IFindByIdController<UserVO, UserService>,
    ISearchController<UserQuery, UserVO, UserService> {

  private final UserService userService;

  @Override
  public UserService getService() {
    return userService;
  }

  @Override
  public String getResourceKey() {
    return UserConstant.Resource.KEY_USER;
  }

  @Override
  public String getResourceName() {
    return UserConstant.Resource.NAME_USER;
  }
}
```

约束：

- `@RequestMapping` 优先使用 `UserConstant.Urls.*`
- `getResourceKey()` 优先返回 `UserConstant.Resource.KEY_*`
- `getResourceName()` 优先返回 `UserConstant.Resource.NAME_*`，用于 OpenAPI 公共摘要输出，例如把 `创建资源` 渲染为 `创建用户`
- 新代码不要直接写 `"authorization.user"` 这类字面量

## 样板三：Service / Manager 抛业务错误

示例：

```java
if (alreadyAssigned) {
  throw StatefulException.of(UserConstant.ErrorCode.USER_GROUP_ALREADY_ASSIGNED);
}
```

带参数示例：

```java
if (builtinRole) {
  throw StatefulException.of(UserConstant.ErrorCode.BUILTIN_USER_ROLE_CANNOT_REMOVE, roleName);
}
```

约束：

- 模块私有业务规则使用 `{Object}Constant.ErrorCode`
- 框架通用错误继续使用根级 `ErrorCode`
- 不要在代码里直接硬编码中文错误文案

## 样板四：`messages*.properties`

路径建议：

```text
{module}-core/src/main/resources/
  i18n/ineed/security/authorization/user/
    message.multiproperties
    messages.properties
    messages_en.properties
    messages_zh_TW.properties
```

默认语言：

```properties
# Errors
<REGISTERED_ERROR_1>=用户已经分配到该分组，不允许重复分配
<REGISTERED_ERROR_2>=系统内置用户的默认角色不允许移除：{0}

# Common
security.user.accountStatus.enabled=启用

# Entity - User
com.ineed.security.authorization.user.User=用户
com.ineed.security.authorization.user.User.account=账号
com.ineed.security.authorization.user.User.nickname=昵称
isec_user.uk.uk_isec_user_account=用户已存在：账号不允许重复

# Entity - UserGroup
com.ineed.security.authorization.user.UserGroup=用户分组关系
isec_user_group.fk.not-found.fk_isec_user_group_user_id=用户不存在，不能建立用户分组关系
isec_user_group.fk.in-use.fk_isec_user_group_user_id=用户数据被用户分组关系使用，不能删除或更新

# Entity - Group
com.ineed.security.authorization.user.Group=分组
com.ineed.security.authorization.user.Group.name=分组名称
isec_group.uk.uk_isec_group_name=分组已存在：分组名称不允许重复
```

英文：

```properties
# Errors
<REGISTERED_ERROR_1>=User has already been assigned to the group, duplicate assignment is not allowed
<REGISTERED_ERROR_2>=Default role of builtin user cannot be removed: {0}

# Common
security.user.accountStatus.enabled=Enabled

# Entity - User
com.ineed.security.authorization.user.User=User
com.ineed.security.authorization.user.User.account=Account
com.ineed.security.authorization.user.User.nickname=Nickname
isec_user.uk.uk_isec_user_account=User already exists: account must be unique

# Entity - UserGroup
com.ineed.security.authorization.user.UserGroup=User Group Relation
isec_user_group.fk.not-found.fk_isec_user_group_user_id=User does not exist and the user group relation cannot be created
isec_user_group.fk.in-use.fk_isec_user_group_user_id=User data is referenced by user group relation and cannot be deleted or updated

# Entity - Group
com.ineed.security.authorization.user.Group=Group
com.ineed.security.authorization.user.Group.name=Group Name
isec_group.uk.uk_isec_group_name=Group already exists: group name must be unique
```

繁体中文：

```properties
# Errors
<REGISTERED_ERROR_1>=使用者已經分配到該分組，不允許重複分配
<REGISTERED_ERROR_2>=系統內建使用者的預設角色不允許移除：{0}

# Common
security.user.accountStatus.enabled=啟用

# Entity - User
com.ineed.security.authorization.user.User=使用者
com.ineed.security.authorization.user.User.account=帳號
com.ineed.security.authorization.user.User.nickname=暱稱
isec_user.uk.uk_isec_user_account=使用者已存在：帳號不允許重複

# Entity - UserGroup
com.ineed.security.authorization.user.UserGroup=使用者分組關係
isec_user_group.fk.not-found.fk_isec_user_group_user_id=使用者不存在，不能建立使用者分組關係
isec_user_group.fk.in-use.fk_isec_user_group_user_id=使用者資料被使用者分組關係使用，不能刪除或更新

# Entity - Group
com.ineed.security.authorization.user.Group=分組
com.ineed.security.authorization.user.Group.name=分組名稱
isec_group.uk.uk_isec_group_name=分組已存在：分組名稱不允許重複
```

顺序要求：

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

对象、字段 key 使用业务基础包 key；数据库约束 key 使用表名 key：

- `{basePackage}.{Entity}`
- `{basePackage}.{Entity}.{fieldName}`
- `{tableName}.uk.{dbConstraintName}`
- `{tableName}.fk.not-found.{dbConstraintName}`
- `{tableName}.fk.in-use.{dbConstraintName}`

其中 `{basePackage}` 是业务基础包，不包含 `contract.ao`、`contract.query`、`contract.vo`、`jpa.entity`、`jdbc.entity`、
`mybatisflex.entity`、`r2dbc.entity` 等实现分层片段；`{Entity}` 使用实体类简单名。`{tableName}` 必须使用数据库真实表名，
例如 `isec_user` 或 `isec_user_group`。`{dbConstraintName}` 必须使用数据库返回的真实约束名，例如 `uk_isec_user_account`
或 `fk_isec_user_group_user_id`。外键 key 中的 `{tableName}` 必须是引用方表，也就是 FK 所在表；同一个 FK 约束必须同时准备引用目标不存在、引用目标正在被使用两类文案。历史 `{basePackage}.{Entity}.fk.{dbConstraintName}` 和根级
`constraint.*` 仅作为兼容回退，不再新增。

如果业务代码需要主动抛出 FK 被占用类错误，必须先在 `{Object}Constant.I18nKey` 中定义常量，再引用常量；重复数据业务校验应传实体类名和字段名，由 core 统一转换对象与字段文案：

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

不要在 `Manager`、`Service`、`Repository` 或 `Mapper` 中直接写完整 i18n key 字符串。

## 样板五：`message.multiproperties`

如果团队使用 MultiProperties 编辑器维护多语言资源，建议同时生成：

```xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<MultiProperties xmlns="hu.skzs.multiproperties">
    <Version>1.2</Version>
    <Name>message</Name>
    <Description></Description>
    <Handler>Java Properties Handler</Handler>
    <Columns>
        <Key>
            <Width>200</Width>
        </Key>
        <Column>
            <Name></Name>
            <Description></Description>
            <Width>380</Width>
            <HandlerConfiguration>/{module}-core/src/main/resources/i18n/ineed/security/authorization/user/messages.properties|false|false|false|false|ISO-8859-1</HandlerConfiguration>
        </Column>
        <Column>
            <Name>en</Name>
            <Description></Description>
            <Width>538</Width>
            <HandlerConfiguration>/{module}-core/src/main/resources/i18n/ineed/security/authorization/user/messages_en.properties|false|false|false|false|ISO-8859-1</HandlerConfiguration>
        </Column>
        <Column>
            <Name>zh_TW</Name>
            <Description></Description>
            <Width>348</Width>
            <HandlerConfiguration>/{module}-core/src/main/resources/i18n/ineed/security/authorization/user/messages_zh_TW.properties|false|false|false|false|ISO-8859-1</HandlerConfiguration>
        </Column>
    </Columns>
    <Records>
        <Comment>
            <Value>Errors</Value>
        </Comment>
        <Property>
            <Name>REGISTERED_ERROR_1</Name>
            <Disabled>false</Disabled>
            <MultiLine>false</MultiLine>
            <Value disabled="false">用户已经分配到该分组，不允许重复分配</Value>
            <Value disabled="false">User has already been assigned to the group, duplicate assignment is not allowed</Value>
            <Value disabled="false">使用者已經分配到該分組，不允許重複分配</Value>
        </Property>
        <Comment>
            <Value>Common</Value>
        </Comment>
        <Property>
            <Name>security.user.accountStatus.enabled</Name>
            <Disabled>false</Disabled>
            <MultiLine>false</MultiLine>
            <Value disabled="false">启用</Value>
            <Value disabled="false">Enabled</Value>
            <Value disabled="false">啟用</Value>
        </Property>
        <Comment>
            <Value>Entity - User</Value>
        </Comment>
        <Property>
            <Name>com.ineed.security.authorization.user.User</Name>
            <Disabled>false</Disabled>
            <MultiLine>false</MultiLine>
            <Value disabled="false">用户</Value>
            <Value disabled="false">User</Value>
            <Value disabled="false">使用者</Value>
        </Property>
        <Property>
            <Name>com.ineed.security.authorization.user.User.account</Name>
            <Disabled>false</Disabled>
            <MultiLine>false</MultiLine>
            <Value disabled="false">账号</Value>
            <Value disabled="false">Account</Value>
            <Value disabled="false">帳號</Value>
        </Property>
    </Records>
</MultiProperties>
```

说明：

- `message.multiproperties` 是编辑视图，不是运行时加载入口
- 它应与三份 `messages*.properties` 保持同步
- 它不能只维护错误码；对象名、字段名、AO / Query / VO 对外字段名和关键枚举标签也必须同步维护
- 新增模块错误码或字段文案时，建议优先改 `message.multiproperties`，再同步导出

## 样板六：对象与字段文案

统一使用 `{basePackage}.{Entity}.{fieldName}` 格式：

```properties
com.ineed.security.authorization.user.User=用户
com.ineed.security.authorization.user.User.account=账号
com.ineed.security.authorization.user.User.nickname=昵称
```

约束：

- `{basePackage}` 使用业务基础包；例如 `com.ineed.security.authorization.user`
- `{Entity}` 使用实体类简单名，保持 UpperCamelCase
- 对象和字段 key 必须跨 `AO / Query / VO / Entity` 复用，同一个业务字段只维护一份文案
- 不要使用持久化实现类全名作为 i18n key（例如 `com.ineed.security.authorization.user.jdbc.entity.User.account`）
- `applicationKey` 仍用于 `META-INF/ineed/modules/*.json`、权限 `permissionKey`、菜单资源标识等业务标识，不作为对象/字段 i18n key 前缀

如果 AO 和 Entity 的文案必须区分（例如 AO 中需要更面向用户的标签），可以显式补：

```properties
com.ineed.security.authorization.user.UserAO.account=登录账号
```

## 推荐生成顺序

创建新模块时，建议按以下顺序生成：

1. 先写 `{Object}Constant`
2. 再接控制器 `@RequestMapping`、`getResourceKey()` 和 `getResourceName()`
3. 再补模块内 `ErrorCode`
4. 再补 `message.multiproperties`
5. 再补 `messages.properties`
6. 最后补 `messages_en.properties` 和 `messages_zh_TW.properties`

这样能避免代码先落地、常量和文案后补时发生漂移。

## 自检清单

提交前至少确认：

1. 控制器没有直接写资源路径字面量
2. 控制器没有直接写资源标识字面量
3. 模块私有业务错误没有直接写中文消息
4. 模块私有 `ErrorCode` 已配齐 `message.multiproperties` 和三份 `messages*.properties`
5. 文案占位符统一使用 `{0}`、`{1}` 风格
6. 对象名和字段名 key 与运行时类名一致
7. 所有出现在 `messages*.properties` 的模块私有对象名、字段名和关键枚举标签 key 都已同步补齐到 `message.multiproperties`
8. UK/FK 约束文案 key 已在 `{Object}Constant.I18nKey` 中定义，业务代码没有直接硬编码完整 key 字符串

> 最后更新：2026-05-15
