Selaa lähdekoodia

feat(cw): add budget execution import backend and frontend files

wuzhen 9 kuukautta sitten
vanhempi
commit
fe0d528fcc

+ 75 - 0
backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwBudgetExecutionImportController.java

@@ -0,0 +1,75 @@
+package org.dromara.cw.controller;
+
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.cw.domain.vo.CwBudgetExecutionImportVo;
+import org.dromara.cw.listener.CwBudgetExecutionImportListener;
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.http.MediaType;
+
+import java.io.InputStream;
+
+/**
+ * 预算执行导入 Controller
+ */
+@Slf4j
+@RestController
+@RequestMapping("/cw/budgetExecutionImport")
+public class CwBudgetExecutionImportController extends BaseController {
+
+    /**
+     * 导入数据
+     *
+     * @param file 导入文件
+     */
+    @Log(title = "预算执行导入", businessType = BusinessType.IMPORT)
+    @SaCheckPermission("cw:budgetExecutionImport:import")
+    @PostMapping(value = "/importData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public R<Void> importData(@RequestPart("file") MultipartFile file) throws Exception {
+        try (InputStream is = file.getInputStream()) {
+            ExcelUtil.importExcel(is, CwBudgetExecutionImportVo.class, new CwBudgetExecutionImportListener());
+            return R.ok();
+        } catch (Exception e) {
+            log.error("导入失败", e);
+            throw e;
+        }
+    }
+
+    /**
+     * 获取导入模板(导出空模板)
+     */
+    @SaCheckPermission("cw:budgetExecutionImport:import")
+    @PostMapping("/importTemplate")
+    public void importTemplate(HttpServletResponse response) {
+        ExcelUtil.exportExcel(java.util.List.of(), "预算执行导入模板", CwBudgetExecutionImportVo.class, response);
+    }
+
+    /**
+     * 查询导入批次分页列表
+     */
+    @SaCheckPermission("cw:budgetExecutionImport:list")
+    @GetMapping("/batch/list")
+    public org.dromara.common.mybatis.core.page.TableDataInfo<?> batchList(org.dromara.common.mybatis.core.page.PageQuery pageQuery) {
+        var service = org.dromara.common.core.utils.SpringUtils.getBean(org.dromara.cw.service.ICwBudgetExecutionImportService.class);
+        return service.queryBatchPageList(pageQuery);
+    }
+
+    /**
+     * 查询导入明细分页列表
+     */
+    @SaCheckPermission("cw:budgetExecutionImport:list")
+    @GetMapping("/item/list")
+    public org.dromara.common.mybatis.core.page.TableDataInfo<?> itemList(@RequestParam("batchId") Long batchId, org.dromara.common.mybatis.core.page.PageQuery pageQuery) {
+        var service = org.dromara.common.core.utils.SpringUtils.getBean(org.dromara.cw.service.ICwBudgetExecutionImportService.class);
+        return service.queryItemPageList(batchId, pageQuery);
+    }
+}
+
+

+ 58 - 0
backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwBudgetExecutionImportBatch.java

@@ -0,0 +1,58 @@
+package org.dromara.cw.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 预算执行导入批次对象 cw_budget_execution_import_batch
+ *
+ * @author ruoyi
+ * @date 2026-01-05
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("cw_budget_execution_import_batch")
+public class CwBudgetExecutionImportBatch extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id")
+    private Long id;
+
+    private String batchNo;
+
+    private String fileName;
+
+    private Integer totalCount;
+
+    private Integer successCount;
+
+    private Integer failedCount;
+
+    private Long importBy;
+
+    private Date importTime;
+
+    private String status;
+
+    private String remark;
+
+    private BigDecimal totalAmount;
+
+    private Long createBy;
+
+    private Date createTime;
+
+    private Long updateBy;
+
+    private Date updateTime;
+}
+
+
+

+ 100 - 0
backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwBudgetExecutionImportItem.java

