机具领料模版下载与导入

This commit is contained in:
hayu 2025-12-30 13:48:42 +08:00
parent 5a513f49d1
commit 8fa6e994f0
8 changed files with 604 additions and 5 deletions

View File

@ -37,6 +37,8 @@
<input type="text" name="keyWord" placeholder="这里输入物资名称" class="input-large">
&nbsp;&nbsp;<button id='searchBtn' class="btn btn-warning btn-sm" title="过滤" type="button" onclick="getbaseList(1)"><i class="icon-search bigger-110 icon-only"></i></button>
<button id="addBtn" class="btn btn-success btn-sm" title="新增" type="button">新增</button>
<button class="btn btn-fail btn-sm" title="导入模板下载" type="button" onclick="templateDownload()">导入模板下载</button>
<button class="btn btn-sm btn-danger" title="批量导入" type="button" onclick="importData()">批量导入</button>
<button class="btn btn-fail btn-sm" title="批量确认" type="button" onclick="batchConfirmation()">批量确认</button>
<button class="btn btn-sm btn-danger" title="批量删除" type="button" onclick="batchDeletion()">批量删除</button>

View File

@ -633,3 +633,175 @@ function backShowChenkStatus(){
})
return vals;
}
/**
* 导入模板下载功能
*/
function templateDownload() {
// 1. 校验领料时间(与新增/删除逻辑保持一致,非今日不允许下载)
if (applyDate !== today) {
layer.alert('领料时间不是今天,不允许下载导入模板', {
skin: 'layui-layer-molv',
closeBtn: 0
});
return;
}
// 2. 构造下载请求携带必要参数taskId保证模板与当前任务关联
var taskId = localStorage.getItem("taskId");
var token = $("#token").val(); // 携带令牌,防止重复请求
var downloadUrl = bonuspath + '/backstage/receiveDetails/downloadTemplate';
// 3. 构造隐藏表单提交解决GET请求参数暴露/大小限制问题支持POST下载
var $form = $("<form>").attr({
"method": "POST",
"action": downloadUrl,
"target": "_blank" // 新窗口打开,不阻塞当前页面
});
// 添加请求参数
$form.append($("<input>").attr({
"type": "hidden",
"name": "taskId",
"value": taskId
}));
$form.append($("<input>").attr({
"type": "hidden",
"name": "token",
"value": token
}));
// 插入页面并提交
$("body").append($form);
$form.submit();
// 提交后移除表单
$form.remove();
// 4. 友好提示
layer.msg('正在下载导入模板,请稍后...', {
icon: 16,
shade: 0.1,
time: 1500
});
}
/**
* 批量导入功能
*/
function importData() {
// 1. 校验领料时间(与其他操作逻辑保持一致)
if (applyDate !== today) {
layer.alert('领料时间不是今天,不允许进行批量导入操作', {
skin: 'layui-layer-molv',
closeBtn: 0
});
return;
}
// 2. 弹出文件选择层使用layui/layer实现美观的文件上传界面
layer.open({
type: 1,
title: ['批量导入物资数据', 'background-color: #438EB9;color:#fff'],
shadeClose: false,
shade: 0.3,
area: ['450px', '220px'],
content: '<div style="padding: 20px;">' +
' <div class="layui-form-item" style="margin-bottom: 15px;">' +
' <label style="display: inline-block; width: 80px; text-align: right; margin-right: 10px;">选择文件:</label>' +
' <input type="file" id="importFile" accept=".xlsx,.xls" style="display: inline-block; width: 280px;" />' +
' <span style="color: #999; font-size: 12px;">仅支持xlsx/xls格式</span>' +
' </div>' +
' <div style="text-align: center; margin-top: 20px;">' +
' <button id="submitImport" class="btn btn-success btn-sm" style="margin-right: 20px;">确认导入</button>' +
' <button id="cancelImport" class="btn btn-default btn-sm">取消</button>' +
' </div>' +
'</div>',
success: function(layero, index) {
// 3. 取消按钮事件
$("#cancelImport").on('click', function() {
layer.close(index);
// 清空文件选择
$("#importFile").val("");
});
// 4. 确认导入按钮事件
$("#submitImport").on('click', function() {
var file = $("#importFile")[0].files[0];
// 4.1 校验文件是否选择
if (!file) {
layer.tips('请先选择要导入的Excel文件', '#importFile', {
tips: [1, '#FF5722'],
time: 2000
});
return;
}
// 4.2 校验文件格式
var fileName = file.name;
var suffix = fileName.substring(fileName.lastIndexOf(".")).toLowerCase();
if (suffix !== ".xlsx" && suffix !== ".xls") {
layer.alert('仅支持导入xlsx/xls格式的Excel文件请重新选择', {
skin: 'layui-layer-molv',
closeBtn: 0
});
$("#importFile").val("");
return;
}
// 4.3 构造FormData用于上传文件
var formData = new FormData();
formData.append("importFile", file); // 文件对象
formData.append("taskId", localStorage.getItem("taskId")); // 当前任务ID
formData.append("token", $("#token").val()); // 令牌
// 4.4 显示加载提示
var loadIndex = layer.load(2, {
shade: [0.3, '#000'],
time: 0
});
// 4.5 异步上传文件使用jQuery Ajax实现文件上传
$.ajax({
url: bonuspath + '/backstage/receiveDetails/importData',
type: 'POST',
data: formData,
contentType: false, // 必须设置为false让浏览器自动处理Content-Type
processData: false, // 必须设置为false不处理表单数据
dataType: 'json',
success: function(data) {
layer.close(loadIndex);
layer.close(index);
$("#importFile").val("");
if (data.resCode === "0000") {
layer.alert(data.resMsg, {
skin: 'layui-layer-molv',
closeBtn: 0
}, function() {
getbaseList(1);
layer.closeAll();
});
} else {
// 拆分错误信息,实现换行展示
var errorMsg = data.resMsg.replace(/\n/g, '<br/>');
layer.alert(errorMsg, {
skin: 'layui-layer-molv',
closeBtn: 0,
html: true // 允许展示HTML标签br换行
});
}
},
error: function(xhr, status, error) {
// 异常处理
layer.close(loadIndex);
layer.close(index);
$("#importFile").val("");
layer.alert('导入请求异常,请联系管理员', {
skin: 'layui-layer-molv',
closeBtn: 0
});
console.error("导入异常:", error);
}
});
});
}
});
}

