Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
3393b7ba6d
|
|
@ -0,0 +1,499 @@
|
||||||
|
package com.bonus.material.common.utils;
|
||||||
|
|
||||||
|
import com.bonus.common.core.web.domain.AjaxResult;
|
||||||
|
import com.bonus.material.materialStation.domain.ProAuthorizeDetails;
|
||||||
|
import com.bonus.system.api.RemoteFileService;
|
||||||
|
import org.apache.poi.hwpf.HWPFDocument;
|
||||||
|
import org.apache.poi.hwpf.usermodel.Picture;
|
||||||
|
import org.apache.poi.openxml4j.opc.OPCPackage;
|
||||||
|
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
|
||||||
|
import org.apache.poi.xwpf.usermodel.*;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Word 解析工具类(兼容本地路径与 URL,支持 doc/docx 文字与图片提取)
|
||||||
|
* 依赖:Apache POI (poi-ooxml, poi-scratchpad)
|
||||||
|
*
|
||||||
|
* 使用:
|
||||||
|
* List<ProAuthorizeDetails> list = WordParserUtil.parseWordFile(multipartFile, filePathUrlOrLocalPath, sysFileService);
|
||||||
|
*
|
||||||
|
* 注意:sysFileService.upload(...) 应返回 AjaxResult 或类似结构,且包含 data.url 字段(你项目的上传逻辑)。
|
||||||
|
*/
|
||||||
|
public class WordParserUtil {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(WordParserUtil.class);
|
||||||
|
|
||||||
|
// 当 filePath 为 URL 时,先下载到本地临时文件再解析
|
||||||
|
private static final String TEMP_PREFIX = "word_parse_";
|
||||||
|
|
||||||
|
// 身份证号正则(15 或 18,最后一位可能是 Xx)
|
||||||
|
private static final Pattern ID_PATTERN = Pattern.compile("(\\d{15}|\\d{17}[0-9Xx])");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一入口
|
||||||
|
*
|
||||||
|
* @param file 前端上传的 MultipartFile(原始文件)
|
||||||
|
* @param filePath sysFileService.upload 返回的文件访问路径(可以是 http(s) URL 或 本地路径)
|
||||||
|
* @param sysFileService 你项目的文件上传服务实例(用于上传解析出的图片)
|
||||||
|
*/
|
||||||
|
public static Map<String, Object> parseWordFile(MultipartFile file,String fileName, String filePath, RemoteFileService sysFileService) throws Exception {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
|
||||||
|
List<ProAuthorizeDetails> receivers = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
String originalName = file.getOriginalFilename();
|
||||||
|
if (originalName == null) {
|
||||||
|
originalName = "upload.docx";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据扩展名选择解析
|
||||||
|
String lower = originalName.toLowerCase();
|
||||||
|
File localFile;
|
||||||
|
if (filePath.startsWith("http://") || filePath.startsWith("https://")) {
|
||||||
|
localFile = downloadToLocal(filePath);
|
||||||
|
} else {
|
||||||
|
Path p = Paths.get(filePath);
|
||||||
|
if (Files.exists(p)) {
|
||||||
|
localFile = p.toFile();
|
||||||
|
} else {
|
||||||
|
localFile = writeMultipartToTemp(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.endsWith(".docx")) {
|
||||||
|
try (FileInputStream fis2 = new FileInputStream(localFile)) {
|
||||||
|
receivers = parseDocx(fis2, sysFileService);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("文件格式不对,请下载模版(仅支持docx)");
|
||||||
|
}
|
||||||
|
|
||||||
|
result.put("success", true);
|
||||||
|
result.put("receivers", receivers); // 识别出来的委托人
|
||||||
|
result.put("filePath", filePath); // 文档路径,始终返回
|
||||||
|
result.put("fileName", fileName);
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", "文档解析失败");
|
||||||
|
result.put("filePath", filePath);
|
||||||
|
result.put("fileName", fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* ================= helper: 下载/写临时文件 ================= */
|
||||||
|
|
||||||
|
private static File downloadToLocal(String fileUrl) throws IOException {
|
||||||
|
// 对 URL 的文件名部分做编码(处理中文/空格)
|
||||||
|
String encoded = encodeChineseInUrl(fileUrl);
|
||||||
|
URL url = new URL(encoded);
|
||||||
|
|
||||||
|
Path tempDir = Files.createTempDirectory(TEMP_PREFIX);
|
||||||
|
String fileName = fileUrl.substring(fileUrl.lastIndexOf('/') + 1);
|
||||||
|
Path target = tempDir.resolve(fileName);
|
||||||
|
|
||||||
|
try (InputStream in = url.openStream()) {
|
||||||
|
Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("下载远程文件失败: {}, encodedUrl={}", fileUrl, encoded, e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
return target.toFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String encodeChineseInUrl(String url) throws UnsupportedEncodingException {
|
||||||
|
int idx = url.lastIndexOf('/');
|
||||||
|
if (idx < 0) {
|
||||||
|
return URLEncoder.encode(url, "UTF-8").replace("+", "%20");
|
||||||
|
}
|
||||||
|
String prefix = url.substring(0, idx + 1);
|
||||||
|
String fileName = url.substring(idx + 1);
|
||||||
|
return prefix + URLEncoder.encode(fileName, "UTF-8").replace("+", "%20");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static File writeMultipartToTemp(MultipartFile file) throws IOException {
|
||||||
|
Path tempDir = Files.createTempDirectory(TEMP_PREFIX);
|
||||||
|
String fileName = file.getOriginalFilename() == null ? UUID.randomUUID().toString() + ".docx" : file.getOriginalFilename();
|
||||||
|
Path target = tempDir.resolve(fileName);
|
||||||
|
try (InputStream in = file.getInputStream()) {
|
||||||
|
Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
return target.toFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 文本抽取(鲁棒) ================= */
|
||||||
|
|
||||||
|
// 归一化文本:去零宽字符、合并空白等
|
||||||
|
private static String normalizeText(String text) {
|
||||||
|
if (text == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
text = text.replace('\u00A0', ' '); // NBSP
|
||||||
|
text = text.replaceAll("[\\u200B-\\u200F\\uFEFF]", "");
|
||||||
|
text = text.replaceAll("[\\p{Cntrl}&&[^\r\n\t]]", " ");
|
||||||
|
text = text.replaceAll("\\s+", " ");
|
||||||
|
return text.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鲁棒提取姓名+身份证对
|
||||||
|
private static List<ProAuthorizeDetails> extractReceivers(String text) {
|
||||||
|
List<ProAuthorizeDetails> list = new ArrayList<>();
|
||||||
|
if (text == null || text.trim().isEmpty()) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
String norm = normalizeText(text);
|
||||||
|
|
||||||
|
// 支持: (孙贤红)身份证号123... / 孙贤红 身份证号123... / 被授权人姓名:孙贤红 身份证号...
|
||||||
|
Pattern p1 = Pattern.compile("[\\((\\[]?\\s*([\\p{IsHan}·•・]{2,10})\\s*[\\))\\]]?\\s*身份证号\\s*(\\d{15}|\\d{17}[0-9Xx])");
|
||||||
|
Matcher m1 = p1.matcher(norm);
|
||||||
|
while (m1.find()) {
|
||||||
|
ProAuthorizeDetails d = new ProAuthorizeDetails();
|
||||||
|
d.setName(m1.group(1).trim());
|
||||||
|
d.setIdNumber(m1.group(2).trim());
|
||||||
|
list.add(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 若 p1 未找到,再试带“被授权人姓名/委托人”等标签的模式
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
Pattern p2 = Pattern.compile("(被授权人姓名|委托人姓名|委托人|被授权人)[::]?\\s*([\\p{IsHan}·•・]{2,10}).{0,50}?身份证号\\s*(\\d{15}|\\d{17}[0-9Xx])");
|
||||||
|
Matcher m2 = p2.matcher(norm);
|
||||||
|
while (m2.find()) {
|
||||||
|
ProAuthorizeDetails d = new ProAuthorizeDetails();
|
||||||
|
d.setName(m2.group(2).trim());
|
||||||
|
d.setIdNumber(m2.group(3).trim());
|
||||||
|
list.add(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 上下文判定(正面/背面) ================= */
|
||||||
|
|
||||||
|
private static boolean contextSuggestsFront(String ctx) {
|
||||||
|
if (ctx == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String lower = ctx.toLowerCase();
|
||||||
|
return lower.contains("正面") || lower.contains("头像") || lower.contains("人像") || lower.contains("正 ");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean contextSuggestsBack(String ctx) {
|
||||||
|
if (ctx == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String lower = ctx.toLowerCase();
|
||||||
|
return lower.contains("背面") || lower.contains("国徽") || lower.contains("背 ");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 图片上传(调用 sysFileService.upload) ================= */
|
||||||
|
|
||||||
|
// 你项目的 sysFileService.upload(MultipartFile) 应返回 AjaxResult,data 中包含 url 字段
|
||||||
|
private static String saveImageWithSysFileService(byte[] data, String ext, RemoteFileService sysFileService) {
|
||||||
|
try {
|
||||||
|
String fileName = UUID.randomUUID().toString().replace("-", "") + "." + (ext == null ? "jpg" : ext);
|
||||||
|
MultipartFile multipartFile = new MockMultipartFile(fileName, fileName, "image/" + (ext == null ? "jpeg" : ext), data);
|
||||||
|
|
||||||
|
AjaxResult uploadRes = sysFileService.upload(multipartFile);
|
||||||
|
if (uploadRes != null && uploadRes.isSuccess()) {
|
||||||
|
Object d = uploadRes.get("data");
|
||||||
|
if (d instanceof Map) {
|
||||||
|
Map map = (Map) d;
|
||||||
|
Object url = map.get("url");
|
||||||
|
if (url != null) {
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("图片上传到 sysFileService 失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= docx 解析(文字 + 图片) ================= */
|
||||||
|
|
||||||
|
private static List<ProAuthorizeDetails> parseDocx(InputStream inputStream, RemoteFileService sysFileService) throws Exception {
|
||||||
|
List<ProAuthorizeDetails> receivers = new ArrayList<>();
|
||||||
|
XWPFDocument doc = new XWPFDocument(OPCPackage.open(inputStream));
|
||||||
|
|
||||||
|
// 1) 提取全文文本并识别委托人姓名/身份证
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (XWPFParagraph p : doc.getParagraphs()) {
|
||||||
|
sb.append(p.getText()).append(" ");
|
||||||
|
}
|
||||||
|
for (XWPFTable t : doc.getTables()) {
|
||||||
|
for (XWPFTableRow r : t.getRows()) {
|
||||||
|
for (XWPFTableCell c : r.getTableCells()) {
|
||||||
|
sb.append(c.getText()).append(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String fullText = sb.toString();
|
||||||
|
receivers = extractReceivers(fullText); // 你已有的鲁棒方法
|
||||||
|
|
||||||
|
// 2) 遍历文档结构,收集图片 & 标识(注意保持全局 paraIndex)
|
||||||
|
List<ImgHolder> images = new ArrayList<>();
|
||||||
|
List<Marker> markers = new ArrayList<>();
|
||||||
|
int globalParaIndex = 0;
|
||||||
|
int tableIdx = 0;
|
||||||
|
int globalImageCounter = 0;
|
||||||
|
|
||||||
|
List<IBodyElement> bodyElements = doc.getBodyElements();
|
||||||
|
for (IBodyElement be : bodyElements) {
|
||||||
|
if (be.getElementType() == BodyElementType.PARAGRAPH) {
|
||||||
|
XWPFParagraph p = (XWPFParagraph) be;
|
||||||
|
String text = normalizeText(p.getText());
|
||||||
|
String cellKey = "body";
|
||||||
|
// 标识:只匹配委托人相关
|
||||||
|
if (text.contains("委托人") && (text.contains("正面") || text.contains("背面"))) {
|
||||||
|
Marker mk = new Marker();
|
||||||
|
mk.isFront = text.contains("正面");
|
||||||
|
mk.paraIndex = globalParaIndex;
|
||||||
|
mk.cellKey = cellKey;
|
||||||
|
mk.text = text;
|
||||||
|
markers.add(mk);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片(段落内)
|
||||||
|
if (p.getRuns() != null) {
|
||||||
|
for (XWPFRun run : p.getRuns()) {
|
||||||
|
List<XWPFPicture> pics = run.getEmbeddedPictures();
|
||||||
|
if (pics != null && !pics.isEmpty()) {
|
||||||
|
for (XWPFPicture pic : pics) {
|
||||||
|
XWPFPictureData pd = pic.getPictureData();
|
||||||
|
ImgHolder ih = new ImgHolder();
|
||||||
|
ih.data = pd.getData();
|
||||||
|
ih.ext = pd.suggestFileExtension();
|
||||||
|
ih.cellKey = cellKey;
|
||||||
|
ih.paraIndex = globalParaIndex;
|
||||||
|
ih.url = null;
|
||||||
|
ih.imageIndex = globalImageCounter++;
|
||||||
|
images.add(ih);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globalParaIndex++;
|
||||||
|
} else if (be.getElementType() == BodyElementType.TABLE) {
|
||||||
|
XWPFTable table = (XWPFTable) be;
|
||||||
|
for (int r = 0; r < table.getRows().size(); r++) {
|
||||||
|
XWPFTableRow row = table.getRows().get(r);
|
||||||
|
List<XWPFTableCell> cells = row.getTableCells();
|
||||||
|
for (int c = 0; c < cells.size(); c++) {
|
||||||
|
XWPFTableCell cell = cells.get(c);
|
||||||
|
String cellKey = "t" + tableIdx + "_r" + r + "_c" + c;
|
||||||
|
for (XWPFParagraph p : cell.getParagraphs()) {
|
||||||
|
String text = normalizeText(p.getText());
|
||||||
|
if (text.contains("委托人") && (text.contains("正面") || text.contains("背面"))) {
|
||||||
|
Marker mk = new Marker();
|
||||||
|
mk.isFront = text.contains("正面");
|
||||||
|
mk.paraIndex = globalParaIndex;
|
||||||
|
mk.cellKey = cellKey;
|
||||||
|
mk.text = text;
|
||||||
|
markers.add(mk);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.getRuns() != null) {
|
||||||
|
for (XWPFRun run : p.getRuns()) {
|
||||||
|
List<XWPFPicture> pics = run.getEmbeddedPictures();
|
||||||
|
if (pics != null && !pics.isEmpty()) {
|
||||||
|
for (XWPFPicture pic : pics) {
|
||||||
|
XWPFPictureData pd = pic.getPictureData();
|
||||||
|
ImgHolder ih = new ImgHolder();
|
||||||
|
ih.data = pd.getData();
|
||||||
|
ih.ext = pd.suggestFileExtension();
|
||||||
|
ih.cellKey = cellKey;
|
||||||
|
ih.paraIndex = globalParaIndex;
|
||||||
|
ih.url = null;
|
||||||
|
ih.imageIndex = globalImageCounter++;
|
||||||
|
images.add(ih);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globalParaIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tableIdx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有任何标识(markers),尽量 fallback:不做这里的自动索引分配(后面会尝试按剩余图片补位)
|
||||||
|
if (markers.isEmpty()) {
|
||||||
|
// 直接尝试按“模板索引规则”作为兜底(例如单人取 images[0]/images[2],两人取 [2,4] & [0,1] 等)
|
||||||
|
// 但首先还是希望尽量靠标识。下面的代码会在分配后进行补位。
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) 按标识顺序分配图片给委托人(只关注委托人)
|
||||||
|
Set<Integer> usedImageIndices = new HashSet<>();
|
||||||
|
int delegateIdx = 0; // 当前分配到第几个委托人(0-based)
|
||||||
|
|
||||||
|
for (Marker mk : markers) {
|
||||||
|
// 只处理“委托人”的标识(忽略“法人代表”或其它)
|
||||||
|
if (mk.text == null || !mk.text.contains("委托人")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (receivers.isEmpty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int targetReceiver = Math.min(delegateIdx, receivers.size() - 1);
|
||||||
|
|
||||||
|
// 找到最佳图片候选:优先同 cell、paraIndex >= marker.paraIndex、未被用过
|
||||||
|
ImgHolder found = null;
|
||||||
|
for (ImgHolder ih : images) {
|
||||||
|
if (usedImageIndices.contains(ih.imageIndex)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(ih.cellKey, mk.cellKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ih.paraIndex >= mk.paraIndex) { found = ih; break; }
|
||||||
|
}
|
||||||
|
// 如果未找到同 cell 的,则找全局 paraIndex >= marker.paraIndex
|
||||||
|
if (found == null) {
|
||||||
|
for (ImgHolder ih : images) {
|
||||||
|
if (usedImageIndices.contains(ih.imageIndex)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ih.paraIndex >= mk.paraIndex) { found = ih; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 最后兜底:任意未使用图片
|
||||||
|
if (found == null) {
|
||||||
|
for (ImgHolder ih : images) {
|
||||||
|
if (!usedImageIndices.contains(ih.imageIndex)) { found = ih; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found != null) {
|
||||||
|
// 上传(惰性):若还未上传(url==null)则上传一次
|
||||||
|
if (found.url == null) {
|
||||||
|
String uploaded = saveImageWithSysFileService(found.data, found.ext, sysFileService);
|
||||||
|
found.url = uploaded;
|
||||||
|
}
|
||||||
|
if (found.url != null) {
|
||||||
|
ProAuthorizeDetails p = receivers.get(targetReceiver);
|
||||||
|
if (mk.isFront) {
|
||||||
|
p.setFrontUrl(found.url);
|
||||||
|
} else {
|
||||||
|
p.setBackUrl(found.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
usedImageIndices.add(found.imageIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果这是“背面”,则才认为一组委托人完成,delegateIdx++(开始下一人)
|
||||||
|
if (!mk.isFront) {
|
||||||
|
delegateIdx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) 补位:若某些委托人缺少 front/back,用剩余图片补上(不覆盖已有)
|
||||||
|
if (!receivers.isEmpty() && !images.isEmpty()) {
|
||||||
|
for (int i = 0; i < receivers.size(); i++) {
|
||||||
|
ProAuthorizeDetails p = receivers.get(i);
|
||||||
|
if ((p.getFrontUrl() == null || p.getFrontUrl().isEmpty()) ||
|
||||||
|
(p.getBackUrl() == null || p.getBackUrl().isEmpty())) {
|
||||||
|
|
||||||
|
for (ImgHolder ih : images) {
|
||||||
|
if (usedImageIndices.contains(ih.imageIndex)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 上传并分配
|
||||||
|
if (ih.url == null) {
|
||||||
|
ih.url = saveImageWithSysFileService(ih.data, ih.ext, sysFileService);
|
||||||
|
}
|
||||||
|
if (ih.url == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (p.getFrontUrl() == null || p.getFrontUrl().isEmpty()) {
|
||||||
|
p.setFrontUrl(ih.url);
|
||||||
|
usedImageIndices.add(ih.imageIndex);
|
||||||
|
} else if (p.getBackUrl() == null || p.getBackUrl().isEmpty()) {
|
||||||
|
p.setBackUrl(ih.url);
|
||||||
|
usedImageIndices.add(ih.imageIndex);
|
||||||
|
}
|
||||||
|
if ((p.getFrontUrl() != null && !p.getFrontUrl().isEmpty()) &&
|
||||||
|
(p.getBackUrl() != null && !p.getBackUrl().isEmpty())) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return receivers;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private static int handleParagraph(XWPFParagraph p, List<ProAuthorizeDetails> receivers,
|
||||||
|
RemoteFileService sysFileService, int personIndex) {
|
||||||
|
String text = normalizeText(p.getText());
|
||||||
|
|
||||||
|
if (text.contains("身份证(正面)") || text.contains("身份证(背面)")) {
|
||||||
|
for (XWPFRun run : p.getRuns()) {
|
||||||
|
for (XWPFPicture pic : run.getEmbeddedPictures()) {
|
||||||
|
XWPFPictureData pd = pic.getPictureData();
|
||||||
|
String url = saveImageWithSysFileService(pd.getData(), pd.suggestFileExtension(), sysFileService);
|
||||||
|
if (url == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.contains("委托人")) {
|
||||||
|
// 委托人
|
||||||
|
if (!receivers.isEmpty()) {
|
||||||
|
ProAuthorizeDetails person = receivers.get(Math.min(personIndex, receivers.size() - 1));
|
||||||
|
if (text.contains("正面")) {
|
||||||
|
person.setFrontUrl(url);
|
||||||
|
} else {
|
||||||
|
person.setBackUrl(url);
|
||||||
|
personIndex++; // 背面处理完 → 下一个人
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return personIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private static class ImgHolder {
|
||||||
|
byte[] data;
|
||||||
|
String ext;
|
||||||
|
String cellKey; // 如 "body" 或 "t0_r1_c2"
|
||||||
|
int paraIndex;
|
||||||
|
String url; // 上传后返回的 url(惰性赋值)
|
||||||
|
int imageIndex; // 全局顺序索引(可选,用于调试)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Marker {
|
||||||
|
boolean isFront; // true = 正面, false = 背面
|
||||||
|
int paraIndex;
|
||||||
|
String cellKey;
|
||||||
|
String text; // 原始文本,用于调试
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ import com.bonus.common.core.web.page.TableDataInfo;
|
||||||
import com.bonus.common.log.annotation.SysLog;
|
import com.bonus.common.log.annotation.SysLog;
|
||||||
import com.bonus.common.log.enums.OperaType;
|
import com.bonus.common.log.enums.OperaType;
|
||||||
import com.bonus.material.common.annotation.PreventRepeatSubmit;
|
import com.bonus.material.common.annotation.PreventRepeatSubmit;
|
||||||
|
import com.bonus.material.common.utils.WordParserUtil;
|
||||||
import com.bonus.material.materialStation.domain.ProAuthorizeDetails;
|
import com.bonus.material.materialStation.domain.ProAuthorizeDetails;
|
||||||
import com.bonus.material.materialStation.domain.ProAuthorizeInfo;
|
import com.bonus.material.materialStation.domain.ProAuthorizeInfo;
|
||||||
import com.bonus.material.materialStation.service.ProAuthorizeService;
|
import com.bonus.material.materialStation.service.ProAuthorizeService;
|
||||||
|
|
@ -19,13 +20,17 @@ import com.bonus.system.api.RemoteFileService;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -81,6 +86,26 @@ public class ProAuthorizeController extends BaseController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/parseWord")
|
||||||
|
public AjaxResult parseWord(@RequestParam("file") MultipartFile file) {
|
||||||
|
try {
|
||||||
|
AjaxResult result = sysFileService.upload(file);
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
return AjaxResult.error("文件上传失败");
|
||||||
|
}
|
||||||
|
Map<String, Object> jsonObject = (Map<String, Object>) result.get("data");
|
||||||
|
String fileName = file.getOriginalFilename();
|
||||||
|
String filePath = jsonObject.get("url").toString();
|
||||||
|
Map<String, Object> data = WordParserUtil.parseWordFile(file,fileName, filePath, sysFileService);
|
||||||
|
return AjaxResult.success(data);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("解析失败", e);
|
||||||
|
return AjaxResult.error("解析失败,请按模版填写内容");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 授权提交
|
* 授权提交
|
||||||
*/
|
*/
|
||||||
|
|
@ -138,5 +163,15 @@ public class ProAuthorizeController extends BaseController {
|
||||||
return AjaxResult.success(ListPagingUtil.paging(pageIndex, pageSize, new ArrayList<>()));
|
return AjaxResult.success(ListPagingUtil.paging(pageIndex, pageSize, new ArrayList<>()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 授权委托书模版下载
|
||||||
|
*/
|
||||||
|
@ApiOperation(value = "授权委托书模版下载")
|
||||||
|
@PostMapping("/downLoad")
|
||||||
|
public void downLoadExcelFile(){
|
||||||
|
HttpServletResponse resp = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getResponse();
|
||||||
|
service.downLoadTemplate(resp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import com.bonus.material.materialStation.domain.ProAuthorizeInfo;
|
||||||
import org.springframework.web.bind.annotation.RequestPart;
|
import org.springframework.web.bind.annotation.RequestPart;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -57,4 +58,10 @@ public interface ProAuthorizeService {
|
||||||
int updateAuthorizeInfoSign(ProAuthorizeDetails bean);
|
int updateAuthorizeInfoSign(ProAuthorizeDetails bean);
|
||||||
|
|
||||||
List<ProAuthorizeInfo> getAuthorList(ProAuthorizeInfo bean);
|
List<ProAuthorizeInfo> getAuthorList(ProAuthorizeInfo bean);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 授权委托书模版下载
|
||||||
|
* @param resp
|
||||||
|
*/
|
||||||
|
void downLoadTemplate(HttpServletResponse resp);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import com.bonus.material.materialStation.mapper.ProAuthorizeMapper;
|
||||||
import com.bonus.material.materialStation.service.ProAuthorizeService;
|
import com.bonus.material.materialStation.service.ProAuthorizeService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import okhttp3.*;
|
import okhttp3.*;
|
||||||
|
import org.apache.commons.io.IOUtils;
|
||||||
import org.hibernate.validator.internal.util.StringHelper;
|
import org.hibernate.validator.internal.util.StringHelper;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
@ -22,10 +23,8 @@ import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.io.ByteArrayOutputStream;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.File;
|
import java.io.*;
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
|
@ -283,6 +282,38 @@ public class ProAuthorizeServiceImpl implements ProAuthorizeService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void downLoadTemplate(HttpServletResponse response) {
|
||||||
|
// 模板名称
|
||||||
|
String templateName = "授权委托书模板.doc";
|
||||||
|
try (
|
||||||
|
InputStream input = this.getClass().getClassLoader().getResourceAsStream("template/授权委托书模板.doc");
|
||||||
|
OutputStream out = response.getOutputStream()
|
||||||
|
) {
|
||||||
|
if (input == null) {
|
||||||
|
throw new FileNotFoundException("模板文件不存在: " + templateName);
|
||||||
|
}
|
||||||
|
// 设置响应头
|
||||||
|
response.setCharacterEncoding("UTF-8");
|
||||||
|
response.setContentType("application/msword");
|
||||||
|
response.setHeader(
|
||||||
|
"Content-Disposition",
|
||||||
|
"attachment;filename=" + new String(templateName.getBytes("UTF-8"), "ISO-8859-1")
|
||||||
|
);
|
||||||
|
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
|
||||||
|
// 缓冲区传输
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
int bytesToRead;
|
||||||
|
while ((bytesToRead = input.read(buffer)) != -1) {
|
||||||
|
out.write(buffer, 0, bytesToRead);
|
||||||
|
}
|
||||||
|
out.flush();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("下载模板失败: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断关键字是否包含在item中
|
* 判断关键字是否包含在item中
|
||||||
* @param item
|
* @param item
|
||||||
|
|
|
||||||
|
|
@ -340,6 +340,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
tk.create_time AS createTime,
|
tk.create_time AS createTime,
|
||||||
tk.remark,
|
tk.remark,
|
||||||
tk.CODE AS repairNum,
|
tk.CODE AS repairNum,
|
||||||
|
rad.audit_time as auditTime,
|
||||||
GROUP_CONCAT(DISTINCT mt4.type_id) AS firstId
|
GROUP_CONCAT(DISTINCT mt4.type_id) AS firstId
|
||||||
FROM
|
FROM
|
||||||
tm_task tk
|
tm_task tk
|
||||||
|
|
@ -402,6 +403,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
tk.create_time AS createTime,
|
tk.create_time AS createTime,
|
||||||
tk.remark,
|
tk.remark,
|
||||||
tk.CODE AS repairNum,
|
tk.CODE AS repairNum,
|
||||||
|
rad.audit_time as auditTime,
|
||||||
GROUP_CONCAT(DISTINCT mt4.type_id) AS firstId
|
GROUP_CONCAT(DISTINCT mt4.type_id) AS firstId
|
||||||
FROM
|
FROM
|
||||||
tm_task tk
|
tm_task tk
|
||||||
|
|
@ -449,9 +451,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
GROUP BY
|
GROUP BY
|
||||||
tk.CODE
|
tk.CODE
|
||||||
) AS combined_results
|
) AS combined_results
|
||||||
|
<choose>
|
||||||
|
<when test="appTaskStatus != null and appTaskStatus == 11">
|
||||||
|
ORDER BY auditTime DESC
|
||||||
|
</when>
|
||||||
|
<otherwise>
|
||||||
ORDER BY
|
ORDER BY
|
||||||
taskStatus,
|
taskStatus,
|
||||||
createTime DESC; -- 统一在外部排序
|
createTime DESC
|
||||||
|
</otherwise>
|
||||||
|
</choose>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectTypeNameByTaskId" resultType="java.lang.String">
|
<select id="selectTypeNameByTaskId" resultType="java.lang.String">
|
||||||
|
|
|
||||||
Binary file not shown.
Loading…
Reference in New Issue