Fix PDF font loading for project overview reports
This commit is contained in:
3
assets/图谱.txt
Normal file
3
assets/图谱.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
关系图谱:http://64.202.65.112:8082/atlas/refactor/#/home/graph/downloadService?id=lanxitest&mode=K_EXPAND&type=NORMAL&atlasToken=2C914E5E1FBFBC4AD15163E0AB03B800¶ms={"vId":"rel_node/15942f5b84bada01ccd25f5e5678ac22"}
|
||||||
|
|
||||||
|
资金流图谱:http://64.202.65.112:8082/atlas/refactor/#/home/graph/downloadService?id=ccdi_lanxi_trans&mode=K_EXPAND&type=NORMAL&atlasToken=F4BBA291A285858BAF4526C6EC312388¶ms={"vId":"idno_node/f2f797081494c5c0555a3bbf0f57c5e7"}
|
||||||
@@ -14,9 +14,11 @@ import com.ruoyi.common.exception.ServiceException;
|
|||||||
import com.ruoyi.common.utils.file.FileUtils;
|
import com.ruoyi.common.utils.file.FileUtils;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
|
import java.io.Closeable;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.text.DecimalFormat;
|
import java.text.DecimalFormat;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
@@ -32,6 +34,7 @@ import org.apache.pdfbox.pdmodel.PDPage;
|
|||||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
import org.apache.pdfbox.pdmodel.font.PDType0Font;
|
import org.apache.pdfbox.pdmodel.font.PDType0Font;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,6 +46,11 @@ public class CcdiProjectOverviewReportPdfExporter {
|
|||||||
private static final String CONTENT_TYPE = "application/pdf";
|
private static final String CONTENT_TYPE = "application/pdf";
|
||||||
private static final DateTimeFormatter EXPORT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
private static final DateTimeFormatter EXPORT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||||
private static final DecimalFormat MONEY_FORMAT = new DecimalFormat("#,##0.00");
|
private static final DecimalFormat MONEY_FORMAT = new DecimalFormat("#,##0.00");
|
||||||
|
private final String pdfFontPath;
|
||||||
|
|
||||||
|
public CcdiProjectOverviewReportPdfExporter(@Value("${ccdi.report.pdf-font-path:}") String pdfFontPath) {
|
||||||
|
this.pdfFontPath = pdfFontPath;
|
||||||
|
}
|
||||||
|
|
||||||
public void export(HttpServletResponse response, CcdiProjectOverviewReportVO report) throws IOException {
|
public void export(HttpServletResponse response, CcdiProjectOverviewReportVO report) throws IOException {
|
||||||
response.setContentType(CONTENT_TYPE);
|
response.setContentType(CONTENT_TYPE);
|
||||||
@@ -51,9 +59,8 @@ public class CcdiProjectOverviewReportPdfExporter {
|
|||||||
safeFileName(report.getProject().getProjectName()) + "_初核结果报告.pdf"
|
safeFileName(report.getProject().getProjectName()) + "_初核结果报告.pdf"
|
||||||
);
|
);
|
||||||
|
|
||||||
try (PDDocument document = new PDDocument()) {
|
try (PDDocument document = new PDDocument(); LoadedChineseFont loadedFont = loadChineseFont(document)) {
|
||||||
PDType0Font font = loadChineseFont(document);
|
PdfPageWriter writer = new PdfPageWriter(document, loadedFont.font());
|
||||||
PdfPageWriter writer = new PdfPageWriter(document, font);
|
|
||||||
writer.newPage();
|
writer.newPage();
|
||||||
writeCover(writer, report);
|
writeCover(writer, report);
|
||||||
writeUploadSubjects(writer, report.getUploadSubjects());
|
writeUploadSubjects(writer, report.getUploadSubjects());
|
||||||
@@ -204,44 +211,113 @@ public class CcdiProjectOverviewReportPdfExporter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private PDType0Font loadChineseFont(PDDocument document) throws IOException {
|
private LoadedChineseFont loadChineseFont(PDDocument document) throws IOException {
|
||||||
List<String> candidates = List.of(
|
if (pdfFontPath == null || pdfFontPath.isBlank()) {
|
||||||
"C:/Windows/Fonts/NotoSansSC-VF.ttf",
|
throw new ServiceException("未配置PDF中文字体路径,无法导出PDF报告");
|
||||||
"C:/Windows/Fonts/simhei.ttf",
|
|
||||||
"C:/Windows/Fonts/simsunb.ttf",
|
|
||||||
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttf",
|
|
||||||
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttf",
|
|
||||||
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"
|
|
||||||
);
|
|
||||||
for (String path : candidates) {
|
|
||||||
File file = new File(path);
|
|
||||||
if (!file.exists() || !file.isFile()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String lowerPath = path.toLowerCase();
|
|
||||||
if (lowerPath.endsWith(".ttf")) {
|
|
||||||
return PDType0Font.load(document, file);
|
|
||||||
}
|
|
||||||
if (lowerPath.endsWith(".ttc")) {
|
|
||||||
PDType0Font font = loadFirstCollectionFont(document, file);
|
|
||||||
if (font != null) {
|
|
||||||
return font;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
throw new ServiceException("未找到可用中文字体,无法导出PDF报告");
|
String path = pdfFontPath.trim();
|
||||||
|
File file = new File(path);
|
||||||
|
if (!file.exists() || !file.isFile()) {
|
||||||
|
throw new ServiceException("配置的PDF中文字体路径不可用:" + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
String lowerPath = path.toLowerCase();
|
||||||
|
if (lowerPath.endsWith(".ttf")) {
|
||||||
|
return new LoadedChineseFont(PDType0Font.load(document, file), null);
|
||||||
|
}
|
||||||
|
if (lowerPath.endsWith(".ttc")) {
|
||||||
|
LoadedChineseFont loadedFont = loadPreferredCollectionFont(document, file);
|
||||||
|
if (loadedFont != null) {
|
||||||
|
return loadedFont;
|
||||||
|
}
|
||||||
|
throw new ServiceException("配置的PDF中文字体不支持导出:" + path);
|
||||||
|
}
|
||||||
|
if (lowerPath.endsWith(".otf")) {
|
||||||
|
return new LoadedChineseFont(loadOpenTypeFont(document, file), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ServiceException("配置的PDF中文字体格式不支持:" + path);
|
||||||
}
|
}
|
||||||
|
|
||||||
private PDType0Font loadFirstCollectionFont(PDDocument document, File file) throws IOException {
|
private PDType0Font loadOpenTypeFont(PDDocument document, File file) throws IOException {
|
||||||
|
try (var inputStream = Files.newInputStream(file.toPath())) {
|
||||||
|
return PDType0Font.load(document, inputStream, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoadedChineseFont loadPreferredCollectionFont(PDDocument document, File file) throws IOException {
|
||||||
|
LoadedChineseFont font = loadCollectionFontByName(document, file);
|
||||||
|
return font == null ? loadFirstCollectionFont(document, file) : font;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoadedChineseFont loadCollectionFontByName(PDDocument document, File file) throws IOException {
|
||||||
|
TrueTypeCollection collection = new TrueTypeCollection(file);
|
||||||
AtomicReference<PDType0Font> font = new AtomicReference<>();
|
AtomicReference<PDType0Font> font = new AtomicReference<>();
|
||||||
try (TrueTypeCollection collection = new TrueTypeCollection(file)) {
|
try {
|
||||||
collection.processAllFonts(typeFont -> {
|
collection.processAllFonts(typeFont -> {
|
||||||
if (font.get() == null) {
|
if (font.get() == null && supportsTrueTypeSubset(typeFont)
|
||||||
|
&& isSimplifiedChineseFont(typeFont.getName())) {
|
||||||
font.set(PDType0Font.load(document, typeFont, true));
|
font.set(PDType0Font.load(document, typeFont, true));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (font.get() != null) {
|
||||||
|
return new LoadedChineseFont(font.get(), collection);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (font.get() == null) {
|
||||||
|
collection.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSimplifiedChineseFont(String fontName) {
|
||||||
|
if (fontName == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String lowerName = fontName.toLowerCase();
|
||||||
|
return lowerName.contains("sc")
|
||||||
|
|| lowerName.contains("hans")
|
||||||
|
|| lowerName.contains("gb")
|
||||||
|
|| lowerName.contains("hei")
|
||||||
|
|| lowerName.contains("song")
|
||||||
|
|| lowerName.contains("yahei")
|
||||||
|
|| lowerName.contains("simsun")
|
||||||
|
|| lowerName.contains("simhei");
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoadedChineseFont loadFirstCollectionFont(PDDocument document, File file) throws IOException {
|
||||||
|
TrueTypeCollection collection = new TrueTypeCollection(file);
|
||||||
|
AtomicReference<PDType0Font> font = new AtomicReference<>();
|
||||||
|
try {
|
||||||
|
collection.processAllFonts(typeFont -> {
|
||||||
|
if (font.get() == null && supportsTrueTypeSubset(typeFont)) {
|
||||||
|
font.set(PDType0Font.load(document, typeFont, true));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (font.get() != null) {
|
||||||
|
return new LoadedChineseFont(font.get(), collection);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (font.get() == null) {
|
||||||
|
collection.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean supportsTrueTypeSubset(org.apache.fontbox.ttf.TrueTypeFont typeFont) throws IOException {
|
||||||
|
return typeFont.getTableMap().containsKey("glyf");
|
||||||
|
}
|
||||||
|
|
||||||
|
private record LoadedChineseFont(PDType0Font font, Closeable fontSource) implements Closeable {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
if (fontSource != null) {
|
||||||
|
fontSource.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return font.get();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<IndexedUploadSubject> indexedRows(List<CcdiProjectOverviewReportUploadSubjectVO> rows) {
|
private List<IndexedUploadSubject> indexedRows(List<CcdiProjectOverviewReportUploadSubjectVO> rows) {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
|
|||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewStatVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewStatVO;
|
||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO;
|
||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO;
|
||||||
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
|
import java.io.File;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -19,13 +21,14 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.springframework.mock.web.MockHttpServletResponse;
|
import org.springframework.mock.web.MockHttpServletResponse;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
class CcdiProjectOverviewReportPdfExporterTest {
|
class CcdiProjectOverviewReportPdfExporterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldExportOverviewReportPdf() throws Exception {
|
void shouldExportOverviewReportPdf() throws Exception {
|
||||||
CcdiProjectOverviewReportPdfExporter exporter = new CcdiProjectOverviewReportPdfExporter();
|
CcdiProjectOverviewReportPdfExporter exporter = new CcdiProjectOverviewReportPdfExporter(resolveTestFontPath());
|
||||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||||
|
|
||||||
exporter.export(response, buildReport());
|
exporter.export(response, buildReport());
|
||||||
@@ -36,6 +39,41 @@ class CcdiProjectOverviewReportPdfExporterTest {
|
|||||||
assertTrue(response.getContentAsByteArray().length > 1000);
|
assertTrue(response.getContentAsByteArray().length > 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectBlankPdfFontPath() {
|
||||||
|
CcdiProjectOverviewReportPdfExporter exporter = new CcdiProjectOverviewReportPdfExporter("");
|
||||||
|
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||||
|
|
||||||
|
ServiceException exception = assertThrows(ServiceException.class, () -> exporter.export(response, buildReport()));
|
||||||
|
|
||||||
|
assertEquals("未配置PDF中文字体路径,无法导出PDF报告", exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectUnavailablePdfFontPath() {
|
||||||
|
String missingPath = "/tmp/ccdi-missing-pdf-font-" + System.nanoTime() + ".ttc";
|
||||||
|
CcdiProjectOverviewReportPdfExporter exporter = new CcdiProjectOverviewReportPdfExporter(missingPath);
|
||||||
|
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||||
|
|
||||||
|
ServiceException exception = assertThrows(ServiceException.class, () -> exporter.export(response, buildReport()));
|
||||||
|
|
||||||
|
assertTrue(exception.getMessage().contains(missingPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveTestFontPath() {
|
||||||
|
List<String> candidates = List.of(
|
||||||
|
"/System/Library/Fonts/STHeiti Medium.ttc",
|
||||||
|
"/System/Library/Fonts/STHeiti Light.ttc",
|
||||||
|
"/System/Library/Fonts/Hiragino Sans GB.ttc",
|
||||||
|
"/System/Library/Fonts/Supplemental/Songti.ttc",
|
||||||
|
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"
|
||||||
|
);
|
||||||
|
return candidates.stream()
|
||||||
|
.filter(path -> new File(path).isFile())
|
||||||
|
.findFirst()
|
||||||
|
.orElse(candidates.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
private CcdiProjectOverviewReportVO buildReport() {
|
private CcdiProjectOverviewReportVO buildReport() {
|
||||||
CcdiProjectOverviewReportVO report = new CcdiProjectOverviewReportVO();
|
CcdiProjectOverviewReportVO report = new CcdiProjectOverviewReportVO();
|
||||||
CcdiProject project = new CcdiProject();
|
CcdiProject project = new CcdiProject();
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ FROM eclipse-temurin:21-jre-jammy
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends fontconfig fonts-wqy-microhei \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY backend/ruoyi-admin.jar /app/ruoyi-admin.jar
|
COPY backend/ruoyi-admin.jar /app/ruoyi-admin.jar
|
||||||
|
|
||||||
RUN mkdir -p /app/data/ruoyi /app/logs
|
RUN mkdir -p /app/data/ruoyi /app/logs
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# 2026-05-07 NAS Docker 部署实施记录
|
||||||
|
|
||||||
|
## 保存路径确认
|
||||||
|
|
||||||
|
- 目标目录:`docs/reports/implementation/`
|
||||||
|
- 文档用途:记录本次 NAS Docker 部署操作、影响范围与验证结果
|
||||||
|
- 路径检查结果:符合仓库实施记录归档规范
|
||||||
|
|
||||||
|
## 本次操作
|
||||||
|
|
||||||
|
- 在本地仓库 `/Users/wkc/Desktop/ccdi/ccdi` 执行 NAS Docker 部署。
|
||||||
|
- 按仓库前端规则先执行 `source ~/.nvm/nvm.sh && nvm use`,确认 Node 版本为 `v14.21.3`、npm 版本为 `6.14.18`。
|
||||||
|
- 执行部署脚本:`deploy/deploy-to-nas.sh`。
|
||||||
|
- 脚本自动完成:
|
||||||
|
- 后端打包:`mvn clean package -DskipTests`
|
||||||
|
- 前端打包:`npm run build:prod`
|
||||||
|
- 组装部署目录:`.deploy/ccdi-package`
|
||||||
|
- 上传到 NAS 并执行远端 `docker compose up -d --build`
|
||||||
|
- 部署目标:
|
||||||
|
- SSH:`116.62.17.81:9444`
|
||||||
|
- 远端目录:`/volume1/webapp/ccdi`
|
||||||
|
|
||||||
|
## 影响范围
|
||||||
|
|
||||||
|
- 远端部署目录 `/volume1/webapp/ccdi` 已刷新为本次构建产物。
|
||||||
|
- 远端 Docker 服务已重建并启动:
|
||||||
|
- `ccdi-backend`
|
||||||
|
- `ccdi-frontend`
|
||||||
|
- `ccdi-lsfx-mock`
|
||||||
|
- 本次操作未修改业务代码。
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
### 本地构建验证
|
||||||
|
|
||||||
|
- Maven 聚合打包成功,`ruoyi-admin/target/ruoyi-admin.jar` 已生成。
|
||||||
|
- Vue 生产构建成功,`ruoyi-ui/dist` 已生成。
|
||||||
|
- 前端构建存在资源体积告警,未出现构建失败。
|
||||||
|
|
||||||
|
### 远端容器验证
|
||||||
|
|
||||||
|
- `docker compose ps` 结果:
|
||||||
|
- `ccdi-backend`:`Up About a minute`
|
||||||
|
- `ccdi-frontend`:`Up About a minute`
|
||||||
|
- `ccdi-lsfx-mock`:`Up About a minute`
|
||||||
|
- 端口映射结果:
|
||||||
|
- `62318 -> backend:8080`
|
||||||
|
- `62319 -> frontend:80`
|
||||||
|
- `62320 -> mock:8000`
|
||||||
|
|
||||||
|
### NAS 本机访问验证
|
||||||
|
|
||||||
|
- `http://127.0.0.1:62319/` 返回 `200`
|
||||||
|
- `http://127.0.0.1:62318/swagger-ui/index.html` 返回 `200`
|
||||||
|
- `http://127.0.0.1:62320/docs` 返回 `200`
|
||||||
|
|
||||||
|
### 公网访问验证
|
||||||
|
|
||||||
|
- `http://116.62.17.81:62319/` 返回 `200`
|
||||||
|
- `http://116.62.17.81:62318/swagger-ui/index.html` 返回 `200`
|
||||||
|
- `http://116.62.17.81:62320/docs` 返回 `200`
|
||||||
|
|
||||||
|
### 后端日志验证
|
||||||
|
|
||||||
|
- 后端启动 profile:`nas`
|
||||||
|
- TongWeb `8080` 已启动
|
||||||
|
- `RuoYiApplication` 启动完成
|
||||||
|
- 日志输出“若依启动成功”
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# 生产 MySQL 初始化文件同步实施记录
|
||||||
|
|
||||||
|
## 保存路径确认
|
||||||
|
|
||||||
|
- 实施记录保存路径:`docs/reports/implementation/2026-05-07-production-mysql-init-sync.md`
|
||||||
|
- 本次为生产 MySQL 初始化 SQL 同步,使用实施记录目录保存。
|
||||||
|
|
||||||
|
## 修改内容
|
||||||
|
|
||||||
|
1. 更新 `sql/ccdi_prod_init_20260428.sql`,补入招聘信息维护所需的 `ccdi_recruit_type` 字典类型。
|
||||||
|
2. 补入 `ccdi_recruit_type` 字典数据:
|
||||||
|
- `SOCIAL`:社招
|
||||||
|
- `CAMPUS`:校招
|
||||||
|
3. 核对招聘信息相关结构,确认生产初始化 SQL 中已包含:
|
||||||
|
- `ccdi_staff_recruitment.id` 自增主键
|
||||||
|
- `ccdi_staff_recruitment.recruit_id` 普通索引
|
||||||
|
- `ccdi_staff_recruitment_work.recruitment_id` 非空字段
|
||||||
|
- `idx_recruitment_id` 与 `idx_recruitment_id_sort_order` 索引
|
||||||
|
|
||||||
|
## 影响范围
|
||||||
|
|
||||||
|
- 影响生产空库初始化脚本。
|
||||||
|
- 新生产环境导入初始化 SQL 后,招聘信息导入模板可直接读取招聘类型字典下拉数据。
|
||||||
|
- 不影响现有数据库的增量迁移脚本;存量库仍按 `sql/migration/2026-05-07-add-staff-recruitment-type-dict.sql` 执行。
|
||||||
|
|
||||||
|
## 验证情况
|
||||||
|
|
||||||
|
- 已检查生产初始化 SQL 中包含 `ccdi_recruit_type` 字典类型与 `SOCIAL`、`CAMPUS` 两条字典数据。
|
||||||
|
- 已检查招聘主从表结构已同步最新主键与关联字段形态。
|
||||||
|
- 已执行 `git diff --check`,未发现空白或补丁格式问题。
|
||||||
|
- 本次未执行数据库导入验证,避免误将生产初始化脚本导入当前开发库造成清库风险。
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# 项目总览 PDF 中文字体配置化修复实施记录
|
||||||
|
|
||||||
|
## 保存路径确认
|
||||||
|
|
||||||
|
- 文档类型:实施记录
|
||||||
|
- 保存路径:`docs/reports/implementation/`
|
||||||
|
|
||||||
|
## 问题说明
|
||||||
|
|
||||||
|
- 异常信息:`com.ruoyi.common.exception.ServiceException: 未找到可用中文字体,无法导出PDF报告`
|
||||||
|
- 触发链路:结果总览导出 PDF 时,`CcdiProjectOverviewReportPdfExporter` 需要通过 PDFBox 加载中文字体;原实现依赖代码内置候选路径,部署环境字体路径不可控时会导致导出失败。
|
||||||
|
|
||||||
|
## 修改内容
|
||||||
|
|
||||||
|
- 修改 `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewReportPdfExporter.java`
|
||||||
|
- 删除 Java 代码内置多路径候选列表。
|
||||||
|
- 通过 `ccdi.report.pdf-font-path` 读取 profile 配置中的单个字体路径。
|
||||||
|
- 配置为空时提示 `未配置PDF中文字体路径,无法导出PDF报告`。
|
||||||
|
- 配置文件不存在、格式不支持或字体不可导出时,错误信息包含配置路径。
|
||||||
|
- `.ttc` 字体集合优先选择支持 TrueType 子集化的简体中文字体名,未命中时再使用集合内首个支持子集化的字体。
|
||||||
|
- 字体集合源在 `document.save()` 完成后再关闭,避免 PDFBox 保存阶段继续读取字体时源文件已关闭。
|
||||||
|
- 增加 `.otf` 字体文件加载支持。
|
||||||
|
- 修改 `ruoyi-admin/src/main/resources/application-dev.yml`
|
||||||
|
- 增加本地 macOS 字体路径:`/System/Library/Fonts/STHeiti Medium.ttc`。
|
||||||
|
- 修改 `ruoyi-admin/src/main/resources/application-nas.yml`
|
||||||
|
- 修改 `ruoyi-admin/src/main/resources/application-uat.yml`
|
||||||
|
- 修改 `ruoyi-admin/src/main/resources/application-pro.yml`
|
||||||
|
- 增加 Linux 字体路径:`/usr/share/fonts/truetype/wqy/wqy-microhei.ttc`。
|
||||||
|
- 未修改 `application.yml`,不提供全局默认值。
|
||||||
|
- 修改 `docker/backend/Dockerfile`
|
||||||
|
- 后端镜像安装 `fontconfig` 与 `fonts-wqy-microhei`,确保 NAS Docker 部署环境具备可用中文字体。
|
||||||
|
- 修改 `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewReportPdfExporterTest.java`
|
||||||
|
- 正常导出用例显式传入测试环境字体路径。
|
||||||
|
- 新增空配置与不存在路径的异常覆盖。
|
||||||
|
|
||||||
|
## 影响范围
|
||||||
|
|
||||||
|
- 影响结果总览 PDF 导出功能。
|
||||||
|
- 不改变报告数据组装、表格内容、前端下载接口和文件名规则。
|
||||||
|
- 不在 `application.yml` 增加默认配置,缺少 profile 配置时导出时报错。
|
||||||
|
- Docker 镜像构建会新增中文字体包安装步骤。
|
||||||
|
|
||||||
|
## 验证记录
|
||||||
|
|
||||||
|
- 已执行:`mvn -pl ccdi-project -Dtest=CcdiProjectOverviewReportPdfExporterTest -Dsurefire.failIfNoSpecifiedTests=false test`
|
||||||
|
- 结果:失败,失败原因是当前工作区已有其他源码编译错误,未进入 PDF 导出测试。
|
||||||
|
- 主要错误包括:`getDataTable` 调用参数不匹配、流水接口字段和方法签名不匹配。
|
||||||
|
- 已执行:手工编译 PDF 导出器及其依赖 VO 到 `ccdi-project/target/classes` 后,运行 `mvn -pl ccdi-project -Dtest=CcdiProjectOverviewReportPdfExporterTest -Dsurefire.failIfNoSpecifiedTests=false -Dmaven.main.skip=true test`
|
||||||
|
- 结果:失败,失败原因是 Maven 仍尝试编译 `ccdi-project` 下全量测试类,而当前 `target/classes` 缺少其他测试依赖的业务类。
|
||||||
|
- 已执行:手工编译 `CcdiProjectOverviewReportPdfExporterTest` 后,通过 JUnit Platform Launcher 指定运行该测试类
|
||||||
|
- 结果:通过,`Tests found: 3`,`Tests succeeded: 3`,`Tests failed: 0`。
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# 2026-05-08 NAS Docker 部署实施记录
|
||||||
|
|
||||||
|
## 保存路径确认
|
||||||
|
|
||||||
|
- 目标目录:`docs/reports/implementation/`
|
||||||
|
- 文档用途:记录本次 NAS Docker 部署操作、影响范围与验证结果
|
||||||
|
- 路径检查结果:符合仓库实施记录归档规范
|
||||||
|
|
||||||
|
## 本次操作
|
||||||
|
|
||||||
|
- 在本地仓库 `/Users/wkc/Desktop/ccdi/ccdi` 执行 NAS Docker 部署。
|
||||||
|
- 按前端规则执行 `source ~/.nvm/nvm.sh && nvm use`,确认 Node 版本为 `v14.21.3`、npm 版本为 `6.14.18`。
|
||||||
|
- 执行部署脚本:`deploy/deploy-to-nas.sh`。
|
||||||
|
- 脚本完成以下动作:
|
||||||
|
- 后端打包:`mvn clean package -DskipTests`
|
||||||
|
- 前端打包:`npm run build:prod`
|
||||||
|
- 组装部署目录:`.deploy/ccdi-package`
|
||||||
|
- 上传到 NAS:`/volume1/webapp/ccdi`
|
||||||
|
- 远端执行 `docker compose up -d --build`
|
||||||
|
- 部署目标:
|
||||||
|
- SSH:`116.62.17.81:9444`
|
||||||
|
- 远端目录:`/volume1/webapp/ccdi`
|
||||||
|
|
||||||
|
## 影响范围
|
||||||
|
|
||||||
|
- 远端部署目录 `/volume1/webapp/ccdi` 已刷新为本次构建产物。
|
||||||
|
- 远端 Docker 镜像已重新构建。
|
||||||
|
- 远端 Docker 服务已重建并启动:
|
||||||
|
- `ccdi-backend`
|
||||||
|
- `ccdi-frontend`
|
||||||
|
- `ccdi-lsfx-mock`
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
### 本地构建验证
|
||||||
|
|
||||||
|
- Maven 聚合打包成功,`ruoyi-admin/target/ruoyi-admin.jar` 已生成。
|
||||||
|
- Vue 生产构建成功,`ruoyi-ui/dist` 已生成。
|
||||||
|
- 前端构建存在资源体积告警,未出现构建失败。
|
||||||
|
|
||||||
|
### 远端容器验证
|
||||||
|
|
||||||
|
- `docker compose ps` 结果:
|
||||||
|
- `ccdi-backend`:`Up`
|
||||||
|
- `ccdi-frontend`:`Up`
|
||||||
|
- `ccdi-lsfx-mock`:`Up`
|
||||||
|
- 端口映射结果:
|
||||||
|
- `62318 -> backend:8080`
|
||||||
|
- `62319 -> frontend:80`
|
||||||
|
- `62320 -> mock:8000`
|
||||||
|
|
||||||
|
### NAS 本机访问验证
|
||||||
|
|
||||||
|
- `http://127.0.0.1:62319/` 返回 `200`
|
||||||
|
- `http://127.0.0.1:62318/swagger-ui/index.html` 返回 `200`
|
||||||
|
- `http://127.0.0.1:62320/docs` 返回 `200`
|
||||||
|
|
||||||
|
### 公网访问验证
|
||||||
|
|
||||||
|
- `http://116.62.17.81:62319/` 返回 `200`
|
||||||
|
- `http://116.62.17.81:62318/swagger-ui/index.html` 返回 `200`
|
||||||
|
- `http://116.62.17.81:62320/docs` 返回 `200`
|
||||||
|
- `POST http://116.62.17.81:62318/login/test?username=admin&password=admin123` 返回 `200`
|
||||||
|
|
||||||
|
### 后端日志验证
|
||||||
|
|
||||||
|
- 后端启动 profile:`nas`
|
||||||
|
- TongWeb `8080` 已启动。
|
||||||
|
- `RuoYiApplication` 启动完成。
|
||||||
|
- 日志输出“若依启动成功”。
|
||||||
@@ -3,6 +3,10 @@ ruoyi:
|
|||||||
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
|
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
|
||||||
profile: D:/ruoyi/uploadPath
|
profile: D:/ruoyi/uploadPath
|
||||||
|
|
||||||
|
ccdi:
|
||||||
|
report:
|
||||||
|
pdf-font-path: /System/Library/Fonts/STHeiti Medium.ttc
|
||||||
|
|
||||||
|
|
||||||
server:
|
server:
|
||||||
# 服务器的HTTP端口,默认为8080
|
# 服务器的HTTP端口,默认为8080
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ ruoyi:
|
|||||||
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
|
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
|
||||||
profile: backend/uploadPath
|
profile: backend/uploadPath
|
||||||
|
|
||||||
|
ccdi:
|
||||||
|
report:
|
||||||
|
pdf-font-path: /usr/share/fonts/truetype/wqy/wqy-microhei.ttc
|
||||||
|
|
||||||
|
|
||||||
# 开发环境配置
|
# 开发环境配置
|
||||||
server:
|
server:
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ ruoyi:
|
|||||||
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
|
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
|
||||||
profile: backend/uploadPath
|
profile: backend/uploadPath
|
||||||
|
|
||||||
|
ccdi:
|
||||||
|
report:
|
||||||
|
pdf-font-path: /usr/share/fonts/truetype/wqy/wqy-microhei.ttc
|
||||||
|
|
||||||
|
|
||||||
# 开发环境配置
|
# 开发环境配置
|
||||||
server:
|
server:
|
||||||
@@ -31,9 +35,9 @@ spring:
|
|||||||
druid:
|
druid:
|
||||||
# 主库数据源
|
# 主库数据源
|
||||||
master:
|
master:
|
||||||
url: jdbc:mysql://158.234.199.250:3306/ccdi?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
|
url: jdbc:mysql://64.116.19.156:3306/ccdi?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
|
||||||
username: dbicm
|
username: lx_ai
|
||||||
password: Kfcx@1234
|
password: lx-ai@9520
|
||||||
# 从库数据源
|
# 从库数据源
|
||||||
slave:
|
slave:
|
||||||
# 从数据源开关/默认关闭
|
# 从数据源开关/默认关闭
|
||||||
@@ -88,7 +92,7 @@ spring:
|
|||||||
# redis 配置
|
# redis 配置
|
||||||
redis:
|
redis:
|
||||||
# 地址
|
# 地址
|
||||||
host: r-kz640f6b20dac724.redis.rds.ops.dc-tst-zj96596.com
|
host: 64.116.19.155
|
||||||
# 端口,默认为6379
|
# 端口,默认为6379
|
||||||
port: 6379
|
port: 6379
|
||||||
# 数据库索引
|
# 数据库索引
|
||||||
@@ -111,10 +115,8 @@ spring:
|
|||||||
# 流水分析平台配置
|
# 流水分析平台配置
|
||||||
lsfx:
|
lsfx:
|
||||||
api:
|
api:
|
||||||
|
|
||||||
base-url: http://158.234.196.5:82/c4c3
|
|
||||||
# 生产环境
|
# 生产环境
|
||||||
# base-url: http://64.202.32.176/c4c3
|
base-url: http://64.202.32.176/c4c3
|
||||||
|
|
||||||
# 认证配置
|
# 认证配置
|
||||||
app-id: remote_app
|
app-id: remote_app
|
||||||
@@ -143,4 +145,4 @@ lsfx:
|
|||||||
|
|
||||||
credit-parse:
|
credit-parse:
|
||||||
api:
|
api:
|
||||||
url: http://192.168.0.111:62320/xfeature-mngs/conversation/htmlEval
|
url: http://64.202.94.120:8081/xfeature-mngs/conversation/htmlEval
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ ruoyi:
|
|||||||
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
|
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
|
||||||
profile: backend/uploadPath
|
profile: backend/uploadPath
|
||||||
|
|
||||||
|
ccdi:
|
||||||
|
report:
|
||||||
|
pdf-font-path: /usr/share/fonts/truetype/wqy/wqy-microhei.ttc
|
||||||
|
|
||||||
|
|
||||||
# 开发环境配置
|
# 开发环境配置
|
||||||
server:
|
server:
|
||||||
|
|||||||
@@ -1655,6 +1655,16 @@ INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `cre
|
|||||||
/*!40000 ALTER TABLE `sys_dict_type` ENABLE KEYS */;
|
/*!40000 ALTER TABLE `sys_dict_type` ENABLE KEYS */;
|
||||||
UNLOCK TABLES;
|
UNLOCK TABLES;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Supplemental bootstrap data for table `sys_dict_type`
|
||||||
|
--
|
||||||
|
|
||||||
|
LOCK TABLES `sys_dict_type` WRITE;
|
||||||
|
/*!40000 ALTER TABLE `sys_dict_type` DISABLE KEYS */;
|
||||||
|
INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (118,'招聘类型','ccdi_recruit_type','0','admin','2026-05-07 00:05:00','',NULL,'招聘信息-招聘类型');
|
||||||
|
/*!40000 ALTER TABLE `sys_dict_type` ENABLE KEYS */;
|
||||||
|
UNLOCK TABLES;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Dumping data for table `sys_dict_data`
|
-- Dumping data for table `sys_dict_data`
|
||||||
--
|
--
|
||||||
@@ -1665,6 +1675,16 @@ INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value
|
|||||||
/*!40000 ALTER TABLE `sys_dict_data` ENABLE KEYS */;
|
/*!40000 ALTER TABLE `sys_dict_data` ENABLE KEYS */;
|
||||||
UNLOCK TABLES;
|
UNLOCK TABLES;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Supplemental bootstrap data for table `sys_dict_data`
|
||||||
|
--
|
||||||
|
|
||||||
|
LOCK TABLES `sys_dict_data` WRITE;
|
||||||
|
/*!40000 ALTER TABLE `sys_dict_data` DISABLE KEYS */;
|
||||||
|
INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (174,1,'社招','SOCIAL','ccdi_recruit_type','','success','Y','0','admin','2026-05-07 00:05:00','',NULL,'社会招聘'),(175,2,'校招','CAMPUS','ccdi_recruit_type','','info','N','0','admin','2026-05-07 00:05:00','',NULL,'校园招聘');
|
||||||
|
/*!40000 ALTER TABLE `sys_dict_data` ENABLE KEYS */;
|
||||||
|
UNLOCK TABLES;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Dumping data for table `sys_post`
|
-- Dumping data for table `sys_post`
|
||||||
--
|
--
|
||||||
|
|||||||
Reference in New Issue
Block a user