@@ -0,0 +1,100 @@
+package org.dromara.cw.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.annotation.TableField;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 预算执行导入明细对象 cw_budget_execution_import_item
+ *
+ * @author ruoyi
+ * @date 2026-01-05
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("cw_budget_execution_import_item")
+public class CwBudgetExecutionImportItem extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id")
+    private Long id;
+
+    private Long batchId;
+
+    private Integer rowNo;
+
+    private String serialNo;
+
+    private String purpose;
+
+    private BigDecimal amount;
+
+    private String deptPaymentCategory;
+
+    private String funcSubject;
+
+    private String fundNature;
+
+    private String indicatorSource;
+
+    private String paymentType;
+
+    private String payeeBank;
+
+    private String payee;
+
+    private String payeeAccount;
+
+    private String projectType;
+
+    private String project;
+
+    private java.sql.Date receiptDate;
+
+    private String localIndicatorDocNo;
+
+    private String budgetSource;
+
+    private String indicatorId;
+
+    @TableField("ext_field_1")
+    private String extField1;
+
+    @TableField("ext_field_2")
+    private String extField2;
+
+    @TableField("ext_field_3")
+    private String extField3;
+
+    @TableField("ext_field_4")
+    private String extField4;
+
+    @TableField("ext_field_5")
+    private String extField5;
+
+    private String otherFields;
+
+    private String rawData;
+
+    private String importStatus;
+
+    private String errorMessage;
+
+    private Long createBy;
+
+    private Date createTime;
+
+    private Date updateTime;
+
+    private Long updateBy;
+}
+
+
+

+ 43 - 0
backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/vo/CwBudgetExecutionImportVo.java

@@ -0,0 +1,43 @@
+package org.dromara.cw.domain.vo;
+
+import lombok.Data;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * Excel 导入映射 VO
+ */
+@Data
+public class CwBudgetExecutionImportVo {
+    private String serialNo;
+    private String purpose;
+    private BigDecimal amount;
+    private String deptPaymentCategory;
+    private String funcSubject;
+    private String fundNature;
+    private String indicatorSource;
+    private String paymentType;
+    private String payeeBank;
+    private String payee;
+    private String payeeAccount;
+    private String projectType;
+    private String project;
+    /**
+     * 收到回单的时间,使用 String 避免 Excel 解析严格类型导致异常,
+     * 在 Listener 中做灵活解析为 Date
+     */
+    private String receiptDate;
+    private String localIndicatorDocNo;
+    private String budgetSource;
+    private String indicatorId;
+    private String extField1;
+    private String extField2;
+    private String extField3;
+    private String extField4;
+    private String extField5;
+    private String otherFields;
+    // 额外字段:原始行号由监听器填充
+    private Integer rowNo;
+}
+
+

+ 139 - 0
backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/listener/CwBudgetExecutionImportListener.java

