From fbc1c1590e7e81a550beb50c7cf65c663c7c5b41 Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期二, 01 九月 2026 16:42:06 +0800
Subject: [PATCH] 数据导入与报表生成多项改进
---
traffic-audit-web/src/views/DataImport.vue | 78 +++++++++-
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java | 280 ++++++++++++++++++++++++++++++++++++++-
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java | 18 +
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/controller/DataImportController.java | 14 ++
4 files changed, 365 insertions(+), 25 deletions(-)
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/dataimport/controller/DataImportController.java b/traffic-audit-server/src/main/java/com/trafficaudit/dataimport/controller/DataImportController.java
index 5700733..18e657b 100644
--- a/traffic-audit-server/src/main/java/com/trafficaudit/dataimport/controller/DataImportController.java
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/dataimport/controller/DataImportController.java
@@ -267,6 +267,20 @@
}
}
+ /** 澶氭枃浠舵壒閲忓鍏ワ細鍓嶇涓�娆¢�夋嫨澶氫釜 Excel 涓婁紶锛坱ype 涓烘暟鎹被鍨嬶紝鎶曡祫绫诲瀷鎸夊競宸炲崟琛ㄨВ鏋愶級 */
+ @PostMapping("/batch")
+ public Result<Object> importBatch(@RequestParam("type") String type,
+ @RequestParam("period") String period,
+ @RequestParam("files") List<MultipartFile> files) {
+ try {
+ Map<String, Object> result = importService.importBatchFiles(type, period, files);
+ return Result.ok(result);
+ } catch (Exception e) {
+ log.error("batch import error", e);
+ return Result.error("鎵归噺瀵煎叆澶辫触: " + e.getMessage());
+ }
+ }
+
/** 鎶曡祫甯傚窞鍗曡〃鎵归噺瀵煎叆锛圡2锛夛細鎵弿 docs/鎶曡祫/杈撳叆/{绫诲埆}/{X鏈坿 鐩綍鍏ㄩ儴甯傚窞鎶ヨ〃锛宑ategory=瀹㈣繍绔欏満/鐗╂祦鍥尯 */
@PostMapping("/investCityBatch")
public Result<Object> importInvestCityBatch(@RequestParam("period") String period,
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java b/traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java
index 58b5305..7d2605d 100644
--- a/traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java
@@ -168,6 +168,7 @@
Map<String, Integer> colMap;
try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
Sheet sheet = wb.getSheetAt(0);
+ requireHeaderCols(sheet, "閬撹矾璐х墿杩愯緭鏈堝害鐢熶骇鎯呭喌", "浼佷笟鍚嶇О", "璐ц繍閲廮鎬昏");
colMap = buildHeaderMap(sheet.getRow(0));
for (int r = 1; r <= sheet.getLastRowNum(); r++) {
Row row = sheet.getRow(r);
@@ -284,6 +285,7 @@
try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
// sheet0: 鍏矾鏃呭6鏈堟姤
Sheet sheet = wb.getSheetAt(0);
+ requireHeaderCols(sheet, "鍏矾鏃呭杩愯緭鏈堝害鐢熶骇鎯呭喌", "浼佷笟鍚嶇О", "瀹㈣繍閲廮鎬昏");
Map<String, Integer> colMap = buildHeaderMap(sheet.getRow(0));
for (int r = 1; r <= sheet.getLastRowNum(); r++) {
Row row = sheet.getRow(r);
@@ -476,6 +478,7 @@
try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
Sheet sheet = wb.getSheetAt(0);
Map<String, Integer> colMap = buildHeaderMap(sheet.getRow(0));
+ requireHeaderCols(sheet, "鏃呭杩愭斂杞﹁締鏁�", "浼佷笟鍚嶇О", "杞﹁締鏁�", "杞藉浣嶆暟");
for (int r = 1; r <= sheet.getLastRowNum(); r++) {
Row row = sheet.getRow(r);
if (row == null) continue;
@@ -530,6 +533,7 @@
try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
// sheet0: 绗簩瀛e害璐ц溅鑳借�楁槑缁嗭紙姣忚宸﹀彸鍚�5缁勮溅杈嗭紝姣忕粍鍥哄畾18鍒楋級
Sheet sheet = wb.getSheetAt(0);
+ requireHeaderKeyword(sheet, "閬撹矾璐ц繍杞﹁締鑳芥簮娑堣�楁儏鍐�", "浼佷笟鍚嶇О", "杞︾墝鍙�");
for (int r = 1; r <= sheet.getLastRowNum(); r++) {
Row row = sheet.getRow(r);
if (row == null) continue;
@@ -648,6 +652,7 @@
List<EnergyAuthVehicle> authList = new ArrayList<>();
try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
Sheet sheet = wb.getSheetAt(0);
+ requireHeaderKeyword(sheet, "鑳借�楄溅杈嗚繍鏀夸俊鎭�", "杞︾墝鍙�");
for (int r = 1; r <= sheet.getLastRowNum(); r++) {
Row row = sheet.getRow(r);
if (row == null) continue;
@@ -716,6 +721,7 @@
int failRows = 0;
try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
Sheet sheet = wb.getSheetAt(0);
+ requireHeaderKeyword(sheet, "杩愭斂杞﹁締", "浼佷笟鍚嶇О");
for (int r = 1; r <= sheet.getLastRowNum(); r++) {
Row row = sheet.getRow(r);
if (row == null) continue;
@@ -763,6 +769,7 @@
int failRows = 0;
try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
Sheet sheet = wb.getSheetAt(0);
+ requireHeaderKeyword(sheet, "杞ㄨ抗閲岀▼", "浼佷笟鍚嶇О");
for (int r = 1; r <= sheet.getLastRowNum(); r++) {
Row row = sheet.getRow(r);
if (row == null) continue;
@@ -806,6 +813,14 @@
List<ScaleSplitTransport> list = new ArrayList<>();
int failRows = 0;
try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
+ boolean hasPeriodSheet = false;
+ for (int i = 0; i < wb.getNumberOfSheets(); i++) {
+ String sn = wb.getSheetAt(i).getSheetName();
+ if (sn != null && (sn.contains("褰撴湀") || sn.contains("绱"))) { hasPeriodSheet = true; break; }
+ }
+ if (!hasPeriodSheet) {
+ throw new RuntimeException("鎵�閫夋暟鎹被鍨嬨�愯涓婅涓嬫媶鍒嗐�戜笌鏂囦欢鍐呭涓嶇锛氭湭鎵惧埌鍚� 褰撴湀/绱 鐨� sheet锛岃纭鏄惁閫夐敊浜嗘暟鎹被鍨�");
+ }
for (int i = 0; i < wb.getNumberOfSheets(); i++) {
Sheet sheet = wb.getSheetAt(i);
String sheetName = sheet.getSheetName();
@@ -896,6 +911,18 @@
int monthCount = Integer.parseInt(period.split("-")[1]);
try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
Sheet sheet = wb.getSheetAt(0);
+ boolean cityRowFound = false;
+ for (int r = 1; r <= Math.min(sheet.getLastRowNum(), 30); r++) {
+ Row rr = sheet.getRow(r);
+ if (rr == null) continue;
+ String nm = getString(rr, 0);
+ if (nm == null || nm.trim().isEmpty()) continue;
+ String region = RegionUtil.normalizeCityName(nm);
+ if (RegionUtil.cityList().contains(region) || "婀栧寳鐪�".equals(region)) { cityRowFound = true; break; }
+ }
+ if (!cityRowFound) {
+ throw new RuntimeException("鎵�閫夋暟鎹被鍨嬨�愯揣杩愰噺鍛ㄨ浆閲忋�戜笌鏂囦欢鍐呭涓嶇锛氭湭璇嗗埆鍒板競宸炶锛堥鍒楀簲涓哄競宸炲悕绉帮級锛岃纭鏄惁閫夐敊浜嗘暟鎹被鍨�");
+ }
Map<String, FreightTurnoverImport> freightMap = new HashMap<>();
Map<String, FreightTurnoverImport> turnoverMap = new HashMap<>();
Set<String> seen = new HashSet<>();
@@ -1152,6 +1179,33 @@
}
}
+ /** 绫诲瀷-琛ㄥご鏍¢獙锛氭寜蹇呴渶鍒楀悕锛堣В鏋愬櫒瀹為檯渚濊禆鐨勫垪锛夛紝缂哄け鍗虫姏"绫诲瀷涓庢枃浠朵笉绗�" */
+ private void requireHeaderCols(Sheet sheet, String typeLabel, String... cols) {
+ Map<String, Integer> colMap = buildHeaderMap(sheet.getRow(0));
+ List<String> missing = new ArrayList<>();
+ for (String c : cols) {
+ if (!colMap.containsKey(c)) missing.add(c);
+ }
+ if (!missing.isEmpty()) {
+ throw new RuntimeException("鎵�閫夋暟鎹被鍨嬨��" + typeLabel + "銆戜笌鏂囦欢鍐呭涓嶇锛氳〃澶寸己灏戝繀闇�鍒椼��" + String.join("銆戙��", missing) + "銆戯紝璇风‘璁ゆ槸鍚﹂�夐敊浜嗘暟鎹被鍨�");
+ }
+ }
+
+ /** 绫诲瀷-琛ㄥご鏍¢獙锛氭壂鎻忚〃澶磋锛屽懡涓换涓�鍏抽敭瀛楀嵆閫氳繃 */
+ private void requireHeaderKeyword(Sheet sheet, String typeLabel, String... keywords) {
+ Row header = sheet == null ? null : sheet.getRow(0);
+ if (header != null) {
+ for (Cell cell : header) {
+ String v = FORMATTER.formatCellValue(cell);
+ if (v == null || v.isEmpty()) continue;
+ for (String k : keywords) {
+ if (v.contains(k)) return;
+ }
+ }
+ }
+ throw new RuntimeException("鎵�閫夋暟鎹被鍨嬨��" + typeLabel + "銆戜笌鏂囦欢鍐呭涓嶇锛氳〃澶存湭鍖呭惈銆�" + String.join("銆戙��", keywords) + "銆戠瓑鍒楋紝璇风‘璁ゆ槸鍚﹂�夐敊浜嗘暟鎹被鍨�");
+ }
+
/** 鎸夎〃澶村悕绉板缓绔嬪垪鍙锋槧灏勶紝鍏煎鏂版棫妯℃澘鍒椾綅宸紓 */
private Map<String, Integer> buildHeaderMap(Row header) {
Map<String, Integer> colMap = new HashMap<>();
@@ -1398,7 +1452,7 @@
}
}
if (headerRow < 0) {
- throw new RuntimeException("鏈壘鍒拌〃澶磋锛堥渶鍖呭惈 搴忓彿/椤圭洰鍚嶇О 鍒楋級");
+ throw new RuntimeException("鎵�閫夋暟鎹被鍨嬨��" + (passenger ? "鎶曡祫鏈堟姤锛堝杩愮珯鍦猴級" : "鎶曡祫鏈堟姤锛堢墿娴佸洯鍖猴級") + "銆戜笌鏂囦欢鍐呭涓嶇锛氭湭鎵惧埌琛ㄥご琛岋紙闇�鍖呭惈 搴忓彿/椤圭洰鍚嶇О 鍒楋級锛岃纭鏄惁閫夐敊浜嗘暟鎹被鍨�");
}
for (int r = headerRow + 1; r <= sheet.getLastRowNum(); r++) {
Row row = sheet.getRow(r);
@@ -1553,11 +1607,19 @@
}
fileResults.add(fr);
}
- // 骞傜瓑娓呯悊锛氬悓绫诲埆鍚屾姤琛ㄦ湡鏈堝害鏁版嵁 + INVEST 瀹℃牳缁撴灉锛堥」鐩富妗h法鏈堜繚鐣欙紝鎸夊悕 upsert锛�
- cleanInvestMonthly(category, period);
+ // 骞傜瓑娓呯悊锛氭寜鏈鏂囦欢娑夊強甯傚窞娓呯悊鏈堝害鏁版嵁 + INVEST 瀹℃牳缁撴灉锛堥」鐩富妗h法鏈堜繚鐣欙紝鎸夊悕 upsert锛�
+ cleanInvestMonthlyByCity(category, period, rows);
+ Map<String, Set<String>> knownCities = loadInvestKnownCities(category);
+ Map<String, String> fileFirstMismatch = new LinkedHashMap<>();
int success = 0;
List<String> failDetails = new ArrayList<>();
for (InvestCityRow item : rows) {
+ String mismatch = investCityMismatchReason(item, knownCities);
+ if (mismatch != null) {
+ failDetails.add(mismatch);
+ fileFirstMismatch.putIfAbsent(item.fileName, mismatch);
+ continue;
+ }
try {
saveInvestProjectRow(item.project, item.monthly);
success++;
@@ -1569,6 +1631,11 @@
failDetails.add(item.fileName + " 绗� " + item.excelRow + " 琛岋紙" + item.project.getProjectName() + "锛夛細" + reason);
log.error("Invest city insert error: file {}, row {}", item.fileName, item.excelRow, e);
}
+ }
+ // 鎶婇涓競宸為敊浣嶆彁绀烘寕鍒板搴旀枃浠剁粨鏋滀笂锛屼究浜庨〉闈㈠脊绐楃洿鎺ョ湅鍒�
+ for (Map<String, Object> fr2 : fileResults) {
+ String msg = fileFirstMismatch.get(fr2.get("fileName"));
+ if (msg != null && fr2.get("error") == null) fr2.put("error", msg);
}
String errorDetail = String.join("\n", failDetails);
if (errorDetail.length() > 4000) errorDetail = errorDetail.substring(0, 4000) + "\n鈥︹��";
@@ -1587,6 +1654,119 @@
result.put("fiveYearTemplateProjects", fiveYearNames.size());
result.put("fiveYearMatched", fiveYearMatched);
log.info("Invest city {} {} imported: {} rows (fiveYear matched {}/{})",
+ category, period, success, fiveYearMatched, fiveYearNames.size());
+ return result;
+ }
+
+ /**
+ * 澶氭枃浠舵壒閲忓鍏ワ細鍓嶇涓�娆¢�夋嫨澶氫釜 Excel 涓婁紶銆�
+ * 鎶曡祫绫诲瀷锛坕nvestment/investmentLogistics锛夋寜甯傚窞鍗曡〃瑙f瀽锛堟枃浠跺悕/濉姤鍗曚綅璇嗗埆甯傚窞锛屽崄浜斾簲鑷姩鏍囪锛夛紱
+ * 鍏朵粬绫诲瀷閫愪釜璧扮幇鏈夎В鏋愰�昏緫锛坕mportDirFile锛夈��
+ */
+ public Map<String, Object> importBatchFiles(String type, String period, List<MultipartFile> files) throws Exception {
+ if (files == null || files.isEmpty()) {
+ throw new RuntimeException("鏈帴鏀跺埌浠讳綍鏂囦欢");
+ }
+ if ("investment".equals(type) || "investmentLogistics".equals(type)) {
+ String category = "investment".equals(type) ? "瀹㈣繍绔欏満" : "鐗╂祦鍥尯";
+ return importInvestCityFiles(period, category, files);
+ }
+ List<Map<String, Object>> fileResults = new ArrayList<>();
+ int totalSuccess = 0, totalFail = 0;
+ for (MultipartFile f : files) {
+ Map<String, Object> fr = new LinkedHashMap<>();
+ fr.put("fileName", f.getOriginalFilename());
+ try {
+ ImportResult r = importDirFile(type, f, period);
+ fr.put("success", r.getSuccess());
+ fr.put("fail", r.getFail());
+ if (r.getFail() > 0 && r.getFailDetails() != null && !r.getFailDetails().isEmpty()) {
+ fr.put("error", r.getFailDetails().get(0));
+ }
+ totalSuccess += r.getSuccess();
+ totalFail += r.getFail();
+ } catch (Exception e) {
+ fr.put("success", 0);
+ fr.put("fail", 0);
+ fr.put("error", e.getMessage() == null ? e.toString() : e.getMessage());
+ totalFail++;
+ }
+ fileResults.add(fr);
+ }
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("type", type);
+ result.put("total", files.size());
+ result.put("success", totalSuccess);
+ result.put("fail", totalFail);
+ result.put("files", fileResults);
+ return result;
+ }
+
+ /** 鎶曡祫澶氭枃浠舵壒閲忥細閫愭枃浠舵寜甯傚窞鍗曡〃瑙f瀽锛岀粺涓�骞傜瓑娓呯悊钀藉簱锛屾壒娆℃眹鎬� */
+ public Map<String, Object> importInvestCityFiles(String period, String category, List<MultipartFile> files) throws Exception {
+ boolean passenger = "瀹㈣繍绔欏満".equals(category);
+ Set<String> fiveYearNames = loadFiveYearProjectNames();
+ List<InvestCityRow> rows = new ArrayList<>();
+ List<Map<String, Object>> fileResults = new ArrayList<>();
+ for (MultipartFile f : files) {
+ Map<String, Object> fr = new LinkedHashMap<>();
+ fr.put("fileName", f.getOriginalFilename());
+ try (Workbook wb = WorkbookFactory.create(f.getInputStream())) {
+ parseInvestCityWorkbook(wb, f.getOriginalFilename(), category, period, fiveYearNames, rows, fr);
+ } catch (Exception e) {
+ fr.put("success", 0);
+ fr.put("error", e.getMessage() == null ? e.toString() : e.getMessage());
+ log.error("invest city file parse error: {}", f.getOriginalFilename(), e);
+ }
+ fileResults.add(fr);
+ }
+ String catDir = passenger ? "瀹㈣繍锛堝杩愮珯锛�" : "鐗╂祦锛堢墿娴佸洯鍖猴級";
+ cleanInvestMonthlyByCity(category, period, rows);
+ Map<String, Set<String>> knownCities = loadInvestKnownCities(category);
+ Map<String, String> fileFirstMismatch = new LinkedHashMap<>();
+ int success = 0;
+ List<String> failDetails = new ArrayList<>();
+ for (InvestCityRow item : rows) {
+ String mismatch = investCityMismatchReason(item, knownCities);
+ if (mismatch != null) {
+ failDetails.add(mismatch);
+ fileFirstMismatch.putIfAbsent(item.fileName, mismatch);
+ continue;
+ }
+ try {
+ saveInvestProjectRow(item.project, item.monthly);
+ success++;
+ } catch (Exception e) {
+ Throwable cause = e;
+ while (cause.getCause() != null) cause = cause.getCause();
+ String reason = cause.getMessage();
+ if (reason == null || reason.trim().isEmpty()) reason = e.getMessage();
+ failDetails.add(item.fileName + " 绗� " + item.excelRow + " 琛岋紙" + item.project.getProjectName() + "锛夛細" + reason);
+ log.error("Invest city insert error: file {}, row {}", item.fileName, item.excelRow, e);
+ }
+ }
+ // 鎶婇涓競宸為敊浣嶆彁绀烘寕鍒板搴旀枃浠剁粨鏋滀笂锛屼究浜庨〉闈㈠脊绐楃洿鎺ョ湅鍒�
+ for (Map<String, Object> fr2 : fileResults) {
+ String msg = fileFirstMismatch.get(fr2.get("fileName"));
+ if (msg != null && fr2.get("error") == null) fr2.put("error", msg);
+ }
+ String errorDetail = String.join("\n", failDetails);
+ if (errorDetail.length() > 4000) errorDetail = errorDetail.substring(0, 4000) + "\n鈥︹��";
+ recordBatch("澶氭枃浠舵壒閲忓鍏ワ紙" + catDir + "/" + period + "锛�", "INVEST_" + category + "_CITY",
+ period, rows.size(), success, failDetails.size(), errorDetail);
+ int fiveYearMatched = 0;
+ for (InvestCityRow item : rows) {
+ if (Integer.valueOf(1).equals(item.project.getIsFiveYear())) fiveYearMatched++;
+ }
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("total", rows.size());
+ result.put("success", success);
+ result.put("fail", failDetails.size());
+ result.put("failDetails", failDetails);
+ result.put("files", fileResults);
+ result.put("fiveYearTemplateProjects", fiveYearNames.size());
+ result.put("fiveYearMatched", fiveYearMatched);
+ log.info("Invest city multi-file {} {} imported: {} rows (fiveYear matched {}/{})",
category, period, success, fiveYearMatched, fiveYearNames.size());
return result;
}
@@ -1627,6 +1807,7 @@
}
String city = cityFromFileName(fileName);
if (city == null) city = cityFromSheetHeader(best);
+ fr.put("city", city == null ? "" : city);
String currentCity = city;
Map<String, Integer> col = bestCol;
int nameCol = col.get("name");
@@ -1637,6 +1818,15 @@
Row row = best.getRow(r);
if (row == null) continue;
String name = cellStr(row, nameCol);
+ String seq = hasSeq ? cellStr(row, seqCol) : null;
+ // 甯傚窞灏忚妭琛岋細閮ㄥ垎姹囨�昏〃鎶婂競宸炲悕鍐欏湪搴忓彿鍒楋紙濡傚崄鍫版眹鎬昏〃 A 鍒楀啓"姝︽眽"锛夛紝闇�鍚屾牱璇嗗埆
+ if (seq != null && !seq.trim().isEmpty()) {
+ String seqCity = investmentCity(seq.trim());
+ if (seqCity != null) {
+ currentCity = seqCity;
+ continue;
+ }
+ }
if (name == null || name.trim().isEmpty()) continue;
String nt = name.trim().replaceAll("[\r\n]+", "");
if (nt.contains("鍚堣") || nt.contains("鎬昏")) continue;
@@ -1645,7 +1835,6 @@
if (nt.replace(" ", "").matches(".*灏廫\s*璁�.*")) continue;
if (nt.startsWith("濉姤") || nt.startsWith("鍗曚綅璐熻矗浜�") || nt.matches("^\\d+銆�.*")) continue;
if (hasSeq) {
- String seq = cellStr(row, seqCol);
if (seq == null || seq.trim().isEmpty()) {
// 甯傚窞灏忚妭琛岋紙濡� "姝︽眽" "澶� 闂�"锛夛紝鏇存柊褰撳墠甯傚窞
String c = investmentCity(nt);
@@ -1884,7 +2073,7 @@
return null;
}
- /** 鍚岀被鍒悓鎶ヨ〃鏈熸湀搴︽暟鎹� + INVEST 瀹℃牳缁撴灉娓呯悊锛堝箓绛夛級 */
+ /** 鍚岀被鍒悓鎶ヨ〃鏈熸湀搴︽暟鎹� + INVEST 瀹℃牳缁撴灉娓呯悊锛堝箓绛夛紝鍏ㄧ渷姹囨�诲ぇ琛ㄥ叏閲忔竻鐞嗭級 */
private void cleanInvestMonthly(String category, String period) {
List<InvestmentProject> sameCat = investProjectMapper.selectList(
new LambdaQueryWrapper<InvestmentProject>().eq(InvestmentProject::getCategory, category));
@@ -1895,15 +2084,60 @@
.eq(InvestmentMonthly::getReportPeriod, period)
.in(InvestmentMonthly::getProjectId, catProjectIds));
}
+ deleteInvestAuditResults(period, null);
+ }
+
+ /** 鎸夊競宸炴竻鐞嗭細鍙垹闄ゆ湰娆℃枃浠舵秹鍙婂競宸烇紙鍚湭璇嗗埆甯傚窞鐨� null 椤圭洰锛夌殑鏈堝害鏁版嵁 + INVEST 瀹℃牳缁撴灉 */
+ private void cleanInvestMonthlyByCity(String category, String period, List<InvestCityRow> rows) {
+ Set<String> citySet = new HashSet<>();
+ boolean hasNullCity = false;
+ for (InvestCityRow item : rows) {
+ String c = item.project.getCity();
+ if (c == null || c.trim().isEmpty()) hasNullCity = true;
+ else citySet.add(c.trim());
+ }
+ if (citySet.isEmpty() && !hasNullCity) return;
+ boolean cleanNullCity = hasNullCity;
+ LambdaQueryWrapper<InvestmentProject> pw = new LambdaQueryWrapper<InvestmentProject>()
+ .eq(InvestmentProject::getCategory, category);
+ pw.and(w -> {
+ if (!citySet.isEmpty()) w.in(InvestmentProject::getCity, citySet);
+ if (cleanNullCity) {
+ if (!citySet.isEmpty()) w.or();
+ w.isNull(InvestmentProject::getCity);
+ }
+ });
+ List<InvestmentProject> cityProjects = investProjectMapper.selectList(pw);
+ java.util.Set<Long> catProjectIds = new HashSet<>();
+ for (InvestmentProject pc : cityProjects) catProjectIds.add(pc.getId());
+ if (catProjectIds.isEmpty()) return;
+ List<InvestmentMonthly> oldMonthlies = investMonthlyMapper.selectList(
+ new LambdaQueryWrapper<InvestmentMonthly>()
+ .eq(InvestmentMonthly::getReportPeriod, period)
+ .in(InvestmentMonthly::getProjectId, catProjectIds));
+ java.util.Set<Long> oldMonthlyIds = new HashSet<>();
+ for (InvestmentMonthly m : oldMonthlies) oldMonthlyIds.add(m.getId());
+ investMonthlyMapper.delete(new LambdaQueryWrapper<InvestmentMonthly>()
+ .eq(InvestmentMonthly::getReportPeriod, period)
+ .in(InvestmentMonthly::getProjectId, catProjectIds));
+ deleteInvestAuditResults(period, oldMonthlyIds);
+ }
+
+ /** 娓呯悊 INVEST 瀹℃牳缁撴灉锛坢onthlyIds 涓� null 琛ㄧず璇ユ湡闂村叏閲忔竻鐞嗭級 */
+ private void deleteInvestAuditResults(String period, java.util.Set<Long> monthlyIds) {
List<Long> investRuleIds = new ArrayList<>();
for (com.trafficaudit.rulemanage.entity.AuditRule rule : ruleMapper.selectList(null)) {
if ("INVEST".equals(rule.getReportType())) investRuleIds.add(rule.getId());
}
- if (!investRuleIds.isEmpty()) {
- auditResultMapper.delete(new LambdaQueryWrapper<AuditResult>()
- .eq(AuditResult::getReportPeriod, period)
- .in(AuditResult::getRuleId, investRuleIds));
+ if (investRuleIds.isEmpty()) return;
+ LambdaQueryWrapper<AuditResult> w = new LambdaQueryWrapper<AuditResult>()
+ .eq(AuditResult::getReportPeriod, period)
+ .in(AuditResult::getRuleId, investRuleIds);
+ if (monthlyIds != null) {
+ if (monthlyIds.isEmpty()) return;
+ w.in(AuditResult::getReportId, monthlyIds);
}
+ auditResultMapper.delete(w);
}
/** 椤圭洰涓绘。 upsert锛堟寜 椤圭洰鍚�+绫诲埆锛�+ 鏈堝害鍐欏叆 */
@@ -1922,6 +2156,32 @@
monthly.setProjectId(exist.getId());
}
investMonthlyMapper.insert(monthly);
+ }
+
+ /** 杞藉叆鏌愮被鍒�岃鑼冨寲椤圭洰鍚� -> 宸插叆搴撳競宸為泦鍚堛�嶏紝鐢ㄤ簬甯傚窞涓�鑷存�ф牎楠岋紙鍚嶇О鍦ㄥ涓競宸為噸澶嶆椂鏀惧純鏍¢獙锛� */
+ private Map<String, Set<String>> loadInvestKnownCities(String category) {
+ Map<String, Set<String>> map = new HashMap<>();
+ List<InvestmentProject> list = investProjectMapper.selectList(
+ new LambdaQueryWrapper<InvestmentProject>().eq(InvestmentProject::getCategory, category));
+ for (InvestmentProject p : list) {
+ if (p.getCity() == null || p.getCity().trim().isEmpty()) continue;
+ map.computeIfAbsent(normalizeProjectName(p.getProjectName()), k -> new HashSet<>())
+ .add(p.getCity().trim());
+ }
+ return map;
+ }
+
+ /** 甯傚窞涓�鑷存�ф牎楠岋細搴撲腑鍞竴褰掑睘甯傚窞涓庢湰娆¤В鏋愪笉涓�鑷存椂杩斿洖閿欒璇存槑锛屽惁鍒欒繑鍥� null */
+ private String investCityMismatchReason(InvestCityRow item, Map<String, Set<String>> knownCities) {
+ String parsed = item.project.getCity();
+ if (parsed == null || parsed.trim().isEmpty()) return null;
+ Set<String> known = knownCities.get(normalizeProjectName(item.project.getProjectName()));
+ if (known == null || known.size() != 1) return null;
+ String knownCity = known.iterator().next();
+ if (knownCity.equals(parsed.trim())) return null;
+ return item.fileName + " 绗� " + item.excelRow + " 琛岋紙" + item.project.getProjectName()
+ + "锛夛細甯傚窞涓�鑷存�ф牎楠屾湭閫氳繃鈥斺�斿簱涓椤圭洰褰掑睘銆�" + knownCity + "銆嶏紝鏈鏂囦欢瑙f瀽涓恒��" + parsed.trim()
+ + "銆嶏紝鐤戜技甯傚窞閿欎綅锛屽凡璺宠繃瀵煎叆锛岃鏍稿婧愭枃浠�";
}
private String cellStr(Row row, int idx) {
@@ -1959,6 +2219,7 @@
int failRows = 0;
try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
Sheet sheet = wb.getSheetAt(0);
+ requireHeaderKeyword(sheet, "鎶曡祫绯荤粺瀵煎嚭", "椤圭洰鍚嶇О");
for (int r = 1; r <= sheet.getLastRowNum(); r++) {
Row row = sheet.getRow(r);
if (row == null) continue;
@@ -2198,6 +2459,7 @@
List<CityTaxiAuth> list = new ArrayList<>();
try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
Sheet sheet = wb.getSheetAt(0);
+ requireHeaderKeyword(sheet, "鍑虹杞﹁繍鏀胯溅杈�", "杞︾墝");
for (int r = 1; r <= sheet.getLastRowNum(); r++) {
Row row = sheet.getRow(r);
if (row == null) continue;
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
index 9415269..945a762 100644
--- a/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
@@ -3221,11 +3221,11 @@
}
/** 琛�1 椤圭洰琛屽~鍏咃紙妯℃澘琛�/鎻掑叆琛屽叡鐢紱cityText 涓庢ā鏉� B 鍒楁牸寮忎竴鑷达級 */
- private void fillInvestPlanProjectRow(HSSFRow row, InvestmentProject p, InvestmentMonthly m, int seq, String cityText) {
+ private void fillInvestPlanProjectRow(HSSFRow row, InvestmentProject p, InvestmentMonthly m, int seq, String cityText, String displayName) {
hssfNum(row, 0, (double) seq);
hssfText(row, 1, cityText);
hssfText(row, 2, valueOf(p.getCounty()));
- hssfText(row, 3, p.getProjectName());
+ hssfText(row, 3, displayName != null && !displayName.trim().isEmpty() ? displayName.trim() : p.getProjectName());
hssfText(row, 4, valueOf(p.getConstructNature()));
hssfNum(row, 5, parseYearNum(p.getStartTime()));
hssfNum(row, 6, parseYearNum(p.getEndTime()));
@@ -3332,6 +3332,7 @@
// 鍘熶綅閫愯鏇挎崲锛氬悎璁�/鍧楁爣棰�/甯傚窞灏忚妭/鍒嗙粍/椤圭洰琛岋紱鏈尮閰嶇殑妯℃澘琛屾竻绌烘牱渚嬫暟鎹紙缁撴瀯/鍚堝苟/鍒楀涓嶅彉锛�
int seq = 0;
int si = -1;
+ java.util.Set<Long> usedProjectIds = new HashSet<>();
for (int r = 5; r <= lastRow; r++) {
HSSFRow row = sheet.getRow(r);
if (row == null) continue;
@@ -3391,11 +3392,12 @@
// 椤圭洰琛岋細鎸� 妯℃澘鍚嶇О+甯傚窞+鍧楃被鍒� 鍖归厤 DB 椤圭洰锛屾湭鍖归厤鏁磋娓呯┖
blankInvestRow(row, 25);
if (blockCategory == null) continue; // 鑰佹棫璐ц溅娈垫棤鏁版嵁婧�
- InvestmentProject p = matchInvest(projects, d, b, blockCategory);
+ InvestmentProject p = matchInvest(projects, d, b, blockCategory, usedProjectIds);
if (p == null) continue;
+ usedProjectIds.add(p.getId());
if (sec != null) sec.matched.add(p.getId());
InvestmentMonthly m = mByProject.get(p.getId());
- fillInvestPlanProjectRow(row, p, m, ++seq, b);
+ fillInvestPlanProjectRow(row, p, m, ++seq, b, d);
}
// 鍔ㄦ�佹彃琛岋細DB 椤圭洰鏁� > 妯℃澘棰勭暀琛屾暟鏃讹紝鍦ㄥ競宸炲皬鑺傛湯灏捐ˉ榻愶紙鑷簳鍚戜笂鎻掕锛岄伩鍏嶈鍙烽敊浣嶏級
for (int i = sections.size() - 1; i >= 0; i--) {
@@ -3415,7 +3417,7 @@
HSSFRow row = sheet.getRow(first + j);
if (row == null) continue;
InvestmentProject p = extra.get(j);
- fillInvestPlanProjectRow(row, p, mByProject.get(p.getId()), 0, p.getCity());
+ fillInvestPlanProjectRow(row, p, mByProject.get(p.getId()), 0, p.getCity(), p.getProjectName());
}
}
renumberInvestSeq(sheet, "investPlan");
@@ -3725,6 +3727,11 @@
/** 鍦ㄥ�欓�夐」鐩睜涓寜鍚嶇О鍖归厤 DB 椤圭洰锛堝彲閫� 甯傚窞/绫诲埆 杩囨护 + 鍘垮尯鍓嶇紑娑堟锛夛紝鏈懡涓繑鍥� null */
private InvestmentProject matchInvest(List<InvestmentProject> pool, String name, String cityFull, String category) {
+ return matchInvest(pool, name, cityFull, category, null);
+ }
+
+ /** 甯﹀凡鍗犵敤鎺掗櫎鐨勫尮閰嶏細鍚屼竴 DB 椤圭洰鍙厑璁稿~鍏呬竴琛岋紙閬垮厤妯℃澘涓�鏈�/浜屾湡鍚屽悕琛岄噸澶嶈緭鍑猴級 */
+ private InvestmentProject matchInvest(List<InvestmentProject> pool, String name, String cityFull, String category, Set<Long> excludeIds) {
if (name == null || name.trim().isEmpty()) return null;
String norm = normInvestName(name);
if (norm.isEmpty()) return null;
@@ -3732,6 +3739,7 @@
String county = extractInvestCounty(name);
List<InvestmentProject> cands = new java.util.ArrayList<>();
for (InvestmentProject p : pool) {
+ if (excludeIds != null && excludeIds.contains(p.getId())) continue;
if (category != null && !category.equals(p.getCategory())) continue;
if (cityCanon != null && !cityCanon.equals(RegionUtil.normalizeCityName(p.getCity()))) continue;
cands.add(p);
diff --git a/traffic-audit-web/src/views/DataImport.vue b/traffic-audit-web/src/views/DataImport.vue
index e010f1c..8e277e2 100644
--- a/traffic-audit-web/src/views/DataImport.vue
+++ b/traffic-audit-web/src/views/DataImport.vue
@@ -51,15 +51,15 @@
<el-button size="mini" type="primary" icon="el-icon-upload2" :loading="cityDirLoading"
@click="importCityDir(moduleSel[g.key])">浠庣洰褰曟壒閲忓鍏�</el-button>
</div>
- <div v-else-if="moduleSel[g.key]" class="module-file">
- <div class="file-name" :title="fileNames[moduleSel[g.key]]">{{ fileNames[moduleSel[g.key]] || '鏈�夋嫨鏂囦欢' }}</div>
+ <div v-else-if="moduleSel[g.key] && !isDirOnlyType(moduleSel[g.key])" class="module-file">
+ <div class="file-name" :title="fileNamesText(moduleSel[g.key])">{{ fileNamesText(moduleSel[g.key]) || '鏈�夋嫨鏂囦欢' }}</div>
<div class="file-btns">
- <el-upload action="#" :auto-upload="false" :show-file-list="false" accept=".xlsx,.xls"
+ <el-upload action="#" :auto-upload="false" :show-file-list="false" multiple accept=".xlsx,.xls,.et"
:ref="'up' + g.key" :on-change="f => onFilePicked(g.key, f)" style="display:inline-block">
<el-button size="mini" type="primary" plain icon="el-icon-folder-opened">閫夋嫨鏂囦欢</el-button>
</el-upload>
- <el-button size="mini" type="primary" icon="el-icon-upload2" :disabled="!files[moduleSel[g.key]]"
- @click="importNow(g.key)">绔嬪嵆瀵煎叆</el-button>
+ <el-button size="mini" type="primary" icon="el-icon-upload2" :disabled="!fileCount(moduleSel[g.key])"
+ @click="importNow(g.key)">{{ fileCount(moduleSel[g.key]) > 1 ? '鎵归噺瀵煎叆 ' + fileCount(moduleSel[g.key]) + ' 涓枃浠�' : '绔嬪嵆瀵煎叆' }}</el-button>
</div>
</div>
</div>
@@ -316,22 +316,31 @@
return map[t] || ''
},
onFilePicked(key, file) {
- this.$set(this.files, this.moduleSel[key], file.raw)
- this.$set(this.fileNames, this.moduleSel[key], file.name)
+ const type = this.moduleSel[key]
+ if (!type) return
+ const list = (this.files[type] || []).filter(f => f.name !== file.name)
+ list.push(file.raw)
+ this.$set(this.files, type, list)
+ this.$set(this.fileNames, type, list.map(f => f.name))
const up = this.$refs['up' + key]
if (up && up.clearFiles) up.clearFiles()
},
importNow(key) {
const type = this.moduleSel[key]
if (!type) return
- if (this.isCityDirType(type)) {
+ if (this.isDirOnlyType(type)) {
this.importCityDir(type)
this.moduleSel[key] = ''
this.importType = ''
return
}
- const file = this.files[type]
- if (!file) { this.$message.warning('璇峰厛涓鸿绫诲瀷閫夋嫨鏂囦欢'); return }
+ const fileList = this.files[type] || []
+ if (!fileList.length) { this.$message.warning('璇峰厛涓鸿绫诲瀷閫夋嫨鏂囦欢'); return }
+ if (fileList.length > 1 || type === 'investment' || type === 'investmentLogistics') {
+ this.batchImport(type, key, fileList)
+ return
+ }
+ const file = fileList[0]
if (!this.period) { this.$message.warning('璇峰厛閫夋嫨鎶ヨ〃鏈�'); return }
const g = this.groups.find(x => x.key === key)
const t = g.types.find(x => x.value === type)
@@ -357,10 +366,57 @@
this.$delete(this.fileNames, type)
})
},
+ fileCount(type) { return (this.files[type] || []).length },
+ fileNamesText(type) {
+ const list = this.fileNames[type] || []
+ return list.length ? '宸查�� ' + list.length + ' 涓枃浠讹細' + list.join('銆�') : ''
+ },
+ batchImport(type, key, fileList) {
+ if (!this.period) { this.$message.warning('璇峰厛閫夋嫨鎶ヨ〃鏈�'); return }
+ const fd = new FormData()
+ fd.append('type', type)
+ fd.append('period', this.period)
+ fileList.forEach(f => fd.append('files', f))
+ const g = this.groups.find(x => x.key === key)
+ const label = g ? g.name : type
+ this.$message({ message: '姝e湪鎵归噺瀵煎叆 ' + fileList.length + ' 涓枃浠垛��', duration: 3000 })
+ api.post('/import/batch', fd, { timeout: 600000 })
+ .then(res => {
+ const d = (res && res.data) || {}
+ if (!d.files) {
+ this.$message.error((res && res.message) || '鎵归噺瀵煎叆澶辫触')
+ return
+ }
+ const files = d.files || []
+ const errCount = files.filter(f => f.error || f.success === 0).length
+ const html = files.map(f =>
+ '<div style="margin:6px 0;line-height:1.6"><b>' + (f.fileName || '') + '</b>' +
+ (f.city ? '锛�' + f.city + '锛�' : '') + '锛氳В鏋� ' + (f.success || 0) + ' 鏉�' +
+ (f.error ? ' <span style="color:#F56C6C">' + f.error + '</span>' : '') + '</div>'
+ ).join('')
+ this.$alert(html, (label || type) + ' 鎵归噺瀵煎叆瀹屾垚锛氬叡 ' + d.total + ' 涓枃浠�' +
+ (d.fail > 0 ? '锛屽け璐� ' + d.fail + ' 鏉�' : '') + '锛堟垚鍔� ' + d.success + ' 鏉★級', {
+ dangerouslyUseHTMLString: true,
+ type: errCount > 0 ? 'warning' : 'success',
+ confirmButtonText: '鐭ラ亾浜�'
+ })
+ if (type === 'investment' || type === 'investmentLogistics') {
+ this.checkInvest(this.period)
+ }
+ this.afterImport()
+ this.files[type] = null
+ this.$delete(this.fileNames, type)
+ })
+ .catch(e => {
+ if (e && e._toast) return
+ this.$message.error((e.response && e.response.data && e.response.data.message) || '鎵归噺瀵煎叆澶辫触')
+ })
+ },
isCityDirType(t) { return t === 'investCityPassenger' || t === 'investCityLogistics' },
+ isDirOnlyType(t) { return t === 'investCityPassenger' || t === 'investCityLogistics' },
importCityDir(type) {
if (!this.period) { this.$message.warning('璇峰厛閫夋嫨鎶ヨ〃鏈�'); return }
- const category = type === 'investCityPassenger' ? '瀹㈣繍绔欏満' : '鐗╂祦鍥尯'
+ const category = (type === 'investCityPassenger' || type === 'investment') ? '瀹㈣繍绔欏満' : '鐗╂祦鍥尯'
const fd = new FormData()
fd.append('period', this.period)
fd.append('category', category)
--
Gitblit v1.9.1