支持店铺通过excel批量更新商品库存功能

This commit is contained in:
pikachu1995@126.com 2024-06-27 17:09:25 +08:00
parent 6f9486f065
commit 159fe37120
8 changed files with 425 additions and 9 deletions

View File

@ -89,6 +89,7 @@ public enum ResultCode {
VIRTUAL_GOODS_NOT_NEED_TEMP(11015, "虚拟商品无需选择配送模板"),
GOODS_NOT_EXIST_STORE(11017, "当前用户无权操作此商品"),
GOODS_TYPE_ERROR(11016, "需选择商品类型"),
GOODS_STOCK_IMPORT_ERROR(11018, "导入商品库存失败,请检查表格数据"),
/**
* 参数

View File

@ -27,6 +27,7 @@ public class GoodsSearchParams extends PageVO {
private static final long serialVersionUID = 2544015852728566887L;
@ApiModelProperty(value = "商品编号")
private String goodsId;

View File

@ -23,4 +23,19 @@ public class GoodsSkuStockDTO {
@ApiModelProperty(value = "预警库存")
private Integer alertQuantity;
@ApiModelProperty(value = "规格信息")
private String simpleSpecs;
@ApiModelProperty(value = "商品编号")
private String sn;
@ApiModelProperty(value = "商品名称")
private String goodsName;
/**
* @see cn.lili.modules.goods.entity.enums.GoodsStockTypeEnum
*/
@ApiModelProperty(value = "类型")
private String type;
}

View File