@@ -0,0 +1,139 @@
+package org.dromara.cw.listener;
+
+import cn.idev.excel.context.AnalysisContext;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.excel.core.ExcelResult;
+import org.dromara.common.excel.core.DefaultExcelListener;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.core.utils.DateUtils;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.cw.domain.CwBudgetExecutionImportItem;
+import org.dromara.cw.domain.CwBudgetExecutionImportBatch;
+import org.dromara.cw.domain.vo.CwBudgetExecutionImportVo;
+import org.dromara.cw.service.ICwBudgetExecutionImportService;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 预算执行导入监听器
+ */
+@Slf4j
+public class CwBudgetExecutionImportListener extends DefaultExcelListener<CwBudgetExecutionImportVo> {
+
+    private final ICwBudgetExecutionImportService importService;
+    private final Long operUserId;
+    private final List<CwBudgetExecutionImportItem> buffer = new ArrayList<>();
+
+    public CwBudgetExecutionImportListener() {
+        super(true);
+        this.importService = SpringUtils.getBean(ICwBudgetExecutionImportService.class);
+        this.operUserId = LoginHelper.getUserId();
+    }
+
+    @Override
+    public void invoke(CwBudgetExecutionImportVo data, AnalysisContext context) {
+        try {
+            CwBudgetExecutionImportItem item = new CwBudgetExecutionImportItem();
+            // 尽量从解析上下文获取原始 Excel 行号(更可靠),否则使用 VO 中的 rowNo
+            try {
+                int rowIndex = context.readRowHolder().getRowIndex(); // 0-based
+                item.setRowNo(rowIndex + 1);
+            } catch (Exception ex) {
+                // fallback
+                item.setRowNo(data.getRowNo());
+            }
+            item.setSerialNo(data.getSerialNo());
+            item.setPurpose(data.getPurpose());
+            item.setAmount(data.getAmount());
+            item.setDeptPaymentCategory(data.getDeptPaymentCategory());
+            item.setFuncSubject(data.getFuncSubject());
+            item.setFundNature(data.getFundNature());
+            item.setIndicatorSource(data.getIndicatorSource());
+            item.setPaymentType(data.getPaymentType());
+            item.setPayeeBank(data.getPayeeBank());
+            item.setPayee(data.getPayee());
+            item.setPayeeAccount(data.getPayeeAccount());
+            item.setProjectType(data.getProjectType());
+            item.setProject(data.getProject());
+            // 灵活解析回单时间(Excel 中可能为字符串或日期格式不一致)
+            try {
+                if (data.getReceiptDate() != null) {
+                    // receiptDate 在 VO 中为 String(已改),尝试解析为 Date
+                    java.util.Date parsed = DateUtils.parseDate(data.getReceiptDate());
+                    if (parsed != null) {
+                        item.setReceiptDate(new java.sql.Date(parsed.getTime()));
+                    } else {
+                        // 解析失败时记录为 null,并在明细中标记错误以便人工排查
+                        item.setReceiptDate(null);
+                        item.setImportStatus("fail");
+                        item.setErrorMessage("回单时间解析失败: " + data.getReceiptDate());
+                        // 将该行仍加入buffer以便入库和后续查看;可以选择跳过或计为失败
+                    }
+                }
+            } catch (Exception e) {
+                item.setReceiptDate(null);
+                item.setImportStatus("fail");
+                item.setErrorMessage("回单时间解析异常: " + e.getMessage());
+            }
+            item.setLocalIndicatorDocNo(data.getLocalIndicatorDocNo());
+            item.setBudgetSource(data.getBudgetSource());
+            item.setIndicatorId(data.getIndicatorId());
+            item.setExtField1(data.getExtField1());
+            item.setExtField2(data.getExtField2());
+            item.setExtField3(data.getExtField3());
+            item.setExtField4(data.getExtField4());
+            item.setExtField5(data.getExtField5());
+            item.setOtherFields(data.getOtherFields());
+            item.setImportStatus("pending");
+            item.setCreateBy(operUserId);
+            item.setCreateTime(new Date());
+            buffer.add(item);
+        } catch (Exception e) {
+            log.error("导入行处理异常", e);
+        }
+    }
+
+    @Override
+    public void doAfterAllAnalysed(AnalysisContext context) {
+        log.info("Excel 解析完成,准备写入数据库,行数={}", buffer.size());
+        if (!buffer.isEmpty()) {
+            // 创建批次
+            CwBudgetExecutionImportBatch batch = new CwBudgetExecutionImportBatch();
+            batch.setBatchNo(String.valueOf(System.currentTimeMillis()));
+            batch.setFileName("excel_import");
+            batch.setTotalCount(buffer.size());
+            batch.setSuccessCount(0);
+            batch.setFailedCount(0);
+            batch.setImportBy(operUserId);
+            batch.setImportTime(new Date());
+            batch.setStatus("running");
+            batch.setCreateBy(operUserId);
+            batch.setCreateTime(new Date());
+            Long batchId = importService.insertBatch(batch);
+
+            // 赋值 batchId 并批量插入
+            BigDecimal totalAmount = BigDecimal.ZERO;
+            for (int i = 0; i < buffer.size(); i++) {
+                CwBudgetExecutionImportItem it = buffer.get(i);
+                it.setBatchId(batchId);
+                // 确保 rowNo 非空(数据库约束),若为空则用解析顺序作为行号
+                if (it.getRowNo() == null) {
+                    it.setRowNo(i + 1);
+                }
+                totalAmount = totalAmount.add(it.getAmount() == null ? BigDecimal.ZERO : it.getAmount());
+            }
+            importService.batchInsertItems(buffer);
+
+            // 更新批次汇总
+            importService.updateBatchSummary(batchId, buffer.size(), 0, totalAmount);
+            log.info("导入完成,batchId={}, totalAmount={}, rows={}", batchId, totalAmount, buffer.size());
+        }
+    }
+
+    // 使用 DefaultExcelListener 提供的默认实现
+}
+
+

