新增:批量导入商品
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
package com.gxwebsoft.shop.controller;
|
||||
|
||||
import cn.afterturn.easypoi.excel.ExcelExportUtil;
|
||||
import cn.afterturn.easypoi.excel.ExcelImportUtil;
|
||||
import cn.afterturn.easypoi.excel.entity.ExportParams;
|
||||
import cn.afterturn.easypoi.excel.entity.ImportParams;
|
||||
import cn.binarywang.wx.miniapp.api.WxMaQrcodeService;
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.binarywang.wx.miniapp.api.impl.WxMaQrcodeServiceImpl;
|
||||
@@ -18,6 +22,7 @@ import com.gxwebsoft.common.system.entity.DictData;
|
||||
import com.gxwebsoft.shop.entity.*;
|
||||
import com.gxwebsoft.shop.service.*;
|
||||
import com.gxwebsoft.shop.param.GoodsParam;
|
||||
import com.gxwebsoft.shop.param.GoodsImportParam;
|
||||
import com.gxwebsoft.common.core.web.ApiResult;
|
||||
import com.gxwebsoft.common.core.web.PageResult;
|
||||
import com.gxwebsoft.common.core.web.PageParam;
|
||||
@@ -26,20 +31,32 @@ import com.gxwebsoft.common.core.annotation.OperationLog;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
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.WorkbookFactory;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 商品记录表控制器
|
||||
@@ -240,6 +257,195 @@ public class GoodsController extends BaseController {
|
||||
return fail("删除失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量导入商品(对应 goods-import.xlsx 第一页)
|
||||
* 表头:序号、作用分类(除消费者外可见)、品种、品牌、厂家、销量、规格、产品图片、成本(来华+运费)、商城售价(无标注按厂家)、产品分类(所有可见)、公司
|
||||
*/
|
||||
@ApiOperation("批量导入商品")
|
||||
@PostMapping("/import")
|
||||
public ApiResult<List<String>> importBatch(@RequestParam("file") MultipartFile file) {
|
||||
List<String> errorMessages = new ArrayList<>();
|
||||
int insertCount = 0;
|
||||
int updateCount = 0;
|
||||
|
||||
try {
|
||||
List<GoodsImportParam> list = null;
|
||||
int usedTitleRows = 0;
|
||||
int usedHeadRows = 0;
|
||||
int[][] tryConfigs = new int[][]{{1, 1}, {0, 1}, {0, 2}, {0, 3}};
|
||||
|
||||
for (int[] config : tryConfigs) {
|
||||
list = filterEmptyRows(tryImport(file, config[0], config[1]));
|
||||
if (list != null && !list.isEmpty()) {
|
||||
usedTitleRows = config[0];
|
||||
usedHeadRows = config[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (list == null || list.isEmpty()) {
|
||||
return fail("未读取到数据,请确认模板表头与示例格式一致", null);
|
||||
}
|
||||
|
||||
User loginUser = getLoginUser();
|
||||
Integer currentUserId = loginUser != null ? loginUser.getUserId() : null;
|
||||
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
|
||||
Integer defaultMerchantId = loginUser != null ? loginUser.getMerchantId() : null;
|
||||
|
||||
Map<String, Merchant> merchantCache = new HashMap<>();
|
||||
Merchant defaultMerchant = defaultMerchantId != null ? merchantService.getById(defaultMerchantId) : null;
|
||||
Map<Integer, String> imageLinkMap = readHyperlinksByHeader(file, 0, usedTitleRows, usedHeadRows, "产品图片");
|
||||
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
GoodsImportParam param = list.get(i);
|
||||
int excelRowNumber = i + 1 + usedTitleRows + usedHeadRows;
|
||||
try {
|
||||
Goods item = convertImportParamToEntity(param);
|
||||
String imageLink = imageLinkMap.get(i);
|
||||
if (isBlank(item.getImage()) && !isBlank(imageLink)) {
|
||||
item.setImage(imageLink);
|
||||
}
|
||||
// 兼容数据库 shop_goods.files 非空约束:默认使用image或空串兜底
|
||||
if (isBlank(item.getFiles())) {
|
||||
item.setFiles(!isBlank(item.getImage()) ? item.getImage() : "");
|
||||
}
|
||||
|
||||
if (isBlank(item.getGoodsName())) {
|
||||
errorMessages.add("第" + excelRowNumber + "行:品种(商品名称)不能为空");
|
||||
continue;
|
||||
}
|
||||
|
||||
Integer merchantId = defaultMerchantId != null ? defaultMerchantId : 0;
|
||||
Merchant merchant = defaultMerchant;
|
||||
if (merchantId == 0 && !isBlank(param.getCompany())) {
|
||||
merchant = resolveMerchantByName(param.getCompany(), merchantCache);
|
||||
if (merchant != null && merchant.getMerchantId() != null) {
|
||||
merchantId = merchant.getMerchantId();
|
||||
}
|
||||
}
|
||||
item.setMerchantId(merchantId);
|
||||
if (isBlank(item.getMerchantName())) {
|
||||
if (merchant != null && !isBlank(merchant.getMerchantName())) {
|
||||
item.setMerchantName(merchant.getMerchantName());
|
||||
} else if (!isBlank(param.getCompany())) {
|
||||
item.setMerchantName(param.getCompany());
|
||||
}
|
||||
}
|
||||
|
||||
if (item.getUserId() == null && currentUserId != null) {
|
||||
item.setUserId(currentUserId);
|
||||
}
|
||||
if (item.getTenantId() == null) {
|
||||
if (currentTenantId != null) {
|
||||
item.setTenantId(currentTenantId);
|
||||
} else if (merchant != null && merchant.getTenantId() != null) {
|
||||
item.setTenantId(merchant.getTenantId());
|
||||
}
|
||||
}
|
||||
if (item.getType() == null) {
|
||||
item.setType(0);
|
||||
}
|
||||
if (item.getSpecs() == null) {
|
||||
item.setSpecs(0);
|
||||
}
|
||||
if (item.getDeductStockType() == null) {
|
||||
item.setDeductStockType(10);
|
||||
}
|
||||
if (item.getIsShow() == null) {
|
||||
item.setIsShow(0);
|
||||
}
|
||||
if (item.getStatus() == null) {
|
||||
item.setStatus(0);
|
||||
}
|
||||
if (item.getRecommend() == null) {
|
||||
item.setRecommend(0);
|
||||
}
|
||||
if (item.getDeleted() == null) {
|
||||
item.setDeleted(0);
|
||||
}
|
||||
if (item.getStock() == null) {
|
||||
item.setStock(0);
|
||||
}
|
||||
if (item.getSales() == null) {
|
||||
item.setSales(0);
|
||||
}
|
||||
|
||||
// upsert:同商户下按商品名称匹配
|
||||
Goods existing = goodsService.getOne(
|
||||
new LambdaQueryWrapper<Goods>()
|
||||
.eq(Goods::getMerchantId, merchantId)
|
||||
.eq(Goods::getGoodsName, item.getGoodsName())
|
||||
.last("limit 1")
|
||||
);
|
||||
|
||||
if (existing == null) {
|
||||
if (goodsService.save(item)) {
|
||||
insertCount++;
|
||||
} else {
|
||||
errorMessages.add("第" + excelRowNumber + "行:保存失败");
|
||||
}
|
||||
} else {
|
||||
mergeImportedToExisting(existing, item);
|
||||
if (goodsService.updateById(existing)) {
|
||||
updateCount++;
|
||||
} else {
|
||||
errorMessages.add("第" + excelRowNumber + "行:更新失败");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
errorMessages.add("第" + excelRowNumber + "行:" + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
String message = "导入完成,新增" + insertCount + "条";
|
||||
if (updateCount > 0) {
|
||||
message += ",更新" + updateCount + "条";
|
||||
}
|
||||
if (errorMessages.isEmpty()) {
|
||||
return success(message, null);
|
||||
}
|
||||
message += ",失败" + errorMessages.size() + "条";
|
||||
return success(message, errorMessages);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return fail("导入失败:" + e.getMessage(), null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载商品导入模板(仅生成第一张sheet的表头及示例数据)
|
||||
*/
|
||||
@ApiOperation("下载商品导入模板")
|
||||
@GetMapping("/import/template")
|
||||
public void downloadImportTemplate(HttpServletResponse response) throws IOException {
|
||||
List<GoodsImportParam> templateList = new ArrayList<>();
|
||||
|
||||
GoodsImportParam example = new GoodsImportParam();
|
||||
example.setCode("1");
|
||||
example.setEnsureTag("示例分类");
|
||||
example.setGoodsName("示例商品");
|
||||
example.setBrand("示例品牌");
|
||||
example.setSupplierName("示例厂家");
|
||||
example.setSales("2000+");
|
||||
example.setUnitName("40袋/盒");
|
||||
example.setImage("");
|
||||
example.setCostPrice("8.2");
|
||||
example.setSalePrice("44.97");
|
||||
example.setCategory("示例分类");
|
||||
example.setCompany("示例公司");
|
||||
templateList.add(example);
|
||||
|
||||
ExportParams exportParams = new ExportParams("商品导入模板", "商品");
|
||||
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, GoodsImportParam.class, templateList);
|
||||
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
String filename = java.net.URLEncoder.encode("goods_import_template.xlsx", StandardCharsets.UTF_8);
|
||||
response.setHeader("Content-Disposition", "attachment; filename=" + filename);
|
||||
|
||||
workbook.write(response.getOutputStream());
|
||||
workbook.close();
|
||||
}
|
||||
|
||||
@ApiOperation("统计信息")
|
||||
@GetMapping("/data")
|
||||
public ApiResult<Map<String, Integer>> data(GoodsParam param) {
|
||||
@@ -418,4 +624,239 @@ public class GoodsController extends BaseController {
|
||||
return success("生成成功", serverUrl.concat("/" + filePath));
|
||||
}
|
||||
|
||||
private List<GoodsImportParam> tryImport(MultipartFile file, int titleRows, int headRows) throws Exception {
|
||||
ImportParams importParams = new ImportParams();
|
||||
importParams.setTitleRows(titleRows);
|
||||
importParams.setHeadRows(headRows);
|
||||
importParams.setStartSheetIndex(0);
|
||||
importParams.setSheetNum(1);
|
||||
return ExcelImportUtil.importExcel(file.getInputStream(), GoodsImportParam.class, importParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取指定表头列的超链接地址,按数据行顺序返回(key从0开始)。
|
||||
*/
|
||||
private Map<Integer, String> readHyperlinksByHeader(MultipartFile file, int sheetIndex, int titleRows, int headRows, String headerName) throws Exception {
|
||||
Map<Integer, String> result = new HashMap<>();
|
||||
if (isBlank(headerName)) {
|
||||
return result;
|
||||
}
|
||||
try (InputStream is = file.getInputStream(); org.apache.poi.ss.usermodel.Workbook workbook = WorkbookFactory.create(is)) {
|
||||
Sheet sheet = workbook.getSheetAt(sheetIndex);
|
||||
if (sheet == null) {
|
||||
return result;
|
||||
}
|
||||
int headerRowNum = titleRows + headRows - 1;
|
||||
Row headerRow = sheet.getRow(headerRowNum);
|
||||
if (headerRow == null) {
|
||||
return result;
|
||||
}
|
||||
int colIndex = -1;
|
||||
for (int c = headerRow.getFirstCellNum(); c < headerRow.getLastCellNum(); c++) {
|
||||
Cell cell = headerRow.getCell(c);
|
||||
if (cell != null && headerName.equals(cell.getStringCellValue())) {
|
||||
colIndex = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (colIndex < 0) {
|
||||
return result;
|
||||
}
|
||||
int dataStartRow = titleRows + headRows;
|
||||
for (int r = dataStartRow; r <= sheet.getLastRowNum(); r++) {
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
Cell cell = row.getCell(colIndex);
|
||||
if (cell != null && cell.getHyperlink() != null) {
|
||||
String address = cell.getHyperlink().getAddress();
|
||||
if (!isBlank(address)) {
|
||||
result.put(r - dataStartRow, address);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤掉完全空白的导入行,避免空行导致导入失败
|
||||
*/
|
||||
private List<GoodsImportParam> filterEmptyRows(List<GoodsImportParam> rawList) {
|
||||
if (rawList == null || rawList.isEmpty()) {
|
||||
return rawList;
|
||||
}
|
||||
rawList.removeIf(this::isEmptyImportRow);
|
||||
return rawList;
|
||||
}
|
||||
|
||||
private boolean isEmptyImportRow(GoodsImportParam param) {
|
||||
if (param == null) {
|
||||
return true;
|
||||
}
|
||||
return isBlank(param.getGoodsName())
|
||||
&& isBlank(param.getCode())
|
||||
&& isBlank(param.getCategory())
|
||||
&& isBlank(param.getCompany())
|
||||
&& isBlank(param.getSupplierName())
|
||||
&& isBlank(param.getSalePrice())
|
||||
&& isBlank(param.getCostPrice());
|
||||
}
|
||||
|
||||
private boolean isBlank(String value) {
|
||||
return value == null || value.trim().isEmpty();
|
||||
}
|
||||
|
||||
private Merchant resolveMerchantByName(String merchantName, Map<String, Merchant> cache) {
|
||||
if (isBlank(merchantName)) {
|
||||
return null;
|
||||
}
|
||||
if (cache.containsKey(merchantName)) {
|
||||
return cache.get(merchantName);
|
||||
}
|
||||
Merchant merchant = merchantService.getOne(
|
||||
new LambdaQueryWrapper<Merchant>()
|
||||
.eq(Merchant::getMerchantName, merchantName)
|
||||
.last("limit 1")
|
||||
);
|
||||
cache.put(merchantName, merchant);
|
||||
return merchant;
|
||||
}
|
||||
|
||||
private Goods convertImportParamToEntity(GoodsImportParam param) {
|
||||
Goods entity = new Goods();
|
||||
if (param == null) {
|
||||
return entity;
|
||||
}
|
||||
entity.setCode(trimToNull(param.getCode()));
|
||||
entity.setGoodsName(trimToNull(param.getGoodsName()));
|
||||
entity.setEnsureTag(trimToNull(param.getEnsureTag()));
|
||||
entity.setCategory(trimToNull(param.getCategory()));
|
||||
entity.setUnitName(trimToNull(param.getUnitName()));
|
||||
entity.setImage(trimToNull(param.getImage()));
|
||||
// 兼容数据库 shop_goods.files 非空约束:默认使用image或空串兜底
|
||||
entity.setFiles(!isBlank(entity.getImage()) ? entity.getImage() : "");
|
||||
entity.setSupplierName(trimToNull(param.getSupplierName()));
|
||||
entity.setMerchantName(trimToNull(param.getCompany()));
|
||||
|
||||
Integer sales = parseSales(param.getSales());
|
||||
if (sales != null) {
|
||||
entity.setSales(sales);
|
||||
}
|
||||
|
||||
BigDecimal cost = parseBigDecimal(param.getCostPrice());
|
||||
BigDecimal sale = parseBigDecimal(param.getSalePrice());
|
||||
if (cost != null) {
|
||||
entity.setPrice(cost);
|
||||
}
|
||||
if (sale != null) {
|
||||
entity.setSalePrice(sale);
|
||||
entity.setOriginPrice(sale);
|
||||
} else if (cost != null) {
|
||||
entity.setSalePrice(cost);
|
||||
entity.setOriginPrice(cost);
|
||||
}
|
||||
|
||||
String comments = buildComments(param);
|
||||
if (!isBlank(comments)) {
|
||||
entity.setComments(comments);
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
private void mergeImportedToExisting(Goods existing, Goods incoming) {
|
||||
if (existing == null || incoming == null) {
|
||||
return;
|
||||
}
|
||||
if (!isBlank(incoming.getCode())) {
|
||||
existing.setCode(incoming.getCode());
|
||||
}
|
||||
if (!isBlank(incoming.getEnsureTag())) {
|
||||
existing.setEnsureTag(incoming.getEnsureTag());
|
||||
}
|
||||
if (!isBlank(incoming.getCategory())) {
|
||||
existing.setCategory(incoming.getCategory());
|
||||
}
|
||||
if (!isBlank(incoming.getUnitName())) {
|
||||
existing.setUnitName(incoming.getUnitName());
|
||||
}
|
||||
if (!isBlank(incoming.getImage())) {
|
||||
existing.setImage(incoming.getImage());
|
||||
}
|
||||
if (!isBlank(incoming.getFiles())) {
|
||||
existing.setFiles(incoming.getFiles());
|
||||
}
|
||||
if (!isBlank(incoming.getSupplierName())) {
|
||||
existing.setSupplierName(incoming.getSupplierName());
|
||||
}
|
||||
if (!isBlank(incoming.getMerchantName())) {
|
||||
existing.setMerchantName(incoming.getMerchantName());
|
||||
}
|
||||
if (!isBlank(incoming.getComments())) {
|
||||
existing.setComments(incoming.getComments());
|
||||
}
|
||||
if (incoming.getSales() != null) {
|
||||
existing.setSales(incoming.getSales());
|
||||
}
|
||||
if (incoming.getPrice() != null) {
|
||||
existing.setPrice(incoming.getPrice());
|
||||
}
|
||||
if (incoming.getSalePrice() != null) {
|
||||
existing.setSalePrice(incoming.getSalePrice());
|
||||
}
|
||||
if (incoming.getOriginPrice() != null) {
|
||||
existing.setOriginPrice(incoming.getOriginPrice());
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal parseBigDecimal(String value) {
|
||||
if (isBlank(value)) {
|
||||
return null;
|
||||
}
|
||||
String v = value.trim();
|
||||
// 去掉可能存在的中文/符号
|
||||
v = v.replace(",", ",");
|
||||
v = v.replaceAll("[^0-9+\\-.,]", "");
|
||||
v = v.replace(",", "");
|
||||
if (isBlank(v)) {
|
||||
return null;
|
||||
}
|
||||
return new BigDecimal(v);
|
||||
}
|
||||
|
||||
private Integer parseSales(String value) {
|
||||
if (isBlank(value)) {
|
||||
return null;
|
||||
}
|
||||
String digits = value.replaceAll("[^0-9]", "");
|
||||
if (isBlank(digits)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(digits);
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String v = value.trim();
|
||||
return v.isEmpty() ? null : v;
|
||||
}
|
||||
|
||||
private String buildComments(GoodsImportParam param) {
|
||||
if (param == null) {
|
||||
return null;
|
||||
}
|
||||
String brand = trimToNull(param.getBrand());
|
||||
if (brand == null) {
|
||||
return null;
|
||||
}
|
||||
return "品牌:" + brand;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
57
src/main/java/com/gxwebsoft/shop/param/GoodsImportParam.java
Normal file
57
src/main/java/com/gxwebsoft/shop/param/GoodsImportParam.java
Normal file
@@ -0,0 +1,57 @@
|
||||
package com.gxwebsoft.shop.param;
|
||||
|
||||
import cn.afterturn.easypoi.excel.annotation.Excel;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 商品导入参数(对应 goods-import.xlsx 第一页)
|
||||
*
|
||||
* 表头:序号、作用分类(除消费者外可见)、品种、品牌、厂家、销量、规格、产品图片、成本(来华+运费)、商城售价(无标注按厂家)、产品分类(所有可见)、公司
|
||||
*
|
||||
* 说明:产品图片列支持直接填写图片URL,或在单元格中设置超链接(导入时读取超链接地址)。
|
||||
*
|
||||
* @author 科技小王子
|
||||
* @since 2025-12-30
|
||||
*/
|
||||
@Data
|
||||
public class GoodsImportParam implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Excel(name = "序号")
|
||||
private String code;
|
||||
|
||||
@Excel(name = "作用分类(除消费者外可见)")
|
||||
private String ensureTag;
|
||||
|
||||
@Excel(name = "品种")
|
||||
private String goodsName;
|
||||
|
||||
@Excel(name = "品牌")
|
||||
private String brand;
|
||||
|
||||
@Excel(name = "厂家")
|
||||
private String supplierName;
|
||||
|
||||
@Excel(name = "销量")
|
||||
private String sales;
|
||||
|
||||
@Excel(name = "规格")
|
||||
private String unitName;
|
||||
|
||||
@Excel(name = "产品图片")
|
||||
private String image;
|
||||
|
||||
@Excel(name = "成本(来华+运费)")
|
||||
private String costPrice;
|
||||
|
||||
@Excel(name = "商城售价(无标注按厂家)")
|
||||
private String salePrice;
|
||||
|
||||
@Excel(name = "产品分类(所有可见)")
|
||||
private String category;
|
||||
|
||||
@Excel(name = "公司")
|
||||
private String company;
|
||||
}
|
||||
Reference in New Issue
Block a user