View File

@ -306,5 +306,25 @@
LEFT JOIN wf_info_record wir ON wir.SUP_ID = wtr.ID
WHERE wtr.SUP_ID = #{taskId} and wir.MA_ID
</select>
<select id="getMaTypeByNameAndModel" resultType="com.bonus.lease.beans.ReceiveDetailsBean">
SELECT
mt.ID as maModelId,
mt.PARENT_ID as maTypeId,
mt2.`NAME`
FROM
mm_type mt
LEFT JOIN mm_type mt2 on mt2.ID=mt.PARENT_ID
WHERE
mt.`name`=#{maModel}
and mt.`level`='4'
and mt2.`NAME`=#{maType};
</select>
<select id="getUserByUserName" resultType="com.bonus.lease.beans.ReceiveDetailsBean">
SELECT id as customerSrepId,
`NAME` as `name`
FROM pm_user
WHERE `NAME` = #{customerSrep}
and POST_NAME LIKE CONCAT('%', '客服代表', '%')
</select>
</mapper>

Binary file not shown.

View File

@ -1,28 +1,46 @@
package com.bonus.lease.controller;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.bonus.sys.*;
import com.bonus.sys.beans.UserBean;
//导入POI核心类
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.DateUtil;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.bonus.lease.beans.OutStorageBean;
import com.bonus.lease.beans.ReceiveDetailsBean;
import com.bonus.lease.service.OutStorageService;
import com.bonus.lease.service.ReceiveDetailsService;
import com.bonus.sys.AjaxRes;
import com.bonus.sys.BaseController;
import com.bonus.sys.GlobalConst;
import com.bonus.sys.Page;
import org.springframework.web.multipart.MultipartFile;
@Controller
@RequestMapping("/backstage/receiveDetails/")
@ -447,4 +465,315 @@ public class ReceiveDetailsController extends BaseController<ReceiveDetailsBean>
// list.add("是否确认");
// return list;
// }
/**
* 下载物资导入模板
* @param taskId 任务ID
* @param token 令牌
* @param request HTTP请求对象显式声明参数Spring自动注入
* @param response 响应对象
*/
@RequestMapping("/downloadTemplate")
// 注意文件下载接口建议移除@ResponseBody因为要直接写入响应流无需Spring序列化返回值
public void downloadTemplate(String taskId, String token,
HttpServletRequest request, // 关键添加request参数声明
HttpServletResponse response) {
// 1. 令牌校验此时request已正常获取可直接调用getSession()
HttpSession session = request.getSession();
String sessionToken = (String) session.getAttribute("TOKEN_IN_SESSION");
// if (token == null || !token.equals(sessionToken)) {
// // 令牌无效处理
// try {
// response.getWriter().write("令牌无效,请刷新页面重试");
// } catch (IOException e) {
// e.printStackTrace();
// }
// return;
// }
// 2. 模板文件生成/读取建议将模板放在项目resources/template目录下
String templatePath = "template/机具领料导入模板.xlsx";
File templateFile = null;
try {
// 读取模板文件
Resource resource = new ClassPathResource(templatePath);
templateFile = resource.getFile();
// 3. 设置响应头实现文件下载
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode("物资导入模板.xlsx", "UTF-8"));
response.setContentLength((int) templateFile.length());
// 4. 写入响应流
FileInputStream fis = new FileInputStream(templateFile);
ServletOutputStream sos = response.getOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
sos.write(buffer, 0, len);
}
sos.flush();
fis.close();
sos.close();
// // 5. 移除令牌可选防止重复下载
// session.removeAttribute("TOKEN_IN_SESSION");
} catch (Exception e) {
e.printStackTrace();
// 下载失败处理
try {
response.getWriter().write("模板下载失败,请联系管理员");
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
@RequestMapping("/importData")
@ResponseBody
public Map<String, Object> importData(@RequestParam("importFile") MultipartFile importFile,
String taskId,
String token,
HttpSession session) {
Map<String, Object> result = new HashMap<>();
List<String> errorMsgList = new ArrayList<>();
Map<String, Integer> existTypeModelMap = new HashMap<>();
List<ReceiveDetailsBean> dataList = new ArrayList<>();
// 1. 基础校验
if (importFile == null || importFile.isEmpty()) {
result.put("resCode", "9999");
result.put("resMsg", "请选择要导入的Excel文件");
return result;
}
// // 2. 令牌校验
// String sessionToken = (String) session.getAttribute("TOKEN_IN_SESSION");
// if (token == null || !token.equals(sessionToken)) {
// result.put("resCode", "9999");
// result.put("resMsg", "令牌无效,请刷新页面重试");
// return result;
// }
Workbook workbook = null;
InputStream inputStream = null;
try {
// 3. 获取文件输入流和文件名
inputStream = importFile.getInputStream();
String fileName = importFile.getOriginalFilename();
if (fileName == null || (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx"))) {
result.put("resCode", "9999");
result.put("resMsg", "仅支持导入.xls或.xlsx格式的Excel文件");
return result;
}
// 4. 创建Workbook低版本POI兼容
if (fileName.endsWith(".xls")) {
workbook = new HSSFWorkbook(inputStream);
} else {
workbook = new XSSFWorkbook(inputStream);
}
// 5. 获取工作表
Sheet sheet = workbook.getSheetAt(0);
if (sheet == null) {
result.put("resCode", "9999");
result.put("resMsg", "Excel文件中无有效工作表请检查");
return result;
}
// 6. 遍历数据行
int firstRowNum = sheet.getFirstRowNum();
int lastRowNum = sheet.getLastRowNum();
// 只有表头无数据
if (firstRowNum == lastRowNum) {
result.put("resCode", "9999");
result.put("resMsg", "Excel文件中无有效数据请检查");
return result;
}
// 遍历所有数据行跳过表头
for (int i = firstRowNum + 2; i <= lastRowNum; i++) {
Row row = sheet.getRow(i);
if (row == null) {
continue;
}
ReceiveDetailsBean bean = new ReceiveDetailsBean();
int excelRowNum = i + 1;
StringBuilder rowErrorMsg = new StringBuilder();
// 低版本POI兼容手动判断单元格是否为null无MissingCellPolicy
Cell maTypeCell = row.getCell(0);
String maType = getCellStringValue(maTypeCell);
bean.setMaType(maType);
Cell maModelCell = row.getCell(1);
String maModel = getCellStringValue(maModelCell);
bean.setMaModel(maModel);
Cell customerSrepCell = row.getCell(2);
String customerSrep = getCellStringValue(customerSrepCell);
bean.setCustomerSrep(customerSrep);
Cell machinesNumCell = row.getCell(3);
String machinesNum = getCellStringValue(machinesNumCell);
bean.setMachinesNum(machinesNum);
// 7. 原有校验逻辑完全不变
if (StringHelper.isEmpty(maType)) {
rowErrorMsg.append("物资名称为空;");
}
if (StringHelper.isEmpty(maModel)) {
rowErrorMsg.append("规格型号为空;");
}
if (StringHelper.isEmpty(customerSrep)) {
rowErrorMsg.append("客服代表为空;");
}
if (StringHelper.isEmpty(machinesNum)) {
rowErrorMsg.append("机具数量为空;");
} else {
try {
int machineNum = Integer.parseInt(machinesNum.trim());
if (machineNum <= 0) {
rowErrorMsg.append("机具数量必须为正整数;");
}
} catch (NumberFormatException e) {
rowErrorMsg.append("机具数量格式错误,必须为正整数;");
}
}
// 8. 物资名称+规格型号存在性校验不变
if (rowErrorMsg.length() == 0) {
ReceiveDetailsBean receiveDetailsBean = service.getMaTypeByNameAndModel(bean);
if (receiveDetailsBean == null) {
rowErrorMsg.append("物资名称或规格型号不存在;");
} else {
bean.setMaModelId(receiveDetailsBean.getMaModelId());
}
}
// 9. 客服代表合法性校验不变
if (rowErrorMsg.length() == 0) {
ReceiveDetailsBean kfBean = service.getUserByUserName(bean);
if (kfBean == null) {
rowErrorMsg.append("客服代表名称不存在或角色不符;");
} else {
bean.setCustomerSrep(kfBean.getCustomerSrepId());
}
}
// 10. 数据库重复校验不变
if (rowErrorMsg.length() == 0) {
bean.setTaskId(taskId);
List<ReceiveDetailsBean> list = service.findBean(bean);
if (list.size() > 0) {
rowErrorMsg.append("该物资类型+规格型号已在数据库中存在,请勿重复添加;");
}
}
// 11. Excel内部重复校验不变
if (rowErrorMsg.length() == 0) {
String typeModelKey = maType.trim() + "_" + maModel.trim();
if (existTypeModelMap.containsKey(typeModelKey)) {
int firstRowNum2 = existTypeModelMap.get(typeModelKey);
rowErrorMsg.append("物资类型+规格型号与第").append(firstRowNum2).append("行重复;");
} else {
existTypeModelMap.put(typeModelKey, excelRowNum);
}
}
// 12. 收集错误信息不变
if (rowErrorMsg.length() > 0) {
errorMsgList.add("" + excelRowNum + "行:" + rowErrorMsg.toString().substring(0, rowErrorMsg.length() - 1));
} else {
dataList.add(bean);
}
}
// 13. 错误判断与批量插入不变
if (!errorMsgList.isEmpty()) {
result.put("resCode", "9999");
StringBuilder allErrorMsg = new StringBuilder("导入失败,存在以下错误:\n");
for (String errorMsg : errorMsgList) {
allErrorMsg.append(errorMsg).append("\n");
}
result.put("resMsg", allErrorMsg.toString().trim());
return result;
}
boolean importSuccess = service.batchImport(dataList, taskId);
if (importSuccess) {
result.put("resCode", "0000");
result.put("resMsg", "批量导入成功,共导入" + dataList.size() + "条数据");
} else {
result.put("resCode", "9999");
result.put("resMsg", "批量导入失败,数据库插入异常");
}
// session.removeAttribute("TOKEN_IN_SESSION");
} catch (Exception e) {
e.printStackTrace();
result.put("resCode", "9999");
result.put("resMsg", "导入异常:" + e.getMessage());
} finally {
// 关闭资源不变
try {
if (workbook != null) {
workbook.close();
}
if (inputStream != null) {
inputStream.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return result;
}
// 低版本POI兼容的单元格值转换方法无CellType和MissingCellPolicy依赖
private String getCellStringValue(Cell cell) {
if (cell == null) {
return "";
}
String cellValue = "";
int cellType = cell.getCellType();
// 处理公式单元格
if (cellType == Cell.CELL_TYPE_FORMULA) {
cellType = cell.getCachedFormulaResultType();
}
// 使用低版本POI静态常量判断类型
switch (cellType) {
case Cell.CELL_TYPE_STRING:
cellValue = cell.getStringCellValue().trim();
break;
case Cell.CELL_TYPE_NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
cellValue = cell.getDateCellValue().toString();
} else {
BigDecimal bigDecimal = new BigDecimal(cell.getNumericCellValue());
cellValue = bigDecimal.toString().trim();
if (cellValue.endsWith(".0")) {
cellValue = cellValue.substring(0, cellValue.lastIndexOf("."));
}
}
break;
case Cell.CELL_TYPE_BOOLEAN:
cellValue = String.valueOf(cell.getBooleanCellValue()).trim();
break;
case Cell.CELL_TYPE_BLANK:
cellValue = "";
break;
default:
cellValue = "";
break;
}
return cellValue;
}
}

View File

@ -50,4 +50,17 @@ public interface ReceiveDetailsDao extends BaseDao<ReceiveDetailsBean> {
public int batchDeletion(ReceiveDetailsBean o);
/**
* 根据物资名称和规格型号查询物资
* @param bean
* @return
*/
ReceiveDetailsBean getMaTypeByNameAndModel(ReceiveDetailsBean bean);
/**
* 根据用户名查询用户
* @param bean
* @return
*/
ReceiveDetailsBean getUserByUserName(ReceiveDetailsBean bean);
}

View File

@ -36,4 +36,26 @@ public interface ReceiveDetailsService extends BaseService<ReceiveDetailsBean> {
public String batchDeletion(ReceiveDetailsBean o);
/**
* 根据物资名称和规格型号查询物资
* @param bean
* @return
*/
ReceiveDetailsBean getMaTypeByNameAndModel(ReceiveDetailsBean bean);
/**
* 根据用户名查询用户
* @param bean
* @return
*/
ReceiveDetailsBean getUserByUserName(ReceiveDetailsBean bean);
/**
* 批量导入
* @param dataList
* @param taskId
* @param leasePlanOutId
* @return
*/
boolean batchImport(List<ReceiveDetailsBean> dataList, String taskId);
}

View File

@ -2,6 +2,7 @@ package com.bonus.lease.service;
import java.util.List;
import com.google.protobuf.ServiceException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -24,6 +25,7 @@ import com.bonus.sys.beans.UserBean;
import com.bonus.sys.dao.UserDao;
import com.bonus.wf.beans.TaskRecordBean;
import com.bonus.wf.dao.TaskRecordDao;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
@Service("receiveDetails")
public class ReceiveDetailsServiceImp extends BaseServiceImp<ReceiveDetailsBean> implements ReceiveDetailsService {
@ -233,4 +235,43 @@ public class ReceiveDetailsServiceImp extends BaseServiceImp<ReceiveDetailsBean>
}
}
@Override
public ReceiveDetailsBean getMaTypeByNameAndModel(ReceiveDetailsBean bean) {
try {
return dao.getMaTypeByNameAndModel(bean);
} catch (Exception e) {
logger.error(e.toString(), e);
return new ReceiveDetailsBean();
}
}
@Override
public ReceiveDetailsBean getUserByUserName(ReceiveDetailsBean bean) {
try {
return dao.getUserByUserName(bean);
} catch (Exception e) {
logger.error(e.toString(), e);
return new ReceiveDetailsBean();
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean batchImport(List<ReceiveDetailsBean> dataList, String taskId) {
try {
for (ReceiveDetailsBean bean : dataList) {
bean.setTaskId(taskId);
int res = dao.insertBean(bean);
if (res != 1) {
throw new ServiceException("新增类型数据失败");
}
}
return true;
} catch (Exception e) {
logger.error(e.toString(), e);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return false;
}
}
}