商品,品牌,商品分类excle导入新功能,未测试

This commit is contained in:
liyj 2025-08-05 10:43:57 +08:00
parent c31b094cce
commit 8b65072fea
9 changed files with 604 additions and 1 deletions

View File

@ -0,0 +1,92 @@
package com.suisung.mall.shop.sync.controller;
import com.suisung.mall.common.api.CommonResult;
import com.suisung.mall.common.service.impl.BaseControllerImpl;
import com.suisung.mall.shop.sync.service.ShopSyncImportService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
/**
* 商品数据导入
* 包括分类和商品的 导入导入之后自动生成分类的其他属性由于系统的局限性品牌不做导入默认生成其他匹配
*/
@RestController
@RequestMapping("/admin/shop/shop-sync-import")
@lombok.extern.slf4j.Slf4j
public class ShopSyncImportController extends BaseControllerImpl {
@Autowired
private ShopSyncImportService shopSyncImportService;
/**
* 品牌导入模板下载
* @return
*/
@ApiOperation(value = "品牌导入模板下载", notes = "模板下载")
@RequestMapping(value = "/brandTemplate", method = RequestMethod.GET)
public void brandTemplate(HttpServletResponse response) {
shopSyncImportService.downloadBrandTemplate(response);
}
/**
* 商品分类导入模板下载
* @return
*/
@ApiOperation(value = "商品分类导入模板下载", notes = "模板下载")
@RequestMapping(value = "/categoryTemplate", method = RequestMethod.GET)
public void categoryTemplate(HttpServletResponse response) {
shopSyncImportService.downloadCategoryTemplate(response);
}
/**
* 商品导入模板下载
* @return
*/
@ApiOperation(value = "商品导入模板下载", notes = "模板下载")
@RequestMapping(value = "/shopTemplate", method = RequestMethod.GET)
public void shopTemplate(HttpServletResponse response) {
shopSyncImportService.downloadShopsTemplate(response);
}
/**
* 品牌数据导入
* @return
*/
@ApiOperation(value = "品牌数据导入", notes = "品牌数据导入")
@RequestMapping(value = "/brandImportData", method = RequestMethod.POST)
public CommonResult brandImportData(@RequestParam("file") MultipartFile file,@RequestParam("storeId")String storeId) {
// ImportResult result = productMappingService.importData(file);
// return !result.getErrorMessages().isEmpty() ?CommonResult.failed((IErrorCode) result.getErrorMessages()):CommonResult.success(result);
return shopSyncImportService.importBrandData(file,storeId);
}
/**
* 商品分类导入数据
* @return
*/
@ApiOperation(value = "商品分类数据导入", notes = "分类数据导入")
@RequestMapping(value = "/categoryImportData", method = RequestMethod.POST)
public CommonResult categoryImportData(@RequestParam("file") MultipartFile file,@RequestParam("storeId")String storeId) {
// ImportResult result = productMappingService.importData(file);
// return !result.getErrorMessages().isEmpty() ?CommonResult.failed((IErrorCode) result.getErrorMessages()):CommonResult.success(result);
return shopSyncImportService.importCategoryData(file,storeId);
}
/**
* 商品导入数据
* @return
*/
@ApiOperation(value = "商品数据导入", notes = "分类数据导入")
@RequestMapping(value = "/shopImportData", method = RequestMethod.POST)
public CommonResult shopImportData(@RequestParam("file") MultipartFile file,@RequestParam("storeId")String storeId) {
// ImportResult result = productMappingService.importData(file);
// return !result.getErrorMessages().isEmpty() ?CommonResult.failed((IErrorCode) result.getErrorMessages()):CommonResult.success(result);
shopSyncImportService.importShopsData(file,storeId);
return CommonResult.success("服务器正则处理文件,稍后查看商品列表");
}
}

View File

@ -28,5 +28,6 @@ public class SxCategoryModel {
private String first_category_name; private String first_category_name;
@ApiModelProperty(value = "第二级父类") @ApiModelProperty(value = "第二级父类")
private String second_category_name; private String second_category_name;
@ApiModelProperty(value = "品牌名称")
private String brandName;
} }

View File

