feat: 使用json、excel批量修改多语言

This commit is contained in:
bunny 2025-04-23 23:41:01 +08:00
parent a20f272694
commit 3f4d145f99
13 changed files with 359 additions and 139 deletions

View File

@ -1,49 +0,0 @@
package cn.bunny.services.service.configuration.impl;
import cn.bunny.domain.i18n.entity.I18n;
import cn.bunny.services.mapper.configuration.I18nMapper;
import cn.bunny.services.utils.system.I18nUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@SpringBootTest
class I18nServiceImplTest extends ServiceImpl<I18nMapper, I18n> {
@Test
void downloadI18n() {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream);
// 查找默认语言内容
List<I18n> i18nList = list();
HashMap<String, Object> hashMap = I18nUtil.getMap(i18nList);
hashMap.forEach((k, v) -> {
try {
ZipEntry zipEntry = new ZipEntry(k + ".json");
zipOutputStream.putNextEntry(zipEntry);
zipOutputStream.write(JSON.toJSONString(v).getBytes(StandardCharsets.UTF_8));
zipOutputStream.closeEntry();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
try {
zipOutputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,87 @@
package impl;
import cn.bunny.domain.i18n.entity.I18n;
import cn.bunny.domain.i18n.excel.I18nExcel;
import cn.bunny.services.mapper.configuration.I18nMapper;
import cn.bunny.services.utils.i8n.I18nUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class I18nServiceImplTest extends ServiceImpl<I18nMapper, I18n> {
@Test
void downloadI18n() {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream);
// 查找默认语言内容
List<I18n> i18nList = list();
HashMap<String, Object> hashMap = I18nUtil.getMap(i18nList);
hashMap.forEach((k, v) -> {
try {
ZipEntry zipEntry = new ZipEntry(k + ".json");
zipOutputStream.putNextEntry(zipEntry);
zipOutputStream.write(JSON.toJSONString(v).getBytes(StandardCharsets.UTF_8));
zipOutputStream.closeEntry();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
try {
zipOutputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Test
void downloadI18nByExcel() {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) {
// 查找默认语言内容
List<I18n> i18nList = list();
Map<String, List<I18nExcel>> hashMap = i18nList.stream()
.collect(Collectors.groupingBy(
I18n::getTypeName,
Collectors.mapping((I18n i18n) -> {
String keyName = i18n.getKeyName();
String translation = i18n.getTranslation();
return I18nExcel.builder().keyName(keyName).translation(translation).build();
}, Collectors.toList())
));
hashMap.forEach((key, value) -> {
// EasyExcel.write(key + ".xlsx", I18nExcel.class).sheet(key).doWrite(value);
try {
ZipEntry zipEntry = new ZipEntry(key + ".xlsx");
zipOutputStream.putNextEntry(zipEntry);
// 直接写入到ZipOutputStream
EasyExcel.write(zipOutputStream, I18nExcel.class).sheet(key).doWrite(value);
zipOutputStream.closeEntry();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -60,8 +60,8 @@ public class I18nController {
@Operation(summary = "下载多语言配置", description = "下载多语言配置")
@GetMapping("downloadI18n")
public ResponseEntity<byte[]> downloadI18n() {
return i18nService.downloadI18n();
public ResponseEntity<byte[]> downloadI18n(String type) {
return i18nService.downloadI18n(type);
}
@Operation(summary = "添加多语言", description = "添加多语言")

View File

@ -25,4 +25,9 @@ public class I18nUpdateByFileDto {
@NotNull(message = "文件不能为空")
private MultipartFile file;
@Schema(name = "fileType", title = "文件类型/json/excel")
@NotNull(message = "文件不能为空")
@NotBlank(message = "多语言key不能为空")
private String fileType;
}

View File

@ -0,0 +1,43 @@
package cn.bunny.domain.i18n.excel;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentFontStyle;
import com.alibaba.excel.annotation.write.style.HeadFontStyle;
import com.alibaba.excel.annotation.write.style.HeadStyle;
import com.alibaba.excel.enums.BooleanEnum;
import com.alibaba.excel.enums.poi.FillPatternTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/* I8n 转 Excel */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
// fontHeightInPoints字体大小颜色color 绿色
@HeadFontStyle(fontHeightInPoints = 30, color = 14, bold = BooleanEnum.TRUE)
// fillForegroundColor 将背景填充为白色
@HeadStyle(fillForegroundColor = 9, fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND)
public class I18nExcel {
@Schema(name = "keyName", title = "多语言key")
@ExcelProperty("keyName/多语言主键")
// 列宽
@ColumnWidth(66)
// fontHeightInPoints字体大小颜色color 绿色
@ContentFontStyle(fontHeightInPoints = 18, color = 17, bold = BooleanEnum.TRUE)
private String keyName;
@Schema(name = "translation", title = "多语言翻译名称")
@ExcelProperty("translation/多语言翻译")
// 列宽
@ColumnWidth(166)
// fontHeightInPoints字体大小颜色color 天蓝色
@ContentFontStyle(fontHeightInPoints = 18, color = 40, bold = BooleanEnum.TRUE)
private String translation;
}

View File

@ -0,0 +1,79 @@
package cn.bunny.services.excel;
import cn.bunny.domain.i18n.entity.I18n;
import cn.bunny.domain.i18n.excel.I18nExcel;
import cn.bunny.domain.vo.result.ResultCodeEnum;
import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.service.configuration.I18nService;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.util.ListUtils;
import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
// 有个很重要的点 I18nExcelListener 不能被spring管理要每次读取excel都要new,然后里面用到spring可以构造方法传进去
@Slf4j
public class I18nExcelListener implements ReadListener<I18nExcel> {
private static final int BATCH_COUNT = 100;
private final I18nService i18nService;
private final String type;
private List<I18nExcel> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
public I18nExcelListener(I18nService i18nService, String type) {
this.i18nService = i18nService;
this.type = type;
}
/**
* 这个每一条数据解析都会来调用
*
* @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()}
* @param context AnalysisContext
*/
@Override
public void invoke(I18nExcel data, AnalysisContext context) {
cachedDataList.add(data);
// 达到BATCH_COUNT了需要去存储一次数据库防止数据几万条数据在内存容易OOM
if (cachedDataList.size() >= BATCH_COUNT) {
saveData();
// 存储完成清理 list
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}
/**
* 所有数据解析完成了 都会来调用
*
* @param context AnalysisContext
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
saveData();
}
/**
* 加上存储数据库
*/
private void saveData() {
List<I18n> i18nList = cachedDataList.stream().map(item -> {
String key = item.getKeyName();
String value = item.getTranslation();
I18n i18n = new I18n();
i18n.setTypeName(type);
i18n.setKeyName(key);
i18n.setTranslation(value);
return i18n;
}).toList();
if (i18nList.isEmpty()) {
throw new AuthCustomerException(ResultCodeEnum.DATA_TOO_LARGE);
}
i18nService.saveBatch(i18nList);
System.out.println(JSON.toJSONString(i18nList));
}
}

View File

@ -63,9 +63,10 @@ public interface I18nService extends IService<I18n> {
/**
* 下载多语言配置
*
* @param type 下载类型
* @return 文件内容
*/
ResponseEntity<byte[]> downloadI18n();
ResponseEntity<byte[]> downloadI18n(String type);
/**
* 用文件更新多语言

View File

@ -1,5 +1,6 @@
package cn.bunny.services.service.configuration.impl;
import cn.bunny.domain.constant.FileType;
import cn.bunny.domain.entity.BaseEntity;
import cn.bunny.domain.i18n.dto.I18nAddDto;
import cn.bunny.domain.i18n.dto.I18nDto;
@ -7,14 +8,17 @@ import cn.bunny.domain.i18n.dto.I18nUpdateByFileDto;
import cn.bunny.domain.i18n.dto.I18nUpdateDto;
import cn.bunny.domain.i18n.entity.I18n;
import cn.bunny.domain.i18n.entity.I18nType;
import cn.bunny.domain.i18n.excel.I18nExcel;
import cn.bunny.domain.i18n.vo.I18nVo;
import cn.bunny.domain.vo.result.PageResult;
import cn.bunny.domain.vo.result.ResultCodeEnum;
import cn.bunny.services.excel.I18nExcelListener;
import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.mapper.configuration.I18nMapper;
import cn.bunny.services.mapper.configuration.I18nTypeMapper;
import cn.bunny.services.service.configuration.I18nService;
import cn.bunny.services.utils.system.I18nUtil;
import cn.bunny.services.utils.i8n.I18nUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import com.baomidou.mybatisplus.core.metadata.IPage;
@ -37,12 +41,11 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
@ -78,7 +81,6 @@ public class I18nServiceImpl extends ServiceImpl<I18nMapper, I18n> implements I1
return hashMap;
}
/**
* * 获取管理多语言列表
*
@ -154,35 +156,30 @@ public class I18nServiceImpl extends ServiceImpl<I18nMapper, I18n> implements I1
/**
* 下载多语言配置
*
* @param type 下载类型
* @return 文件内容
*/
@Override
public ResponseEntity<byte[]> downloadI18n() {
public ResponseEntity<byte[]> downloadI18n(String type) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) {
// 查找默认语言内容
List<I18n> i18nList = list();
HashMap<String, Object> hashMap = I18nUtil.getMap(i18nList);
hashMap.forEach((k, v) -> {
try {
ZipEntry zipEntry = new ZipEntry(k + ".json");
zipOutputStream.putNextEntry(zipEntry);
if (type.equals(FileType.JSON)) {
I18nUtil.writeJson(i18nList, zipOutputStream);
} else if (type.equals(FileType.EXCEL)) {
I18nUtil.writeExcel(i18nList, zipOutputStream);
}
zipOutputStream.write(JSON.toJSONString(v).getBytes(StandardCharsets.UTF_8));
zipOutputStream.closeEntry();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
} catch (IOException e) {
throw new RuntimeException(e);
}
// 设置响应头
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Disposition", "attachment; filename=i18n.zip");
headers.add("Content-Disposition", "attachment; filename=" + "i18n.zip");
headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
headers.add("Pragma", "no-cache");
headers.add("Expires", "0");
@ -201,12 +198,11 @@ public class I18nServiceImpl extends ServiceImpl<I18nMapper, I18n> implements I1
public void updateI18nByFile(I18nUpdateByFileDto dto) {
String type = dto.getType();
MultipartFile file = dto.getFile();
String fileType = dto.getFileType();
// 判断是否有这个语言的key
List<I18nType> i18nTypeList = i18nTypeMapper.selectList(Wrappers.<I18nType>lambdaQuery().eq(I18nType::getTypeName, type));
if (i18nTypeList.isEmpty() && !file.isEmpty()) {
throw new AuthCustomerException(ResultCodeEnum.DATA_NOT_EXIST);
}
if (i18nTypeList.isEmpty() && !file.isEmpty()) throw new AuthCustomerException(ResultCodeEnum.DATA_NOT_EXIST);
try {
// 内容是否为空
@ -216,8 +212,6 @@ public class I18nServiceImpl extends ServiceImpl<I18nMapper, I18n> implements I1
}
// 内容存在删除这个数据库中所有关于这个key的多语言
Map<String, Object> parseObject = JSON.parseObject(content, new TypeReference<>() {
});
List<I18n> i18nList = baseMapper.selectList(Wrappers.<I18n>lambdaQuery().eq(I18n::getTypeName, type));
List<Long> ids = i18nList.stream().map(BaseEntity::getId).toList();
if (!ids.isEmpty()) {
@ -225,17 +219,25 @@ public class I18nServiceImpl extends ServiceImpl<I18nMapper, I18n> implements I1
}
// 存入内容
i18nList = parseObject.entrySet().stream().map(item -> {
String key = item.getKey();
String value = item.getValue().toString();
if (fileType.equals(FileType.JSON)) {
Map<String, Object> parseObject = JSON.parseObject(content, new TypeReference<>() {
});
i18nList = parseObject.entrySet().stream().map(item -> {
String key = item.getKey();
String value = item.getValue().toString();
I18n i18n = new I18n();
i18n.setTypeName(type);
i18n.setKeyName(key);
i18n.setTranslation(value);
return i18n;
}).toList();
saveBatch(i18nList);
} else if (fileType.equals(FileType.EXCEL)) {
InputStream fileInputStream = file.getInputStream();
EasyExcel.read(fileInputStream, I18nExcel.class, new I18nExcelListener(this, type)).sheet().doRead();
}
I18n i18n = new I18n();
i18n.setTypeName(type);
i18n.setKeyName(key);
i18n.setTranslation(value);
return i18n;
}).toList();
saveBatch(i18nList);
} catch (IOException e) {
throw new RuntimeException(e);
}

View File

@ -1,32 +0,0 @@
package cn.bunny.services.utils.email;
import cn.bunny.domain.email.entity.EmailTemplate;
import cn.bunny.services.mapper.configuration.EmailTemplateMapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.HashMap;
@SpringBootTest
class ConcreteSenderEmailTemplateTest extends AbstractSenderEmailTemplate {
@Autowired
private EmailTemplateMapper emailTemplateMapper;
@Autowired
private PasswordEncoder passwordEncoder;
@Test
void sendEmailTemplate() {
EmailTemplate emailTemplate = emailTemplateMapper.selectOne(Wrappers.<EmailTemplate>lambdaQuery().eq(EmailTemplate::getId, 1791870020197625858L));
sendEmail("xaher94124@birige.com", emailTemplate, new HashMap<>());
}
@Test
void updateUserPasswordByAdmin() {
String encode = passwordEncoder.encode("123456");
System.out.println(encode);
}
}

View File

@ -0,0 +1,89 @@
package cn.bunny.services.utils.i8n;
import cn.bunny.domain.i18n.entity.I18n;
import cn.bunny.domain.i18n.excel.I18nExcel;
import com.alibaba.excel.EasyExcel;
import com.alibaba.fastjson2.JSON;
import org.jetbrains.annotations.NotNull;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class I18nUtil {
@NotNull
public static HashMap<String, Object> getMap(@NotNull List<I18n> i18nList) {
// 整理集合
Map<String, Map<String, String>> map = i18nList.stream()
.collect(Collectors.groupingBy(
I18n::getTypeName,
Collectors.toMap(I18n::getKeyName, I18n::getTranslation)));
// 返回集合
return new HashMap<>(map);
}
/**
* 使用zip写入json
*
* @param i18nList i18nList
* @param zipOutputStream zipOutputStream
*/
public static void writeJson(List<I18n> i18nList, ZipOutputStream zipOutputStream) {
HashMap<String, Object> hashMap = getMap(i18nList);
hashMap.forEach((k, v) -> {
try {
ZipEntry zipEntry = new ZipEntry(k + ".json");
zipOutputStream.putNextEntry(zipEntry);
zipOutputStream.write(JSON.toJSONString(v).getBytes(StandardCharsets.UTF_8));
zipOutputStream.closeEntry();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
/**
* 使用zip写入excel
*
* @param i18nList i18nList
* @param zipOutputStream zipOutputStream
*/
public static void writeExcel(List<I18n> i18nList, ZipOutputStream zipOutputStream) {
Map<String, List<I18nExcel>> hashMap = i18nList.stream()
.collect(Collectors.groupingBy(
I18n::getTypeName,
Collectors.mapping((I18n i18n) -> {
String keyName = i18n.getKeyName();
String translation = i18n.getTranslation();
return I18nExcel.builder().keyName(keyName).translation(translation).build();
}, Collectors.toList())
));
hashMap.forEach((key, value) -> {
try {
// 创建临时ByteArrayOutputStream
ByteArrayOutputStream excelOutputStream = new ByteArrayOutputStream();
ZipEntry zipEntry = new ZipEntry(key + ".xlsx");
zipOutputStream.putNextEntry(zipEntry);
// 先写入到临时流
EasyExcel.write(excelOutputStream, I18nExcel.class).sheet(key).doWrite(value);
zipOutputStream.write(excelOutputStream.toByteArray());
zipOutputStream.closeEntry();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}

View File

@ -1,23 +0,0 @@
package cn.bunny.services.utils.system;
import cn.bunny.domain.i18n.entity.I18n;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class I18nUtil {
@NotNull
public static HashMap<String, Object> getMap(List<I18n> i18nList) {
// 整理集合
Map<String, Map<String, String>> map = i18nList.stream()
.collect(Collectors.groupingBy(
I18n::getTypeName,
Collectors.toMap(I18n::getKeyName, I18n::getTranslation)));
// 返回集合
return new HashMap<>(map);
}
}

View File

@ -57,6 +57,12 @@
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!-- Excel表操作 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,12 @@
package cn.bunny.domain.constant;
public class FileType {
public static final String JPG = "jpg";
public static final String PNG = "png";
public static final String GIF = "gif";
public static final String BMP = "bmp";
public static final String TIFF = "tiff";
public static final String JSON = "json";
public static final String EXCEL = "excel";
}