init: WordFileParse docx→HTML 解析服务(含 Word EQ 域公式修复)

服务:Spring Boot 3.2 + docx4j 8.3.9,POST /api/v3/server_office_file/file/parse
      接受 .docx,返回 HTML(公式 → LaTeX,图片 → RustFS 外链)

公式支持三类来源:
- OMML(Word 2007+ 公式)      → Python omml_mathml2latex
- MathType OLE 对象            → Ruby mathtype2mathml → MathML → LaTeX
- Word EQ 域(老教辅主力)      → EqToLatexUtil(本次新增)

本次改动:
1. 新增 java-svc/.../core/EqToLatexUtil.java:
   移植自 jby backend-services office-core,EQ 语法 → LaTeX
   (\f→\dfrac、\r→\sqrt、\i→\int/\sum/\prod、\s→^{}/_{}、\b→\left\right、
     \a→array、\x→\boxed/\overline、\o\al 堆叠、圈码、化学符号、极限、连等式)
   品牌标记 JBYU* → WPU*,与采集端一致
2. CtoMathFinder:接上 EQ 分支(原为 not-ported 桩),域代码经
   EqToLatexUtil.convert 转 LaTeX 写入 eqLatexMap(WPEQNUM 占位符)
3. WordParseUtil.dealPlaceholder:EQ 的 LaTeX 同样加 \( \) 包裹
   (原先因 WPEQNUM 例外不加包裹,前端/MathJax 不渲染)

修前症状:教辅 Word 里分式/方程组/上下标整段丢失(如 {−1/3, 1} 变成 {-,1})
修后实测:46 份教辅 2908 处 EQ 域全部还原,公式总数 2954 处、零未包裹残留

安全:application.yml(含 RustFS 密钥)不入库,提供 application.yml.example;
      OssService 的 @Value 默认密钥已清空
This commit is contained in:
2026-09-29 09:43:13 +08:00
commit 3271fee5cc
360 changed files with 86082 additions and 0 deletions

View File