@ -0,0 +1,23 @@
package com.suisung.mall.shop.sync.exelModel;
import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
public class BrandModelExcel{
public static final String TEMPLATE_NAME = "品牌导入模板.xlsx";
@ApiModelProperty("品牌名称")
@ExcelProperty(value = "商品名称", index = 0)
private String brand_name;
@ApiModelProperty("品牌描述")
@ExcelProperty(value = "品牌描述", index = 1)
private String brand_desc;
@ApiModelProperty("是否推荐")
@ExcelIgnore
private String brand_recommend="0";
}

View File

@ -0,0 +1,44 @@
package com.suisung.mall.shop.sync.exelModel;
import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 模型对应
*/
@Data
public class SxCategoryModelExcel {
public static final String TEMPLATE_NAME = "商品分类导入模板.xlsx";
@ApiModelProperty(value = "商品分类名称")
@ExcelProperty(value = "商品分类名称", index = 0)
private String category_name;
@ApiModelProperty(value = "分类图片")
@ExcelIgnore
private String category_image="https://digitalassets.tesla.com/tesla-contents/image/upload/f_auto,q_auto/Homepage-Model-Y-2-Promo-Hero-Tablet-CN.png";
@ApiModelProperty(value = "是否允许虚拟商品(ENUM):1-是; 0-否")
@ExcelIgnore
private Integer category_virtual_enable=0;
/**
* 产品类型=商品分类名称
*/
@ApiModelProperty(value = "产品类型")
@ExcelIgnore
private String product_type=category_name;
/**
* 第一级分类 当前分类的最顶层 生鲜->蔬菜->菜苗 如果当前分类category_name为菜苗则第一级分类是生鲜第二级分类是蔬菜如果当前分类是蔬菜则第一级分类是生鲜第二级分类为空
* 第二级分类 当前分类最顶层数的第二层
*/
@ApiModelProperty(value = "第一级分类")
@ExcelProperty(value = "第一级分类", index = 1)
private String first_category_name;
@ApiModelProperty(value = "第二级分类")
@ExcelProperty(value = "第二级分类", index =2)
private String second_category_name;
@ApiModelProperty(value = "品牌名称")
@ExcelIgnore
private String brandName="其他品牌";
}

View File

@ -0,0 +1,52 @@
package com.suisung.mall.shop.sync.exelModel;
import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
/**
* 思迅同步商品数据入口数据
*/
@Data
public class SxGoosModelExcel {
public static final String TEMPLATE_NAME = "商品导入模板.xlsx";
@ApiModelProperty("商品名称")
@ExcelProperty(value = "商品名称", index = 0)
private String product_name;
@ApiModelProperty("商品货号")
@ExcelProperty(value = "商品货号", index = 1)
private String product_number;
@ApiModelProperty("商品条形码")
@ExcelProperty(value = "商品条形码", index = 2)
private String product_barcode;
@ApiModelProperty("所属分类")
@ExcelProperty(value = "所属分类", index = 3)
private String first_category_name;
@ApiModelProperty("零售价")
@ExcelProperty(value = "零售价", index = 4)
private BigDecimal retail_price;
@ApiModelProperty("原价")
@ExcelIgnore
private BigDecimal original_price=retail_price;
@ApiModelProperty("库存")
@ExcelProperty(value = "库存", index = 5)
private BigDecimal stock;
@ApiModelProperty("规格单位")
@ExcelProperty(value = "规格单位", index = 6)
private String unit;
@ApiModelProperty("最大购买商品量")
@ExcelProperty(value = "最大购买商品量", index = 7)
private Integer buy_limit;
}

View File