@ -0,0 +1,32 @@
package cn.lili.modules.goods.entity.enums;
/**
* 库存操作类型
*/
public enum GoodsStockTypeEnum {
SUB(""),
ADD("");
private final String description;
GoodsStockTypeEnum(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
// 根据描述获取枚举实例
public static GoodsStockTypeEnum fromDescription(String description) {
for (GoodsStockTypeEnum type : GoodsStockTypeEnum.values()) {
if (type.getDescription().equals(description)) {
return type;
}
}
throw new IllegalArgumentException("No matching enum constant for description: " + description);
}
}

View File

@ -3,6 +3,8 @@ package cn.lili.modules.goods.mapper;
import cn.lili.modules.goods.entity.dos.GoodsSku;
import cn.lili.modules.goods.entity.dto.GoodsSkuDTO;
import cn.lili.modules.goods.entity.dto.GoodsSkuStockDTO;
import cn.lili.modules.order.order.entity.dto.OrderExportDTO;
import cn.lili.modules.order.order.entity.vo.OrderSimpleVO;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
@ -115,6 +117,7 @@ public interface GoodsSkuMapper extends BaseMapper<GoodsSku> {
@Select("SELECT *,g.params as params FROM li_goods_sku gs inner join li_goods g on gs.goods_id = g.id ${ew.customSqlSegment}")
IPage<GoodsSkuDTO> queryByParams(IPage<GoodsSkuDTO> page, @Param(Constants.WRAPPER) Wrapper<GoodsSkuDTO> queryWrapper);
@Select("SELECT id as sku_id, quantity, goods_id FROM li_goods_sku ${ew.customSqlSegment}")
@Select("SELECT id as sku_id, quantity, goods_id,simple_specs,sn,goods_name FROM li_goods_sku ${ew.customSqlSegment}")
List<GoodsSkuStockDTO> queryStocks(@Param(Constants.WRAPPER) Wrapper<GoodsSku> queryWrapper);
}

View File

@ -12,7 +12,9 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
@ -146,7 +148,20 @@ public interface GoodsSkuService extends IService<GoodsSku> {
* @return 商品sku信息
*/
IPage<GoodsSku> getGoodsSkuByPage(GoodsSearchParams searchParams);
/**
* 查询导出商品库存
*
* @param searchParams 查询参数
* @return 导出商品库存
*/
void queryExportStock(HttpServletResponse response, GoodsSearchParams searchParams);
/**
* 导入商品库存
* @param storeId 店铺ID
* @param files 文件
*/
void importStock(String storeId, MultipartFile files);
/**
* 分页查询商品sku信息
@ -187,6 +202,7 @@ public interface GoodsSkuService extends IService<GoodsSku> {
* @param goodsSkuStockDTOS sku库存修改实体
*/
void updateStocks(List<GoodsSkuStockDTO> goodsSkuStockDTOS);
void updateStocksByType(List<GoodsSkuStockDTO> goodsSkuStockDTOS);
/**
* 更新SKU预警库存
@ -207,6 +223,7 @@ public interface GoodsSkuService extends IService<GoodsSku> {
* @param quantity 设置的库存数量
*/
void updateStock(String skuId, Integer quantity);
void updateStock(String skuId, Integer quantity,String type);
/**
* 获取商品sku库存

View File

@ -1,6 +1,8 @@
package cn.lili.modules.goods.serviceimpl;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.lili.cache.Cache;
@ -23,6 +25,7 @@ import cn.lili.modules.goods.entity.dto.GoodsSkuStockDTO;
import cn.lili.modules.goods.entity.enums.GoodsAuthEnum;
import cn.lili.modules.goods.entity.enums.GoodsSalesModeEnum;
import cn.lili.modules.goods.entity.enums.GoodsStatusEnum;
import cn.lili.modules.goods.entity.enums.GoodsStockTypeEnum;
import cn.lili.modules.goods.entity.vos.GoodsSkuSpecVO;
import cn.lili.modules.goods.entity.vos.GoodsSkuVO;
import cn.lili.modules.goods.entity.vos.GoodsVO;
@ -54,12 +57,22 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.*;
import java.util.stream.Collectors;
@ -493,6 +506,115 @@ public class GoodsSkuServiceImpl extends ServiceImpl<GoodsSkuMapper, GoodsSku> i
return this.page(PageUtil.initPage(searchParams), searchParams.queryWrapper());
}
@Override
public void queryExportStock(HttpServletResponse response, GoodsSearchParams searchParams) {
List<GoodsSkuStockDTO> goodsSkuStockDTOList = this.baseMapper.queryStocks(searchParams.queryWrapper());
XSSFWorkbook workbook = initStockExportData(goodsSkuStockDTOList);
try {
// 设置响应头
String fileName = URLEncoder.encode("商品库存", "UTF-8");
response.setContentType("application/vnd.ms-excel;charset=UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");
ServletOutputStream out = response.getOutputStream();
workbook.write(out);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
workbook.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void importStock(String storeId, MultipartFile file) {
List<GoodsSkuStockDTO> goodsSkuStockDTOList = new ArrayList<>();
try (InputStream inputStream = file.getInputStream()) {
// 使用 WorkbookFactory.create 方法读取 Excel 文件
Workbook workbook = WorkbookFactory.create(inputStream);
Sheet sheet = workbook.getSheetAt(0); // 我们只读取第一个sheet
// 检查第一个sheet的行数是否超过10002行
if (sheet.getPhysicalNumberOfRows() > 10002) {
throw new ServiceException(ResultCode.GOODS_STOCK_IMPORT_ERROR, "Excel行数超过10002行");
}
// 遍历行和单元格
Iterator<Row> rowIterator = sheet.rowIterator();
int rowIndex = 0;
while (rowIterator.hasNext()) {
Row row = rowIterator.next();
rowIndex++;
// 跳过表头
if (rowIndex < 3) {
continue;
}
List<Object> objects = new ArrayList<>();
for (int i = 0; i < 4; i++) {
objects.add(getCellValue(row.getCell(i)));
}
log.error(getCellValue(row.getCell(2)));
log.error(getCellValue(row.getCell(3)));
// 判断数据格式
if (!"".equals(getCellValue(row.getCell(2))) && !"".equals(getCellValue(row.getCell(2)))) {
throw new ServiceException(ResultCode.GOODS_STOCK_IMPORT_ERROR, "库存修改方向列数据必须是“增”或“减”");
} else if (!NumberUtil.isInteger(getCellValue(row.getCell(3))) || Integer.parseInt(getCellValue(row.getCell(3))) < 0) {
throw new ServiceException(ResultCode.GOODS_STOCK_IMPORT_ERROR, "库存必须是正整数");
} else if (this.count(new LambdaQueryWrapper<GoodsSku>()
.eq(GoodsSku::getGoodsId, getCellValue(row.getCell(0)))
.eq(GoodsSku::getId, getCellValue(row.getCell(1)))
.eq(GoodsSku::getStoreId, storeId)) == 0) {
throw new ServiceException(ResultCode.GOODS_STOCK_IMPORT_ERROR, "" + rowIndex + "行商品不存在");
}
GoodsSkuStockDTO goodsSkuStockDTO = new GoodsSkuStockDTO();
goodsSkuStockDTO.setGoodsId(getCellValue(row.getCell(0)));
goodsSkuStockDTO.setSkuId(getCellValue(row.getCell(1)));
goodsSkuStockDTO.setType(GoodsStockTypeEnum.fromDescription(getCellValue(row.getCell(2))).name());
goodsSkuStockDTO.setQuantity(Integer.parseInt(getCellValue(row.getCell(3))));
goodsSkuStockDTOList.add(goodsSkuStockDTO);
}
} catch (IOException e) {
log.error("IOException occurred while processing the Excel file.", e);
throw new ServiceException(ResultCode.GOODS_STOCK_IMPORT_ERROR, e.getMessage());
}
// 批量修改商品库存
this.updateStocksByType(goodsSkuStockDTOList);
}
private String getCellValue(Cell cell) {
if (cell == null) {
return "";
}
switch (cell.getCellType()) {
case STRING:
return cell.getStringCellValue();
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue().toString();
} else {
// 将数值转换为整数以去掉小数点
double numericValue = cell.getNumericCellValue();
if (numericValue == (long) numericValue) {
return String.valueOf((long) numericValue);
} else {
return String.valueOf(numericValue);
}
}
case BOOLEAN:
return String.valueOf(cell.getBooleanCellValue());
case FORMULA:
return cell.getCellFormula();
default:
return "";
}
}
@Override
public IPage<GoodsSkuDTO> getGoodsSkuDTOByPage(Page<GoodsSkuDTO> page, Wrapper<GoodsSkuDTO> queryWrapper) {
return this.baseMapper.queryByParams(page, queryWrapper);
@ -529,6 +651,27 @@ public class GoodsSkuServiceImpl extends ServiceImpl<GoodsSkuMapper, GoodsSku> i
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateStocksByType(List<GoodsSkuStockDTO> goodsSkuStockDTOS) {
// 获取所有的goodsID并去除相同的goodsID
List<String> goodsIds = goodsSkuStockDTOS.stream()
.map(GoodsSkuStockDTO::getGoodsId)
.distinct()
.collect(Collectors.toList());
//更新SKU库存
for (GoodsSkuStockDTO goodsSkuStockDTO : goodsSkuStockDTOS) {
this.updateStock(goodsSkuStockDTO.getSkuId(), goodsSkuStockDTO.getQuantity(), goodsSkuStockDTO.getType());
}
//更新SPU库存
for (String goodsId : goodsIds) {
goodsService.updateStock(goodsId);
}
}
@Override
public void updateAlertQuantity(GoodsSkuStockDTO goodsSkuStockDTO) {
GoodsSku goodsSku = this.getById(goodsSkuStockDTO.getSkuId());
@ -559,7 +702,6 @@ public class GoodsSkuServiceImpl extends ServiceImpl<GoodsSkuMapper, GoodsSku> i
this.updateBatchById(goodsSkuList);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateStock(String skuId, Integer quantity) {
@ -591,6 +733,45 @@ public class GoodsSkuServiceImpl extends ServiceImpl<GoodsSkuMapper, GoodsSku> i
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateStock(String skuId, Integer quantity, String type) {
GoodsSku goodsSku = getGoodsSkuByIdFromCache(skuId);
if (goodsSku != null) {
//计算修改库存
if (type.equals(GoodsStockTypeEnum.ADD.name())) {
quantity = Convert.toInt(NumberUtil.add(goodsSku.getQuantity(), quantity));
} else {
quantity = Convert.toInt(NumberUtil.sub(goodsSku.getQuantity(), quantity));
}
goodsSku.setQuantity(quantity);
//判断商品sku是否已经下架(修改商品库存为0时 会自动下架商品),再次更新商品库存时 需更新商品索引
boolean isFlag = goodsSku.getQuantity() <= 0;
boolean update = this.update(new LambdaUpdateWrapper<GoodsSku>().eq(GoodsSku::getId, skuId).set(GoodsSku::getQuantity, quantity));
if (update) {
cache.remove(CachePrefix.GOODS.getPrefix() + goodsSku.getGoodsId());
}
cache.put(GoodsSkuService.getCacheKeys(skuId), goodsSku);
cache.put(GoodsSkuService.getStockCacheKey(skuId), quantity);
this.promotionGoodsService.updatePromotionGoodsStock(goodsSku.getId(), quantity);
//商品库存为0是删除商品索引
if (quantity <= 0) {
goodsIndexService.deleteIndexById(goodsSku.getId());
}
//商品SKU库存为0并且商品sku状态为上架时更新商品库存
if (isFlag && CharSequenceUtil.equals(goodsSku.getMarketEnable(), GoodsStatusEnum.UPPER.name())) {
List<String> goodsIds = new ArrayList<>();
goodsIds.add(goodsSku.getGoodsId());
applicationEventPublisher.publishEvent(new TransactionCommitSendMQEvent("更新商品", rocketmqCustomProperties.getGoodsTopic(),
GoodsTagsEnum.UPDATE_GOODS_INDEX.name(), goodsIds));
}
}
}
@Override
public Integer getStock(String skuId) {
String cacheKeys = GoodsSkuService.getStockCacheKey(skuId);
@ -780,4 +961,150 @@ public class GoodsSkuServiceImpl extends ServiceImpl<GoodsSkuMapper, GoodsSku> i
return skuSpecVOList;
}
/**
* 初始化填充商品库存导出数据
*
* @param goodsSkuStockDTOList 导出的库存数据
* @return 商品库存导出列表
*/
private XSSFWorkbook initStockExportData(List<GoodsSkuStockDTO> goodsSkuStockDTOList) {
XSSFWorkbook workbook = new XSSFWorkbook();
// 创建模板
this.createTemplate(workbook);
// 创建sku库存列表
this.skuStockList(workbook, goodsSkuStockDTOList);
// 创建sku库存列表
this.skuList(workbook, goodsSkuStockDTOList);
return workbook;
}
/**
* 创建模板
*
* @param workbook
*/
private void createTemplate(XSSFWorkbook workbook) {
Sheet templateSheet = workbook.createSheet("商品库存编辑模板");
// 创建表头
Row description = templateSheet.createRow(0);
description.setHeightInPoints(90);
Cell descriptionCell = description.createCell(0);
descriptionCell.setCellValue("填写说明(请勿删除本说明):\n" +
"1.可批量设置多个商品的库存一次最多10000行\n" +
"2.库存修改方向:选择库存方向后,会在原先库存基础上进行增加或者减少\n" +
"3.库存变更数量需要为整数不能填写0和负数");
// 合并描述行的单元格
templateSheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 3));
// 设置描述行单元格样式例如自动换行
CellStyle descriptionStyle = workbook.createCellStyle();
descriptionStyle.setWrapText(true);
descriptionStyle.setAlignment(HorizontalAlignment.LEFT);
descriptionStyle.setVerticalAlignment(VerticalAlignment.CENTER);
descriptionCell.setCellStyle(descriptionStyle);
// 创建表头
Row header = templateSheet.createRow(1);
String[] headers = {"商品ID必填", "skuID必填", "库存修改方向(必填,填 增 或者 减)", "库存变更数量(必填)"};
CellStyle headerStyle = workbook.createCellStyle();
Font headerFont = workbook.createFont();
headerFont.setBold(true);
headerStyle.setFont(headerFont);
for (int i = 0; i < headers.length; i++) {
Cell cell = header.createCell(i);
cell.setCellValue(headers[i]);
cell.setCellStyle(headerStyle);
}
//修改列宽
templateSheet.setColumnWidth(0, 30 * 256);
templateSheet.setColumnWidth(1, 30 * 256);
templateSheet.setColumnWidth(2, 40 * 256);
templateSheet.setColumnWidth(3, 25 * 256);
// 设置下拉列表数据验证
DataValidationHelper validationHelper = templateSheet.getDataValidationHelper();
DataValidationConstraint constraint = validationHelper.createExplicitListConstraint(new String[]{"", ""});
CellRangeAddressList addressList = new CellRangeAddressList(2, 10001, 2, 2); // 从第3行到第10002行第3列
DataValidation validation = validationHelper.createValidation(constraint, addressList);
validation.setSuppressDropDownArrow(true);
validation.setShowErrorBox(true);
templateSheet.addValidationData(validation);
}
/**
* 创建sku库存列表
*
* @param workbook
*/
private void skuStockList(XSSFWorkbook workbook, List<GoodsSkuStockDTO> goodsSkuStockDTOList) {
Sheet skuListSheet = workbook.createSheet("商品库存信息");
// 创建表头
Row header = skuListSheet.createRow(0);
String[] headers = {"商品ID", "商品名称", "规格ID(SKUID)", "规格名称", "货号", "当前库存数量"};
for (int i = 0; i < headers.length; i++) {
Cell cell = header.createCell(i);
cell.setCellValue(headers[i]);
}
// 填充数据
for (int i = 0; i < goodsSkuStockDTOList.size(); i++) {
GoodsSkuStockDTO dto = goodsSkuStockDTOList.get(i);
Row row = skuListSheet.createRow(i + 1);
row.createCell(0).setCellValue(dto.getGoodsId());
row.createCell(1).setCellValue(dto.getGoodsName());
row.createCell(2).setCellValue(dto.getSkuId());
row.createCell(3).setCellValue(dto.getSimpleSpecs());
row.createCell(4).setCellValue(dto.getSn());
row.createCell(5).setCellValue(dto.getQuantity());
}
//修改列宽
skuListSheet.setColumnWidth(0, 30 * 256);
skuListSheet.setColumnWidth(1, 30 * 256);
skuListSheet.setColumnWidth(2, 30 * 256);
skuListSheet.setColumnWidth(3, 30 * 256);
skuListSheet.setColumnWidth(4, 30 * 256);
skuListSheet.setColumnWidth(5, 15 * 256);
}
private void skuList(XSSFWorkbook workbook, List<GoodsSkuStockDTO> goodsSkuStockDTOList) {
Sheet skuListSheet = workbook.createSheet("商品规格");
// 创建表头
Row header = skuListSheet.createRow(0);
String[] headers = {"商品ID", "商品名称", "规格ID(SKUID)", "规格名称", "货号"};
for (int i = 0; i < headers.length; i++) {
Cell cell = header.createCell(i);
cell.setCellValue(headers[i]);
}
// 填充数据
for (int i = 0; i < goodsSkuStockDTOList.size(); i++) {
GoodsSkuStockDTO dto = goodsSkuStockDTOList.get(i);
Row row = skuListSheet.createRow(i + 1);
row.createCell(0).setCellValue(dto.getGoodsId());
row.createCell(1).setCellValue(dto.getGoodsName());
row.createCell(2).setCellValue(dto.getSkuId());
row.createCell(3).setCellValue(dto.getSimpleSpecs());
row.createCell(4).setCellValue(dto.getSn());
}
//修改列宽
skuListSheet.setColumnWidth(0, 30 * 256);
skuListSheet.setColumnWidth(1, 30 * 256);
skuListSheet.setColumnWidth(2, 30 * 256);
skuListSheet.setColumnWidth(3, 30 * 256);
skuListSheet.setColumnWidth(4, 30 * 256);
}
}

