zhizhijie
3 小时以前 799ec6799ad9e994f7d369f059ca7682962f648f
fix: 能耗导入同报表期并发重导互斥锁 + 空车牌(0)过滤 + 前端导入按钮防连点
2个文件已修改
38 ■■■■ 已修改文件
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/DataImport.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java
@@ -67,6 +67,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import com.trafficaudit.dataimport.dto.ImportResult;
import org.apache.poi.ss.usermodel.DateUtil;
import java.util.Comparator;
@@ -94,6 +95,9 @@
    /** H204 燃料1计量单位码 -> 中文单位 */
    private static final Map<String, String> ENERGY_FUEL_UNIT_NAMES = new HashMap<>();
    /** 能耗导入按报表期互斥,防止同报表期并发重导互相覆盖 */
    private final ConcurrentHashMap<String, Object> energyImportLocks = new ConcurrentHashMap<>();
    static {
        ENERGY_VEHICLE_TYPE_NAMES.put("01", "普通货车");
@@ -527,6 +531,11 @@
    /** 能耗明细每组固定18列,车牌号起始列(0-based):3,21,39,57,75 */
    private static final int[] ENERGY_GROUP_COLS = {3, 21, 39, 57, 75};
    /** 能耗导入按报表期互斥锁(H204 明细与运政信息共用,避免同报表期并发重导互相覆盖) */
    private Object energyLock(String period) {
        return energyImportLocks.computeIfAbsent(period == null ? "" : period, k -> new Object());
    }
    public ImportResult importEnergyMonthly(MultipartFile file, String period) throws Exception {
        List<EnergyRow> rows = new ArrayList<>();
        List<EnergyAuthVehicle> authList = new ArrayList<>();
@@ -541,7 +550,7 @@
                if (enterpriseName == null || enterpriseName.trim().isEmpty()) continue;
                for (int group : ENERGY_GROUP_COLS) {
                    String plate = getString(row, group);
                    if (plate == null || plate.trim().isEmpty()) continue;
                    if (plate == null || plate.trim().isEmpty() || "0".equals(plate.trim())) continue;
                    EnergyVehicleQuarterly e = new EnergyVehicleQuarterly();
                    e.setReportPeriod(period);
                    e.setRegionCode(getString(row, 0));
@@ -580,7 +589,7 @@
                    Row row = authSheet.getRow(r);
                    if (row == null) continue;
                    String plate = getString(row, 0);
                    if (plate == null || plate.trim().isEmpty()) continue;
                    if (plate == null || plate.trim().isEmpty() || "0".equals(plate.trim())) continue;
                    EnergyAuthVehicle auth = new EnergyAuthVehicle();
                    auth.setReportPeriod(period);
                    auth.setPlateNo(plate.trim());
@@ -597,6 +606,8 @@
        }
        // 幂等:同报表期重新导入,先删除旧数据(两表 + H204 规则审核结果)
        // 加锁:同报表期并发重导互斥,防止 delete/insert 交错导致数据残留或翻倍
        synchronized (energyLock(period)) {
        energyMapper.delete(new LambdaQueryWrapper<EnergyVehicleQuarterly>()
            .eq(EnergyVehicleQuarterly::getReportPeriod, period));
        energyAuthMapper.delete(new LambdaQueryWrapper<EnergyAuthVehicle>()
@@ -646,6 +657,7 @@
        log.info("Energy monthly imported: {} vehicles, auth {} rows for {}", success, authSuccess, period);
        return new ImportResult(success, failDetails.size(), failDetails);
    }
    }
    /** 能耗车辆运政信息(独立模板:车牌号/车辆类型/燃料类型/标记吨位/准牵引质量) */
    public ImportResult importEnergyAuth(MultipartFile file, String period) throws Exception {
@@ -657,7 +669,7 @@
                Row row = sheet.getRow(r);
                if (row == null) continue;
                String plate = getString(row, 0);
                if (plate == null || plate.trim().isEmpty()) continue;
                if (plate == null || plate.trim().isEmpty() || "0".equals(plate.trim())) continue;
                EnergyAuthVehicle auth = new EnergyAuthVehicle();
                auth.setReportPeriod(period);
                auth.setPlateNo(plate.trim());
@@ -671,7 +683,8 @@
            log.error("Energy auth parse error", e);
            throw e;
        }
        // 幂等:同报表期重新导入,先删除旧数据
        // 幂等:同报表期重新导入,先删除旧数据;与 H204 明细共用报表期锁,防并发互相覆盖
        synchronized (energyLock(period)) {
        energyAuthMapper.delete(new LambdaQueryWrapper<EnergyAuthVehicle>()
            .eq(EnergyAuthVehicle::getReportPeriod, period));
        deleteRuleResults("H204", period);
@@ -696,6 +709,7 @@
        log.info("Energy auth imported: {} rows for {}", success, period);
        return new ImportResult(success, failDetails.size(), failDetails);
    }
    }
    /** H204 行记录:保留 Excel 行号用于失败定位 */
    private static class EnergyRow {
traffic-audit-web/src/views/DataImport.vue
@@ -56,9 +56,9 @@
                  <div class="file-btns">
                    <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-button size="mini" type="primary" plain icon="el-icon-folder-opened" :disabled="importing">选择文件</el-button>
                    </el-upload>
                    <el-button size="mini" type="primary" icon="el-icon-upload2" :disabled="!fileCount(moduleSel[g.key])"
                    <el-button size="mini" type="primary" icon="el-icon-upload2" :loading="importing" :disabled="importing || !fileCount(moduleSel[g.key])"
                               @click="importNow(g.key)">{{ fileCount(moduleSel[g.key]) > 1 ? '批量导入 ' + fileCount(moduleSel[g.key]) + ' 个文件' : '立即导入' }}</el-button>
                  </div>
                </div>
@@ -155,6 +155,7 @@
      importType: 'h2032',
      period: (() => { const d = new Date(); return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') })(),
      cityDirLoading: false,
      importing: false,
      sideOpen: false,
      result: '',
      error: '',
@@ -318,6 +319,7 @@
    onFilePicked(key, file) {
      const type = this.moduleSel[key]
      if (!type) return
      if (this.importing) { this.$message.warning('正在导入中,请稍候再选择文件'); return }
      const list = (this.files[type] || []).filter(f => f.name !== file.name)
      list.push(file.raw)
      this.$set(this.files, type, list)
@@ -336,12 +338,14 @@
      }
      const fileList = this.files[type] || []
      if (!fileList.length) { this.$message.warning('请先为该类型选择文件'); return }
      if (!this.period) { this.$message.warning('请先选择报表期'); return }
      if (this.importing) { this.$message.warning('正在导入中,请稍候…'); return }
      this.importing = true
      if (fileList.length > 1 || type === 'investment' || type === 'investmentLogistics') {
        this.batchImport(type, key, fileList)
        this.batchImport(type, key, fileList).finally(() => { this.importing = false })
        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)
      const row = {
@@ -364,7 +368,7 @@
        this.importType = ''
        this.files[type] = null
        this.$delete(this.fileNames, type)
      })
      }).finally(() => { this.importing = false })
    },
    fileCount(type) { return (this.files[type] || []).length },
    fileNamesText(type) {
@@ -380,7 +384,7 @@
      const g = this.groups.find(x => x.key === key)
      const label = g ? g.name : type
      this.$message({ message: '正在批量导入 ' + fileList.length + ' 个文件…', duration: 3000 })
      api.post('/import/batch', fd, { timeout: 600000 })
      return api.post('/import/batch', fd, { timeout: 600000 })
        .then(res => {
          const d = (res && res.data) || {}
          if (!d.files) {