@ -0,0 +1,118 @@
package com.suisung.mall.shop.sync.listen;
import cn.hutool.json.JSONArray;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.google.gson.Gson;
import com.suisung.mall.shop.sync.exelModel.SxGoosModelExcel;
import com.suisung.mall.shop.sync.service.SyncThirdDataService;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
@Component
@Slf4j
public class ShopBatchSubmitListener extends AnalysisEventListener<SxGoosModelExcel> {
// 批处理阈值
private static final int BATCH_SIZE = 10;
// 数据缓存
private List<SxGoosModelExcel> cachedDataList = new ArrayList<>(BATCH_SIZE);
private SyncThirdDataService syncThirdDataService;
// 线程池配置
private final ExecutorService executorService;
private List<Future<?>> futures ;
private AtomicInteger success;
private AtomicInteger fails;
private AtomicInteger batchSize;
@Setter
@Getter
private String storeId;
@Setter
@Getter
private String isNegativeAllowed;
@Setter
@Getter
private Map<String,Integer> brandMaps;
public ShopBatchSubmitListener(SyncThirdDataService syncThirdDataService) {
this.syncThirdDataService = syncThirdDataService;
// 创建线程池根据CPU核心数优化
int corePoolSize = Runtime.getRuntime().availableProcessors();
this.executorService = Executors.newFixedThreadPool(corePoolSize);
this.futures = new ArrayList<>();
this.success = new AtomicInteger();
this.fails = new AtomicInteger();
this.batchSize= new AtomicInteger();
}
@Override
public void invoke(SxGoosModelExcel sxGoosModelExcel, AnalysisContext analysisContext) {
synchronized (cachedDataList) {
cachedDataList.add(sxGoosModelExcel);
// 达到批处理阈值时提交
if (cachedDataList.size() >= BATCH_SIZE) {
batchSize.incrementAndGet();
submitBatch();
// 提交后清空缓存
cachedDataList.clear();
}
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
synchronized (cachedDataList) {
// 处理最后一批不足BATCH_SIZE的数据
if (!cachedDataList.isEmpty()) {
batchSize.incrementAndGet();
submitBatch();
cachedDataList.clear();
}
}
// 等待所有任务完成
for (Future<?> future : futures) {
try {
log.info("任务结果:{}" ,future.get());
} catch (Exception e) {
log.info("任务执行异常: {}", e.getMessage());
}
}
log.info("Excel解析完成总处理条数: {}" , context.readSheetHolder().getTotal());
log.info("成功数量:{};失败数量:{}",success.get(),fails.get());
// 关闭线程池
executorService.shutdown();
}
private void submitBatch() {
// 复制当前批次数据避免异步修改
List<SxGoosModelExcel> batchCopy = new ArrayList<>(cachedDataList);
futures.add(executorService.submit(()->{
try {
Gson gson=new Gson();
String jsonShops=gson.toJson(batchCopy);
JSONArray jsonArray=new JSONArray(jsonShops);
syncThirdDataService.baseSaveOrUpdateGoodsBatch(jsonArray,storeId,isNegativeAllowed,brandMaps);
log.info("已提交批次: {} 条", cachedDataList.size());
success.getAndIncrement();
return "完成"+batchSize.get();
} catch (Exception e) {
fails.getAndIncrement();
return "失败:"+batchSize.get()+";失败原因:"+e.getMessage();
}
}));
}
}

View File

@ -0,0 +1,30 @@
package com.suisung.mall.shop.sync.service;
import com.suisung.mall.common.api.CommonResult;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
public interface ShopSyncImportService{
// 下载品牌导入模板
void downloadBrandTemplate(HttpServletResponse response);
// 下载商品分类导入模板
void downloadCategoryTemplate(HttpServletResponse response);
// 下载商品导入模板
void downloadShopsTemplate(HttpServletResponse response);
// 导入品牌Excel数据
CommonResult importBrandData(MultipartFile file,String storeId);
// 导入商品分类Excel数据
CommonResult importCategoryData(MultipartFile file,String storeId);
// 导入商品Excel数据
void importShopsData(MultipartFile file,String storeId);
}

View File

@ -10,6 +10,8 @@ package com.suisung.mall.shop.sync.service;
import cn.hutool.json.JSONArray; import cn.hutool.json.JSONArray;
import com.suisung.mall.common.api.CommonResult; import com.suisung.mall.common.api.CommonResult;
import com.suisung.mall.common.modules.base.ShopBaseProductBrand;
import com.suisung.mall.common.modules.base.ShopBaseProductCategory;
import com.suisung.mall.common.pojo.req.SyncThirdMemberReq; import com.suisung.mall.common.pojo.req.SyncThirdMemberReq;
import com.suisung.mall.common.pojo.res.ThirdApiRes; import com.suisung.mall.common.pojo.res.ThirdApiRes;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
@ -169,4 +171,30 @@ public interface SyncThirdDataService {
ThirdApiRes syncRefreshTime(@RequestParam String appKey, @RequestParam String sign); ThirdApiRes syncRefreshTime(@RequestParam String appKey, @RequestParam String sign);
CommonResult importLibProductImg(String updateTime); CommonResult importLibProductImg(String updateTime);
/**
* 保存品牌接口
* 内部使用
* @return
*/
int baseSaveOrUpdateShopBaseProductBrandBatch(List<ShopBaseProductBrand> goodBrandList,String storeId,JSONArray brandListJSON);
/**
* 保存商品分类接口
* 内部使用
* @return
*/
int baseSaveOrUpdateShopBaseProductCategoryBatch(List<ShopBaseProductCategory> list , JSONArray categoryListJSON,
String storeId);
/**
* 保存商品接口
* 内部使用
* @return
*/
int baseSaveOrUpdateGoodsBatch(JSONArray goodsListJSON,String storeId,String isNegativeAllowed,
Map<String,Integer> brandMaps);
} }