View File

@ -1,6 +1,7 @@
package cn.lili.controller.goods;
import cn.lili.common.aop.annotation.DemoSite;
import cn.lili.common.context.ThreadContextHolder;
import cn.lili.common.enums.ResultCode;
import cn.lili.common.enums.ResultUtil;
import cn.lili.common.exception.RetryException;
@ -34,8 +35,11 @@ import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.index.reindex.BulkByScrollResponse;
import org.elasticsearch.index.reindex.DeleteByQueryRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.io.IOException;
@ -66,14 +70,7 @@ public class GoodsStoreController {
*/
@Autowired
private GoodsSkuService goodsSkuService;
/**
* 店铺详情
*/
@Autowired
private StoreDetailService storeDetailService;
@Autowired
private EsGoodsIndexService esGoodsIndexService;
@ApiOperation(value = "分页获取商品列表")
@GetMapping(value = "/list")
@ -231,4 +228,27 @@ public class GoodsStoreController {
}
@ApiOperation(value = "分页获取商品Sku列表")
@GetMapping(value = "/queryExportStock")
public void queryExportStock(GoodsSearchParams goodsSearchParams) {
//获取当前登录商家账号
String storeId = Objects.requireNonNull(UserContext.getCurrentUser()).getStoreId();
goodsSearchParams.setStoreId(storeId);
HttpServletResponse response = ThreadContextHolder.getHttpResponse();
goodsSkuService.queryExportStock(response,goodsSearchParams);
}
@ApiOperation(value = "上传商品库存列表")
@PostMapping(value = "/importStockExcel", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResultMessage<Object> importStockExcel(@RequestPart("files") MultipartFile files) {
//获取当前登录商家账号
String storeId = Objects.requireNonNull(UserContext.getCurrentUser()).getStoreId();
goodsSkuService.importStock(storeId,files);
return ResultUtil.success(ResultCode.SUCCESS);
}
}