diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 0000000..0bd0248
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,199 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 0000000..2cc7d4a
Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..642d572
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,2 @@
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
diff --git a/src/main/java/com/gxwebsoft/shop/controller/GoodsController.java b/src/main/java/com/gxwebsoft/shop/controller/GoodsController.java
index ae46fcc..15d209b 100644
--- a/src/main/java/com/gxwebsoft/shop/controller/GoodsController.java
+++ b/src/main/java/com/gxwebsoft/shop/controller/GoodsController.java
@@ -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> importBatch(@RequestParam("file") MultipartFile file) {
+ List errorMessages = new ArrayList<>();
+ int insertCount = 0;
+ int updateCount = 0;
+
+ try {
+ List 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 merchantCache = new HashMap<>();
+ Merchant defaultMerchant = defaultMerchantId != null ? merchantService.getById(defaultMerchantId) : null;
+ Map 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()
+ .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 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