View File

@ -0,0 +1,215 @@
package com.suisung.mall.shop.sync.service.impl;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.google.gson.Gson;
import com.suisung.mall.common.api.CommonResult;
import com.suisung.mall.common.exception.ApiException;
import com.suisung.mall.common.modules.base.ShopBaseProductBrand;
import com.suisung.mall.common.modules.base.ShopBaseProductCategory;
import com.suisung.mall.common.modules.sync.StoreDbConfig;
import com.suisung.mall.shop.base.service.ShopBaseProductBrandService;
import com.suisung.mall.shop.sync.excleHandle.TemplateStyleHandler;
import com.suisung.mall.shop.sync.exelModel.*;
import com.suisung.mall.shop.sync.listen.ShopBatchSubmitListener;
import com.suisung.mall.shop.sync.service.ShopSyncImportService;
import com.suisung.mall.shop.sync.service.StoreDbConfigService;
import com.suisung.mall.shop.sync.service.SyncThirdDataService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Service
@Slf4j
public class ShopSyncImportServiceImpl implements ShopSyncImportService {
@Value("${file.upload-dir}")
private String uploadDir;
@Autowired
private SyncThirdDataService syncThirdDataService;
@Autowired
private ShopBaseProductBrandService productBrandService;
@Autowired
private StoreDbConfigService storeDbConfigService;
private final int limitCnt = 100;
@Override
public void downloadBrandTemplate(HttpServletResponse response) {
try {
// 设置响应头
setExcelResponseHeader(response, BrandModelExcel.TEMPLATE_NAME);
// 创建空模板
EasyExcel.write(response.getOutputStream(), BrandModelExcel.class)
.sheet("商品品牌")
.registerWriteHandler(new TemplateStyleHandler())
.doWrite(new ArrayList<>());
} catch (IOException e) {
log.error("商品品牌下载模板失败", e);
throw new RuntimeException("商品品牌下载模板失败");
}
}
@Override
public void downloadCategoryTemplate(HttpServletResponse response) {
try {
// 设置响应头
setExcelResponseHeader(response, SxCategoryModelExcel.TEMPLATE_NAME);
// 创建空模板
EasyExcel.write(response.getOutputStream(), SxCategoryModelExcel.class)
.sheet("商品分类")
.registerWriteHandler(new TemplateStyleHandler())
.doWrite(new ArrayList<>());
} catch (IOException e) {
log.error("商品分类下载模板失败", e);
throw new RuntimeException("商品分类下载模板失败");
}
}
@Override
public void downloadShopsTemplate(HttpServletResponse response) {
try {
// 设置响应头
setExcelResponseHeader(response, SxGoosModelExcel.TEMPLATE_NAME);
// 创建空模板
EasyExcel.write(response.getOutputStream(), SxGoosModelExcel.class)
.sheet("商品")
.registerWriteHandler(new TemplateStyleHandler())
.doWrite(new ArrayList<>());
} catch (IOException e) {
log.error("商品下载模板失败", e);
throw new RuntimeException("商品下载模板失败");
}
}
@Override
public CommonResult importBrandData(MultipartFile file,String storeId) {
String fileName = storeUploadedFile(file);
try {
List<BrandModelExcel> excelList = readBrandExcelData(fileName);
if(excelList.isEmpty()) {
return CommonResult.failed("品牌数据为空");
}
if(excelList.size()>limitCnt){
return CommonResult.failed("导入品牌数据超过"+limitCnt+"");
}
Gson gson=new Gson();
String brandJsonStr=gson.toJson(excelList);
JSONArray jsonArray=new JSONArray(brandJsonStr);
List<ShopBaseProductBrand> goodBrandList = JSONUtil.toList(jsonArray, ShopBaseProductBrand.class);
syncThirdDataService.baseSaveOrUpdateShopBaseProductBrandBatch(goodBrandList,storeId,jsonArray);
return CommonResult.success();
} catch (Exception e) {
log.error("导入品牌数据失败", e);
throw new RuntimeException("导入品牌数据失败: " + e.getMessage());
}
}
@Override
public CommonResult importCategoryData(MultipartFile file,String storeId) {
String fileName = storeUploadedFile(file);
try {
List<SxCategoryModelExcel> excelList = readCategoryExcelData(fileName);
if(excelList.isEmpty()) {
return CommonResult.failed("商品分类数据为空");
}
if(excelList.size()>limitCnt){
return CommonResult.failed("导入商品分类数据超过"+limitCnt+"");
}
Gson gson=new Gson();
String brandJsonStr=gson.toJson(excelList);
JSONArray jsonArray=new JSONArray(brandJsonStr);
List<ShopBaseProductCategory> goodBrandList = JSONUtil.toList(jsonArray, ShopBaseProductCategory.class);
syncThirdDataService.baseSaveOrUpdateShopBaseProductCategoryBatch(goodBrandList,jsonArray,storeId);
return CommonResult.success();
} catch (Exception e) {
log.error("导入数据失败", e);
throw new ApiException("导入数据失败: " + e.getMessage());
}
}
@Override
@Async
public void importShopsData(MultipartFile file,String storeId) {
String fileName = storeUploadedFile(file);
readAndImportShopsExcelData(fileName,storeId);
}
// 存储上传文件
private String storeUploadedFile(MultipartFile file) {
if (file.isEmpty()) {
throw new RuntimeException("上传文件为空");
}
try {
String fileName = System.currentTimeMillis() + "_" + file.getOriginalFilename();
String prexfix = fileName.substring(fileName.lastIndexOf(".")).toLowerCase();//后缀名称
if(!prexfix.contains("xlsx")||!prexfix.contains("xls")) {
throw new ApiException("必须为excel文件");
}
Path filePath = Paths.get(uploadDir, fileName);
Files.createDirectories(filePath.getParent());
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
return filePath.toString();
} catch (IOException e) {
log.error("存储上传文件失败", e);
throw new RuntimeException("存储上传文件失败");
}
}
// 设置Excel响应头
private void setExcelResponseHeader(HttpServletResponse response, String fileName) {
try {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String encodedFileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + encodedFileName);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("文件名编码失败");
}
}
// 读取品牌Excel数据
private List<BrandModelExcel> readBrandExcelData(String filePath) {
return EasyExcel.read(filePath)
.head(BrandModelExcel.class)
.sheet()
.doReadSync();
}
// 读取商品分类Excel数据
private List<SxCategoryModelExcel> readCategoryExcelData(String filePath) {
return EasyExcel.read(filePath)
.head(SxCategoryModelExcel.class)
.sheet()
.doReadSync();
}
// 读取商品Excel数据
private void readAndImportShopsExcelData(String filePath,String storeId) {
Map<String, Integer> brandMaps = productBrandService.getBrandMapByStoreId(storeId);
QueryWrapper<StoreDbConfig> storeDbConfigQueryWrapper = new QueryWrapper<>();
storeDbConfigQueryWrapper.eq("store_id", storeId);
StoreDbConfig storeDbConfig = storeDbConfigService.getOne(storeDbConfigQueryWrapper);
String isNegativeAllowed = storeDbConfig.getIsNegativeAllowed();
ShopBatchSubmitListener shopBatchSubmitListener=new ShopBatchSubmitListener(syncThirdDataService);
shopBatchSubmitListener.setStoreId(storeId);
shopBatchSubmitListener.setBrandMaps(brandMaps);
shopBatchSubmitListener.setIsNegativeAllowed(isNegativeAllowed);
EasyExcel.read(filePath,SxGoosModelExcel.class,shopBatchSubmitListener).sheet().doRead();
}
}