From a26691e3d2ad15a65ddae89a094b55c307a09781 Mon Sep 17 00:00:00 2001 From: gaoqr <13665037151@163.com> Date: Wed, 12 Nov 2025 14:59:36 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E6=96=B0=E5=A2=9E=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E7=BF=BB=E8=AF=91=E6=8E=A5=E5=8F=A3=E3=80=81=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E8=8F=9C=E5=8D=95=E6=8E=A5=E5=8F=A3=E6=96=B0=E5=A2=9E=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E5=90=8D=E5=9B=BD=E9=99=85=E5=8C=96=E8=83=BD=E5=8A=9B?= =?UTF-8?q?=EF=BC=9B2=E3=80=81=E6=96=B0=E5=A2=9E=E5=AD=97=E5=85=B8?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=A0=87=E7=AD=BE=E7=BF=BB=E8=AF=91=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E3=80=81=E5=AD=97=E5=85=B8=E8=8E=B7=E5=8F=96=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E4=BF=AE=E6=94=B9=E4=B8=BA=E4=B8=80=E6=AC=A1=E6=80=A7?= =?UTF-8?q?=E8=BF=94=E5=9B=9E=E6=89=80=E6=9C=89=E8=AF=AD=E8=A8=80=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E3=80=81=E6=96=B0=E5=A2=9E=E5=AD=97=E5=85=B8=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=9B=BD=E9=99=85=E5=8C=96=E7=89=88=E6=9C=AC=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2/=E6=9B=B4=E6=96=B0=E6=8E=A5=E5=8F=A3=E3=80=81?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=AD=97=E5=85=B8=E6=95=B0=E6=8D=AE=E5=9B=BD?= =?UTF-8?q?=E9=99=85=E5=8C=96=E7=BC=93=E5=AD=98=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../redis/constants/RedisKeyConstants.java | 5 + .../system/enums/ErrorCodeConstants.java | 9 +- .../admin/dict/DictDataController.java | 28 +++- .../dict/vo/data/DictDataSimpleRespVO.java | 2 + .../dict/vo/data/DictDataTransReqVO.java | 18 +++ .../admin/permission/MenuController.java | 7 + .../permission/vo/menu/MenuTransReqVO.java | 18 +++ .../dal/dataobject/dict/DictDatai18nDO.java | 39 +++++ .../dict/DictDatai18nVersionDO.java | 25 +++ .../dal/dataobject/permission/MenuI18nDO.java | 37 +++++ .../dal/mysql/dict/DictDataI18nMapper.java | 28 ++++ .../mysql/dict/DictDataI18nVersionMapper.java | 19 +++ .../dal/mysql/permission/MenuI18nMapper.java | 14 ++ .../dal/mysql/permission/MenuMapper.java | 11 ++ ...DictDataI18nCacheRefreshRedisListener.java | 31 ++++ .../system/enums/common/LanguageEnum.java | 20 +++ .../framework/trans/config/TransApi.java | 70 ++++++++ .../trans/config/TransConfiguration.java | 9 ++ .../trans/config/TransProperties.java | 20 +++ .../system/framework/trans/core/HttpGet.java | 149 +++++++++++++++++ .../system/framework/trans/core/MD5.java | 127 +++++++++++++++ .../system/framework/trans/package-info.java | 6 + .../system/service/dict/DictDataService.java | 28 ++++ .../service/dict/DictDataServiceImpl.java | 151 +++++++++++++++++- .../service/permission/MenuService.java | 9 +- .../service/permission/MenuServiceImpl.java | 102 ++++++++++-- .../util/locale/DictDataI18nCacheUtils.java | 40 +++++ .../system/util/locale/LocaleUtils.java | 33 ++++ .../src/main/resources/application.yaml | 7 + .../mapper/dict/DictDataI18nMapper.xml | 23 +++ .../mapper/dict/DictDataI18nVersionMapper.xml | 8 + .../main/resources/mapper/menu/MenuMapper.xml | 52 ++++++ 32 files changed, 1122 insertions(+), 23 deletions(-) create mode 100644 cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/dict/vo/data/DictDataTransReqVO.java create mode 100644 cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/permission/vo/menu/MenuTransReqVO.java create mode 100644 cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/dataobject/dict/DictDatai18nDO.java create mode 100644 cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/dataobject/dict/DictDatai18nVersionDO.java create mode 100644 cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/dataobject/permission/MenuI18nDO.java create mode 100644 cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/dict/DictDataI18nMapper.java create mode 100644 cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/dict/DictDataI18nVersionMapper.java create mode 100644 cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/permission/MenuI18nMapper.java create mode 100644 cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/redis/listener/SystemDictDataI18nCacheRefreshRedisListener.java create mode 100644 cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/enums/common/LanguageEnum.java create mode 100644 cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/config/TransApi.java create mode 100644 cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/config/TransConfiguration.java create mode 100644 cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/config/TransProperties.java create mode 100644 cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/core/HttpGet.java create mode 100644 cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/core/MD5.java create mode 100644 cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/package-info.java create mode 100644 cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/util/locale/DictDataI18nCacheUtils.java create mode 100644 cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/util/locale/LocaleUtils.java create mode 100644 cf-module-system/cf-module-system-biz/src/main/resources/mapper/dict/DictDataI18nMapper.xml create mode 100644 cf-module-system/cf-module-system-biz/src/main/resources/mapper/dict/DictDataI18nVersionMapper.xml create mode 100644 cf-module-system/cf-module-system-biz/src/main/resources/mapper/menu/MenuMapper.xml diff --git a/cf-framework/cf-spring-boot-starter-redis/src/main/java/com/cf/imes/framework/redis/constants/RedisKeyConstants.java b/cf-framework/cf-spring-boot-starter-redis/src/main/java/com/cf/imes/framework/redis/constants/RedisKeyConstants.java index fc712ea03..5107c089b 100644 --- a/cf-framework/cf-spring-boot-starter-redis/src/main/java/com/cf/imes/framework/redis/constants/RedisKeyConstants.java +++ b/cf-framework/cf-spring-boot-starter-redis/src/main/java/com/cf/imes/framework/redis/constants/RedisKeyConstants.java @@ -22,4 +22,9 @@ public class RedisKeyConstants { * 软件购买 */ public static final String PRODUCT_PURCHASE_LOCK_KEY = "product_purchase:%s%s"; + + /** + * 字典数据i18n缓存刷新 + */ + public static final String DICT_DATA_I18N_CACHE_REFRESH = "DICT_DATA_I18N_CACHE_REFRESH"; } diff --git a/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/ErrorCodeConstants.java b/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/ErrorCodeConstants.java index a02a118f8..e24607a81 100644 --- a/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/ErrorCodeConstants.java +++ b/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/ErrorCodeConstants.java @@ -26,6 +26,7 @@ public class ErrorCodeConstants { public static final ErrorCode MENU_EXISTS_CHILDREN = new ErrorCode(1_002_001_004, "存在子菜单,无法删除"); public static final ErrorCode MENU_PARENT_NOT_DIR_OR_MENU = new ErrorCode(1_002_001_005, "父菜单的类型必须是目录或者菜单"); public static final ErrorCode MANAGEMENT_MENU_OPERATION_PERMISSION_ERROR = new ErrorCode(1_002_001_006, "管理端菜单操作权限不足"); + public static final ErrorCode MENU_TRANS_ERROR = new ErrorCode(1_002_001_007, "菜单翻译异常,error_code :{}"); // ========== 角色模块 1-002-002-000 ========== public static final ErrorCode ROLE_NOT_EXISTS = new ErrorCode(1_002_002_000, "角色不存在"); @@ -75,13 +76,6 @@ public class ErrorCodeConstants { public static final ErrorCode DEPT_NOT_ALLOWED_LOGIN = new ErrorCode(1_002_004_011, "部门({})已被禁用,无法登录"); public static final ErrorCode PARENT_DEPT_DISABLE = new ErrorCode(1_002_004_012, "上级部门({})已被禁用,请开启后操作或选择其他上级部门"); - - // ========== 岗位模块 1-002-005-000 ========== - public static final ErrorCode POST_NOT_FOUND = new ErrorCode(1_002_005_000, "当前岗位不存在"); - public static final ErrorCode POST_NOT_ENABLE = new ErrorCode(1_002_005_001, "岗位({}) 不处于开启状态,不允许选择"); - public static final ErrorCode POST_NAME_DUPLICATE = new ErrorCode(1_002_005_002, "已经存在该名字的岗位"); - public static final ErrorCode POST_CODE_DUPLICATE = new ErrorCode(1_002_005_003, "已经存在该编码的岗位"); - // ========== 字典类型 1-002-006-000 ========== public static final ErrorCode DICT_TYPE_NOT_EXISTS = new ErrorCode(1_002_006_001, "当前字典类型不存在"); public static final ErrorCode DICT_TYPE_NOT_ENABLE = new ErrorCode(1_002_006_002, "字典类型处于关闭状态,不允许新增或编辑"); @@ -93,6 +87,7 @@ public class ErrorCodeConstants { public static final ErrorCode DICT_DATA_NOT_EXISTS = new ErrorCode(1_002_007_001, "当前字典数据不存在"); public static final ErrorCode DICT_DATA_NOT_ENABLE = new ErrorCode(1_002_007_002, "字典数据({})不处于开启状态,不允许选择"); public static final ErrorCode DICT_DATA_VALUE_DUPLICATE = new ErrorCode(1_002_007_003, "已经存在该值的字典数据"); + public static final ErrorCode DICT_DATA_TRANS_ERROR = new ErrorCode(1_002_007_004, "字典数据标签翻译异常,error_code :{}"); // ========== 通知公告 1-002-008-000 ========== public static final ErrorCode NOTICE_NOT_FOUND = new ErrorCode(1_002_008_001, "当前通知公告不存在"); diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/dict/DictDataController.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/dict/DictDataController.java index 236e589e7..8e9283100 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/dict/DictDataController.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/dict/DictDataController.java @@ -1,6 +1,5 @@ package com.cf.imes.module.system.controller.admin.dict; -import com.cf.imes.framework.common.enums.CommonStatusEnum; import com.cf.imes.framework.common.pojo.CommonResult; import com.cf.imes.framework.common.pojo.PageParam; import com.cf.imes.framework.common.pojo.PageResult; @@ -11,6 +10,7 @@ import com.cf.imes.module.system.controller.admin.dict.vo.data.DictDataPageReqVO import com.cf.imes.module.system.controller.admin.dict.vo.data.DictDataRespVO; import com.cf.imes.module.system.controller.admin.dict.vo.data.DictDataSaveReqVO; import com.cf.imes.module.system.controller.admin.dict.vo.data.DictDataSimpleRespVO; +import com.cf.imes.module.system.controller.admin.dict.vo.data.DictDataTransReqVO; import com.cf.imes.module.system.dal.dataobject.dict.DictDataDO; import com.cf.imes.module.system.service.dict.DictDataService; import io.swagger.v3.oas.annotations.Operation; @@ -25,6 +25,7 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import java.io.IOException; import java.util.List; +import java.util.Map; import static com.cf.imes.framework.common.pojo.CommonResult.success; import static com.cf.imes.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; @@ -66,10 +67,8 @@ public class DictDataController { @GetMapping(value = {"/list-all-simple", "simple-list"}) @Operation(summary = "获得全部字典数据列表", description = "一般用于管理后台缓存字典数据在本地") // 无需添加权限认证,因为前端全局都需要 - public CommonResult> getSimpleDictDataList() { - List list = dictDataService.getDictDataList( - CommonStatusEnum.ENABLE.getStatus(), null); - return success(BeanUtils.toBean(list, DictDataSimpleRespVO.class)); + public CommonResult>> getSimpleDictDataList() { + return success(dictDataService.getAllLanguageDictData()); } @GetMapping("/page") @@ -101,4 +100,23 @@ public class DictDataController { BeanUtils.toBean(list, DictDataRespVO.class)); } + @PostMapping("/trans") + @Operation(summary = "字典数据标签对应语种翻译") + public CommonResult trans(@RequestBody DictDataTransReqVO reqVO){ + dictDataService.trans(reqVO); + return success(true); + } + + @GetMapping("/i18n/version") + @Operation(summary = "获取字典数据国际化版本号") + public CommonResult getVersion() { + return success(dictDataService.getI18nVersion()); + } + + @PostMapping("/i18n/version") + @Operation(summary = "更新字典数据国际化版本号") + public CommonResult updateVersion() { + dictDataService.updateI18nVersion(); + return success(true); + } } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/dict/vo/data/DictDataSimpleRespVO.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/dict/vo/data/DictDataSimpleRespVO.java index 7302c0f70..cb4b36b67 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/dict/vo/data/DictDataSimpleRespVO.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/dict/vo/data/DictDataSimpleRespVO.java @@ -1,10 +1,12 @@ package com.cf.imes.module.system.controller.admin.dict.vo.data; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Schema(description = "管理后台 - 数据字典精简 Response VO") @Data +@JsonInclude(JsonInclude.Include.NON_EMPTY) public class DictDataSimpleRespVO { @Schema(description = "字典类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "gender") diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/dict/vo/data/DictDataTransReqVO.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/dict/vo/data/DictDataTransReqVO.java new file mode 100644 index 000000000..1be3ee2aa --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/dict/vo/data/DictDataTransReqVO.java @@ -0,0 +1,18 @@ +package com.cf.imes.module.system.controller.admin.dict.vo.data; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * + * @author Gqr + * @since 2025/11/11 10:56 + */ +@Schema(description = "管理后台 - 字典数据标签 Request VO") +@Data +public class DictDataTransReqVO { + @Schema(description = "目标语种", example = "en") + @NotNull(message = "目标语种不能为空") + private String targetLanguage; +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/permission/MenuController.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/permission/MenuController.java index 37f0b1b4b..1e220fb7b 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/permission/MenuController.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/permission/MenuController.java @@ -11,6 +11,7 @@ import com.cf.imes.module.system.controller.admin.permission.vo.menu.MenuListReq import com.cf.imes.module.system.controller.admin.permission.vo.menu.MenuRespVO; import com.cf.imes.module.system.controller.admin.permission.vo.menu.MenuSaveVO; import com.cf.imes.module.system.controller.admin.permission.vo.menu.MenuSimpleRespVO; +import com.cf.imes.module.system.controller.admin.permission.vo.menu.MenuTransReqVO; import com.cf.imes.module.system.dal.dataobject.permission.MenuDO; import com.cf.imes.module.system.dal.dataobject.permission.RoleDO; import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO; @@ -159,4 +160,10 @@ public class MenuController { return success(BeanUtils.toBean(menu, MenuRespVO.class)); } + @PostMapping("/trans") + @Operation(summary = "菜单对应语种翻译") + public CommonResult trans(@RequestBody MenuTransReqVO reqVO) { + menuService.trans(reqVO); + return success(true); + } } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/permission/vo/menu/MenuTransReqVO.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/permission/vo/menu/MenuTransReqVO.java new file mode 100644 index 000000000..1a58c02e7 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/permission/vo/menu/MenuTransReqVO.java @@ -0,0 +1,18 @@ +package com.cf.imes.module.system.controller.admin.permission.vo.menu; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * + * @author Gqr + * @since 2025/11/11 10:56 + */ +@Schema(description = "管理后台 - 菜单翻译 Request VO") +@Data +public class MenuTransReqVO { + @Schema(description = "目标语种", example = "en") + @NotNull(message = "目标语种不能为空") + private String targetLanguage; +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/dataobject/dict/DictDatai18nDO.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/dataobject/dict/DictDatai18nDO.java new file mode 100644 index 000000000..7fc720bd7 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/dataobject/dict/DictDatai18nDO.java @@ -0,0 +1,39 @@ +package com.cf.imes.module.system.dal.dataobject.dict; + +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 字典数据标签国际化表 + * + * @author ruoyi + */ +@TableName("system_dict_data_i18n") +@KeySequence("system_dict_data_i18n_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class DictDatai18nDO { + + /** + * 主键 + */ + @TableId + private Long id; + + /** + * 字典数据主键 + */ + private Long dictDataId; + + /** + * 语种 + */ + private String locale; + + /** + * 字典标签 + */ + private String label; + +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/dataobject/dict/DictDatai18nVersionDO.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/dataobject/dict/DictDatai18nVersionDO.java new file mode 100644 index 000000000..0ccc1d192 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/dataobject/dict/DictDatai18nVersionDO.java @@ -0,0 +1,25 @@ +package com.cf.imes.module.system.dal.dataobject.dict; + +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 字典数据标签国际化版本号表 + * + * @author ruoyi + */ +@TableName("system_dict_data_i18n_version") +@KeySequence("system_dict_data_i18n_version_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class DictDatai18nVersionDO { + + /** + * 主键 + */ + @TableId + private Long id; + + private Integer version; +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/dataobject/permission/MenuI18nDO.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/dataobject/permission/MenuI18nDO.java new file mode 100644 index 000000000..54569dbfa --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/dataobject/permission/MenuI18nDO.java @@ -0,0 +1,37 @@ +package com.cf.imes.module.system.dal.dataobject.permission; + +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 菜单国际化DO + * @author Gqr + * @since 2025/11/11 11:08 + */ +@TableName("system_menu_i18n") +@KeySequence("system_menu_i18n_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class MenuI18nDO { + /** + * 主键 + */ + @TableId + private Long id; + + /** + * 菜单id + */ + private Long menuId; + + /** + * 语种 + */ + private String locale; + + /** + * 菜单名称 + */ + private String menuName; +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/dict/DictDataI18nMapper.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/dict/DictDataI18nMapper.java new file mode 100644 index 000000000..ca58a5d92 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/dict/DictDataI18nMapper.java @@ -0,0 +1,28 @@ +package com.cf.imes.module.system.dal.mysql.dict; + +import com.cf.imes.framework.mybatis.core.mapper.BaseMapperX; +import com.cf.imes.module.system.dal.dataobject.dict.DictDataDO; +import com.cf.imes.module.system.dal.dataobject.dict.DictDatai18nDO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/** + * 字典数据标签国际化 Mapper + * + * @author Gqr + * @since 2025/11/11 17:10 + */ +@Mapper +public interface DictDataI18nMapper extends BaseMapperX { + /** + * 查询指定语种下的字典数据 + * + * @param ids + * @param locale + * @return + */ + List getLocaleDictData(@Param("ids") Collection ids, @Param("locale") String locale); +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/dict/DictDataI18nVersionMapper.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/dict/DictDataI18nVersionMapper.java new file mode 100644 index 000000000..8db533aba --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/dict/DictDataI18nVersionMapper.java @@ -0,0 +1,19 @@ +package com.cf.imes.module.system.dal.mysql.dict; + +import com.cf.imes.framework.mybatis.core.mapper.BaseMapperX; +import com.cf.imes.module.system.dal.dataobject.dict.DictDatai18nVersionDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 字典数据标签国际化版本号 Mapper + * + * @author Gqr + * @since 2025/11/11 17:10 + */ +@Mapper +public interface DictDataI18nVersionMapper extends BaseMapperX { + /** + * 更新版本号 + */ + void updateVersion(); +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/permission/MenuI18nMapper.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/permission/MenuI18nMapper.java new file mode 100644 index 000000000..aca21e31b --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/permission/MenuI18nMapper.java @@ -0,0 +1,14 @@ +package com.cf.imes.module.system.dal.mysql.permission; + +import com.cf.imes.framework.mybatis.core.mapper.BaseMapperX; +import com.cf.imes.module.system.dal.dataobject.permission.MenuI18nDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 菜单国际化 Mapper + * @author Gqr + * @since 2025/11/11 11:10 + */ +@Mapper +public interface MenuI18nMapper extends BaseMapperX { +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/permission/MenuMapper.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/permission/MenuMapper.java index c131a900c..e862712bd 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/permission/MenuMapper.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/permission/MenuMapper.java @@ -5,7 +5,9 @@ import com.cf.imes.framework.mybatis.core.query.LambdaQueryWrapperX; import com.cf.imes.module.system.controller.admin.permission.vo.menu.MenuListReqVO; import com.cf.imes.module.system.dal.dataobject.permission.MenuDO; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.Collection; import java.util.List; @Mapper @@ -30,4 +32,13 @@ public interface MenuMapper extends BaseMapperX { default List selectListByPermission(String permission) { return selectList(MenuDO::getPermission, permission); } + + /** + * 查询对应范围的菜单 + * + * @param ids 菜单编号集合 + * @param isManage 是否是管理后台 + * @return + */ + List selectListIn(@Param("ids") Collection ids, @Param("isManage") boolean isManage, @Param("language") String language); } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/redis/listener/SystemDictDataI18nCacheRefreshRedisListener.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/redis/listener/SystemDictDataI18nCacheRefreshRedisListener.java new file mode 100644 index 000000000..b3bd4f7b5 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/redis/listener/SystemDictDataI18nCacheRefreshRedisListener.java @@ -0,0 +1,31 @@ +package com.cf.imes.module.system.dal.redis.listener; + +import com.cf.imes.framework.mq.redis.core.pubsub.AbstractRedisSimpleMessageListener; +import com.cf.imes.framework.redis.constants.RedisKeyConstants; +import com.cf.imes.module.system.util.locale.DictDataI18nCacheUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.listener.ChannelTopic; +import org.springframework.stereotype.Component; + +/** + * system DICT_DATA_I18N_CACHE_REFRESH redis监听器 + * + * @author Gqr + * @since 2024/6/13 10:32 + */ +@Component +@Slf4j +public class SystemDictDataI18nCacheRefreshRedisListener extends AbstractRedisSimpleMessageListener { + + + public SystemDictDataI18nCacheRefreshRedisListener() { + super.setTopic(new ChannelTopic(RedisKeyConstants.DICT_DATA_I18N_CACHE_REFRESH)); + } + + @Override + public void onMessage(Message message, byte[] bytes) { + log.info("[SystemDictDataI18nCacheRefreshRedisListener][onMessage][{}收到刷新本地字典数据国际化缓存]", message); + DictDataI18nCacheUtils.clearCache(); + } +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/enums/common/LanguageEnum.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/enums/common/LanguageEnum.java new file mode 100644 index 000000000..8709a5170 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/enums/common/LanguageEnum.java @@ -0,0 +1,20 @@ +package com.cf.imes.module.system.enums.common; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 语种枚举 + * + * @author Gqr + * @since 2025/11/12 11:30 + */ +@Getter +@AllArgsConstructor +public enum LanguageEnum { + ZH("zh"), + EN("en"), + CHT("cht"); + + private String language; +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/config/TransApi.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/config/TransApi.java new file mode 100644 index 000000000..c315c81b0 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/config/TransApi.java @@ -0,0 +1,70 @@ +package com.cf.imes.module.system.framework.trans.config; + +import cn.hutool.core.text.UnicodeUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.cf.imes.module.system.framework.trans.core.HttpGet; +import com.cf.imes.module.system.framework.trans.core.MD5; + +import java.util.HashMap; +import java.util.Map; + +/** + * + * @author Gqr + * @since 2025/11/11 9:34 + */ +public class TransApi { + + private String appid; + private String securityKey; + private String apiHost; + + public TransApi(String appid, String securityKey, String apiHost) { + this.appid = appid; + this.securityKey = securityKey; + this.apiHost = apiHost; + } + + public String getTransResult(String query, String from, String to) { + Map params = buildParams(query, from, to); + return HttpGet.get(apiHost, params); + } + + private Map buildParams(String query, String from, String to) { + Map params = new HashMap<>(); + params.put("q", query); + params.put("from", from); + params.put("to", to); + + params.put("appid", appid); + + // 随机数 + String salt = String.valueOf(System.currentTimeMillis()); + params.put("salt", salt); + + // 签名 + String src = appid + query + salt + securityKey; // 加密前的原文 + params.put("sign", MD5.md5(src)); + + return params; + } + + public static void main(String[] args) { + TransApi api = new TransApi("20251110002494620", "_CUXY5Hk4WLBnYRQBcWw","https://fanyi-api.baidu.com/api/trans/vip/translate"); + String query = "测试翻译机\n" + + "语言是什么"; + String json = api.getTransResult(query, "auto", "cht"); + + JSONObject obj = JSON.parseObject(json); + JSONArray arr = obj.getJSONArray("trans_result"); + + for (int i = 0; i < arr.size(); i++) { + JSONObject item = arr.getJSONObject(i); + String src = item.getString("src"); + String dst = item.getString("dst"); + System.out.println(src + " => " + UnicodeUtil.toString(dst)); + } + } +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/config/TransConfiguration.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/config/TransConfiguration.java new file mode 100644 index 000000000..a8939bd62 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/config/TransConfiguration.java @@ -0,0 +1,9 @@ +package com.cf.imes.module.system.framework.trans.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(TransProperties.class) +public class TransConfiguration { +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/config/TransProperties.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/config/TransProperties.java new file mode 100644 index 000000000..33602ccc3 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/config/TransProperties.java @@ -0,0 +1,20 @@ +package com.cf.imes.module.system.framework.trans.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 翻译api配置 + * + * @author Gqr + * @since 2025/11/11 9:23 + */ +@ConfigurationProperties(prefix = "trans.config") +@Data +public class TransProperties { + private String appId; + + private String securityKey; + + private String apiHost; +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/core/HttpGet.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/core/HttpGet.java new file mode 100644 index 000000000..eb19636b5 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/core/HttpGet.java @@ -0,0 +1,149 @@ +package com.cf.imes.module.system.framework.trans.core; + + +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Map; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +/** + * + * + * @author Gqr + * @since 2025/11/11 9:37 + */ +public class HttpGet { + protected static final int SOCKET_TIMEOUT = 10000; // 10S + protected static final String GET = "GET"; + + public static String get(String host, Map params) { + try { + // 设置SSLContext + SSLContext sslcontext = SSLContext.getInstance("TLS"); + sslcontext.init(null, null, null); + + String sendUrl = getUrlWithQueryString(host, params); + + + URL uri = new URL(sendUrl); // 创建URL对象 + HttpURLConnection conn = (HttpURLConnection) uri.openConnection(); + if (conn instanceof HttpsURLConnection) { + ((HttpsURLConnection) conn).setSSLSocketFactory(sslcontext.getSocketFactory()); + } + + conn.setConnectTimeout(SOCKET_TIMEOUT); // 设置相应超时 + conn.setRequestMethod(GET); + int statusCode = conn.getResponseCode(); + if (statusCode != HttpURLConnection.HTTP_OK) { + System.out.println("Http错误码:" + statusCode); + } + + // 读取服务器的数据 + InputStream is = conn.getInputStream(); + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + StringBuilder builder = new StringBuilder(); + String line = null; + while ((line = br.readLine()) != null) { + builder.append(line); + } + + String text = builder.toString(); + + close(br); // 关闭数据流 + close(is); // 关闭数据流 + conn.disconnect(); // 断开连接 + + return text; + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (KeyManagementException e) { + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + + return null; + } + + public static String getUrlWithQueryString(String url, Map params) { + if (params == null) { + return url; + } + + StringBuilder builder = new StringBuilder(url); + if (url.contains("?")) { + builder.append("&"); + } else { + builder.append("?"); + } + + int i = 0; + for (String key : params.keySet()) { + String value = params.get(key); + if (value == null) { // 过滤空的key + continue; + } + + if (i != 0) { + builder.append('&'); + } + + builder.append(key); + builder.append('='); + builder.append(encode(value)); + + i++; + } + + return builder.toString(); + } + + protected static void close(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + /** + * 对输入的字符串进行URL编码, 即转换为%20这种形式 + * + * @param input 原文 + * @return URL编码. 如果编码失败, 则返回原文 + */ + public static String encode(String input) { + if (input == null) { + return ""; + } + + try { + return URLEncoder.encode(input, "utf-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + return input; + } + +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/core/MD5.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/core/MD5.java new file mode 100644 index 000000000..abffee9c3 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/core/MD5.java @@ -0,0 +1,127 @@ +package com.cf.imes.module.system.framework.trans.core; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * + * + * @author Gqr + * @since 2025/11/11 9:36 + */ +public class MD5 { + // 首先初始化一个字符数组,用来存放每个16进制字符 + private static final char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', + 'e', 'f' }; + + /** + * 获得一个字符串的MD5值 + * + * @param input 输入的字符串 + * @return 输入字符串的MD5值 + * + */ + public static String md5(String input) { + if (input == null) + return null; + + try { + try { + + // 拿到一个MD5转换器(如果想要SHA1参数换成”SHA1”) + MessageDigest messageDigest = MessageDigest.getInstance("MD5"); + // 输入的字符串转换成字节数组 + byte[] inputByteArray = input.getBytes("utf-8"); + // inputByteArray是输入字符串转换得到的字节数组 + messageDigest.update(inputByteArray); + // 转换并返回结果,也是字节数组,包含16个元素 + byte[] resultByteArray = messageDigest.digest(); + // 字符数组转换成字符串返回 + return byteArrayToHex(resultByteArray); + } catch (UnsupportedEncodingException e) { + return null; + } + } catch (NoSuchAlgorithmException e) { + return null; + } + } + + /** + * 获取文件的MD5值 + * + * @param file + * @return + */ + public static String md5(File file) { + try { + if (!file.isFile()) { + System.err.println("文件" + file.getAbsolutePath() + "不存在或者不是文件"); + return null; + } + + FileInputStream in = new FileInputStream(file); + + String result = md5(in); + + in.close(); + + return result; + + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + return null; + } + + public static String md5(InputStream in) { + + try { + MessageDigest messagedigest = MessageDigest.getInstance("MD5"); + + byte[] buffer = new byte[1024]; + int read = 0; + while ((read = in.read(buffer)) != -1) { + messagedigest.update(buffer, 0, read); + } + + in.close(); + + String result = byteArrayToHex(messagedigest.digest()); + + return result; + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + return null; + } + + private static String byteArrayToHex(byte[] byteArray) { + // new一个字符数组,这个就是用来组成结果字符串的(解释一下:一个byte是八位二进制,也就是2位十六进制字符(2的8次方等于16的2次方)) + char[] resultCharArray = new char[byteArray.length * 2]; + // 遍历字节数组,通过位运算(位运算效率高),转换成字符放到字符数组中去 + int index = 0; + for (byte b : byteArray) { + resultCharArray[index++] = hexDigits[b >>> 4 & 0xf]; + resultCharArray[index++] = hexDigits[b & 0xf]; + } + + // 字符数组组合成字符串返回 + return new String(resultCharArray); + + } + +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/package-info.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/package-info.java new file mode 100644 index 000000000..c62eebb2f --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/trans/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 system 模块的 framework.trans 封装 + * + * @author 晨丰科技 + */ +package com.cf.imes.module.system.framework.trans; diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/dict/DictDataService.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/dict/DictDataService.java index e546e366c..bd4836980 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/dict/DictDataService.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/dict/DictDataService.java @@ -3,11 +3,14 @@ package com.cf.imes.module.system.service.dict; import com.cf.imes.framework.common.pojo.PageResult; import com.cf.imes.module.system.controller.admin.dict.vo.data.DictDataPageReqVO; import com.cf.imes.module.system.controller.admin.dict.vo.data.DictDataSaveReqVO; +import com.cf.imes.module.system.controller.admin.dict.vo.data.DictDataSimpleRespVO; +import com.cf.imes.module.system.controller.admin.dict.vo.data.DictDataTransReqVO; import com.cf.imes.module.system.dal.dataobject.dict.DictDataDO; import org.springframework.lang.Nullable; import java.util.Collection; import java.util.List; +import java.util.Map; /** * 字典数据 Service 接口 @@ -99,4 +102,29 @@ public interface DictDataService { */ DictDataDO parseDictData(String dictType, String label); + /** + * 字典数据标签对应语种翻译 + * + * @param reqVO + */ + void trans(DictDataTransReqVO reqVO); + + /** + * 获取字典数据国际化版本号 + * + * @return + */ + Integer getI18nVersion(); + + /** + * 更新字典数据国际化版本号 + */ + void updateI18nVersion(); + + /** + * 获取所有语种的字典数据 + * + * @return + */ + Map> getAllLanguageDictData(); } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/dict/DictDataServiceImpl.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/dict/DictDataServiceImpl.java index e004a9a42..944319cb7 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/dict/DictDataServiceImpl.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/dict/DictDataServiceImpl.java @@ -1,6 +1,12 @@ package com.cf.imes.module.system.service.dict; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.text.UnicodeUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.cf.imes.framework.common.enums.CommonStatusEnum; import com.cf.imes.framework.common.exception.util.ServiceExceptionUtil; import com.cf.imes.framework.common.pojo.PageResult; @@ -8,21 +14,41 @@ import com.cf.imes.framework.common.util.collection.CollectionUtils; import com.cf.imes.framework.common.util.object.BeanUtils; import com.cf.imes.module.system.controller.admin.dict.vo.data.DictDataPageReqVO; import com.cf.imes.module.system.controller.admin.dict.vo.data.DictDataSaveReqVO; +import com.cf.imes.module.system.controller.admin.dict.vo.data.DictDataSimpleRespVO; +import com.cf.imes.module.system.controller.admin.dict.vo.data.DictDataTransReqVO; import com.cf.imes.module.system.dal.dataobject.dict.DictDataDO; +import com.cf.imes.module.system.dal.dataobject.dict.DictDatai18nDO; +import com.cf.imes.module.system.dal.dataobject.dict.DictDatai18nVersionDO; import com.cf.imes.module.system.dal.dataobject.dict.DictTypeDO; +import com.cf.imes.module.system.dal.mysql.dict.DictDataI18nMapper; +import com.cf.imes.module.system.dal.mysql.dict.DictDataI18nVersionMapper; import com.cf.imes.module.system.dal.mysql.dict.DictDataMapper; import com.cf.imes.module.system.enums.ErrorCodeConstants; +import com.cf.imes.module.system.enums.common.LanguageEnum; +import com.cf.imes.module.system.framework.trans.config.TransApi; +import com.cf.imes.module.system.framework.trans.config.TransProperties; +import com.cf.imes.module.system.util.locale.DictDataI18nCacheUtils; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import java.util.stream.IntStream; -import static com.cf.imes.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.cf.imes.module.system.enums.ErrorCodeConstants.DICT_DATA_TRANS_ERROR; /** * 字典数据 Service 实现类 @@ -46,6 +72,18 @@ public class DictDataServiceImpl implements DictDataService { @Resource private DictDataMapper dictDataMapper; + @Resource + private DictDataI18nMapper dictDatai18nMapper; + + @Resource + private TransProperties transProperties; + + @Resource + private DictDataI18nVersionMapper dictDataI18nVersionMapper; + + @Resource + private StringRedisTemplate stringRedisTemplate; + @Override public List getDictDataList(Integer status, String dictType) { List list = dictDataMapper.selectListByStatusAndDictType(status, dictType); @@ -170,4 +208,115 @@ public class DictDataServiceImpl implements DictDataService { return dictDataMapper.selectByDictTypeAndLabel(dictType, label); } + @Override + @Transactional(rollbackFor = Exception.class) + public void trans(DictDataTransReqVO reqVO) { + String to = reqVO.getTargetLanguage(); + // 清理对应语种下的历史记录 + int delete = dictDatai18nMapper.delete(new LambdaQueryWrapper() + .eq(DictDatai18nDO::getLocale, to)); + log.info("========== 【字典数据标签国际化】{}清理历史记录,删除 {} 条 =====", to, delete); + + // 查询所有有效的菜单类型 菜单 + List dictDataDOS = dictDataMapper.selectList(new LambdaQueryWrapper() + .eq(DictDataDO::getDeleted, false) + .eq(DictDataDO::getStatus, CommonStatusEnum.ENABLE.getStatus())); + + // 转成有序 map(key = 序号, value = DictDataDO) + Map dataMap = IntStream.range(0, dictDataDOS.size()) + .boxed() + .collect(Collectors.toMap( + i -> i, + dictDataDOS::get, + (v1, v2) -> v1, + LinkedHashMap::new + )); + + // 拼接 menu name,用换行分隔 + String namesForQuery = dataMap.values().stream() + .map(DictDataDO::getLabel) + .filter(Objects::nonNull) + .collect(Collectors.joining("\n")); + + // 请求翻译api + TransApi transApi = new TransApi(transProperties.getAppId(), transProperties.getSecurityKey(), transProperties.getApiHost()); + String transResult = transApi.getTransResult(namesForQuery, "zh", to); + + JSONObject obj = JSON.parseObject(transResult); + String errorCode = obj.getString("error_code"); + if (StringUtils.isNotEmpty(errorCode)) { + throw ServiceExceptionUtil.exception(DICT_DATA_TRANS_ERROR, errorCode); + } + JSONArray arr = obj.getJSONArray("trans_result"); + + List i18nList = new ArrayList<>(); + for (int i = 0; i < arr.size(); i++) { + DictDataDO dictDataDO = dataMap.get(i); + JSONObject item = arr.getJSONObject(i); + String src = item.getString("src"); + String dst = item.getString("dst"); + + if (dictDataDO == null || !dictDataDO.getLabel().equals(UnicodeUtil.toString(src))) { + continue; + } + + String name = UnicodeUtil.toString(dst); + DictDatai18nDO i18n = new DictDatai18nDO(); + i18n.setDictDataId(dictDataDO.getId()); + i18n.setLocale(to); + i18n.setLabel(name); + + i18nList.add(i18n); + } + + if (CollUtil.isNotEmpty(i18nList)) { + dictDatai18nMapper.insertBatch(i18nList); + log.info("========== 【字典数据标签国际化】{}成功,插入 {} 条 =====", to, i18nList.size()); + } + } + + @Override + public Integer getI18nVersion() { + List dictDatai18nVersionDOS = dictDataI18nVersionMapper.selectList(); + if (CollUtil.isNotEmpty(dictDatai18nVersionDOS)) { + return dictDatai18nVersionDOS.get(0).getVersion(); + } + return 0; + } + + @Override + public void updateI18nVersion() { + // 更新版本号 + dictDataI18nVersionMapper.updateVersion(); + + // 通知清理cache + stringRedisTemplate.convertAndSend(com.cf.imes.framework.redis.constants.RedisKeyConstants.DICT_DATA_I18N_CACHE_REFRESH, ""); + } + + @Override + public Map> getAllLanguageDictData() { + Map> cache = DictDataI18nCacheUtils.getCache(); + if (MapUtil.isNotEmpty(cache)) { + return cache; + } + // dictData数据作为中文字典 + List list = dictDataMapper.selectListByStatusAndDictType(CommonStatusEnum.ENABLE.getStatus(), null); + list.sort(COMPARATOR_TYPE_AND_SORT); + + List ids = list.stream().map(DictDataDO::getId).toList(); + + Map> resultMap = new ConcurrentHashMap<>(); + // 获取每种语言的字典数据 + for (LanguageEnum languageEnum : LanguageEnum.values()) { + if (LanguageEnum.ZH.equals(languageEnum)) { + resultMap.put(languageEnum.getLanguage(), BeanUtils.toBean(list, DictDataSimpleRespVO.class)); + } else { + resultMap.put(languageEnum.getLanguage(), BeanUtils.toBean(dictDatai18nMapper.getLocaleDictData(ids, languageEnum.getLanguage()), DictDataSimpleRespVO.class)); + } + } + + // 缓存 + DictDataI18nCacheUtils.putCache(resultMap); + return resultMap; + } } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/permission/MenuService.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/permission/MenuService.java index 3bf907a1d..7eac8db59 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/permission/MenuService.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/permission/MenuService.java @@ -2,11 +2,11 @@ package com.cf.imes.module.system.service.permission; import com.cf.imes.module.system.controller.admin.permission.vo.menu.MenuListReqVO; import com.cf.imes.module.system.controller.admin.permission.vo.menu.MenuSaveVO; +import com.cf.imes.module.system.controller.admin.permission.vo.menu.MenuTransReqVO; import com.cf.imes.module.system.dal.dataobject.permission.MenuDO; import java.util.Collection; import java.util.List; -import java.util.Set; /** * 菜单 Service 接口 @@ -102,4 +102,11 @@ public interface MenuService { * @return */ List getCustomEndPointMenuList(Collection ids); + + /** + * 菜单目标语种翻译 + * + * @param reqVO + */ + void trans(MenuTransReqVO reqVO); } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/permission/MenuServiceImpl.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/permission/MenuServiceImpl.java index a08ab9399..2553a3bc8 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/permission/MenuServiceImpl.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/permission/MenuServiceImpl.java @@ -1,22 +1,34 @@ package com.cf.imes.module.system.service.permission; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.text.UnicodeUtil; import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.cf.imes.framework.common.exception.ServiceException; +import com.cf.imes.framework.common.exception.util.ServiceExceptionUtil; import com.cf.imes.framework.common.util.object.BeanUtils; -import com.cf.imes.framework.mybatis.core.query.LambdaQueryWrapperX; import com.cf.imes.framework.organ.core.aop.OrganIgnore; import com.cf.imes.framework.security.core.util.SecurityFrameworkUtils; import com.cf.imes.module.system.controller.admin.permission.vo.menu.MenuListReqVO; import com.cf.imes.module.system.controller.admin.permission.vo.menu.MenuSaveVO; +import com.cf.imes.module.system.controller.admin.permission.vo.menu.MenuTransReqVO; import com.cf.imes.module.system.dal.dataobject.permission.MenuDO; +import com.cf.imes.module.system.dal.dataobject.permission.MenuI18nDO; import com.cf.imes.module.system.dal.mysql.permission.MenuMapper; +import com.cf.imes.module.system.dal.mysql.permission.MenuI18nMapper; import com.cf.imes.module.system.dal.redis.RedisKeyConstants; import com.cf.imes.module.system.enums.permission.MenuSourceTypeEnum; import com.cf.imes.module.system.enums.permission.MenuTypeEnum; +import com.cf.imes.module.system.framework.trans.config.TransApi; +import com.cf.imes.module.system.framework.trans.config.TransProperties; import com.cf.imes.module.system.service.organ.OrganService; +import com.cf.imes.module.system.util.locale.LocaleUtils; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; @@ -30,8 +42,13 @@ import org.springframework.transaction.annotation.Transactional; import jakarta.annotation.Resource; import java.util.ArrayList; import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import static com.cf.imes.framework.common.exception.util.ServiceExceptionUtil.exception; import static com.cf.imes.framework.common.util.collection.CollectionUtils.convertList; @@ -57,6 +74,12 @@ public class MenuServiceImpl implements MenuService { @Autowired private RedisTemplate redisTemplate; + @Resource + private MenuI18nMapper menui18nMapper; + + @Resource + private TransProperties transProperties; + private static final String FIELD_CHARACTER = ":*:%s"; @@ -327,18 +350,79 @@ public class MenuServiceImpl implements MenuService { @Override @OrganIgnore public List getManageEndPointMenuList(Collection ids) { - return menuMapper.selectList(new LambdaQueryWrapperX() - .inIfPresent(MenuDO::getId, ids) - .ne(MenuDO::getSourceType, MenuSourceTypeEnum.PRODUCTION.getType())); + return menuMapper.selectListIn(ids, true, LocaleUtils.getLocale()); } @Override @OrganIgnore public List getCustomEndPointMenuList(Collection ids) { - return menuMapper.selectList(new LambdaQueryWrapperX() - .inIfPresent(MenuDO::getId, ids) - .and(wr -> wr.or(wrapper -> wrapper - .isNull(MenuDO::getSourceType)) - .or(wrapper -> wrapper.ne(MenuDO::getSourceType, MenuSourceTypeEnum.MANAGEMENT.getType())))); + return menuMapper.selectListIn(ids, false, LocaleUtils.getLocale()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void trans(MenuTransReqVO reqVO) { + String to = reqVO.getTargetLanguage(); + // 清理对应语种下的历史记录 + int delete = menui18nMapper.delete(new LambdaQueryWrapper() + .eq(MenuI18nDO::getLocale, to)); + log.info("========== 【菜单国际化】{}清理历史记录,删除 {} 条 =====", to, delete); + + // 查询所有有效的菜单类型 菜单 + List menuDOS = menuMapper.selectList(new LambdaQueryWrapper() + .eq(MenuDO::getDeleted, false) + .eq(MenuDO::getType, MenuTypeEnum.MENU.getType())); + + // 转成有序 map(key = 序号, value = MenuDO) + Map menuMap = IntStream.range(0, menuDOS.size()) + .boxed() + .collect(Collectors.toMap( + i -> i, + menuDOS::get, + (v1, v2) -> v1, + LinkedHashMap::new + )); + + // 拼接 menu name,用换行分隔 + String namesForQuery = menuMap.values().stream() + .map(MenuDO::getName) + .filter(Objects::nonNull) + .collect(Collectors.joining("\n")); + + // 请求翻译api + TransApi transApi = new TransApi(transProperties.getAppId(), transProperties.getSecurityKey(), transProperties.getApiHost()); + String transResult = transApi.getTransResult(namesForQuery, "zh", to); + + JSONObject obj = JSON.parseObject(transResult); + String errorCode = obj.getString("error_code"); + if (StringUtils.isNotEmpty(errorCode)) { + throw ServiceExceptionUtil.exception(MENU_TRANS_ERROR, errorCode); + } + JSONArray arr = obj.getJSONArray("trans_result"); + + List i18nList = new ArrayList<>(); + for (int i = 0; i < arr.size(); i++) { + MenuDO menuDO = menuMap.get(i); + JSONObject item = arr.getJSONObject(i); + String src = item.getString("src"); + String dst = item.getString("dst"); + + if (menuDO == null || !menuDO.getName().equals(UnicodeUtil.toString(src))) { + continue; + } + + String name = UnicodeUtil.toString(dst); + MenuI18nDO i18n = new MenuI18nDO(); + i18n.setMenuId(menuDO.getId()); + i18n.setLocale(to); + i18n.setMenuName(name); + + i18nList.add(i18n); + } + + if (CollUtil.isNotEmpty(i18nList)) { + menui18nMapper.insertBatch(i18nList); + log.info("========== 【菜单国际化】{}成功,插入 {} 条 =====", to, i18nList.size()); + } } } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/util/locale/DictDataI18nCacheUtils.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/util/locale/DictDataI18nCacheUtils.java new file mode 100644 index 000000000..c0222d905 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/util/locale/DictDataI18nCacheUtils.java @@ -0,0 +1,40 @@ +package com.cf.imes.module.system.util.locale; + +import com.cf.imes.module.system.controller.admin.dict.vo.data.DictDataSimpleRespVO; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 字典数据标签国际化缓存工具类 + * + * @author Gqr + * @since 2025/11/12 13:38 + */ +public class DictDataI18nCacheUtils { + private static final Map> CACHE = new ConcurrentHashMap<>(); + + /** + * 获取缓存 + */ + public static Map> getCache() { + return Collections.unmodifiableMap(CACHE); + } + + /** + * 缓存 + * @param map + */ + public static void putCache(Map> map) { + CACHE.putAll(map); + } + + /** + * 清除缓存 + */ + public static void clearCache() { + CACHE.clear(); + } +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/util/locale/LocaleUtils.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/util/locale/LocaleUtils.java new file mode 100644 index 000000000..12385dd36 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/util/locale/LocaleUtils.java @@ -0,0 +1,33 @@ +package com.cf.imes.module.system.util.locale; + +import org.springframework.context.i18n.LocaleContextHolder; + +import java.util.Locale; + +/** + * 国际化utils + * + * @author Gqr + * @since 2025/11/11 15:15 + */ +public class LocaleUtils { + + /** + * 获取当前语言,中文就默认null + * + * @return + */ + public static String getLocale() { + Locale locale = LocaleContextHolder.getLocale(); + + if (locale == null) { + return null; + } + + if (Locale.CHINA.equals(locale)) { + return null; + } + + return locale.getLanguage(); + } +} diff --git a/cf-module-system/cf-module-system-biz/src/main/resources/application.yaml b/cf-module-system/cf-module-system-biz/src/main/resources/application.yaml index 510d53791..c2bddba0a 100644 --- a/cf-module-system/cf-module-system-biz/src/main/resources/application.yaml +++ b/cf-module-system/cf-module-system-biz/src/main/resources/application.yaml @@ -98,4 +98,11 @@ spring: repositories: enabled: false # 项目未使用到 Spring Data Redis 的 Repository,所以直接禁用,保证启动速度 +trans: + config: + appId: 20251110002494620 + securityKey: _CUXY5Hk4WLBnYRQBcWw + apiHost: https://fanyi-api.baidu.com/api/trans/vip/translate + + debug: false diff --git a/cf-module-system/cf-module-system-biz/src/main/resources/mapper/dict/DictDataI18nMapper.xml b/cf-module-system/cf-module-system-biz/src/main/resources/mapper/dict/DictDataI18nMapper.xml new file mode 100644 index 000000000..e82d8ccf6 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/resources/mapper/dict/DictDataI18nMapper.xml @@ -0,0 +1,23 @@ + + + + + \ No newline at end of file diff --git a/cf-module-system/cf-module-system-biz/src/main/resources/mapper/dict/DictDataI18nVersionMapper.xml b/cf-module-system/cf-module-system-biz/src/main/resources/mapper/dict/DictDataI18nVersionMapper.xml new file mode 100644 index 000000000..b947a1516 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/resources/mapper/dict/DictDataI18nVersionMapper.xml @@ -0,0 +1,8 @@ + + + + + + update system_dict_data_i18n_version set version = version + 1; + + \ No newline at end of file diff --git a/cf-module-system/cf-module-system-biz/src/main/resources/mapper/menu/MenuMapper.xml b/cf-module-system/cf-module-system-biz/src/main/resources/mapper/menu/MenuMapper.xml new file mode 100644 index 000000000..2a894f7e0 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/resources/mapper/menu/MenuMapper.xml @@ -0,0 +1,52 @@ + + + + + \ No newline at end of file