南大三十年教职工疗休养
This commit is contained in:
@@ -69,6 +69,17 @@ public class SysDataUserPullController {
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@SaCheckPermission("sys.data.user.pull")
|
||||
@ApiOperation("同步教职工手机号")
|
||||
public Result syncTeacherMobile() {
|
||||
try {
|
||||
return Result.success(sysUserPullService.syncTeacherMobile());
|
||||
} catch (Exception e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@At
|
||||
@SaCheckPermission("sys.data.user.pull")
|
||||
@ApiOperation("删除用户数据")
|
||||
|
||||
@@ -38,4 +38,11 @@ public interface SysDataUserPullService extends BaseService<Sys_user_source> {
|
||||
* @return
|
||||
*/
|
||||
Map<String, NutMap> pullFinance();
|
||||
|
||||
/**
|
||||
* 从数据中心分页拉取教职工手机号,并按工号同步到 sys_user.mobile。
|
||||
*
|
||||
* @return 同步过程统计信息
|
||||
*/
|
||||
NutMap syncTeacherMobile();
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.budwk.app.sys.services.SysDictService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nutz.aop.interceptor.async.Async;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Chain;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
@@ -47,6 +48,10 @@ import java.util.stream.Collectors;
|
||||
@Slf4j
|
||||
public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source> implements SysDataUserPullService {
|
||||
|
||||
private static final String TEACHER_MOBILE_CONFIG_KEY = "teacherMobile";
|
||||
private static final int TEACHER_MOBILE_PAGE_SIZE = 1000;
|
||||
private static final int TEACHER_MOBILE_DB_BATCH_SIZE = 500;
|
||||
|
||||
@Inject
|
||||
private SysDictService sysDictService;
|
||||
@Inject
|
||||
@@ -400,6 +405,135 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
|
||||
return listMap(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据中心分页同步教职工手机号,并按工号更新 sys_user.mobile。
|
||||
*/
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public NutMap syncTeacherMobile() {
|
||||
DataCenterProperties.Credential credential = dataCenterProperties.credential(TEACHER_MOBILE_CONFIG_KEY);
|
||||
String url = credential.getUrl();
|
||||
String token = credential.getToken();
|
||||
log.info("教职工手机号同步开始:url={}, tokenReady={}", url, StrUtil.isNotBlank(token));
|
||||
|
||||
List<JSONObject> rawDataList = new ArrayList<>();
|
||||
int pageNum = 1;
|
||||
int pageSize = TEACHER_MOBILE_PAGE_SIZE;
|
||||
int total = 0;
|
||||
int pages = 1;
|
||||
do {
|
||||
Map<String, Object> reqBody = buildTeacherMobileRequestBody(pageNum, pageSize);
|
||||
log.info("教职工手机号接口请求:pageNum={}, pageSize={}, body={}", pageNum, pageSize, reqBody);
|
||||
|
||||
HttpRequest httpRequest = HttpUtil.createPost(url);
|
||||
httpRequest.header(Header.CONTENT_TYPE, "application/json");
|
||||
httpRequest.header("X-H3C-TOKEN", token);
|
||||
httpRequest.body(JSONUtil.toJsonStr(reqBody));
|
||||
|
||||
String resBody = httpRequest.execute().body();
|
||||
log.info("教职工手机号接口返回:pageNum={}, response={}", pageNum, resBody);
|
||||
JSONObject resp = JSONUtil.parseObj(resBody);
|
||||
if (!"0".equals(resp.getStr("code"))) {
|
||||
throw new BaseException("获取教职工手机号失败,错误码: " + resp.getStr("code") + ";错误原因:" + resp.getStr("msg"));
|
||||
}
|
||||
|
||||
JSONObject data = resp.getJSONObject("data");
|
||||
if (data != null) {
|
||||
total = data.getInt("total", total);
|
||||
pages = data.getInt("pages", pages);
|
||||
if (pages <= 0 && total > 0) {
|
||||
pages = (int) Math.ceil((double) total / pageSize);
|
||||
}
|
||||
}
|
||||
|
||||
JSONArray records = readTeacherMobileRecords(resp, data);
|
||||
if (CollUtil.isNotEmpty(records)) {
|
||||
rawDataList.addAll(records.stream().map(v -> (JSONObject) v).toList());
|
||||
}
|
||||
log.info("教职工手机号拉取进度:pageNum={}, pages={}, total={}, pulled={}",
|
||||
pageNum, pages, total, rawDataList.size());
|
||||
pageNum++;
|
||||
} while (pageNum <= Math.max(pages, 1));
|
||||
|
||||
Map<String, String> mobileMap = collectTeacherMobileMap(rawDataList);
|
||||
NutMap updateResult = updateTeacherMobile(mobileMap);
|
||||
NutMap result = NutMap.NEW()
|
||||
.addv("total", total)
|
||||
.addv("pages", pages)
|
||||
.addv("pulledCount", rawDataList.size())
|
||||
.addv("validMobileCount", mobileMap.size())
|
||||
.addv("matchedUserCount", updateResult.getInt("matchedUserCount", 0))
|
||||
.addv("updatedUserCount", updateResult.getInt("updatedUserCount", 0));
|
||||
log.info("教职工手机号同步完成:{}", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装手机号接口分页请求体,uid、gh、sjh 为空时表示全量分页拉取。
|
||||
*/
|
||||
private Map<String, Object> buildTeacherMobileRequestBody(int pageNum, int pageSize) {
|
||||
Map<String, Object> reqBody = new LinkedHashMap<>();
|
||||
reqBody.put("uid", null);
|
||||
reqBody.put("gh", null);
|
||||
reqBody.put("sjh", null);
|
||||
reqBody.put("pageNum", pageNum);
|
||||
reqBody.put("pageSize", pageSize);
|
||||
return reqBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取手机号接口明细数据,优先按文档读取顶层 records,同时兼容 data.records。
|
||||
*/
|
||||
private JSONArray readTeacherMobileRecords(JSONObject resp, JSONObject data) {
|
||||
JSONArray records = resp.getJSONArray("records");
|
||||
if (records == null && data != null) {
|
||||
records = data.getJSONArray("records");
|
||||
}
|
||||
return records == null ? new JSONArray() : records;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将接口记录转换为工号到手机号的映射,过滤缺少工号或手机号的无效数据。
|
||||
*/
|
||||
private Map<String, String> collectTeacherMobileMap(List<JSONObject> rawDataList) {
|
||||
Map<String, String> mobileMap = new LinkedHashMap<>();
|
||||
for (JSONObject rawData : rawDataList) {
|
||||
String loginname = StrUtil.trimToEmpty(rawData.getStr("gh"));
|
||||
String mobile = StrUtil.trimToEmpty(rawData.getStr("sjh"));
|
||||
if (StrUtil.isAllNotBlank(loginname, mobile)) {
|
||||
mobileMap.put(loginname, mobile);
|
||||
}
|
||||
}
|
||||
log.info("教职工手机号有效数据整理完成:rawCount={}, validCount={}", rawDataList.size(), mobileMap.size());
|
||||
return mobileMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分批匹配 sys_user.loginname 并更新手机号,避免一次性 IN 条件过长。
|
||||
*/
|
||||
private NutMap updateTeacherMobile(Map<String, String> mobileMap) {
|
||||
if (mobileMap == null || mobileMap.isEmpty()) {
|
||||
return NutMap.NEW().addv("matchedUserCount", 0).addv("updatedUserCount", 0);
|
||||
}
|
||||
int matchedUserCount = 0;
|
||||
int updatedUserCount = 0;
|
||||
List<String> loginNames = new ArrayList<>(mobileMap.keySet());
|
||||
for (int start = 0; start < loginNames.size(); start += TEACHER_MOBILE_DB_BATCH_SIZE) {
|
||||
int end = Math.min(start + TEACHER_MOBILE_DB_BATCH_SIZE, loginNames.size());
|
||||
List<String> batchLoginNames = loginNames.subList(start, end);
|
||||
List<Sys_user> users = dao().query(Sys_user.class, Cnd.where(Sys_user::getLoginname, "in", batchLoginNames));
|
||||
matchedUserCount += users.size();
|
||||
for (Sys_user user : users) {
|
||||
String newMobile = mobileMap.get(user.getLoginname());
|
||||
if (StrUtil.isNotBlank(newMobile) && !Objects.equals(newMobile, user.getMobile())) {
|
||||
updatedUserCount += dao().update(Sys_user.class, Chain.make("mobile", newMobile), Cnd.where(Sys_user::getId, "=", user.getId()));
|
||||
}
|
||||
}
|
||||
log.info("教职工手机号批次更新完成:batchStart={}, batchEnd={}, batchMatched={}, updatedTotal={}",
|
||||
start, end, users.size(), updatedUserCount);
|
||||
}
|
||||
return NutMap.NEW().addv("matchedUserCount", matchedUserCount).addv("updatedUserCount", updatedUserCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, NutMap> pullFinance() {
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.budwk.app.base.exception.BaseException;
|
||||
import io.minio.*;
|
||||
import io.minio.http.Method;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nutz.boot.AppContext;
|
||||
import org.nutz.ioc.Ioc;
|
||||
import org.nutz.ioc.impl.PropertiesProxy;
|
||||
import org.nutz.mvc.Mvcs;
|
||||
@@ -49,7 +50,7 @@ public class SysFileMinIoUtil {
|
||||
* 初始化操作的客户端
|
||||
*/
|
||||
private static void initClient() {
|
||||
Ioc ioc = Mvcs.getIoc();
|
||||
Ioc ioc = AppContext.getDefault().getIoc();
|
||||
PropertiesProxy propertiesProxy = ioc.get(PropertiesProxy.class, "conf");
|
||||
|
||||
String accessKey = propertiesProxy.get("minio.accessKey");
|
||||
|
||||
+236
-40
@@ -91,8 +91,16 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
@@ -103,6 +111,11 @@ import java.util.zip.ZipOutputStream;
|
||||
@Slf4j
|
||||
public class ThirtyTeachTourLedgerController {
|
||||
|
||||
private static final int LEDGER_EXPORT_IMAGE_THREAD_COUNT = 3;
|
||||
private static final int LEDGER_EXPORT_FILE_QUERY_BATCH_SIZE = 500;
|
||||
private static final int LEDGER_EXPORT_IMAGE_TASK_TIMEOUT_SECONDS = 300;
|
||||
private static final String LEDGER_EXPORT_ROW_IMAGES_KEY = "__ledgerExportImages";
|
||||
|
||||
@Inject
|
||||
private ThirtyTeachTourLedgerService tourLedgerService;
|
||||
|
||||
@@ -379,6 +392,7 @@ public class ThirtyTeachTourLedgerController {
|
||||
entities.add(new ExcelExportEntity("年龄", "age", 10));
|
||||
entities.add(new ExcelExportEntity("身份证号", "idCard", 24));
|
||||
entities.add(new ExcelExportEntity("手机号", "mobile", 18));
|
||||
entities.add(new ExcelExportEntity("所属工会", "unionName", 18));
|
||||
entities.add(new ExcelExportEntity("所在单位", "unitName", 26));
|
||||
entities.add(new ExcelExportEntity("乘车地点", "boardingPlace", 18));
|
||||
entities.add(new ExcelExportEntity("报名线路", "lineName", 30));
|
||||
@@ -405,12 +419,13 @@ public class ThirtyTeachTourLedgerController {
|
||||
ExcelExportEntity imageEntity = new ExcelExportEntity("当前时间段图片" + i, "photoCell" + (maxPhotoCount + i), 18);
|
||||
entities.add(imageEntity);
|
||||
}
|
||||
Map<String, LedgerExportImage> imageMap = batchResolveLedgerImages(rows);
|
||||
rows.forEach(row -> {
|
||||
Integer familyCount = row.getInt("familyCount");
|
||||
row.put("familyCountText", (familyCount == null ? 0 : familyCount) + "人");
|
||||
row.put("joinedText", Boolean.TRUE.equals(row.getBoolean("joined")) ? "是" : "否");
|
||||
row.put("overCostReimbursedText", Boolean.TRUE.equals(row.getBoolean("overCostReimbursed")) ? "是" : "否");
|
||||
fillLedgerPhotoRowImages(row, maxPhotoCount, maxCurrentPeriodPhotoCount);
|
||||
fillLedgerPhotoRowImages(row, maxPhotoCount, maxCurrentPeriodPhotoCount, imageMap);
|
||||
});
|
||||
List<List<LedgerExportImage>> rowImages = snapshotLedgerRowImages(rows);
|
||||
|
||||
@@ -439,22 +454,23 @@ public class ThirtyTeachTourLedgerController {
|
||||
/**
|
||||
* 将每条台账的图片材料解析为可写入 Excel 的图片对象,图片列文本保持为空,后续由 POI 手动插入图片。
|
||||
*/
|
||||
private void fillLedgerPhotoRowImages(NutMap row, int maxPhotoCount, int maxCurrentPeriodPhotoCount) {
|
||||
private void fillLedgerPhotoRowImages(NutMap row, int maxPhotoCount, int maxCurrentPeriodPhotoCount,
|
||||
Map<String, LedgerExportImage> imageMap) {
|
||||
List<String> photoFiles = splitPhotoFiles(row.getString("photoFiles", ""));
|
||||
List<String> currentPeriodPhotoFiles = splitPhotoFiles(row.getString("currentPeriodPhotoFiles", ""));
|
||||
if (!photoFiles.isEmpty()) {
|
||||
log.info("三十年教龄疗休养台账30年前图片字段读取:jobNo={}, userName={}, photoCount={}, photoFiles={}",
|
||||
log.debug("三十年教龄疗休养台账30年前图片字段读取:jobNo={}, userName={}, photoCount={}, photoFiles={}",
|
||||
row.getString("jobNo", ""), row.getString("userName", ""), photoFiles.size(), row.getString("photoFiles", ""));
|
||||
}
|
||||
if (!currentPeriodPhotoFiles.isEmpty()) {
|
||||
log.info("三十年教龄疗休养台账当前时间段图片字段读取:jobNo={}, userName={}, photoCount={}, photoFiles={}",
|
||||
log.debug("三十年教龄疗休养台账当前时间段图片字段读取:jobNo={}, userName={}, photoCount={}, photoFiles={}",
|
||||
row.getString("jobNo", ""), row.getString("userName", ""), currentPeriodPhotoFiles.size(), row.getString("currentPeriodPhotoFiles", ""));
|
||||
}
|
||||
// 图片对象按导出列顺序组装:先 30 年前图片,再当前时间段图片,确保同一教职工图片仍在同一行。
|
||||
List<LedgerExportImage> photoImages = new ArrayList<>();
|
||||
photoImages.addAll(resolveLedgerImages(row.getString("photoFiles", ""), 0));
|
||||
photoImages.addAll(resolveLedgerImages(row.getString("currentPeriodPhotoFiles", ""), maxPhotoCount));
|
||||
row.put("photoImages", photoImages);
|
||||
photoImages.addAll(resolveLedgerImages(row.getString("photoFiles", ""), 0, imageMap));
|
||||
photoImages.addAll(resolveLedgerImages(row.getString("currentPeriodPhotoFiles", ""), maxPhotoCount, imageMap));
|
||||
row.put(LEDGER_EXPORT_ROW_IMAGES_KEY, photoImages);
|
||||
for (int i = 1; i <= maxPhotoCount + maxCurrentPeriodPhotoCount; i++) {
|
||||
row.put("photoCell" + i, "");
|
||||
}
|
||||
@@ -490,7 +506,7 @@ public class ThirtyTeachTourLedgerController {
|
||||
}
|
||||
for (NutMap row : rows) {
|
||||
List<LedgerExportImage> images = new ArrayList<>();
|
||||
Object photoImageData = row.get("photoImages");
|
||||
Object photoImageData = row.get(LEDGER_EXPORT_ROW_IMAGES_KEY);
|
||||
if (photoImageData instanceof List) {
|
||||
for (Object image : (List<?>) photoImageData) {
|
||||
if (image instanceof LedgerExportImage) {
|
||||
@@ -532,7 +548,7 @@ public class ThirtyTeachTourLedgerController {
|
||||
anchor.setRow2(excelRowIndex + 1);
|
||||
drawing.createPicture(anchor, pictureIndex);
|
||||
insertedPictureCount++;
|
||||
log.info("三十年教龄疗休养台账图片写入Excel成功:rowIndex={}, columnOffset={}, fileId={}, bytes={}, pictureType={}",
|
||||
log.debug("三十年教龄疗休养台账图片写入Excel成功:rowIndex={}, columnOffset={}, fileId={}, bytes={}, pictureType={}",
|
||||
excelRowIndex, image.columnOffset, image.fileId, image.bytes.length, image.pictureType);
|
||||
}
|
||||
}
|
||||
@@ -540,21 +556,193 @@ public class ThirtyTeachTourLedgerController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 将台账图片字段解析成可插入 Excel 的图片对象,当前优先支持系统文件下载路径。
|
||||
* 先收集本次导出涉及的全部图片,再批量查询 sys_file 并并行读取图片字节,避免逐行逐图查库。
|
||||
* 图片字节处理仅生成不可变结果,Excel Workbook 仍在后续单线程写入,避免 POI 对象并发访问。
|
||||
*/
|
||||
private List<LedgerExportImage> resolveLedgerImages(String photoFiles) {
|
||||
return resolveLedgerImages(photoFiles, 0);
|
||||
private Map<String, LedgerExportImage> batchResolveLedgerImages(List<NutMap> rows) {
|
||||
Map<String, Set<String>> pathCandidateMap = collectLedgerPhotoPathCandidates(rows);
|
||||
List<String> normalizedPaths = new ArrayList<>(pathCandidateMap.keySet());
|
||||
if (normalizedPaths.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, Sys_file> sysFileMap = batchFetchSysFiles(pathCandidateMap);
|
||||
Map<String, LedgerExportImage> imageMap = new LinkedHashMap<>();
|
||||
ExecutorService executorService = Executors.newFixedThreadPool(Math.min(LEDGER_EXPORT_IMAGE_THREAD_COUNT, normalizedPaths.size()));
|
||||
List<Future<LedgerExportImageResolveResult>> futures = new ArrayList<>();
|
||||
for (String normalizedPath : normalizedPaths) {
|
||||
Sys_file sysFile = sysFileMap.get(normalizedPath);
|
||||
if (sysFile == null) {
|
||||
sysFile = sysFileMap.get(extractIdParam(normalizedPath));
|
||||
}
|
||||
Sys_file resolvedSysFile = sysFile;
|
||||
futures.add(executorService.submit(() -> new LedgerExportImageResolveResult(normalizedPath,
|
||||
resolveLedgerImage(normalizedPath, resolvedSysFile))));
|
||||
}
|
||||
executorService.shutdown();
|
||||
for (Future<LedgerExportImageResolveResult> future : futures) {
|
||||
try {
|
||||
LedgerExportImageResolveResult result = future.get(LEDGER_EXPORT_IMAGE_TASK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
if (result != null && result.image != null) {
|
||||
imageMap.put(result.normalizedPath, result.image);
|
||||
if (StrUtil.isNotBlank(result.image.fileId)) {
|
||||
imageMap.putIfAbsent(result.image.fileId, result.image);
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.warn("三十年教龄疗休养台账导出图片并行读取被中断:msg={}", e.getMessage());
|
||||
break;
|
||||
} catch (ExecutionException e) {
|
||||
log.warn("三十年教龄疗休养台账导出图片并行读取失败:msg={}", e.getMessage());
|
||||
} catch (TimeoutException e) {
|
||||
future.cancel(true);
|
||||
log.warn("三十年教龄疗休养台账导出图片并行读取超时:timeoutSeconds={}", LEDGER_EXPORT_IMAGE_TASK_TIMEOUT_SECONDS);
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) {
|
||||
executorService.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
executorService.shutdownNow();
|
||||
}
|
||||
log.info("三十年教龄疗休养台账导出图片批量解析完成:pathCount={}, sysFileCount={}, imageCount={}",
|
||||
normalizedPaths.size(), sysFileMap.size(), imageMap.size());
|
||||
return imageMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将台账图片字段解析成可插入 Excel 的图片对象,并记录图片列偏移,支持多组图片列分区写入。
|
||||
* 按台账图片字段原始顺序收集并去重,同时保留原始路径候选值用于批量匹配历史 sys_file.downloadPath。
|
||||
*/
|
||||
private List<LedgerExportImage> resolveLedgerImages(String photoFiles, int columnOffsetStart) {
|
||||
private Map<String, Set<String>> collectLedgerPhotoPathCandidates(List<NutMap> rows) {
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, Set<String>> pathCandidateMap = new LinkedHashMap<>();
|
||||
for (NutMap row : rows) {
|
||||
splitPhotoFiles(row.getString("photoFiles", "")).forEach(path -> addPhotoPathCandidates(pathCandidateMap, path));
|
||||
splitPhotoFiles(row.getString("currentPeriodPhotoFiles", "")).forEach(path -> addPhotoPathCandidates(pathCandidateMap, path));
|
||||
}
|
||||
return pathCandidateMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同一图片用规范化路径作为回填 key,并保留原始值、解码值和规范化值,避免批量查询漏掉历史保存格式。
|
||||
*/
|
||||
private void addPhotoPathCandidates(Map<String, Set<String>> pathCandidateMap, String photoPath) {
|
||||
String originalPath = StrUtil.trimToEmpty(photoPath);
|
||||
String normalizedPath = normalizePhotoPath(photoPath);
|
||||
if (StrUtil.isNotBlank(normalizedPath)) {
|
||||
Set<String> candidates = pathCandidateMap.computeIfAbsent(normalizedPath, key -> new LinkedHashSet<>());
|
||||
addPhotoPathCandidate(candidates, originalPath);
|
||||
addPhotoPathCandidate(candidates, decodePhotoPath(originalPath));
|
||||
addPhotoPathCandidate(candidates, normalizedPath);
|
||||
if (StrUtil.isNotBlank(Globals.AppDomain) && normalizedPath.startsWith("/")) {
|
||||
addPhotoPathCandidate(candidates, Globals.AppDomain + normalizedPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片路径候选值统一入集合,过滤空值,避免 IN 条件中出现无效参数。
|
||||
*/
|
||||
private void addPhotoPathCandidate(Set<String> candidates, String candidate) {
|
||||
if (StrUtil.isNotBlank(candidate)) {
|
||||
candidates.add(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量查询本次导出涉及的 sys_file 记录,同时兼容按文件 ID、原始 downloadPath 和规范化 downloadPath 保存的历史图片路径。
|
||||
*/
|
||||
private Map<String, Sys_file> batchFetchSysFiles(Map<String, Set<String>> pathCandidateMap) {
|
||||
if (pathCandidateMap == null || pathCandidateMap.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Set<String> fileIds = new LinkedHashSet<>();
|
||||
Set<String> downloadPaths = new LinkedHashSet<>();
|
||||
for (Map.Entry<String, Set<String>> entry : pathCandidateMap.entrySet()) {
|
||||
String normalizedPath = entry.getKey();
|
||||
String fileId = extractIdParam(normalizedPath);
|
||||
if (StrUtil.isNotBlank(fileId)) {
|
||||
fileIds.add(fileId);
|
||||
}
|
||||
for (String candidate : entry.getValue()) {
|
||||
String candidateFileId = extractIdParam(candidate);
|
||||
if (StrUtil.isNotBlank(candidateFileId)) {
|
||||
fileIds.add(candidateFileId);
|
||||
}
|
||||
downloadPaths.add(candidate);
|
||||
}
|
||||
}
|
||||
Map<String, Sys_file> result = new LinkedHashMap<>();
|
||||
querySysFilesByIds(fileIds).forEach(file -> putSysFileIndexes(result, file));
|
||||
querySysFilesByDownloadPaths(downloadPaths).forEach(file -> putSysFileIndexes(result, file));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* sys_file 按文件 ID 批量查询,使用模型字段引用兼容 @Name 主键字段映射。
|
||||
*/
|
||||
private List<Sys_file> querySysFilesByIds(Set<String> values) {
|
||||
if (values == null || values.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<String> valueList = new ArrayList<>(values);
|
||||
List<Sys_file> files = new ArrayList<>();
|
||||
for (int start = 0; start < valueList.size(); start += LEDGER_EXPORT_FILE_QUERY_BATCH_SIZE) {
|
||||
int end = Math.min(start + LEDGER_EXPORT_FILE_QUERY_BATCH_SIZE, valueList.size());
|
||||
files.addAll(sysFileService.query(Cnd.where(Sys_file::getId, "in", valueList.subList(start, end))));
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* sys_file 按下载路径批量查询,按固定大小分片避免超长 IN 条件影响数据库解析。
|
||||
*/
|
||||
private List<Sys_file> querySysFilesByDownloadPaths(Set<String> values) {
|
||||
if (values == null || values.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<String> valueList = new ArrayList<>(values);
|
||||
List<Sys_file> files = new ArrayList<>();
|
||||
for (int start = 0; start < valueList.size(); start += LEDGER_EXPORT_FILE_QUERY_BATCH_SIZE) {
|
||||
int end = Math.min(start + LEDGER_EXPORT_FILE_QUERY_BATCH_SIZE, valueList.size());
|
||||
files.addAll(sysFileService.query(Cnd.where(Sys_file::getDownloadPath, "in", valueList.subList(start, end))));
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同一文件同时建立 id 和 downloadPath 索引,行内回填时统一用规范化路径查找。
|
||||
*/
|
||||
private void putSysFileIndexes(Map<String, Sys_file> result, Sys_file file) {
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
if (StrUtil.isNotBlank(file.getId())) {
|
||||
result.putIfAbsent(file.getId(), file);
|
||||
}
|
||||
if (StrUtil.isNotBlank(file.getDownloadPath())) {
|
||||
result.putIfAbsent(normalizePhotoPath(file.getDownloadPath()), file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将台账图片字段解析成可插入 Excel 的图片对象,并记录图片列偏移,图片字节来自批量预解析结果。
|
||||
*/
|
||||
private List<LedgerExportImage> resolveLedgerImages(String photoFiles, int columnOffsetStart,
|
||||
Map<String, LedgerExportImage> imageMap) {
|
||||
List<LedgerExportImage> images = new ArrayList<>();
|
||||
List<String> photoPaths = splitPhotoFiles(photoFiles);
|
||||
for (int photoIndex = 0; photoIndex < photoPaths.size(); photoIndex++) {
|
||||
String originalPath = photoPaths.get(photoIndex);
|
||||
LedgerExportImage image = resolveLedgerImage(originalPath);
|
||||
String normalizedPath = normalizePhotoPath(originalPath);
|
||||
LedgerExportImage image = imageMap == null ? null : imageMap.get(normalizedPath);
|
||||
if (image == null && imageMap != null) {
|
||||
image = imageMap.get(extractIdParam(normalizedPath));
|
||||
}
|
||||
if (image != null) {
|
||||
images.add(image.withColumnOffset(columnOffsetStart + photoIndex));
|
||||
} else {
|
||||
@@ -567,13 +755,10 @@ public class ThirtyTeachTourLedgerController {
|
||||
/**
|
||||
* 根据后台保存的下载路径定位 sys_file,并读取真实图片字节及 POI 可识别的图片类型。
|
||||
*/
|
||||
private LedgerExportImage resolveLedgerImage(String originalPath) {
|
||||
String normalizedPath = normalizePhotoPath(originalPath);
|
||||
private LedgerExportImage resolveLedgerImage(String normalizedPath, Sys_file sysFile) {
|
||||
String fileId = extractIdParam(normalizedPath);
|
||||
Sys_file sysFile = fetchSysFile(fileId, normalizedPath);
|
||||
if (sysFile == null) {
|
||||
log.warn("三十年教龄疗休养台账图片文件记录不存在:fileId={}, originalPath={}, normalizedPath={}",
|
||||
fileId, originalPath, normalizedPath);
|
||||
log.warn("三十年教龄疗休养台账图片文件记录不存在:fileId={}, normalizedPath={}", fileId, normalizedPath);
|
||||
return null;
|
||||
}
|
||||
byte[] bytes = readSysFileBytes(sysFile);
|
||||
@@ -587,25 +772,11 @@ public class ThirtyTeachTourLedgerController {
|
||||
sysFile.getId(), sysFile.getEngine(), sysFile.getStoragePath(), bytes == null ? 0 : bytes.length, pictureType);
|
||||
return null;
|
||||
}
|
||||
log.info("三十年教龄疗休养台账图片读取成功:fileId={}, engine={}, bucket={}, storagePath={}, bytes={}, pictureType={}",
|
||||
log.debug("三十年教龄疗休养台账图片读取成功:fileId={}, engine={}, bucket={}, storagePath={}, bytes={}, pictureType={}",
|
||||
sysFile.getId(), sysFile.getEngine(), sysFile.getBucket(), sysFile.getStoragePath(), bytes.length, pictureType);
|
||||
return new LedgerExportImage(sysFile.getId(), normalizedPath, bytes, pictureType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 优先按文件ID查询,失败时按 downloadPath 查询,兼容后台保存的是完整下载路径的场景。
|
||||
*/
|
||||
private Sys_file fetchSysFile(String fileId, String normalizedPath) {
|
||||
Sys_file sysFile = null;
|
||||
if (StrUtil.isNotBlank(fileId)) {
|
||||
sysFile = sysFileService.fetch(fileId);
|
||||
}
|
||||
if (sysFile == null && StrUtil.isNotBlank(normalizedPath)) {
|
||||
sysFile = sysFileService.fetch(Cnd.where(Sys_file::getDownloadPath, "=", normalizedPath));
|
||||
}
|
||||
return sysFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据图片字节头判断 POI 图片类型,无法识别时尝试通过 ImageIO 转为 PNG。
|
||||
*/
|
||||
@@ -703,14 +874,10 @@ public class ThirtyTeachTourLedgerController {
|
||||
* 将完整URL规整为站内相对路径,避免导出时重复拼接系统域名。
|
||||
*/
|
||||
private String normalizePhotoPath(String photoFile) {
|
||||
String path = StrUtil.trimToEmpty(photoFile);
|
||||
String path = decodePhotoPath(photoFile);
|
||||
if (StrUtil.isBlank(path)) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
path = URLDecoder.decode(path, StandardCharsets.UTF_8.toString());
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
if (StrUtil.isNotBlank(Globals.AppDomain) && path.startsWith(Globals.AppDomain)) {
|
||||
path = path.substring(Globals.AppDomain.length());
|
||||
}
|
||||
@@ -722,6 +889,21 @@ public class ThirtyTeachTourLedgerController {
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片路径可能由前端 URL 编码后保存,统一解码后再参与匹配和规范化处理。
|
||||
*/
|
||||
private String decodePhotoPath(String photoFile) {
|
||||
String path = StrUtil.trimToEmpty(photoFile);
|
||||
if (StrUtil.isBlank(path)) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return URLDecoder.decode(path, StandardCharsets.UTF_8.toString());
|
||||
} catch (Exception ignored) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 台账导出图片对象,保存 POI 写图所需的图片字节、类型和来源文件信息。
|
||||
*/
|
||||
@@ -749,6 +931,19 @@ public class ThirtyTeachTourLedgerController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片并行读取任务的返回对象,保留规范化路径用于回填到导出行。
|
||||
*/
|
||||
private static class LedgerExportImageResolveResult {
|
||||
private final String normalizedPath;
|
||||
private final LedgerExportImage image;
|
||||
|
||||
private LedgerExportImageResolveResult(String normalizedPath, LedgerExportImage image) {
|
||||
this.normalizedPath = normalizedPath;
|
||||
this.image = image;
|
||||
}
|
||||
}
|
||||
|
||||
private List<NutMap> queryLedgerExportRows(Integer startYear, Integer endYear, String keyword, String unionId, String lineId,
|
||||
String travelPeriod, String lineType, Boolean directFamilyOnly, Boolean overCostOnly) {
|
||||
Cnd cnd = buildQueryCnd(startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType, directFamilyOnly, overCostOnly);
|
||||
@@ -761,6 +956,7 @@ public class ThirtyTeachTourLedgerController {
|
||||
COALESCE(t.age, vu.age) AS age,
|
||||
COALESCE(NULLIF(t.idCard, ''), NULLIF(vu.idCard, ''), '') AS idCard,
|
||||
COALESCE(NULLIF(t.mobile, ''), NULLIF(vu.mobile, ''), '') AS mobile,
|
||||
COALESCE(NULLIF(t.unionName, ''), vu.unionName, '') AS unionName,
|
||||
t.unitName,
|
||||
t.boardingPlace,
|
||||
COALESCE(NULLIF(l.lineName, ''), t.lineName, '') AS lineName,
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- 30年教龄疗休养当前时间段图片材料字段。
|
||||
ALTER TABLE `thirty_teach_tour_user_assignment`
|
||||
ADD COLUMN `currentPeriodPhotoFiles` text DEFAULT NULL COMMENT '当前时间段图片材料' AFTER `photoFiles`;
|
||||
|
||||
ALTER TABLE `thirty_teach_tour_ledger`
|
||||
ADD COLUMN `currentPeriodPhotoFiles` text DEFAULT NULL COMMENT '当前时间段图片材料' AFTER `photoFiles`;
|
||||
@@ -109,6 +109,7 @@ layout("/layouts/platform.html"){
|
||||
<el-table-column label="年龄" prop="age" width="90" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="身份证号" prop="idCard" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="手机号" prop="mobile" width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="所属工会" prop="unionName" min-width="150" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="所在单位" prop="unitName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="乘车地点" prop="boardingPlace" min-width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="报名线路" prop="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
|
||||
Reference in New Issue
Block a user