diff --git a/daxpay-platform/daxpay-platform-capability/capability-audit-log/pom.xml b/daxpay-platform/daxpay-platform-capability/capability-audit-log/pom.xml
index 6a00399c5..b5be58229 100644
--- a/daxpay-platform/daxpay-platform-capability/capability-audit-log/pom.xml
+++ b/daxpay-platform/daxpay-platform-capability/capability-audit-log/pom.xml
@@ -65,6 +65,12 @@
common-config
${project.version}
+
+
+ cn.daxpay.open
+ common-translate
+ ${project.version}
+
diff --git a/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/result/LoginLogResult.java b/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/result/LoginLogResult.java
index 7b12d3d48..c3d505b76 100644
--- a/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/result/LoginLogResult.java
+++ b/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/result/LoginLogResult.java
@@ -1,5 +1,6 @@
package cn.daxpay.open.platform.capability.audit.log.result;
+import cn.daxpay.open.platform.core.annotation.Trans;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
@@ -43,6 +44,7 @@ public class LoginLogResult {
@Schema(description = "操作系统")
private String os;
+ @Trans(i18n = true)
@Schema(description = "提示消息")
private String msg;
diff --git a/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/result/OperateLogResult.java b/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/result/OperateLogResult.java
index 4ee0124de..8d77a8c58 100644
--- a/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/result/OperateLogResult.java
+++ b/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/result/OperateLogResult.java
@@ -1,5 +1,6 @@
package cn.daxpay.open.platform.capability.audit.log.result;
+import cn.daxpay.open.platform.core.annotation.Trans;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
@@ -18,6 +19,7 @@ public class OperateLogResult {
@Schema(description = "操作模块")
private String title;
+ @Trans(i18n = true, i18nPrefix = "enum.operate_log_type")
@Schema(description = "业务类型")
private String businessType;
@@ -60,6 +62,7 @@ public class OperateLogResult {
@Schema(description = "操作状态(0正常 1异常)")
private Boolean success;
+ @Trans(i18n = true)
@Schema(description = "错误消息")
private String errorMsg;
diff --git a/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/service/log/LoginLogService.java b/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/service/log/LoginLogService.java
index 6a134bf66..76e30954d 100644
--- a/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/service/log/LoginLogService.java
+++ b/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/service/log/LoginLogService.java
@@ -1,6 +1,7 @@
package cn.daxpay.open.platform.capability.audit.log.service.log;
import cn.daxpay.open.platform.common.mybatisplus.util.MpUtil;
+import cn.daxpay.open.platform.common.translate.service.TransService;
import cn.daxpay.open.platform.core.exception.DataNotExistException;
import cn.daxpay.open.platform.core.rest.param.PageParam;
import cn.daxpay.open.platform.core.rest.result.PageResult;
@@ -38,6 +39,8 @@ public class LoginLogService {
private final LoginLogDbManager loginLogManager;
+ private final TransService transService;
+
/// 内存缓冲队列
private final BlockingQueue bufferQueue = new LinkedBlockingQueue<>(DEFAULT_QUEUE_CAPACITY);
@@ -118,12 +121,16 @@ public class LoginLogService {
/// 获取
public LoginLogResult findById(Long id) {
- return loginLogManager.findById(id).map(LoginLogDb::toResult).orElseThrow(DataNotExistException::new);
+ LoginLogResult result = loginLogManager.findById(id).map(LoginLogDb::toResult).orElseThrow(DataNotExistException::new);
+ transService.translate(result);
+ return result;
}
/// 分页
public PageResult page(PageParam pageParam, LoginLogQuery query) {
- return MpUtil.toPageResult(loginLogManager.page(pageParam, query));
+ PageResult pageResult = MpUtil.toPageResult(loginLogManager.page(pageParam, query));
+ transService.translate(pageResult);
+ return pageResult;
}
/// 删除
diff --git a/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/service/log/OperateLogService.java b/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/service/log/OperateLogService.java
index 62bdc968a..d5848822d 100644
--- a/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/service/log/OperateLogService.java
+++ b/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/service/log/OperateLogService.java
@@ -1,6 +1,7 @@
package cn.daxpay.open.platform.capability.audit.log.service.log;
import cn.daxpay.open.platform.common.mybatisplus.util.MpUtil;
+import cn.daxpay.open.platform.common.translate.service.TransService;
import cn.daxpay.open.platform.core.exception.DataNotExistException;
import cn.daxpay.open.platform.core.rest.param.PageParam;
import cn.daxpay.open.platform.core.rest.result.PageResult;
@@ -40,6 +41,8 @@ public class OperateLogService {
private final OperateLogDbManager operateLogManager;
private final AuditLogMaskService maskService;
+ private final TransService transService;
+
/// 内存缓冲队列
private final BlockingQueue bufferQueue = new LinkedBlockingQueue<>(DEFAULT_QUEUE_CAPACITY);
@@ -143,12 +146,16 @@ public class OperateLogService {
/// 获取
public OperateLogResult findById(Long id) {
- return operateLogManager.findById(id).map(OperateLogDb::toResult).orElseThrow(DataNotExistException::new);
+ OperateLogResult result = operateLogManager.findById(id).map(OperateLogDb::toResult).orElseThrow(DataNotExistException::new);
+ transService.translate(result);
+ return result;
}
/// 分页
public PageResult page(PageParam pageParam, OperateLogQuery operateLogParam) {
- return MpUtil.toPageResult(operateLogManager.page(pageParam, operateLogParam));
+ PageResult pageResult = MpUtil.toPageResult(operateLogManager.page(pageParam, operateLogParam));
+ transService.translate(pageResult);
+ return pageResult;
}
/// 删除
diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/java/cn/daxpay/open/platform/common/i18n/source/JsonMessageSource.java b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/java/cn/daxpay/open/platform/common/i18n/source/JsonMessageSource.java
index 2c03b3e6e..7dad4de70 100644
--- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/java/cn/daxpay/open/platform/common/i18n/source/JsonMessageSource.java
+++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/java/cn/daxpay/open/platform/common/i18n/source/JsonMessageSource.java
@@ -14,8 +14,10 @@ import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/// # 基于 JSON 文件的消息源, 支持目录嵌套和多文件拆分
@@ -35,8 +37,16 @@ public class JsonMessageSource extends AbstractMessageSource {
@Setter
private Locale defaultLocale = Locale.CHINA;
+ /// 已加载的可用 locale 标签集合(如 en-US, zh-CN),启动时扫描
+ private final Set availableLocaleTags = new HashSet<>();
+
+ /// 语言码 → 首个匹配的地区 locale 标签(如 en → en-US, zh → zh-CN),用于反向回退
+ /// 当请求纯语言码(如 en)而无对应资源目录时, 据此映射到同语言的地区资源
+ private final Map languageToRegionLocale = new HashMap<>();
+
public JsonMessageSource(ResourcePatternResolver resourceResolver) {
this.resourceResolver = resourceResolver;
+ this.scanAvailableLocales();
}
@Override
@@ -51,6 +61,20 @@ public class JsonMessageSource extends AbstractMessageSource {
message = messages.get(code);
}
}
+ // 仍未命中则按语言反向匹配地区: en -> en-US, zh -> zh-CN, zh-Hans -> zh-CN
+ if (message == null) {
+ String lang = locale.getLanguage();
+ if (!lang.isEmpty()) {
+ String regionTag = this.languageToRegionLocale.get(lang);
+ if (regionTag != null) {
+ Locale regionLocale = Locale.forLanguageTag(regionTag);
+ if (!regionLocale.equals(locale)) {
+ messages = this.loadMessages(regionLocale);
+ message = messages.get(code);
+ }
+ }
+ }
+ }
// 仍未命中则用默认 locale 回退
if (message == null && !locale.equals(this.defaultLocale)) {
messages = this.loadMessages(this.defaultLocale);
@@ -114,6 +138,42 @@ public class JsonMessageSource extends AbstractMessageSource {
return relative.replace('/', '.').replace('\\', '.');
}
+ /// 启动时扫描 i18n 目录下所有 locale 子目录,建立「语言码 → 地区 locale」反向映射
+ /// 用于支持请求纯语言码(如 en)时反向匹配到地区资源(如 en-US)
+ private void scanAvailableLocales() {
+ try {
+ Resource[] resources = this.resourceResolver.getResources("classpath*:i18n/**/*.json");
+ for (Resource resource : resources) {
+ String localeTag = this.extractLocaleTag(resource.getURL().toString());
+ if (localeTag != null && this.availableLocaleTags.add(localeTag)) {
+ // 首次发现该 locale, 记录语言 → 地区映射
+ Locale locale = Locale.forLanguageTag(localeTag);
+ String lang = locale.getLanguage();
+ if (!lang.isEmpty()) {
+ this.languageToRegionLocale.putIfAbsent(lang, localeTag);
+ }
+ }
+ }
+ log.debug("扫描到可用 locale: {}, 语言→地区映射: {}", this.availableLocaleTags, this.languageToRegionLocale);
+ } catch (IOException e) {
+ log.warn("扫描可用 locale 失败, 语言→地区反向回退将不可用", e);
+ }
+ }
+
+ /// 从资源 URL 提取 locale 标签
+ /// .../i18n/en-US/enum/xxx.json → "en-US"
+ private String extractLocaleTag(String fileUrl) {
+ String decoded = URLDecoder.decode(fileUrl, StandardCharsets.UTF_8);
+ String marker = "i18n/";
+ int idx = decoded.lastIndexOf(marker);
+ if (idx < 0) {
+ return null;
+ }
+ String after = decoded.substring(idx + marker.length());
+ int slash = after.indexOf('/');
+ return slash > 0 ? after.substring(0, slash) : null;
+ }
+
/// 递归展平 JSON 对象
/// 例如: { "alipay": "支付宝", "amount": { "exceed": "金额超限" } }
/// 前缀 "channel.enum" → channel.enum.alipay="支付宝", channel.enum.amount.exceed="金额超限"
diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/operate_log_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/operate_log_type.json
new file mode 100644
index 000000000..21b20579f
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/operate_log_type.json
@@ -0,0 +1,12 @@
+{
+ "other": "Other",
+ "add": "Add",
+ "update": "Update",
+ "delete": "Delete",
+ "grant": "Grant",
+ "sync": "Sync",
+ "export": "Export",
+ "import": "Import",
+ "force": "Force Logout",
+ "clean": "Clear"
+}
diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/operate_log_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/operate_log_type.json
new file mode 100644
index 000000000..64760c099
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/operate_log_type.json
@@ -0,0 +1,12 @@
+{
+ "other": "其它",
+ "add": "新增",
+ "update": "修改",
+ "delete": "删除",
+ "grant": "授权",
+ "sync": "同步",
+ "export": "导出",
+ "import": "导入",
+ "force": "强退",
+ "clean": "清空"
+}
diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/test/java/cn/daxpay/open/platform/common/i18n/source/JsonMessageSourceTest.java b/daxpay-platform/daxpay-platform-common/common-i18n/src/test/java/cn/daxpay/open/platform/common/i18n/source/JsonMessageSourceTest.java
index 1fb9cd8b3..6532f2c9a 100644
--- a/daxpay-platform/daxpay-platform-common/common-i18n/src/test/java/cn/daxpay/open/platform/common/i18n/source/JsonMessageSourceTest.java
+++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/test/java/cn/daxpay/open/platform/common/i18n/source/JsonMessageSourceTest.java
@@ -64,4 +64,25 @@ class JsonMessageSourceTest {
String message = messageSource.getMessage("channel.error.channelNotFound", null, Locale.forLanguageTag("en-US"));
assertEquals("Unsupported channel code", message);
}
+
+ /// 请求纯语言码 en(无 en/ 目录)时应反向匹配到 en-US 资源,而非回退默认中文
+ @Test
+ void shouldReverseMatchEnToEnUs() {
+ String message = messageSource.getMessage("pay.route.error.noMatch", null, Locale.forLanguageTag("en"));
+ assertEquals("No available payment product matched", message);
+ }
+
+ /// 请求纯语言码 zh(无 zh/ 目录)时应反向匹配到 zh-CN 资源
+ @Test
+ void shouldReverseMatchZhToZhCn() {
+ String message = messageSource.getMessage("pay.route.error.noMatch", null, Locale.forLanguageTag("zh"));
+ assertEquals("未匹配到可用支付产品", message);
+ }
+
+ /// 请求 zh-Hans(uni-app 中文 locale,无对应目录)时应反向匹配到 zh-CN 资源
+ @Test
+ void shouldReverseMatchZhHansToZhCn() {
+ String message = messageSource.getMessage("pay.route.error.noMatch", null, Locale.forLanguageTag("zh-Hans"));
+ assertEquals("未匹配到可用支付产品", message);
+ }
}
diff --git a/daxpay-platform/daxpay-platform-common/common-translate/pom.xml b/daxpay-platform/daxpay-platform-common/common-translate/pom.xml
index 3cb77cf3a..ef95a0aa9 100644
--- a/daxpay-platform/daxpay-platform-common/common-translate/pom.xml
+++ b/daxpay-platform/daxpay-platform-common/common-translate/pom.xml
@@ -20,6 +20,13 @@
${project.version}
+
+
+ cn.daxpay.open
+ common-i18n
+ ${project.version}
+
+
com.baomidou
diff --git a/daxpay-platform/daxpay-platform-common/common-translate/src/main/java/cn/daxpay/open/platform/common/translate/service/TransService.java b/daxpay-platform/daxpay-platform-common/common-translate/src/main/java/cn/daxpay/open/platform/common/translate/service/TransService.java
index 1017cfbdb..a659c8d55 100644
--- a/daxpay-platform/daxpay-platform-common/common-translate/src/main/java/cn/daxpay/open/platform/common/translate/service/TransService.java
+++ b/daxpay-platform/daxpay-platform-common/common-translate/src/main/java/cn/daxpay/open/platform/common/translate/service/TransService.java
@@ -1,5 +1,6 @@
package cn.daxpay.open.platform.common.translate.service;
+import cn.daxpay.open.platform.common.i18n.util.I18nUtil;
import cn.daxpay.open.platform.common.translate.cache.TransCacheKey;
import cn.daxpay.open.platform.common.translate.cache.TransCacheManager;
import cn.daxpay.open.platform.common.translate.model.DictItemData;
@@ -68,7 +69,8 @@ public class TransService {
return;
}
- // 遍历所有对象,区分实体翻译和字典翻译
+ // 遍历所有对象,区分 i18n 翻译、实体翻译和字典翻译
+ List i18nFieldInfos = new ArrayList<>();
Map> entityGroupSourceValues = new LinkedHashMap<>();
List entityFieldInfos = new ArrayList<>();
Map> dictGroupSourceValues = new LinkedHashMap<>();
@@ -77,6 +79,11 @@ public class TransService {
for (Object result : results) {
List metaList = scanTransFields(result);
for (TransMeta meta : metaList) {
+ // i18n 翻译: 字段值即 key, 独立收集, 不走 source / 实体 / 字典逻辑
+ if (meta.annotation().i18n()) {
+ i18nFieldInfos.add(new TransFieldInfo(result, meta.field(), meta.annotation()));
+ continue;
+ }
Object sourceValue = getFieldValue(result, meta.source());
if (sourceValue == null) {
continue;
@@ -104,6 +111,11 @@ public class TransService {
if (!dictGroupSourceValues.isEmpty()) {
processDictTranslations(dictGroupSourceValues, dictFieldInfos);
}
+
+ // 处理 i18n 翻译
+ if (!i18nFieldInfos.isEmpty()) {
+ processI18nTranslations(i18nFieldInfos);
+ }
}
/// 翻译分页结果
@@ -159,6 +171,23 @@ public class TransService {
}
}
+ /// 处理 i18n 消息翻译
+ /// 注解字段本身的值(经 i18nPrefix 拼接后)作为 i18n key, 翻译后回填本字段
+ /// 查不到 key 时 I18nUtil 返回 key 原值, 天然兼容历史脏数据与纯文本
+ private void processI18nTranslations(List fieldInfos) {
+ for (TransFieldInfo info : fieldInfos) {
+ Object sourceValue = getFieldValue(info.result(), info.field().getName());
+ if (sourceValue == null) {
+ continue;
+ }
+ String raw = String.valueOf(sourceValue);
+ String prefix = info.annotation().i18nPrefix();
+ String key = prefix.isEmpty() ? raw : prefix + "." + raw;
+ String translated = I18nUtil.get(key);
+ setFieldValue(info.result(), info.field(), translated);
+ }
+ }
+
/// 根据当前请求语言解析多语言字段名
/// 规则:
/// - result="nameCn" 或 "nameEn" → 显式指定了语言,按指定字段名取值,不切换
diff --git a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/annotation/Trans.java b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/annotation/Trans.java
index 5a19f8f0c..f339cf6bd 100644
--- a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/annotation/Trans.java
+++ b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/annotation/Trans.java
@@ -42,10 +42,27 @@ public @interface Trans {
/// 实体翻译时无需指定
String dictCode() default "";
+ /// 是否作为 i18n messageKey 翻译(i18n 模式)
+ /// 为 true 时,注解字段本身的值(经 i18nPrefix 拼接后)作为 i18n key,
+ /// 通过 MessageSource 翻译后回填到本字段;entity / dictCode / source / on / result / cacheTtl 均忽略
+ /// 查不到 key 时原样返回字段原值,天然兼容历史脏数据与纯文本
+ /// 使用示例:
+ /// ```java
+ /// @Trans(i18n = true) // 字段值即完整 key
+ /// @Trans(i18n = true, i18nPrefix = "enum.operate_log_type") // 字段值为枚举 code,需拼前缀
+ /// ```
+ boolean i18n() default false;
+
+ /// i18n key 前缀(仅 i18n = true 时生效)
+ /// 拼接规则:prefix + "." + 字段值;为空则字段值即完整 key
+ /// 用于枚举 code 翻译:如 businessType 字段值为 "add",i18nPrefix = "enum.operate_log_type" → key = "enum.operate_log_type.add"
+ String i18nPrefix() default "";
+
/// 源字段名(当前 Result 中作为查询条件的字段名)
/// 翻译时从当前 Result 对象中读取此字段的值,作为数据库查询的条件值
/// 例如:source = "mchNo" 表示取当前对象的 mchNo 属性值
- String source();
+ /// i18n 模式下可省略(直接翻译注解字段本身),实体/字典模式必填
+ String source() default "";
/// 目标实体中的匹配字段名
/// 在目标实体中按此字段匹配查询条件,不指定时默认与 source 相同
@@ -56,7 +73,8 @@ public @interface Trans {
/// 目标实体/字典项中要翻译出的字段名
/// 从目标实体匹配到的记录中取出此字段的值,回填到当前被 @Trans 注解的字段
/// 例如:result = "name" 表示从目标实体中取出 name 字段的值作为翻译结果
- String result();
+ /// i18n 模式下可省略(直接翻译注解字段本身),实体/字典模式必填
+ String result() default "";
/// 缓存存活时间(秒)
/// 翻译结果在缓存中的存活时长,超过后自动过期重新查询
diff --git a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/common/OperateLogType.java b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/common/OperateLogType.java
index 16ff98477..2df9a9dc6 100644
--- a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/common/OperateLogType.java
+++ b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/common/OperateLogType.java
@@ -1,8 +1,13 @@
package cn.daxpay.open.platform.core.enums.common;
+import cn.daxpay.open.platform.core.i18n.I18nSupport;
+
+import java.util.Locale;
+
/// # 操作日志业务类型枚举
///
-public enum OperateLogType {
+/// 实现 [I18nSupport], 翻译 key 前缀 `enum.operate_log_type`, 完整 key = 前缀 + "." + code
+public enum OperateLogType implements I18nSupport {
/// 其它
OTHER("其它"),
@@ -44,4 +49,16 @@ public enum OperateLogType {
public String getDescription() {
return description;
}
+
+ /// 枚举编码, 与操作日志入库的 businessType 值一致(name 小写)
+ @Override
+ public String getCode() {
+ return name().toLowerCase(Locale.ROOT);
+ }
+
+ /// 翻译 key 前缀, 对应资源文件 enum/operate_log_type.json
+ @Override
+ public String getI18nPrefix() {
+ return "enum.operate_log_type";
+ }
}