@@ -0,0 +1,235 @@
/*
* Decompiled with CFR 0.152.
*
* Could not load the following classes:
* cn.hutool.core.io.FileUtil
* com.alibaba.fastjson.JSON
* com.alibaba.fastjson.JSONObject
* com.alibaba.fastjson.TypeReference
* com.jby.common.exceptions.BusinessException
* com.jby.common.util.FileUtil
* com.jby.common.util.HttpClientUtil
* com.jby.common.util.RequestHeaderUtil
* com.jby.oss.domain.param.OssParam
* com.jby.system.enums.AttachmentModuleEnum
* org.apache.poi.xwpf.usermodel.XWPFDocument
* org.slf4j.Logger
* org.slf4j.LoggerFactory
* org.springframework.beans.factory.annotation.Autowired
* org.springframework.stereotype.Component
* org.springframework.web.multipart.MultipartFile
*/
package com.jby.officecore.utils;
import cn.hutool.core.io.FileUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.jby.common.exceptions.BusinessException;
import com.jby.common.util.HttpClientUtil;
import com.jby.common.util.RequestHeaderUtil;
import com.jby.officecore.config.OfficeCoreConfig;
import com.jby.officecore.utils.OssServiceUtil;
import com.jby.oss.domain.param.OssParam;
import com.jby.system.enums.AttachmentModuleEnum;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
@Component
public class OfficeFileUtil {
private static final Logger log = LoggerFactory.getLogger(OfficeFileUtil.class);
private static OfficeCoreConfig officeCoreConfig;
@Autowired
public void setOfficeCoreConfig(OfficeCoreConfig officeCoreConfig) {
OfficeFileUtil.officeCoreConfig = officeCoreConfig;
}
public static File writeFile(String content, String savePath) {
FileWriter fw = null;
BufferedWriter bW = null;
try {
File file = com.jby.common.util.FileUtil.getFile((String)savePath);
fw = new FileWriter(file);
bW = new BufferedWriter(fw);
bW.write(content);
File file2 = file;
return file2;
}
catch (Exception e) {
log.error("\u5199\u5165\u6587\u4ef6\u5f02\u5e38, savePath:{}", (Object)savePath, (Object)e);
throw new BusinessException("\u5199\u5165\u6587\u4ef6\u5f02\u5e38");
}
finally {
if (bW != null) {
try {
bW.close();
}
catch (IOException e) {
log.error("close error", (Throwable)e);
}
}
if (fw != null) {
try {
fw.close();
}
catch (IOException e) {
log.error("close error", (Throwable)e);
}
}
}
}
public static File writeWord(XWPFDocument document, String savePath) {
OutputStream os = null;
try {
File file = com.jby.common.util.FileUtil.getFile((String)savePath);
os = Files.newOutputStream(file.toPath(), new OpenOption[0]);
document.write(os);
File file2 = file;
return file2;
}
catch (Exception e) {
log.error("\u751f\u6210word\u6587\u4ef6\u5f02\u5e38, savePath:{}", (Object)savePath, (Object)e);
throw new BusinessException("\u751f\u6210word\u6587\u4ef6\u5f02\u5e38\uff01");
}
finally {
if (Objects.nonNull(os)) {
try {
os.close();
}
catch (IOException e) {
log.error("close error", (Throwable)e);
}
}
if (Objects.nonNull(document)) {
try {
document.close();
}
catch (IOException e) {
log.error("close error", (Throwable)e);
}
}
}
}
public static File writeFile(MultipartFile file, String savePath) {
try {
return OfficeFileUtil.writeFile(file.getInputStream(), savePath);
}
catch (Exception e) {
log.error("\u5199\u5165\u6587\u4ef6\u5f02\u5e38, savePath:{}", (Object)savePath, (Object)e);
throw new BusinessException("\u5199\u5165\u6587\u4ef6\u5f02\u5e38");
}
}
public static File writeFile(InputStream is, String savePath) {
FileOutputStream os = null;
try {
int length;
File saveFile = com.jby.common.util.FileUtil.getFile((String)savePath);
os = new FileOutputStream(saveFile);
byte[] buffer = new byte[15360];
while ((length = is.read(buffer)) != -1) {
((OutputStream)os).write(buffer, 0, length);
}
os.flush();
File file = saveFile;
return file;
}
catch (Exception e) {
log.error("\u5199\u5165\u6587\u4ef6\u5f02\u5e38, savePath:{}", (Object)savePath, (Object)e);
throw new BusinessException("\u5199\u5165\u6587\u4ef6\u5f02\u5e38");
}
finally {
if (is != null) {
try {
is.close();
}
catch (IOException e) {
log.error("close error", (Throwable)e);
}
}
if (os != null) {
try {
((OutputStream)os).close();
}
catch (IOException e) {
log.error("close error", (Throwable)e);
}
}
}
}
/*
* WARNING - Removed try catching itself - possible behaviour change.
*/
public static String mf2svg(InputStream is, String imgPath) {
HashMap<String, Object> params = null;
String result = null;
try {
File localFile = com.jby.common.util.FileUtil.getFile((String)imgPath);
FileUtil.writeFromStream((InputStream)is, (File)localFile);
String accessUrl = OssServiceUtil.uploadFile(new OssParam().setModule(AttachmentModuleEnum.OFFICE_IMG.getModule()).setFile(com.jby.common.util.FileUtil.getMultipartFile((String)localFile.getName(), (File)localFile)));
params = new HashMap<String, Object>();
params.put("fileUrl", accessUrl);
params.put("refreshImg", false);
Map headers = RequestHeaderUtil.getAuthHeaders();
result = HttpClientUtil.doPost((String)officeCoreConfig.getMf2SvgApi(), params, (Map)headers);
JSONObject json = (JSONObject)HttpClientUtil.parseResult((String)result, (TypeReference)new TypeReference<JSONObject>(){});
String string = Objects.isNull(json) ? null : json.getString("svgUrl");
return string;
}
catch (Exception e) {
log.error("mf2svg exception, imgPath:{}, params:{}, result:{}", new Object[]{imgPath, JSON.toJSONString(params), result, e});
String string = null;
return string;
}
finally {
if (is != null) {
try {
is.close();
}
catch (IOException e) {
log.error("close error");
}
}
}
}
public static String mf2svg(InputStream is, String ossFolder, String imgName) {
HashMap<String, Object> params = null;
String result = null;
try {
String imgUrl = OssServiceUtil.uploadStream(new OssParam().setModule(AttachmentModuleEnum.OFFICE_IMG.getModule()).setFolder(ossFolder).setFileName(imgName).setInputStream(is));
params = new HashMap<String, Object>();
params.put("fileUrl", imgUrl);
params.put("refreshImg", false);
Map headers = RequestHeaderUtil.getAuthHeaders();
result = HttpClientUtil.doPost((String)officeCoreConfig.getMf2SvgApi(), params, (Map)headers);
JSONObject json = (JSONObject)HttpClientUtil.parseResult((String)result, (TypeReference)new TypeReference<JSONObject>(){});
return Objects.isNull(json) ? null : json.getString("svgUrl");
}
catch (Exception e) {
log.error("mf2svg exception, ossFolder:{}, imgName:{}, params:{}, result:{}", new Object[]{ossFolder, imgName, JSON.toJSONString(params), result, e});
return null;
}
}
}