1、新增菜单翻译接口、获取菜单接口新增菜单名国际化能力;2、新增字典数据标签翻译接口、字典获取接口修改为一次性返回所有语言数据、新增字典数据国际化版本查询/更新接口、新增字典数据国际化缓存;

This commit is contained in:
gaoqr
2025-11-12 14:59:36 +08:00
parent e1793878b5
commit a26691e3d2
32 changed files with 1122 additions and 23 deletions
@@ -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";
}
@@ -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, "当前通知公告不存在");
@@ -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<List<DictDataSimpleRespVO>> getSimpleDictDataList() {
List<DictDataDO> list = dictDataService.getDictDataList(
CommonStatusEnum.ENABLE.getStatus(), null);
return success(BeanUtils.toBean(list, DictDataSimpleRespVO.class));
public CommonResult<Map<String, List<DictDataSimpleRespVO>>> 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<Boolean> trans(@RequestBody DictDataTransReqVO reqVO){
dictDataService.trans(reqVO);
return success(true);
}
@GetMapping("/i18n/version")
@Operation(summary = "获取字典数据国际化版本号")
public CommonResult<Integer> getVersion() {
return success(dictDataService.getI18nVersion());
}
@PostMapping("/i18n/version")
@Operation(summary = "更新字典数据国际化版本号")
public CommonResult<Boolean> updateVersion() {
dictDataService.updateI18nVersion();
return success(true);
}
}
@@ -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")
@@ -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;
}
@@ -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<Boolean> trans(@RequestBody MenuTransReqVO reqVO) {
menuService.trans(reqVO);
return success(true);
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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<DictDatai18nDO> {
/**
* 查询指定语种下的字典数据
*
* @param ids
* @param locale
* @return
*/
List<DictDataDO> getLocaleDictData(@Param("ids") Collection<Long> ids, @Param("locale") String locale);
}
@@ -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<DictDatai18nVersionDO> {
/**
* 更新版本号
*/
void updateVersion();
}
@@ -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<MenuI18nDO> {
}
@@ -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<MenuDO> {
default List<MenuDO> selectListByPermission(String permission) {
return selectList(MenuDO::getPermission, permission);
}
/**
* 查询对应范围的菜单
*
* @param ids 菜单编号集合
* @param isManage 是否是管理后台
* @return
*/
List<MenuDO> selectListIn(@Param("ids") Collection<Long> ids, @Param("isManage") boolean isManage, @Param("language") String language);
}
@@ -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();
}
}
@@ -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;
}
@@ -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<String, String> params = buildParams(query, from, to);
return HttpGet.get(apiHost, params);
}
private Map<String, String> buildParams(String query, String from, String to) {
Map<String, String> 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));
}
}
}
@@ -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 {
}
@@ -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;
}
@@ -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<String, String> 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<String, String> 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;
}
}
@@ -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);
}
}
@@ -0,0 +1,6 @@
/**
* 属于 system 模块的 framework.trans 封装
*
* @author 晨丰科技
*/
package com.cf.imes.module.system.framework.trans;
@@ -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<String, List<DictDataSimpleRespVO>> getAllLanguageDictData();
}
@@ -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<DictDataDO> getDictDataList(Integer status, String dictType) {
List<DictDataDO> 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<DictDatai18nDO>()
.eq(DictDatai18nDO::getLocale, to));
log.info("========== 【字典数据标签国际化】{}清理历史记录,删除 {} 条 =====", to, delete);
// 查询所有有效的菜单类型 菜单
List<DictDataDO> dictDataDOS = dictDataMapper.selectList(new LambdaQueryWrapper<DictDataDO>()
.eq(DictDataDO::getDeleted, false)
.eq(DictDataDO::getStatus, CommonStatusEnum.ENABLE.getStatus()));
// 转成有序 mapkey = 序号, value = DictDataDO
Map<Integer, DictDataDO> 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<DictDatai18nDO> 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<DictDatai18nVersionDO> 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<String, List<DictDataSimpleRespVO>> getAllLanguageDictData() {
Map<String, List<DictDataSimpleRespVO>> cache = DictDataI18nCacheUtils.getCache();
if (MapUtil.isNotEmpty(cache)) {
return cache;
}
// dictData数据作为中文字典
List<DictDataDO> list = dictDataMapper.selectListByStatusAndDictType(CommonStatusEnum.ENABLE.getStatus(), null);
list.sort(COMPARATOR_TYPE_AND_SORT);
List<Long> ids = list.stream().map(DictDataDO::getId).toList();
Map<String, List<DictDataSimpleRespVO>> 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;
}
}
@@ -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<MenuDO> getCustomEndPointMenuList(Collection<Long> ids);
/**
* 菜单目标语种翻译
*
* @param reqVO
*/
void trans(MenuTransReqVO reqVO);
}
@@ -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<MenuDO> getManageEndPointMenuList(Collection<Long> ids) {
return menuMapper.selectList(new LambdaQueryWrapperX<MenuDO>()
.inIfPresent(MenuDO::getId, ids)
.ne(MenuDO::getSourceType, MenuSourceTypeEnum.PRODUCTION.getType()));
return menuMapper.selectListIn(ids, true, LocaleUtils.getLocale());
}
@Override
@OrganIgnore
public List<MenuDO> getCustomEndPointMenuList(Collection<Long> ids) {
return menuMapper.selectList(new LambdaQueryWrapperX<MenuDO>()
.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<MenuI18nDO>()
.eq(MenuI18nDO::getLocale, to));
log.info("========== 【菜单国际化】{}清理历史记录,删除 {} 条 =====", to, delete);
// 查询所有有效的菜单类型 菜单
List<MenuDO> menuDOS = menuMapper.selectList(new LambdaQueryWrapper<MenuDO>()
.eq(MenuDO::getDeleted, false)
.eq(MenuDO::getType, MenuTypeEnum.MENU.getType()));
// 转成有序 mapkey = 序号, value = MenuDO
Map<Integer, MenuDO> 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<MenuI18nDO> 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());
}
}
}
@@ -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<String, List<DictDataSimpleRespVO>> CACHE = new ConcurrentHashMap<>();
/**
* 获取缓存
*/
public static Map<String, List<DictDataSimpleRespVO>> getCache() {
return Collections.unmodifiableMap(CACHE);
}
/**
* 缓存
* @param map
*/
public static void putCache(Map<String, List<DictDataSimpleRespVO>> map) {
CACHE.putAll(map);
}
/**
* 清除缓存
*/
public static void clearCache() {
CACHE.clear();
}
}
@@ -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();
}
}
@@ -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
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.cf.imes.module.system.dal.mysql.dict.DictDataI18nMapper">
<select id="getLocaleDictData" resultType="com.cf.imes.module.system.dal.dataobject.dict.DictDataDO">
select
d.dict_type,
i.label,
d.value,
d.color_type,
d.css_class
from system_dict_data d
left join system_dict_data_i18n i
on d.id = i.dict_data_id and i.locale = #{locale}
where d.deleted = false
and d.status = '${@com.cf.imes.framework.common.enums.CommonStatusEnum@ENABLE.status}'
<if test="ids != null and !ids.isEmpty()">
and d.id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</if>
</select>
</mapper>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.cf.imes.module.system.dal.mysql.dict.DictDataI18nVersionMapper">
<update id="updateVersion">
update system_dict_data_i18n_version set version = version + 1;
</update>
</mapper>
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.cf.imes.module.system.dal.mysql.permission.MenuMapper">
<select id="selectListIn" resultType="com.cf.imes.module.system.dal.dataobject.permission.MenuDO">
SELECT
m.id,
<if test="language != null">
i.menu_name AS name,
</if>
<if test="language == null">
m.name,
</if>
m.permission,
m.type,
m.sort,
m.parent_id AS parentId,
m.path,
m.icon,
m.component,
m.component_name AS componentName,
m.status,
m.visible,
m.keep_alive AS keepAlive,
m.always_show AS alwaysShow,
m.description,
m.source_type AS sourceType
FROM system_menu m
<if test="language != null">
LEFT JOIN system_menu_i18n i
ON m.id = i.menu_id
AND i.locale = #{language}
</if>
WHERE 1=1
<if test="ids != null and !ids.isEmpty()">
AND m.id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</if>
<if test="isManage">
AND m.source_type != '${@com.cf.imes.module.system.enums.permission.MenuSourceTypeEnum@PRODUCTION.type}'
</if>
<if test="!isManage">
AND (
m.source_type IS NULL
OR m.source_type != '${@com.cf.imes.module.system.enums.permission.MenuSourceTypeEnum@MANAGEMENT.type}'
)
</if>
</select>
</mapper>