+ 14 - 0
backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/mapper/CwBudgetExecutionImportBatchMapper.java

@@ -0,0 +1,14 @@
+package org.dromara.cw.mapper;
+
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.cw.domain.CwBudgetExecutionImportBatch;
+
+/**
+ * 批次 Mapper
+ */
+public interface CwBudgetExecutionImportBatchMapper extends BaseMapperPlus<CwBudgetExecutionImportBatch, CwBudgetExecutionImportBatch> {
+
+}
+
+
+

+ 17 - 0
backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/mapper/CwBudgetExecutionImportItemMapper.java

@@ -0,0 +1,17 @@
+package org.dromara.cw.mapper;
+
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.cw.domain.CwBudgetExecutionImportItem;
+
+import java.util.List;
+
+/**
+ * 明细 Mapper
+ */
+public interface CwBudgetExecutionImportItemMapper extends BaseMapperPlus<CwBudgetExecutionImportItem, CwBudgetExecutionImportItem> {
+
+    int batchInsert(List<CwBudgetExecutionImportItem> list);
+}
+
+
+

+ 24 - 0
backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwBudgetExecutionImportService.java

@@ -0,0 +1,24 @@
+package org.dromara.cw.service;
+
+import org.dromara.cw.domain.CwBudgetExecutionImportBatch;
+import org.dromara.cw.domain.CwBudgetExecutionImportItem;
+
+import java.math.BigDecimal;
+import java.util.List;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.mybatis.core.page.PageQuery;
+
+public interface ICwBudgetExecutionImportService {
+
+    Long insertBatch(CwBudgetExecutionImportBatch batch);
+
+    Boolean batchInsertItems(List<CwBudgetExecutionImportItem> items);
+
+    Boolean updateBatchSummary(Long batchId, int successCount, int failedCount, BigDecimal totalAmount);
+
+    TableDataInfo<CwBudgetExecutionImportBatch> queryBatchPageList(PageQuery pageQuery);
+
+    TableDataInfo<CwBudgetExecutionImportItem> queryItemPageList(Long batchId, PageQuery pageQuery);
+}
+
+

+ 89 - 0
backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwBudgetExecutionImportServiceImpl.java

