|
|
@@ -1,6 +1,7 @@
|
|
|
package shop.alien.storeplatform.service.impl;
|
|
|
|
|
|
import cn.hutool.core.collection.CollectionUtil;
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
import com.alibaba.nacos.common.utils.CollectionUtils;
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
|
@@ -8,23 +9,34 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|
|
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
|
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.apache.poi.ss.usermodel.*;
|
|
|
+import org.apache.poi.xssf.usermodel.*;
|
|
|
import org.springframework.beans.BeanUtils;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
|
import shop.alien.entity.result.R;
|
|
|
import shop.alien.entity.store.LifeGroupBuyThali;
|
|
|
import shop.alien.entity.store.LifeLikeRecord;
|
|
|
import shop.alien.entity.store.StoreImg;
|
|
|
import shop.alien.entity.store.StoreMenu;
|
|
|
+import shop.alien.entity.store.excelVo.util.ExcelImage;
|
|
|
+import shop.alien.entity.store.vo.StoreMenuImportVo;
|
|
|
import shop.alien.entity.store.vo.StoreMenuVo;
|
|
|
import shop.alien.mapper.LifeGroupBuyThaliMapper;
|
|
|
import shop.alien.mapper.LifeLikeRecordMapper;
|
|
|
import shop.alien.mapper.StoreImgMapper;
|
|
|
import shop.alien.mapper.StoreMenuMapper;
|
|
|
+import shop.alien.storeplatform.feign.AlienStoreFeign;
|
|
|
import shop.alien.storeplatform.service.StoreMenuPlatformService;
|
|
|
+import shop.alien.util.ali.AliOSSUtil;
|
|
|
|
|
|
-import java.util.Comparator;
|
|
|
-import java.util.List;
|
|
|
-import java.util.Map;
|
|
|
+import java.io.ByteArrayInputStream;
|
|
|
+import java.io.IOException;
|
|
|
+import java.io.InputStream;
|
|
|
+import java.lang.reflect.Field;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.util.*;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
/**
|
|
|
@@ -33,6 +45,7 @@ import java.util.stream.Collectors;
|
|
|
* @author ssk
|
|
|
* @since 2024-12-05
|
|
|
*/
|
|
|
+@Slf4j
|
|
|
@Service
|
|
|
@RequiredArgsConstructor
|
|
|
public class StoreMenuPlatformServiceImpl extends ServiceImpl<StoreMenuMapper, StoreMenu> implements StoreMenuPlatformService {
|
|
|
@@ -45,6 +58,10 @@ public class StoreMenuPlatformServiceImpl extends ServiceImpl<StoreMenuMapper, S
|
|
|
|
|
|
private final LifeGroupBuyThaliMapper lifeGroupBuyThaliMapper;
|
|
|
|
|
|
+ private final AliOSSUtil aliOSSUtil;
|
|
|
+
|
|
|
+ private final AlienStoreFeign alienStoreFeign;
|
|
|
+
|
|
|
/**
|
|
|
* 获取门店菜单
|
|
|
*
|
|
|
@@ -280,6 +297,402 @@ public class StoreMenuPlatformServiceImpl extends ServiceImpl<StoreMenuMapper, S
|
|
|
public boolean getMenuLikeStatus(String userId, Integer menuId) {
|
|
|
return lifeLikeRecordMapper.selectCount(new QueryWrapper<LifeLikeRecord>().eq("dianzan_id", userId).eq("huifu_id", menuId).eq("delete_flag", 0)) > 0;
|
|
|
}
|
|
|
+ /**
|
|
|
+ * Excel导入门店菜单
|
|
|
+ *
|
|
|
+ * @param file Excel文件
|
|
|
+ * @param storeId 门店id
|
|
|
+ * @return 导入结果
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public R<String> importMenuFromExcel(MultipartFile file, Integer storeId) {
|
|
|
+ log.info("StoreMenuPlatformServiceImpl.importMenuFromExcel storeId={}", storeId);
|
|
|
+
|
|
|
+ if (file == null || file.isEmpty()) {
|
|
|
+ return R.fail("上传文件为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ String fileName = file.getOriginalFilename();
|
|
|
+ if (fileName == null || (!fileName.endsWith(".xlsx") && !fileName.endsWith(".xls"))) {
|
|
|
+ return R.fail("文件格式不正确,请上传Excel文件");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (storeId == null) {
|
|
|
+ return R.fail("门店ID不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ List<String> errorMessages = new ArrayList<>();
|
|
|
+ int successCount = 0;
|
|
|
+ int totalCount = 0;
|
|
|
+
|
|
|
+ try (InputStream inputStream = file.getInputStream();
|
|
|
+ XSSFWorkbook workbook = new XSSFWorkbook(inputStream)) {
|
|
|
+ Sheet sheet = workbook.getSheetAt(0);
|
|
|
+
|
|
|
+ // 获取表头
|
|
|
+ Row headerRow = sheet.getRow(0);
|
|
|
+ if (headerRow == null) {
|
|
|
+ return R.fail("Excel文件格式不正确,缺少表头");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建字段映射(表头名称 -> 列索引)
|
|
|
+ Map<String, Integer> headerMap = new HashMap<>();
|
|
|
+ Field[] fields = StoreMenuImportVo.class.getDeclaredFields();
|
|
|
+ for (int i = 0; i < headerRow.getLastCellNum(); i++) {
|
|
|
+ Cell cell = headerRow.getCell(i);
|
|
|
+ if (cell != null) {
|
|
|
+ String headerName = getCellValueAsString(cell);
|
|
|
+ if (StringUtils.isNotEmpty(headerName)) {
|
|
|
+ headerMap.put(headerName.trim(), i);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取图片映射(行索引 -> 图片字节数组)
|
|
|
+ Map<Integer, byte[]> imageMap = extractImagesFromSheet(sheet);
|
|
|
+
|
|
|
+ // 读取数据行
|
|
|
+ for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
|
|
|
+ Row row = sheet.getRow(rowIndex);
|
|
|
+ if (row == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否为空行
|
|
|
+ boolean isEmptyRow = true;
|
|
|
+ for (int i = 0; i < row.getLastCellNum(); i++) {
|
|
|
+ Cell cell = row.getCell(i);
|
|
|
+ if (cell != null && cell.getCellType() != CellType.BLANK) {
|
|
|
+ String cellValue = getCellValueAsString(cell);
|
|
|
+ if (StringUtils.isNotEmpty(cellValue)) {
|
|
|
+ isEmptyRow = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (isEmptyRow) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ totalCount++;
|
|
|
+ StoreMenuImportVo excelVo = new StoreMenuImportVo();
|
|
|
+
|
|
|
+ // 读取每个字段
|
|
|
+ for (Field field : fields) {
|
|
|
+ field.setAccessible(true);
|
|
|
+ String fieldName = field.getName();
|
|
|
+ Integer colIndex = null;
|
|
|
+
|
|
|
+ // 根据字段名查找对应的表头
|
|
|
+ for (Map.Entry<String, Integer> entry : headerMap.entrySet()) {
|
|
|
+ String headerName = entry.getKey();
|
|
|
+ if (isFieldMatch(fieldName, headerName)) {
|
|
|
+ colIndex = entry.getValue();
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (colIndex == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ Cell cell = row.getCell(colIndex);
|
|
|
+ if (cell == null && !field.isAnnotationPresent(ExcelImage.class)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (field.isAnnotationPresent(ExcelImage.class)) {
|
|
|
+ // 处理图片字段
|
|
|
+ byte[] imageBytes = imageMap.get(rowIndex);
|
|
|
+ if (imageBytes != null && imageBytes.length > 0) {
|
|
|
+ String imageName = "menu_" + storeId + "_" + System.currentTimeMillis() + "_" + rowIndex + ".jpg";
|
|
|
+ MultipartFile multipartFile = new ByteArrayMultipartFile(imageBytes, imageName);
|
|
|
+ JSONObject jsonObject = alienStoreFeign.uploadFile(multipartFile);
|
|
|
+ if (200 == jsonObject.getIntValue("code")) {
|
|
|
+ field.set(excelVo, jsonObject.getJSONArray("data").get(0));
|
|
|
+ } else {
|
|
|
+ field.set(excelVo, "");
|
|
|
+ }
|
|
|
+// String imageUrl = uploadImageToOSS(imageBytes, storeId, rowIndex);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 处理普通字段
|
|
|
+ String cellValue = getCellValueAsString(cell);
|
|
|
+ if (StringUtils.isNotEmpty(cellValue)) {
|
|
|
+ setFieldValue(excelVo, field, cellValue.trim());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("读取字段{}失败:{}", fieldName, e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理导入数据
|
|
|
+ try {
|
|
|
+ validateAndSaveMenu(excelVo, storeId, rowIndex + 1);
|
|
|
+ successCount++;
|
|
|
+ } catch (Exception e) {
|
|
|
+ errorMessages.add(String.format("第%d行:%s", rowIndex + 1, e.getMessage()));
|
|
|
+ log.error("导入第{}行数据失败", rowIndex + 1, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("导入Excel失败", e);
|
|
|
+ return R.fail("导入失败:" + e.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建返回消息
|
|
|
+ StringBuilder message = new StringBuilder();
|
|
|
+ message.append(String.format("导入完成:成功%d条,失败%d条", successCount, totalCount - successCount));
|
|
|
+ if (!errorMessages.isEmpty()) {
|
|
|
+ message.append("\n失败详情:\n");
|
|
|
+ for (int i = 0; i < Math.min(errorMessages.size(), 10); i++) {
|
|
|
+ message.append(errorMessages.get(i)).append("\n");
|
|
|
+ }
|
|
|
+ if (errorMessages.size() > 10) {
|
|
|
+ message.append("...还有").append(errorMessages.size() - 10).append("条错误信息");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return R.success(message.toString());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从Sheet中提取图片
|
|
|
+ */
|
|
|
+ private Map<Integer, byte[]> extractImagesFromSheet(Sheet sheet) {
|
|
|
+ Map<Integer, byte[]> imageMap = new HashMap<>();
|
|
|
+ if (sheet instanceof XSSFSheet) {
|
|
|
+ XSSFSheet xssfSheet = (XSSFSheet) sheet;
|
|
|
+ XSSFDrawing drawing = xssfSheet.getDrawingPatriarch();
|
|
|
+ if (drawing != null) {
|
|
|
+ List<XSSFShape> shapes = drawing.getShapes();
|
|
|
+ for (XSSFShape shape : shapes) {
|
|
|
+ if (shape instanceof XSSFPicture) {
|
|
|
+ XSSFPicture picture = (XSSFPicture) shape;
|
|
|
+ XSSFClientAnchor anchor = (XSSFClientAnchor) picture.getAnchor();
|
|
|
+ int rowIndex = anchor.getRow1();
|
|
|
+ try {
|
|
|
+ // 直接使用 getData() 方法获取图片字节数组
|
|
|
+ byte[] imageBytes = picture.getPictureData().getData();
|
|
|
+ imageMap.put(rowIndex, imageBytes);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("提取第{}行图片失败:{}", rowIndex, e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return imageMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 上传图片到OSS
|
|
|
+ */
|
|
|
+ private String uploadImageToOSS(byte[] imageBytes, Integer storeId, int rowIndex) {
|
|
|
+ try {
|
|
|
+ // 生成文件名
|
|
|
+ String fileName = "menu_" + storeId + "_" + System.currentTimeMillis() + "_" + rowIndex + ".jpg";
|
|
|
+ String prefix = "image/";
|
|
|
+
|
|
|
+ // 创建临时MultipartFile
|
|
|
+ MultipartFile multipartFile = new ByteArrayMultipartFile(imageBytes, fileName);
|
|
|
+
|
|
|
+ // 上传到OSS
|
|
|
+ return aliOSSUtil.uploadFile(multipartFile, prefix + fileName);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("上传图片失败", e);
|
|
|
+ throw new RuntimeException("上传图片失败:" + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 校验并保存菜单
|
|
|
+ */
|
|
|
+ private void validateAndSaveMenu(StoreMenuImportVo excelVo, Integer storeId, int rowNum) {
|
|
|
+ // 校验必填字段
|
|
|
+ if (StringUtils.isEmpty(excelVo.getDishName())) {
|
|
|
+ throw new RuntimeException("菜品名称不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (excelVo.getDishPrice() == null || excelVo.getDishPrice().compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
+ throw new RuntimeException("菜品价格必须大于0");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建StoreMenu对象
|
|
|
+ StoreMenu storeMenu = new StoreMenu();
|
|
|
+ storeMenu.setStoreId(storeId);
|
|
|
+ storeMenu.setDishName(excelVo.getDishName());
|
|
|
+ storeMenu.setDishPrice(excelVo.getDishPrice());
|
|
|
+ storeMenu.setCostPrice(excelVo.getCostPrice());
|
|
|
+ storeMenu.setDishesUnit(excelVo.getDishesUnit());
|
|
|
+ storeMenu.setDescription(excelVo.getDescription());
|
|
|
+ storeMenu.setDishType(excelVo.getDishType());
|
|
|
+
|
|
|
+ // 处理图片
|
|
|
+ if (StringUtils.isNotEmpty(excelVo.getImg())) {
|
|
|
+ StoreImg storeImg = new StoreImg();
|
|
|
+ storeImg.setStoreId(storeId);
|
|
|
+ storeImg.setImgType(7);
|
|
|
+ storeImg.setImgUrl(excelVo.getImg());
|
|
|
+ storeImg.setImgDescription(excelVo.getDishName());
|
|
|
+ storeImgMapper.insert(storeImg);
|
|
|
+ storeMenu.setImgId(storeImg.getId());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置排序
|
|
|
+ LambdaQueryWrapper<StoreMenu> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
+ queryWrapper.eq(StoreMenu::getStoreId, storeId);
|
|
|
+ List<StoreMenu> menuList = this.list(queryWrapper);
|
|
|
+ if (CollectionUtil.isNotEmpty(menuList)) {
|
|
|
+ int maxSort = menuList.stream().map(StoreMenu::getSort).max(Integer::compareTo).orElse(0);
|
|
|
+ storeMenu.setSort(maxSort + 1);
|
|
|
+ } else {
|
|
|
+ storeMenu.setSort(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存菜单
|
|
|
+ boolean flag = this.save(storeMenu);
|
|
|
+ if (!flag) {
|
|
|
+ throw new RuntimeException("保存菜单失败");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断字段名是否匹配表头
|
|
|
+ */
|
|
|
+ private boolean isFieldMatch(String fieldName, String headerName) {
|
|
|
+ // 简单的匹配逻辑,可以根据实际需求调整
|
|
|
+ Map<String, String> fieldMapping = new HashMap<>();
|
|
|
+ fieldMapping.put("dishName", "菜品名称");
|
|
|
+ fieldMapping.put("dishPrice", "价格");
|
|
|
+ fieldMapping.put("costPrice", "成本价");
|
|
|
+ fieldMapping.put("dishesUnit", "单位");
|
|
|
+ fieldMapping.put("img", "图片");
|
|
|
+ fieldMapping.put("description", "描述");
|
|
|
+ fieldMapping.put("dishType", "是否推荐");
|
|
|
+
|
|
|
+ String expectedHeader = fieldMapping.get(fieldName);
|
|
|
+ return expectedHeader != null && expectedHeader.equals(headerName);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 设置字段值
|
|
|
+ */
|
|
|
+ private void setFieldValue(StoreMenuImportVo excelVo, Field field, String cellValue) throws Exception {
|
|
|
+ Class<?> fieldType = field.getType();
|
|
|
+ String fieldName = field.getName();
|
|
|
+ if (fieldType == BigDecimal.class) {
|
|
|
+ try {
|
|
|
+ if (new BigDecimal(cellValue).scale() > 2) {
|
|
|
+ throw new RuntimeException("价格或者成本价格式错误, 请输入数字, 小数点保留两位以内");
|
|
|
+ } else {
|
|
|
+ field.set(excelVo, new BigDecimal(cellValue));
|
|
|
+ }
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ throw new RuntimeException("价格或者成本价格式错误, 请输入数字, 小数点保留两位以内");
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if ("dishType".equals(fieldName)) {
|
|
|
+ String trimmedValue = cellValue.trim();
|
|
|
+ if ("是".equals(trimmedValue)) {
|
|
|
+ field.set(excelVo, 1);
|
|
|
+ } else if ("否".equals(trimmedValue)) {
|
|
|
+ field.set(excelVo, 0);
|
|
|
+ } else {
|
|
|
+ throw new RuntimeException("是否推荐字段格式错误,请输入'是'或'否'");
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ field.set(excelVo, cellValue);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取单元格值(字符串格式)
|
|
|
+ */
|
|
|
+ private String getCellValueAsString(Cell cell) {
|
|
|
+ if (cell == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 临时MultipartFile实现类,用于上传字节数组
|
|
|
+ */
|
|
|
+ private static class ByteArrayMultipartFile implements MultipartFile {
|
|
|
+ private final byte[] content;
|
|
|
+ private final String fileName;
|
|
|
+
|
|
|
+ public ByteArrayMultipartFile(byte[] content, String fileName) {
|
|
|
+ this.content = content;
|
|
|
+ this.fileName = fileName;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String getName() {
|
|
|
+ return "file";
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String getOriginalFilename() {
|
|
|
+ return fileName;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String getContentType() {
|
|
|
+ return "image/jpeg";
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean isEmpty() {
|
|
|
+ return content == null || content.length == 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public long getSize() {
|
|
|
+ return content.length;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public byte[] getBytes() throws IOException {
|
|
|
+ return content;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public InputStream getInputStream() throws IOException {
|
|
|
+ return new ByteArrayInputStream(content);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void transferTo(java.io.File dest) throws IOException, IllegalStateException {
|
|
|
+ java.nio.file.Files.write(dest.toPath(), content);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
|