@@ -0,0 +1,89 @@
+package org.dromara.cw.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.dromara.cw.domain.CwBudgetExecutionImportBatch;
+import org.dromara.cw.domain.CwBudgetExecutionImportItem;
+import org.dromara.cw.mapper.CwBudgetExecutionImportBatchMapper;
+import org.dromara.cw.mapper.CwBudgetExecutionImportItemMapper;
+import org.dromara.cw.service.ICwBudgetExecutionImportService;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+
+/**
+ * 导入服务实现
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class CwBudgetExecutionImportServiceImpl implements ICwBudgetExecutionImportService {
+
+    private final CwBudgetExecutionImportBatchMapper batchMapper;
+    private final CwBudgetExecutionImportItemMapper itemMapper;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long insertBatch(CwBudgetExecutionImportBatch batch) {
+        batch.setCreateTime(new Date());
+        batch.setUpdateTime(new Date());
+        int r = batchMapper.insert(batch);
+        if (r > 0) {
+            return batch.getId();
+        }
+        return null;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean batchInsertItems(List<CwBudgetExecutionImportItem> items) {
+        if (items == null || items.isEmpty()) {
+            return false;
+        }
+        // 避免依赖外部 XML 的 batchInsert 映射,在这里使用 MyBatis-Plus 的通用 insert 批量处理
+        int count = 0;
+        for (CwBudgetExecutionImportItem item : items) {
+            int r = itemMapper.insert(item);
+            if (r > 0) {
+                count++;
+            }
+        }
+        return count == items.size();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean updateBatchSummary(Long batchId, int successCount, int failedCount, BigDecimal totalAmount) {
+        CwBudgetExecutionImportBatch update = new CwBudgetExecutionImportBatch();
+        update.setId(batchId);
+        update.setSuccessCount(successCount);
+        update.setFailedCount(failedCount);
+        update.setTotalCount(successCount + failedCount);
+        update.setTotalAmount(totalAmount);
+        update.setUpdateTime(new Date());
+        int r = batchMapper.updateById(update);
+        return r > 0;
+    }
+
+    @Override
+    public org.dromara.common.mybatis.core.page.TableDataInfo<CwBudgetExecutionImportBatch> queryBatchPageList(org.dromara.common.mybatis.core.page.PageQuery pageQuery) {
+        Page<CwBudgetExecutionImportBatch> page = pageQuery.<CwBudgetExecutionImportBatch>build();
+        var pageResult = batchMapper.selectPage(page, null);
+        return org.dromara.common.mybatis.core.page.TableDataInfo.build(pageResult);
+    }
+
+    @Override
+    public org.dromara.common.mybatis.core.page.TableDataInfo<CwBudgetExecutionImportItem> queryItemPageList(Long batchId, org.dromara.common.mybatis.core.page.PageQuery pageQuery) {
+        Page<CwBudgetExecutionImportItem> page = pageQuery.<CwBudgetExecutionImportItem>build();
+        var lqw = com.baomidou.mybatisplus.core.toolkit.Wrappers.<CwBudgetExecutionImportItem>lambdaQuery().eq(CwBudgetExecutionImportItem::getBatchId, batchId);
+        var pageResult = itemMapper.selectPage(page, lqw);
+        log.info("查询导入明细: batchId={}, pageNum={}, pageSize={}, result total={}", batchId, page.getCurrent(), page.getSize(), pageResult.getTotal());
+        return org.dromara.common.mybatis.core.page.TableDataInfo.build(pageResult);
+    }
+}
+
+

+ 30 - 0
backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/cw/CwBudgetExecutionImportItemMapper.xml

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="org.dromara.cw.mapper.CwBudgetExecutionImportItemMapper">
+
+    <insert id="batchInsert" parameterType="java.util.List">
+        INSERT INTO cw_budget_execution_import_item
+        (id, batch_id, row_no, serial_no, purpose, amount, dept_payment_category, func_subject,
+         fund_nature, indicator_source, payment_type, payee_bank, payee, payee_account,
+         project_type, project, receipt_date, local_indicator_doc_no, budget_source, indicator_id,
+         ext_field_1, ext_field_2, ext_field_3, ext_field_4, ext_field_5,
+         other_fields, raw_data, import_status, error_message, create_by, create_time, update_time, update_by)
+        VALUES
+        <foreach collection="list" item="item" separator=",">
+            (#{item.id}, #{item.batchId}, #{item.rowNo}, #{item.serialNo}, #{item.purpose}, #{item.amount},
+             #{item.deptPaymentCategory}, #{item.funcSubject}, #{item.fundNature}, #{item.indicatorSource},
+             #{item.paymentType}, #{item.payeeBank}, #{item.payee}, #{item.payeeAccount},
+             #{item.projectType}, #{item.project}, #{item.receiptDate}, #{item.localIndicatorDocNo},
+             #{item.budgetSource}, #{item.indicatorId}, #{item.extField1}, #{item.extField2},
+             #{item.extField3}, #{item.extField4}, #{item.extField5},
+             #{item.otherFields}, #{item.rawData}, #{item.importStatus}, #{item.errorMessage},
+             #{item.createBy}, #{item.createTime}, #{item.updateTime}, #{item.updateBy})
+        </foreach>
+    </insert>
+
+</mapper>
+
+
+

+ 54 - 0
web/src/api/cw/budgetExecutionImport.ts

@@ -0,0 +1,54 @@
+import request from '@/utils/request';
+
+/**
+ * 导入预算执行数据
+ * @param file File
+ */
+export const importBudgetExecution = (file: File) => {
+  const formData = new FormData();
+  formData.append('file', file);
+  return request({
+    url: '/cw/budgetExecutionImport/importData',
+    method: 'post',
+    data: formData,
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  });
+};
+
+/**
+ * 下载导入模板
+ */
+export const downloadBudgetExecutionTemplate = () => {
+  return request({
+    url: '/cw/budgetExecutionImport/importTemplate',
+    method: 'post',
+    responseType: 'blob'
+  });
+};
+
+/**
+ * 查询批次分页
+ */
+export const listImportBatches = (params?: any) => {
+  return request({
+    url: '/cw/budgetExecutionImport/batch/list',
+    method: 'get',
+    params
+  });
+};
+
+/**
+ * 查询明细分页
+ */
+export const listImportItems = (batchId: number, params?: any) => {
+  return request({
+    url: '/cw/budgetExecutionImport/item/list',
+    method: 'get',
+    params: { batchId, ...params }
+  });
+};
+
+
+

+ 7 - 0
web/src/views/cw/budgetExecutionImport.vue

@@ -0,0 +1,7 @@
+<script>
+import Comp from './budgetExecutionImport/index.vue';
+export default Comp;
+</script>
+
+
+

+ 254 - 0
web/src/views/cw/budgetExecutionImport/index.vue

@@ -0,0 +1,254 @@
+<template>
+  <div>
+    <div class="page-actions" style="margin-bottom: 12px;">
+      <el-button type="primary" @click="triggerFileInput">导入</el-button>
+    </div>
+
+    <!-- Hidden native file input fallback to ensure upload works reliably -->
+    <input ref="fileInputRef" type="file" accept=".xlsx,.xls" style="display:none" @change="handleLocalFileChange" />
+
+    <import-dialog
+      v-if="false"
+      :visible="importDialogVisible"
+      :uploadUrl="importUrl"
+      :templateUrl="templateUrl"
+      @update:visible="importDialogVisible = $event"
+      @success="onImportSuccess"
+    />
+
+    <el-card>
+      <div style="margin-bottom: 8px;">
+        <span>已选择批次:</span>
+        <el-select v-model="selectedBatchId" placeholder="请选择批次" @change="onBatchChange" style="min-width: 220px;">
+          <el-option
+            v-for="b in batches"
+            :key="b.id"
+            :label="b.batchNo + ' (' + b.createTime + ') - 合计:' + (b.totalAmount || 0)"
+            :value="String(b.id)"
+          />
+        </el-select>
+      </div>
+    <!-- debug info removed -->
+
+      <div style="margin-bottom: 8px; font-weight: 600;">
+        总合计金额:{{ totalAmountDisplay }}
+      </div>
+
+      <div class="table-wrapper">
+        <el-table :data="items" border style="width: 100%; min-width: 1400px;">
+        <el-table-column prop="rowNo" label="序号" width="80" />
+        <el-table-column prop="serialNo" label="序号(原表)" width="120" />
+        <el-table-column prop="purpose" label="用途" min-width="180" />
+        <el-table-column prop="amount" label="金额(元)" width="140" />
+        <el-table-column prop="deptPaymentCategory" label="部门支出款项分类" min-width="160" />
+        <el-table-column prop="funcSubject" label="支出功能分类科目" min-width="160" />
+        <el-table-column prop="fundNature" label="资金性质" width="120" />
+        <el-table-column prop="indicatorSource" label="指标来源" min-width="140" />
+        <el-table-column prop="paymentType" label="支付类型" width="120" />
+        <el-table-column prop="payeeBank" label="收款人开户银行" min-width="180" />
+        <el-table-column prop="payee" label="收款人" width="160" />
+        <el-table-column prop="payeeAccount" label="收款账号" width="180" />
+        <el-table-column prop="projectType" label="项目类型" min-width="160" />
+        <el-table-column prop="project" label="项目" min-width="200" />
+        <el-table-column prop="receiptDate" label="回单时间" width="140" />
+        <el-table-column prop="localIndicatorDocNo" label="本级指标文号" min-width="160" />
+        <el-table-column prop="budgetSource" label="预算来源" min-width="140" />
+        <el-table-column prop="indicatorId" label="指标ID" width="160" />
+        <el-table-column prop="extField1" label="冗余字段1" width="140" />
+        <el-table-column prop="extField2" label="冗余字段2" width="140" />
+        <el-table-column prop="extField3" label="冗余字段3" width="140" />
+        <el-table-column prop="extField4" label="冗余字段4" width="140" />
+        <el-table-column prop="extField5" label="冗余字段5" width="140" />
+        <el-table-column prop="otherFields" label="扩展字段" min-width="200" />
+        </el-table>
+      </div>
+
+      <div style="margin-top: 12px; text-align: right;">
+        <el-pagination
+          background
+          layout="prev, pager, next, jumper"
+          :total="itemsTotal"
+          :page-size="pageSize"
+          :current-page.sync="pageNum"
+          @current-change="fetchItems"
+        />
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, computed, watch, nextTick } from 'vue';
+import ImportDialog from '@/components/ImportDialog/index.vue';
+import { listImportBatches, listImportItems, downloadBudgetExecutionTemplate } from '@/api/cw/budgetExecutionImport';
+import { ElMessage } from 'element-plus';
+import { importBudgetExecution } from '@/api/cw/budgetExecutionImport';
+
+const importDialogVisible = ref(false);
+const importUrl = import.meta.env.VITE_APP_BASE_API + '/cw/budgetExecutionImport/importData';
+const templateUrl = import.meta.env.VITE_APP_BASE_API + '/cw/budgetExecutionImport/importTemplate';
+
+const batches = ref<any[]>([]);
+const selectedBatchId = ref<string | null>(null);
+
+const items = ref<any[]>([]);
+const itemsTotal = ref(0);
+const pageNum = ref(1);
+const pageSize = ref(20);
+
+const totalAmountDisplay = computed(() => {
+  const b = batches.value.find((x) => String(x.id) === selectedBatchId.value);
+  return b ? (b.totalAmount || 0) : 0;
+});
+
+const importUrlLocal = importUrl;
+
+const openImport = () => {
+  importDialogVisible.value = true;
+};
+
+const downloadTemplate = async () => {
+  try {
+    await downloadBudgetExecutionTemplate();
+    // browser will handle download
+  } catch (e) {
+    ElMessage.error('模板下载失败');
+  }
+};
+
+const fileInputRef = ref<HTMLInputElement | null>(null);
+
+const triggerFileInput = () => {
+  fileInputRef.value?.click();
+};
+
+const handleLocalFileChange = async (e: Event) => {
+  const input = e.target as HTMLInputElement;
+  const file = input.files?.[0];
+  if (!file) {
+    ElMessage.warning('请先选择文件');
+    return;
+  }
+  // simple client-side validation
+  const allowedTypes = ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel'];
+  if (file.size / 1024 / 1024 > 10) {
+    ElMessage.error('文件大小不能超过 10MB');
+    input.value = '';
+    return;
+  }
+  const formData = new FormData();
+  formData.append('file', file);
+  try {
+    // 使用对象形式调用以兼容不同环境下的实现细节,避免 createMessage 内部错误
+    ElMessage({ message: '正在导入,请稍候...', type: 'info', duration: 0 });
+    const res: any = await importBudgetExecution(file);
+    ElMessage.closeAll();
+    if (res && res.code === 200) {
+      ElMessage.success('导入成功');
+      fetchBatches();
+      if (selectedBatchId.value) {
+        fetchItems();
+      }
+    } else {
+      ElMessage.error(res?.msg || '导入失败');
+    }
+  } catch (err) {
+    ElMessage.closeAll();
+    console.error(err);
+    ElMessage.error('导入失败,请检查文件格式或网络');
+  } finally {
+    input.value = '';
+  }
+};
+
+const fetchBatches = async () => {
+  try {
+    const res: any = await listImportBatches({ pageNum: 1, pageSize: 100 });
+    console.debug('budgetExecutionImport - batch list raw res:', res);
+    let rows = [];
+    if (res) {
+      // response can be { code, msg, data: { rows } } or { total, rows, code }
+      rows = res.data?.rows ?? res.rows ?? res.data ?? [];
+    }
+    batches.value = Array.isArray(rows) ? rows : [];
+    console.debug('budgetExecutionImport - fetched batches:', batches.value);
+  } catch (e) {
+    console.error('fetchBatches error', e);
+    batches.value = [];
+  }
+};
+// 监听 batches 变化:如果未选择批次则自动选中第一个并加载明细
+watch(
+  () => batches.value,
+  async (val) => {
+    if (val && val.length > 0 && !selectedBatchId.value) {
+      // 等待 DOM 更新再触发
+      selectedBatchId.value = String(val[0].id);
+      await nextTick();
+      console.debug('budgetExecutionImport - auto selected batchId:', selectedBatchId.value);
+      fetchItems(1);
+    }
+  },
+  { immediate: true }
+);
+
+// no debugInfo in production
+
+const fetchItems = async (page = pageNum.value) => {
+  try {
+    if (!selectedBatchId.value) {
+      items.value = [];
+      itemsTotal.value = 0;
+      return;
+    }
+    // Important: pass batchId as string to avoid JS Number precision loss for long IDs
+    const res: any = await listImportItems(selectedBatchId.value as string, { pageNum: page, pageSize: pageSize.value });
+    console.debug('budgetExecutionImport - item list raw res:', res);
+    let rows: any[] = [];
+    let totalCount = 0;
+    if (res) {
+      // support multiple formats: { code, data:{ rows, total } } or { total, rows, code }
+      rows = res.data?.rows ?? res.rows ?? res.data ?? [];
+      totalCount = res.data?.total ?? res.total ?? (Array.isArray(rows) ? rows.length : 0);
+    }
+    items.value = Array.isArray(rows) ? rows : [];
+    itemsTotal.value = totalCount || 0;
+  } catch (e) {
+    console.error('fetchItems error', e);
+    items.value = [];
+    itemsTotal.value = 0;
+  }
+};
+
+const onBatchChange = () => {
+  pageNum.value = 1;
+  fetchItems(1);
+};
+
+const onImportSuccess = () => {
+  importDialogVisible.value = false;
+  fetchBatches();
+  if (selectedBatchId.value) {
+    fetchItems();
+  }
+  ElMessage.success('导入成功');
+};
+
+onMounted(() => {
+  fetchBatches();
+});
+
+defineExpose({});
+</script>
+
+<style scoped>
+.page-actions {
+  display: flex;
+  gap: 8px;
+}
+.table-wrapper {
+  overflow-x: auto;
+}
+</style>
+
+