diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/CanteenDailyMenuImportExcel.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/CanteenDailyMenuImportExcel.java new file mode 100644 index 000000000..69767655e --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/CanteenDailyMenuImportExcel.java @@ -0,0 +1,40 @@ +package com.bsd.pjcloud.canteen.dto; + +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 每日菜谱规范导入模板行 + */ +@Data +public class CanteenDailyMenuImportExcel { + + @ExcelProperty(value = "日期", index = 0) + private String bizDate; + + @ExcelProperty(value = "餐段", index = 1) + private String mealName; + + @ExcelProperty(value = "套餐名", index = 2) + private String comboName; + + @ExcelProperty(value = "菜品名", index = 3) + private String dishName; + + @ExcelProperty(value = "配料", index = 4) + private String ingredients; + + /** 菜品自身单价;不填默认 0 */ + @ExcelProperty(value = "菜品单价(元)", index = 5) + private BigDecimal dishPriceYuan; + + /** 套餐售价(套餐首行填写,可与套餐名合并);单品可留空,用「菜品单价」 */ + @ExcelProperty(value = "套餐价(元)", index = 6) + private BigDecimal priceYuan; + + /** 仅示例说明用,导入逻辑忽略 */ + @ExcelProperty(value = "填写说明", index = 7) + private String tip; +} diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/CanteenSpuExportQuery.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/CanteenSpuExportQuery.java new file mode 100644 index 000000000..cd3ea2871 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/CanteenSpuExportQuery.java @@ -0,0 +1,21 @@ +package com.bsd.pjcloud.canteen.dto; + +import lombok.Data; + +import java.util.List; + +/** + * 菜品导出筛选(POST body,商户/分类 ID 用字符串避免前端雪花精度丢失) + */ +@Data +public class CanteenSpuExportQuery { + + private String name; + + private String categoryId; + + private String merchantId; + + /** 勾选导出时的 SPU id */ + private List ids; +} diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/CanteenSpuImportExcel.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/CanteenSpuImportExcel.java new file mode 100644 index 000000000..2bae58621 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/CanteenSpuImportExcel.java @@ -0,0 +1,60 @@ +package com.bsd.pjcloud.canteen.dto; + +import com.alibaba.excel.annotation.ExcelProperty; +import com.bsd.pjcloud.canteen.excel.ExcelYuanConverter; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 菜品列表 Excel 导入/导出/模板行(单品 + 套餐) + *

套餐名留空或「无」=单品;套餐名相同多行=同一套餐的组成菜。 + *

导出含「菜品ID/套餐ID」,覆盖导入优先按 ID 更新,避免同名重复新增。 + */ +@Data +public class CanteenSpuImportExcel { + + @ExcelProperty(value = "套餐名", index = 0) + private String comboName; + + @ExcelProperty(value = "菜品名称", index = 1) + private String name; + + @ExcelProperty(value = "分类名称", index = 2) + private String categoryName; + + @ExcelProperty(value = "菜品单价(元)", index = 3, converter = ExcelYuanConverter.class) + private BigDecimal priceYuan; + + @ExcelProperty(value = "套餐价(元)", index = 4, converter = ExcelYuanConverter.class) + private BigDecimal comboPriceYuan; + + @ExcelProperty(value = "市场价(元)", index = 5, converter = ExcelYuanConverter.class) + private BigDecimal marketPriceYuan; + + @ExcelProperty(value = "库存", index = 6) + private Integer stock; + + @ExcelProperty(value = "排序", index = 7) + private Integer sort; + + @ExcelProperty(value = "上架状态", index = 8) + private String statusName; + + @ExcelProperty(value = "移动端显示", index = 9) + private String isShowName; + + @ExcelProperty(value = "配料/详情", index = 10) + private String description; + + @ExcelProperty(value = "填写说明", index = 11) + private String tip; + + /** 单品ID,或套餐组成菜的子菜品ID(导出为文本,勿改) */ + @ExcelProperty(value = "菜品ID", index = 12) + private String dishId; + + /** 套餐SPU ID(仅套餐行;导出为文本,勿改) */ + @ExcelProperty(value = "套餐ID", index = 13) + private String comboId; +} diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenSpuEntity.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenSpuEntity.java index 6870408f1..15ed01337 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenSpuEntity.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenSpuEntity.java @@ -60,6 +60,7 @@ public class CanteenSpuEntity extends Model { /** * 商品封面图 */ + @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS) @Schema(description="商品封面图") private String picUrl; @@ -78,9 +79,9 @@ public class CanteenSpuEntity extends Model { private Integer sort; /** - * 商品状态: 1 上架(开启) 0 下架(禁用) -1 回收 + * 商品状态: 0 上架(开启) 1 下架(禁用) -1 回收 */ - @Schema(description="商品状态: 1 上架(开启) 0 下架(禁用) -1 回收") + @Schema(description="商品状态: 0 上架(开启) 1 下架(禁用) -1 回收") private Integer status; /** @@ -98,17 +99,15 @@ public class CanteenSpuEntity extends Model { /** * 商品价格,单位使用:分 */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) @Schema(description="商品价格,单位使用:分") -// @JsonSerialize(using = MoneySerializer.class) // 分 -> 元 -// @JsonDeserialize(using = MoneyDeserializer.class) // 元 -> 分 private Integer price; /** * 市场价,单位使用:分 */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) @Schema(description="市场价,单位使用:分") -// @JsonSerialize(using = MoneySerializer.class) // 分 -> 元 -// @JsonDeserialize(using = MoneyDeserializer.class) // 元 -> 分 private Integer marketPrice; /** diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/excel/ExcelYuanConverter.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/excel/ExcelYuanConverter.java new file mode 100644 index 000000000..d62e7d894 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/excel/ExcelYuanConverter.java @@ -0,0 +1,68 @@ +package com.bsd.pjcloud.canteen.excel; + +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.GlobalConfiguration; +import com.alibaba.excel.metadata.data.ReadCellData; +import com.alibaba.excel.metadata.data.WriteCellData; +import com.alibaba.excel.metadata.property.ExcelContentProperty; + +import java.math.BigDecimal; + +/** + * Excel 金额(元):兼容数字单元格与文本单元格。 + *

字段上指定本 converter 后,按单元格实际类型解析,不依赖 supportExcelTypeKey 匹配。 + */ +public class ExcelYuanConverter implements Converter { + + @Override + public Class supportJavaTypeKey() { + return BigDecimal.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + // 注册表用;字段注解指定本类时仍会直接调用 convertToJavaData + return CellDataTypeEnum.NUMBER; + } + + @Override + public BigDecimal convertToJavaData(ReadCellData cellData, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + if (cellData == null || cellData.getType() == null || cellData.getType() == CellDataTypeEnum.EMPTY) { + return null; + } + if (cellData.getType() == CellDataTypeEnum.NUMBER && cellData.getNumberValue() != null) { + return cellData.getNumberValue(); + } + if (cellData.getType() == CellDataTypeEnum.BOOLEAN && cellData.getBooleanValue() != null) { + return cellData.getBooleanValue() ? BigDecimal.ONE : BigDecimal.ZERO; + } + String raw = cellData.getStringValue(); + if ((raw == null || raw.trim().isEmpty()) && cellData.getNumberValue() != null) { + return cellData.getNumberValue(); + } + if (raw == null || raw.trim().isEmpty()) { + return null; + } + String n = raw.trim().replace(",", "").replace("¥", "").replace("¥", "").replace("元", ""); + if (n.isEmpty()) { + return null; + } + try { + return new BigDecimal(n); + } + catch (NumberFormatException ex) { + return null; + } + } + + @Override + public WriteCellData convertToExcelData(BigDecimal value, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + if (value == null) { + return new WriteCellData<>(""); + } + return new WriteCellData<>(value.stripTrailingZeros().toPlainString()); + } +} diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/BoxesInfoController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/BoxesInfoController.java index a82a15417..cc3cba938 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/BoxesInfoController.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/BoxesInfoController.java @@ -15,7 +15,6 @@ import com.bsd.pjcloud.common.log.annotation.SysLog; import com.bsd.pjcloud.canteen.entity.BoxesInfoEntity; import com.bsd.pjcloud.canteen.service.BoxesInfoService; import com.bsd.pjcloud.canteen.util.CanteenMerchantScopeHelper; -import com.bsd.pjcloud.common.security.util.SecurityUtils; import io.swagger.v3.oas.annotations.Parameter; import org.springframework.beans.BeanUtils; import org.springframework.format.annotation.DateTimeFormat; @@ -96,11 +95,13 @@ public class BoxesInfoController { @PostMapping @PreAuthorize("@pms.hasPermission('canteen_boxesInfo_add')") public R save(@RequestBody BoxesInfoEntity boxesInfo) { - merchantScopeHelper.fillDeptIdByMerchantId(boxesInfo.getMerchantId(), boxesInfo::setDeptId); - if (boxesInfo.getDeptId() == null) { - boxesInfo.setDeptId(SecurityUtils.getUser().getDeptId()); + try { + merchantScopeHelper.fillDeptIdByMerchantId(boxesInfo.getMerchantId(), boxesInfo::setDeptId); + return R.ok(boxesInfoService.saveInfo(boxesInfo)); + } + catch (IllegalArgumentException ex) { + return R.failed(ex.getMessage()); } - return R.ok(boxesInfoService.saveInfo(boxesInfo)); } /** diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenCategoryController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenCategoryController.java index 96072b71f..554e76954 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenCategoryController.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenCategoryController.java @@ -83,7 +83,12 @@ public class CanteenCategoryController { @PostMapping @PreAuthorize("@pms.hasPermission('canteen_canteenCategory_add')") public R save(@RequestBody CanteenCategoryEntity canteenCategory) { - return R.ok(canteenCategoryService.create(canteenCategory)); + try { + return R.ok(canteenCategoryService.create(canteenCategory)); + } + catch (IllegalArgumentException ex) { + return R.failed(ex.getMessage()); + } } /** @@ -97,7 +102,12 @@ public class CanteenCategoryController { @PutMapping @PreAuthorize("@pms.hasPermission('canteen_canteenCategory_edit')") public R updateById(@RequestBody CanteenCategoryEntity canteenCategory) { - return R.ok(canteenCategoryService.edit(canteenCategory)); + try { + return R.ok(canteenCategoryService.edit(canteenCategory)); + } + catch (IllegalArgumentException ex) { + return R.failed(ex.getMessage()); + } } /** diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenDailyMenuController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenDailyMenuController.java index aa54a4780..c7fff071c 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenDailyMenuController.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenDailyMenuController.java @@ -19,8 +19,7 @@ import com.bsd.pjcloud.common.core.util.R; import com.bsd.pjcloud.common.log.annotation.SysLog; import com.bsd.pjcloud.canteen.entity.CanteenDailyMenuEntity; import com.bsd.pjcloud.canteen.service.CanteenDailyMenuService; -import com.bsd.pjcloud.common.security.service.PjcloudUser; -import com.bsd.pjcloud.common.security.util.SecurityUtils; +import com.bsd.pjcloud.canteen.util.CanteenMerchantScopeHelper; import org.springframework.security.access.prepost.PreAuthorize; import com.bsd.pjcloud.common.excel.annotation.ResponseExcel; import io.swagger.v3.oas.annotations.security.SecurityRequirement; @@ -32,6 +31,11 @@ import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import com.bsd.pjcloud.canteen.dto.CanteenDailyMenuImportExcel; +import com.bsd.pjcloud.canteen.excel.CanteenDailyMenuTemplateMergeHandler; +import com.bsd.pjcloud.canteen.service.impl.CanteenDailyMenuImportHelper; +import org.springframework.web.multipart.MultipartFile; + import java.util.List; /** @@ -51,6 +55,10 @@ public class CanteenDailyMenuController { private final CanteenSpuService canteenSpuService; + private final CanteenDailyMenuImportHelper dailyMenuImportHelper; + + private final CanteenMerchantScopeHelper merchantScopeHelper; + /** * 分页查询 * @@ -67,6 +75,17 @@ public class CanteenDailyMenuController { } + /** + * 下载导入模板(须放在 /{id} 之前,避免被路径变量误匹配) + */ + @Operation(summary = "下载每日菜谱导入模板") + @ResponseExcel(name = "每日菜谱导入模板", writeHandler = CanteenDailyMenuTemplateMergeHandler.class) + @GetMapping("/import/template") + @PreAuthorize("@pms.hasPermission('canteen_canteenDailyMenu_add')") + public List downloadImportTemplate() { + return dailyMenuImportHelper.templateRows(); + } + /** * 通过id查询每日菜谱 * @@ -91,9 +110,14 @@ public class CanteenDailyMenuController { @PostMapping @PreAuthorize("@pms.hasPermission('canteen_canteenDailyMenu_add')") public R save(@RequestBody CanteenDailyMenuEntity canteenDailyMenu) { - canteenDailyMenu.setDeptId(SecurityUtils.getUser().getDeptId()); - canteenDailyMenuService.saveToday(canteenDailyMenu); - return R.ok(); + try { + merchantScopeHelper.fillDeptIdByMerchantId(canteenDailyMenu.getMerchantId(), canteenDailyMenu::setDeptId); + canteenDailyMenuService.saveToday(canteenDailyMenu); + return R.ok(); + } + catch (IllegalArgumentException ex) { + return R.failed(ex.getMessage()); + } } /** @@ -118,6 +142,23 @@ public class CanteenDailyMenuController { public R cv(@RequestBody CanteenDailyMenuCVDTO creenDailyMenuCVDTO) { return canteenDailyMenuService.cv(creenDailyMenuCVDTO); } + + @Operation(summary = "Excel导入每日菜谱(规范模板)") + @SysLog("导入每日菜谱") + @PostMapping("/import") + @PreAuthorize("@pms.hasPermission('canteen_canteenDailyMenu_add')") + public R importExcel(@RequestParam("file") MultipartFile file, + @RequestParam("merchantId") Long merchantId, + @RequestParam("categoryId") Long categoryId, + @RequestParam(value = "replaceDay", defaultValue = "true") boolean replaceDay) throws Exception { + try { + return dailyMenuImportHelper.importExcel(file, merchantId, categoryId, replaceDay); + } + catch (IllegalArgumentException ex) { + return R.failed(ex.getMessage()); + } + } + /** * 通过id删除每日菜谱 *1 diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenErpCategoryController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenErpCategoryController.java index 18b9a2b4f..faead4e95 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenErpCategoryController.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenErpCategoryController.java @@ -14,7 +14,6 @@ import com.bsd.pjcloud.common.log.annotation.SysLog; import com.bsd.pjcloud.canteen.entity.CanteenErpCategoryEntity; import com.bsd.pjcloud.canteen.service.CanteenErpCategoryService; import com.bsd.pjcloud.canteen.util.CanteenMerchantScopeHelper; -import com.bsd.pjcloud.common.security.util.SecurityUtils; import org.springframework.security.access.prepost.PreAuthorize; import com.bsd.pjcloud.common.excel.annotation.ResponseExcel; import io.swagger.v3.oas.annotations.security.SecurityRequirement; @@ -84,11 +83,13 @@ public class CanteenErpCategoryController { @PostMapping @PreAuthorize("@pms.hasPermission('canteen_canteenErpCategory_add')" ) public R save(@RequestBody CanteenErpCategoryEntity canteenErpCategory) { - merchantScopeHelper.fillDeptIdByMerchantId(canteenErpCategory.getMerchantId(), canteenErpCategory::setDeptId); - if (canteenErpCategory.getDeptId() == null) { - canteenErpCategory.setDeptId(SecurityUtils.getUser().getDeptId()); + try { + merchantScopeHelper.fillDeptIdByMerchantId(canteenErpCategory.getMerchantId(), canteenErpCategory::setDeptId); + return R.ok(canteenErpCategoryService.save(canteenErpCategory)); + } + catch (IllegalArgumentException ex) { + return R.failed(ex.getMessage()); } - return R.ok(canteenErpCategoryService.save(canteenErpCategory)); } /** diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenMenuRecipeController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenMenuRecipeController.java index 9a5f88cd5..fe2697ece 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenMenuRecipeController.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenMenuRecipeController.java @@ -6,10 +6,10 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.bsd.pjcloud.canteen.dto.menurecipe.CanteenMenuRecipeMatchDTO; import com.bsd.pjcloud.canteen.entity.CanteenMenuRecipeEntity; import com.bsd.pjcloud.canteen.service.CanteenMenuRecipeService; +import com.bsd.pjcloud.canteen.util.CanteenMerchantScopeHelper; import com.bsd.pjcloud.canteen.vo.menurecipe.CanteenMenuRecipeImportResultVO; import com.bsd.pjcloud.common.core.util.R; import com.bsd.pjcloud.common.log.annotation.SysLog; -import com.bsd.pjcloud.common.security.util.SecurityUtils; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -31,6 +31,8 @@ public class CanteenMenuRecipeController { private final CanteenMenuRecipeService canteenMenuRecipeService; + private final CanteenMerchantScopeHelper merchantScopeHelper; + @Operation(summary = "分页查询") @GetMapping("/page") @PreAuthorize("@pms.hasPermission('canteen_canteenMenuRecipe_view')") @@ -58,8 +60,13 @@ public class CanteenMenuRecipeController { @PostMapping @PreAuthorize("@pms.hasPermission('canteen_canteenMenuRecipe_add')") public R save(@RequestBody CanteenMenuRecipeEntity entity) { - entity.setDeptId(SecurityUtils.getUser().getDeptId()); - return R.ok(canteenMenuRecipeService.saveDeep(entity)); + try { + merchantScopeHelper.fillDeptIdByMerchantIdOrCurrentUser(entity.getMerchantId(), entity::setDeptId); + return R.ok(canteenMenuRecipeService.saveDeep(entity)); + } + catch (IllegalArgumentException ex) { + return R.failed(ex.getMessage()); + } } @SysLog("修改排菜菜谱") diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenOnlineSpuController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenOnlineSpuController.java index 08e13f4df..aa7dc20fc 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenOnlineSpuController.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenOnlineSpuController.java @@ -151,8 +151,8 @@ public class CanteenOnlineSpuController { return R.failed("择一分组不存在(可能是前端 long 精度丢失导致 id 被圆整),请重新选择分组"); } } - canteenOnlineSpu.setDeptId(SecurityUtils.getUser().getDeptId()); try { + merchantScopeHelper.fillDeptIdByMerchantId(canteenOnlineSpu.getMerchantId(), canteenOnlineSpu::setDeptId); return R.ok(canteenOnlineSpuService.saveDeep(canteenOnlineSpu)); } catch (IllegalArgumentException ex) { diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenSpuController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenSpuController.java index 77ea3e24c..cc32fbd00 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenSpuController.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenSpuController.java @@ -9,12 +9,17 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.bsd.pjcloud.canteen.constant.StockTypeEnum; import com.bsd.pjcloud.canteen.dto.CanteenDailySpuDTO; +import com.bsd.pjcloud.canteen.dto.CanteenSpuExportQuery; +import com.bsd.pjcloud.canteen.dto.CanteenSpuImportExcel; import com.bsd.pjcloud.canteen.dto.CanteenSpuUpdateStockReqDTO; import com.bsd.pjcloud.canteen.entity.CanteenErpMaterialEntity; import com.bsd.pjcloud.canteen.entity.CanteenOnlineSpuEntity; import com.bsd.pjcloud.canteen.entity.CanteenStockEntity; import com.bsd.pjcloud.canteen.mapper.CanteenStockMapper; import com.bsd.pjcloud.canteen.service.CanteenStockService; +import com.bsd.pjcloud.canteen.excel.CanteenSpuTemplateMergeHandler; +import com.bsd.pjcloud.canteen.service.impl.CanteenSpuImportHelper; +import com.bsd.pjcloud.canteen.util.CanteenMerchantScopeHelper; import com.bsd.pjcloud.canteen.vo.ADCanteenSpuListVO; import com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO; import com.bsd.pjcloud.canteen.vo.erp.CanteenSpuSimpleVO; @@ -35,6 +40,7 @@ import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import java.util.Collections; import java.util.List; @@ -58,6 +64,10 @@ public class CanteenSpuController { private final CanteenStockService canteenStockService; + private final CanteenSpuImportHelper canteenSpuImportHelper; + + private final CanteenMerchantScopeHelper merchantScopeHelper; + /** * 分页查询 * @@ -218,17 +228,67 @@ public class CanteenSpuController { /** - * 导出excel 表格 - * - * @param canteenSpu 查询条件 - * @param ids 导出指定ID - * @return excel 文件流 + * 下载菜品导入模板(单品 + 套餐示例,套餐列已合并) */ - @ResponseExcel - @GetMapping("/export") + @Operation(summary = "下载菜品导入模板") + @ResponseExcel(name = "菜品导入模板", inMemory = true, writeHandler = CanteenSpuTemplateMergeHandler.class) + @GetMapping("/import/template") + @PreAuthorize("@pms.hasPermission('canteen_canteenSpu_add')") + public List downloadImportTemplate() { + return canteenSpuImportHelper.templateRows(); + } + + /** + * Excel 导入菜品(单品 + 套餐) + * + * @param cover true=覆盖(同名更新),false=仅新增(同名跳过) + */ + @Operation(summary = "Excel导入菜品", description = "cover=true 覆盖导入;cover=false 新增导入;支持套餐") + @SysLog("导入菜品spu") + @PostMapping("/import") + @PreAuthorize("@pms.hasPermission('canteen_canteenSpu_add')") + public R importExcel(@RequestParam("file") MultipartFile file, + @RequestParam("merchantId") Long merchantId, + @RequestParam(value = "categoryId", required = false) Long categoryId, + @RequestParam(value = "cover", defaultValue = "false") boolean cover) throws Exception { + try { + return canteenSpuImportHelper.importExcel(file, merchantId, categoryId, cover); + } + catch (IllegalArgumentException ex) { + return R.failed(ex.getMessage()); + } + } + + /** + * 导出excel(POST:筛选条件放 body,避免 GET 丢参/雪花 Long 精度问题;条件与列表 selectPage 一致) + */ + @ResponseExcel(name = "菜品列表", inMemory = true, writeHandler = CanteenSpuTemplateMergeHandler.class) + @PostMapping("/export") @PreAuthorize("@pms.hasPermission('canteen_canteenSpu_export')") - public List export(CanteenSpuEntity canteenSpu, Long[] ids) { - return canteenSpuService.list(Wrappers.lambdaQuery(canteenSpu).in(ArrayUtil.isNotEmpty(ids), CanteenSpuEntity::getId, ids)); + public List export(@RequestBody(required = false) CanteenSpuExportQuery query) { + if (query == null) { + query = new CanteenSpuExportQuery(); + } + Long merchantId = parseLongId(query.getMerchantId()); + Long categoryId = parseLongId(query.getCategoryId()); + Long[] ids = null; + if (CollUtil.isNotEmpty(query.getIds())) { + ids = query.getIds().stream().map(this::parseLongId).filter(Objects::nonNull).toArray(Long[]::new); + } + return canteenSpuImportHelper.toExportRows( + canteenSpuService.listForExport(query.getName(), categoryId, merchantId, ids)); + } + + private Long parseLongId(String raw) { + if (StrUtil.isBlank(raw)) { + return null; + } + try { + return Long.valueOf(raw.trim()); + } + catch (NumberFormatException ex) { + return null; + } } diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/excel/CanteenDailyMenuTemplateMergeHandler.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/excel/CanteenDailyMenuTemplateMergeHandler.java new file mode 100644 index 000000000..a24bc362f --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/excel/CanteenDailyMenuTemplateMergeHandler.java @@ -0,0 +1,75 @@ +package com.bsd.pjcloud.canteen.excel; + +import com.alibaba.excel.write.handler.WorkbookWriteHandler; +import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.util.CellRangeAddress; + +/** + * 每日菜谱导入模板:套餐多行合并日期/餐段/套餐名/套餐价/说明;菜品单价不合并。 + *

列:0日期 1餐段 2套餐名 3菜品名 4配料 5菜品单价 6套餐价 7填写说明 + *

数据行(表头第 0 行):1-3 A套餐,4-5 B套餐,6 午餐单品,7-8 早餐单品 + */ +public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandler { + + @Override + public void afterWorkbookDispose(WriteWorkbookHolder writeWorkbookHolder) { + Sheet sheet = writeWorkbookHolder.getWorkbook().getSheetAt(0); + if (sheet == null) { + return; + } + + merge(sheet, 1, 8, 0, 0); + merge(sheet, 1, 6, 1, 1); + merge(sheet, 7, 8, 1, 1); + // A 套餐:套餐名、套餐价、填写说明(菜品单价列不合并) + merge(sheet, 1, 3, 2, 2); + merge(sheet, 1, 3, 6, 6); + merge(sheet, 1, 3, 7, 7); + // B 套餐 + merge(sheet, 4, 5, 2, 2); + merge(sheet, 4, 5, 6, 6); + merge(sheet, 4, 5, 7, 7); + + CellStyle center = writeWorkbookHolder.getWorkbook().createCellStyle(); + center.setAlignment(HorizontalAlignment.CENTER); + center.setVerticalAlignment(VerticalAlignment.CENTER); + applyStyle(sheet, center, new int[] { 0, 1, 2, 6, 7 }, 1, 8); + + sheet.setColumnWidth(0, 12 * 256); + sheet.setColumnWidth(1, 8 * 256); + sheet.setColumnWidth(2, 10 * 256); + sheet.setColumnWidth(3, 16 * 256); + sheet.setColumnWidth(4, 22 * 256); + sheet.setColumnWidth(5, 14 * 256); + sheet.setColumnWidth(6, 12 * 256); + sheet.setColumnWidth(7, 48 * 256); + } + + private static void merge(Sheet sheet, int firstRow, int lastRow, int firstCol, int lastCol) { + if (firstRow == lastRow && firstCol == lastCol) { + return; + } + sheet.addMergedRegion(new CellRangeAddress(firstRow, lastRow, firstCol, lastCol)); + } + + private static void applyStyle(Sheet sheet, CellStyle style, int[] cols, int firstRow, int lastRow) { + for (int r = firstRow; r <= lastRow; r++) { + Row row = sheet.getRow(r); + if (row == null) { + continue; + } + for (int c : cols) { + Cell cell = row.getCell(c); + if (cell != null) { + cell.setCellStyle(style); + } + } + } + } +} diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/excel/CanteenSpuTemplateMergeHandler.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/excel/CanteenSpuTemplateMergeHandler.java new file mode 100644 index 000000000..500c6e406 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/excel/CanteenSpuTemplateMergeHandler.java @@ -0,0 +1,174 @@ +package com.bsd.pjcloud.canteen.excel; + +import com.alibaba.excel.write.handler.WorkbookWriteHandler; +import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.util.CellRangeAddress; + +/** + * 菜品 Excel:套餐多行时合并套餐级字段(模板/导出通用)。 + *

列:0套餐名 1菜品名称 2分类 3菜品单价 4套餐价 5市场价 6库存 7排序 8状态 9移动端 10详情 11说明 12菜品ID 13套餐ID + *

须配合 {@code @ResponseExcel(inMemory = true)},否则 SXSSF 刷盘后靠前行无法合并。 + */ +public class CanteenSpuTemplateMergeHandler implements WorkbookWriteHandler { + + /** 套餐级合并列:套餐名、套餐价、库存、排序、上架、移动端、说明、套餐ID */ + private static final int[] MERGE_COLS = { 0, 4, 6, 7, 8, 9, 11, 13 }; + + private static final int COL_DISH_ID = 12; + private static final int COL_COMBO_ID = 13; + + @Override + public void afterWorkbookDispose(WriteWorkbookHolder writeWorkbookHolder) { + Sheet sheet = writeWorkbookHolder.getWorkbook().getNumberOfSheets() > 0 + ? writeWorkbookHolder.getWorkbook().getSheetAt(0) : null; + if (sheet == null) { + return; + } + int lastRow = sheet.getLastRowNum(); + if (lastRow < 1) { + return; + } + + int groupStart = -1; + String groupName = null; + for (int r = 1; r <= lastRow + 1; r++) { + boolean end = r > lastRow; + String raw = end ? null : cellText(sheet, r, 0); + boolean blankCell = !end && (raw == null || raw.trim().isEmpty()); + String combo = end ? null : normalizeCombo(raw); + + if (groupStart >= 0) { + boolean continueGroup = !end && (blankCell || (combo != null && combo.equals(groupName))); + if (!continueGroup) { + mergeGroup(sheet, groupStart, r - 1); + groupStart = -1; + groupName = null; + } + } + if (!end && combo != null && groupStart < 0) { + groupStart = r; + groupName = combo; + } + } + + CellStyle center = writeWorkbookHolder.getWorkbook().createCellStyle(); + center.setAlignment(HorizontalAlignment.CENTER); + center.setVerticalAlignment(VerticalAlignment.CENTER); + for (int r = 1; r <= lastRow; r++) { + for (int c : MERGE_COLS) { + Cell cell = getCell(sheet, r, c); + if (cell != null) { + cell.setCellStyle(center); + } + } + } + + // 雪花ID强制文本,避免 Excel 科学计数法丢精度 + CellStyle textStyle = writeWorkbookHolder.getWorkbook().createCellStyle(); + textStyle.setDataFormat(writeWorkbookHolder.getWorkbook().createDataFormat().getFormat("@")); + for (int r = 1; r <= lastRow; r++) { + forceTextId(sheet, r, COL_DISH_ID, textStyle); + forceTextId(sheet, r, COL_COMBO_ID, textStyle); + } + + int[] widths = { 10, 14, 10, 12, 10, 10, 8, 6, 8, 10, 18, 28, 22, 22 }; + for (int i = 0; i < widths.length; i++) { + sheet.setColumnWidth(i, widths[i] * 256); + } + } + + private static void forceTextId(Sheet sheet, int rowIdx, int colIdx, CellStyle textStyle) { + Row row = sheet.getRow(rowIdx); + if (row == null) { + return; + } + Cell cell = row.getCell(colIdx); + if (cell == null || cell.getCellType() == CellType.BLANK) { + return; + } + String text; + if (cell.getCellType() == CellType.NUMERIC) { + text = java.math.BigDecimal.valueOf(cell.getNumericCellValue()).toPlainString().split("\\.")[0]; + } + else if (cell.getCellType() == CellType.STRING) { + text = cell.getStringCellValue(); + } + else { + text = cell.toString(); + } + if (text == null || text.trim().isEmpty()) { + return; + } + cell.setCellStyle(textStyle); + cell.setCellValue(text.trim()); + } + + /** @return 可合并的套餐名;单品标记或空返回 null */ + private static String normalizeCombo(String raw) { + if (raw == null || raw.trim().isEmpty()) { + return null; + } + String c = raw.trim(); + if ("无".equals(c) || "-".equals(c) || "单品".equals(c) || "(无)".equals(c) || "(无)".equals(c)) { + return null; + } + return c; + } + + private static void mergeGroup(Sheet sheet, int firstRow, int lastRow) { + if (firstRow >= lastRow) { + return; + } + for (int c : MERGE_COLS) { + for (int r = firstRow + 1; r <= lastRow; r++) { + Cell cell = getCell(sheet, r, c); + if (cell != null) { + cell.setBlank(); + } + } + sheet.addMergedRegion(new CellRangeAddress(firstRow, lastRow, c, c)); + } + } + + private static String cellText(Sheet sheet, int rowIdx, int colIdx) { + Cell cell = getCell(sheet, rowIdx, colIdx); + if (cell == null) { + return null; + } + CellType type = cell.getCellType(); + if (type == CellType.STRING) { + return cell.getStringCellValue(); + } + if (type == CellType.NUMERIC) { + double v = cell.getNumericCellValue(); + if (v == Math.floor(v) && !Double.isInfinite(v)) { + return String.valueOf((long) v); + } + return String.valueOf(v); + } + if (type == CellType.BLANK) { + return null; + } + try { + return cell.toString(); + } + catch (Exception ex) { + return null; + } + } + + private static Cell getCell(Sheet sheet, int rowIdx, int colIdx) { + Row row = sheet.getRow(rowIdx); + if (row == null) { + return null; + } + return row.getCell(colIdx); + } +} diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenSpuService.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenSpuService.java index 4becfb673..6164cd82e 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenSpuService.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenSpuService.java @@ -49,6 +49,10 @@ public interface CanteenSpuService extends IService { */ Object pageScope(Page page, CanteenSpuEntity canteenSpu); + /** + * 导出列表查询(与列表页 selectPage 同筛选:商户/名称/分类,并排除已关联线上菜品) + */ + List listForExport(String name, Long categoryId, Long merchantId, Long[] ids); /** * 分页查询,并且过滤已选择的菜品 diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenCategoryServiceImpl.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenCategoryServiceImpl.java index 25bfcde82..52897d031 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenCategoryServiceImpl.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenCategoryServiceImpl.java @@ -42,6 +42,9 @@ public class CanteenCategoryServiceImpl extends ServiceImpl templateRows() { + List list = new ArrayList<>(); + // 午餐 A 套餐:套餐价 21;各菜可有自己的菜品单价 + list.add(row("2026-08-17", "午餐", "A套餐", "糖醋排骨", "青莲仔排", "12", "21", + "套餐价只填首行;菜品单价每行可填,不填默认0。套餐名/菜品名在菜品列表已存在则复用,不存在则自动新增")); + list.add(row(null, null, null, "蒜苔炒腊肉", "蒜苔,腊肉,胡萝卜", "8", null, null)); + list.add(row(null, null, null, "清炒时蔬", "空心菜", "3", null, null)); + // 午餐 B 套餐 + list.add(row(null, null, "B套餐", "红烧肉", "五花肉", "10", "18", + "换套餐名即另一套;名称已存在不新增")); + list.add(row(null, null, null, "土豆丝", "土豆", "4", null, null)); + // 单品:套餐名「无」,单价写在菜品单价 + list.add(row(null, null, "无", "千张大骨汤", null, "0", null, + "单品:套餐名填「无」;菜品已存在则复用")); + list.add(row(null, "早餐", "无", "榨菜肉丝面", "榨菜,肉丝", "6.4", null, + "早餐单品")); + list.add(row(null, null, "无", "豆浆", null, "2", null, null)); + return list; + } + + private static CanteenDailyMenuImportExcel row(String date, String meal, String combo, String dish, + String ingredients, String dishPrice, String comboPrice, String tip) { + CanteenDailyMenuImportExcel r = new CanteenDailyMenuImportExcel(); + r.setBizDate(date); + r.setMealName(meal); + r.setComboName(combo); + r.setDishName(dish); + r.setIngredients(ingredients); + if (StrUtil.isNotBlank(dishPrice)) { + r.setDishPriceYuan(new BigDecimal(dishPrice)); + } + if (StrUtil.isNotBlank(comboPrice)) { + r.setPriceYuan(new BigDecimal(comboPrice)); + } + r.setTip(tip); + return r; + } + + @Transactional(rollbackFor = Exception.class) + public R importExcel(MultipartFile file, Long merchantId, Long categoryId, boolean replaceDay) + throws IOException { + if (merchantId == null) { + return R.failed("请选择商户"); + } + if (categoryId == null) { + return R.failed("请选择新建菜品默认分类"); + } + if (file == null || file.isEmpty()) { + return R.failed("请上传 Excel 文件"); + } + List rows = EasyExcel.read(file.getInputStream()) + .head(CanteenDailyMenuImportExcel.class) + .sheet() + .headRowNumber(1) + .doReadSync(); + if (CollUtil.isEmpty(rows)) { + return R.failed("导入数据为空"); + } + normalizeMergedCells(rows); + + Long deptId = merchantScopeHelper.requireDeptIdByMerchantId(merchantId); + + // groupKey = date|hourType|comboOrEmpty + Map> groups = new LinkedHashMap<>(); + List errors = new ArrayList<>(); + int line = 1; + for (CanteenDailyMenuImportExcel row : rows) { + line++; + if (isBlankRow(row)) { + continue; + } + Integer hourType = parseMeal(row.getMealName()); + LocalDate date = parseDate(row.getBizDate()); + String dish = StrUtil.trim(row.getDishName()); + if (date == null) { + errors.add("第" + line + "行:日期格式应为 yyyy-MM-dd"); + continue; + } + if (hourType == null) { + errors.add("第" + line + "行:餐段应为 早餐/午餐/晚餐"); + continue; + } + if (StrUtil.isBlank(dish)) { + errors.add("第" + line + "行:菜品名不能为空"); + continue; + } + String combo = StrUtil.trim(row.getComboName()); + String key = date + "|" + hourType + "|" + StrUtil.blankToDefault(combo, ""); + groups.computeIfAbsent(key, k -> new ArrayList<>()).add(row); + } + if (CollUtil.isNotEmpty(errors)) { + return R.failed(String.join(";", errors.subList(0, Math.min(errors.size(), 20)))); + } + + Set clearedDayHour = new LinkedHashSet<>(); + int createdDish = 0; + int createdCombo = 0; + int scheduled = 0; + + for (Map.Entry> e : groups.entrySet()) { + String[] parts = e.getKey().split("\\|", -1); + LocalDate date = LocalDate.parse(parts[0]); + Integer hourType = Integer.valueOf(parts[1]); + String comboName = parts[2]; + List items = e.getValue(); + + if (StrUtil.isBlank(comboName)) { + for (CanteenDailyMenuImportExcel item : items) { + FindOrCreate r = findOrCreateDish(merchantId, deptId, categoryId, item.getDishName(), + item.getIngredients(), resolveDishPriceFen(item)); + if (r.created) { + createdDish++; + } + schedule(merchantId, deptId, date, hourType, r.spu.getId(), replaceDay, clearedDayHour); + scheduled++; + } + } + else { + List children = new ArrayList<>(); + Integer comboPrice = null; + for (int i = 0; i < items.size(); i++) { + CanteenDailyMenuImportExcel item = items.get(i); + if (i == 0) { + Integer fen = toFen(item.getPriceYuan()); + if (fen != null) { + comboPrice = fen; + } + } + FindOrCreate r = findOrCreateDish(merchantId, deptId, categoryId, item.getDishName(), + item.getIngredients(), resolveDishPriceFen(item)); + if (r.created) { + createdDish++; + } + children.add(r.spu); + } + if (comboPrice == null) { + comboPrice = 0; + } + String mainName = children.get(0).getName(); + String fullComboName = comboName + "-" + mainName; + FindOrCreate combo = findOrCreateCombo(merchantId, deptId, categoryId, fullComboName, comboName, + comboPrice, children); + if (combo.created) { + createdCombo++; + } + schedule(merchantId, deptId, date, hourType, combo.spu.getId(), replaceDay, clearedDayHour); + scheduled++; + } + } + + return R.ok(null, String.format("导入成功:新建单品 %d,新建套餐 %d,排菜 %d", createdDish, createdCombo, scheduled)); + } + + private void schedule(Long merchantId, Long deptId, LocalDate date, Integer hourType, Long spuId, + boolean replaceDay, Set clearedDayHour) { + String dh = date + "|" + hourType; + if (replaceDay && !clearedDayHour.contains(dh)) { + canteenDailyMenuService.remove(Wrappers.lambdaQuery() + .eq(CanteenDailyMenuEntity::getMerchantId, merchantId) + .eq(CanteenDailyMenuEntity::getStartTime, date) + .eq(CanteenDailyMenuEntity::getHourType, hourType)); + clearedDayHour.add(dh); + } + boolean exists = canteenDailyMenuService.count(Wrappers.lambdaQuery() + .eq(CanteenDailyMenuEntity::getMerchantId, merchantId) + .eq(CanteenDailyMenuEntity::getStartTime, date) + .eq(CanteenDailyMenuEntity::getHourType, hourType) + .eq(CanteenDailyMenuEntity::getSpuId, spuId)) > 0; + if (exists) { + return; + } + CanteenDailyMenuEntity menu = new CanteenDailyMenuEntity(); + menu.setMerchantId(merchantId); + menu.setDeptId(deptId); + menu.setStartTime(date); + menu.setHourType(hourType); + menu.setSpuId(spuId); + canteenDailyMenuService.save(menu); + } + + private FindOrCreate findOrCreateDish(Long merchantId, Long deptId, Long categoryId, String name, + String ingredients, Integer priceFen) { + String clean = cleanDishName(name); + CanteenSpuEntity exist = canteenSpuService.getOne(Wrappers.lambdaQuery() + .eq(CanteenSpuEntity::getMerchantId, merchantId) + .eq(CanteenSpuEntity::getName, clean) + .eq(CanteenSpuEntity::getSpuType, 1) + .last("limit 1"), false); + if (exist != null) { + boolean needUpdate = false; + if (StrUtil.isNotBlank(ingredients) && StrUtil.isBlank(exist.getDescription())) { + exist.setDescription(ingredients.trim()); + needUpdate = true; + } + if (priceFen != null && priceFen > 0 && (exist.getPrice() == null || exist.getPrice() == 0)) { + exist.setPrice(priceFen); + exist.setMarketPrice(priceFen); + needUpdate = true; + } + if (needUpdate) { + canteenSpuService.updateById(exist); + } + return new FindOrCreate(exist, false); + } + CanteenSpuEntity spu = newBaseSpu(merchantId, deptId, categoryId, clean, 1); + spu.setDescription(StrUtil.trim(ingredients)); + spu.setPrice(priceFen == null ? 0 : priceFen); + spu.setMarketPrice(spu.getPrice()); + canteenSpuService.save(spu); + return new FindOrCreate(spu, true); + } + + private FindOrCreate findOrCreateCombo(Long merchantId, Long deptId, Long categoryId, String fullName, + String pkgLabel, Integer priceFen, List children) { + CanteenSpuEntity exist = canteenSpuService.getOne(Wrappers.lambdaQuery() + .eq(CanteenSpuEntity::getMerchantId, merchantId) + .eq(CanteenSpuEntity::getName, fullName) + .eq(CanteenSpuEntity::getSpuType, 2) + .last("limit 1"), false); + List items = new ArrayList<>(); + int sort = 0; + for (CanteenSpuEntity child : children) { + CanteenSpuComboItemEntity line = new CanteenSpuComboItemEntity(); + line.setChildSpuId(child.getId()); + line.setQuantity(1); + line.setSortOrder(sort++); + items.add(line); + } + if (exist != null) { + exist.setPrice(priceFen); + exist.setMarketPrice(priceFen); + exist.setComboItems(items); + canteenSpuService.updateDeep(exist); + return new FindOrCreate(exist, false); + } + CanteenSpuEntity combo = newBaseSpu(merchantId, deptId, categoryId, fullName, 2); + combo.setIntroduction(pkgLabel); + combo.setPrice(priceFen); + combo.setMarketPrice(priceFen); + combo.setComboItems(items); + canteenSpuService.saveDeep(combo); + return new FindOrCreate(combo, true); + } + + private CanteenSpuEntity newBaseSpu(Long merchantId, Long deptId, Long categoryId, String name, int spuType) { + CanteenSpuEntity spu = new CanteenSpuEntity(); + spu.setName(name); + spu.setCategoryId(categoryId); + spu.setMerchantId(merchantId); + spu.setDeptId(deptId); + spu.setSpuType(spuType); + spu.setPicUrl(""); + spu.setStatus(0); + spu.setSpecType(Boolean.FALSE); + spu.setSort(0); + spu.setStock(9999); + spu.setSalesCount(0); + spu.setVirtualSalesCount(0); + spu.setBrowseCount(0); + spu.setCostPrice(0); + spu.setSystemType(2L); + spu.setIsShow("0"); + return spu; + } + + private static boolean isBlankRow(CanteenDailyMenuImportExcel row) { + return StrUtil.isBlank(row.getBizDate()) && StrUtil.isBlank(row.getDishName()) + && StrUtil.isBlank(row.getMealName()) && StrUtil.isBlank(row.getComboName()); + } + + /** + * 合并单元格模板读入后,非首行日期/餐段/套餐名为空,需向下填充。 + * 套餐名填「无」「-」「单品」表示单品,不参与套餐继承。 + */ + private static void normalizeMergedCells(List rows) { + String lastDate = null; + String lastMeal = null; + String lastCombo = null; + for (CanteenDailyMenuImportExcel row : rows) { + if (isBlankRow(row)) { + continue; + } + if (StrUtil.isBlank(row.getBizDate())) { + row.setBizDate(lastDate); + } + else { + lastDate = StrUtil.trim(row.getBizDate()); + row.setBizDate(lastDate); + } + if (StrUtil.isBlank(row.getMealName())) { + row.setMealName(lastMeal); + } + else { + lastMeal = StrUtil.trim(row.getMealName()); + row.setMealName(lastMeal); + } + String combo = StrUtil.trim(row.getComboName()); + if (StrUtil.isBlank(combo)) { + row.setComboName(lastCombo); + } + else if (isSingleComboMarker(combo)) { + row.setComboName(null); + lastCombo = null; + } + else { + lastCombo = combo; + row.setComboName(combo); + } + } + } + + private static boolean isSingleComboMarker(String combo) { + return "无".equals(combo) || "-".equals(combo) || "单品".equals(combo) + || "(无)".equals(combo) || "(无)".equals(combo); + } + + private static Integer parseMeal(String meal) { + if (StrUtil.isBlank(meal)) { + return null; + } + String m = meal.trim(); + if (m.contains("早")) { + return 1; + } + if (m.contains("午") || m.contains("中")) { + return 2; + } + if (m.contains("晚")) { + return 3; + } + return null; + } + + private static LocalDate parseDate(String s) { + if (StrUtil.isBlank(s)) { + return null; + } + try { + return LocalDate.parse(s.trim(), DATE_FMT); + } + catch (DateTimeParseException e) { + return null; + } + } + + private static Integer toFen(BigDecimal yuan) { + if (yuan == null) { + return null; + } + return yuan.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP).intValue(); + } + + /** + * 菜品单价:优先「菜品单价(元)」;未填则单品可回退「套餐价(元)」兼容旧模板;都无则 0。 + */ + private static int resolveDishPriceFen(CanteenDailyMenuImportExcel item) { + Integer dishFen = toFen(item.getDishPriceYuan()); + if (dishFen != null) { + return Math.max(dishFen, 0); + } + Integer legacy = toFen(item.getPriceYuan()); + if (legacy != null && StrUtil.isBlank(item.getComboName())) { + return Math.max(legacy, 0); + } + return 0; + } + + static String cleanDishName(String raw) { + if (raw == null) { + return ""; + } + String s = raw.replace("\r", "\n").trim(); + String first = s.split("\n")[0].trim(); + first = first.replaceAll("[\\p{So}\\p{Cn}]", "").trim(); + String cn = first.replaceAll("[A-Za-z].*$", "").trim(); + cn = cn.replaceAll("[@#%&&]+$", "").replaceAll("\\s+", ""); + if (cn.length() >= 2) { + return cn; + } + return first.replaceAll("\\s+", " ").trim(); + } + + private static class FindOrCreate { + final CanteenSpuEntity spu; + final boolean created; + + FindOrCreate(CanteenSpuEntity spu, boolean created) { + this.spu = spu; + this.created = created; + } + } +} diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenInventoryRecordsServiceImpl.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenInventoryRecordsServiceImpl.java index 10ee55013..0f84c15a2 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenInventoryRecordsServiceImpl.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenInventoryRecordsServiceImpl.java @@ -57,10 +57,8 @@ public class CanteenInventoryRecordsServiceImpl extends ServiceImpl pageScope(Page page, @@ -78,7 +79,7 @@ public class CanteenMealPlanServiceImpl extends ServiceImpl pageScope(Page page, @@ -252,7 +253,9 @@ public class CanteenMenuRecipeServiceImpl extends ServiceImpl deptHolder[0] = d); + Long deptId = deptHolder[0]; String mch = StrUtil.blankToDefault(merchantId, null); Map materialCache = new HashMap<>(); diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenOnlineCategoryServiceImpl.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenOnlineCategoryServiceImpl.java index 98162139d..41c2f2fee 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenOnlineCategoryServiceImpl.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenOnlineCategoryServiceImpl.java @@ -1,15 +1,13 @@ package com.bsd.pjcloud.canteen.service.impl; -import cn.hutool.core.util.ObjectUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.bsd.pjcloud.canteen.entity.CanteenOnlineCategoryEntity; import com.bsd.pjcloud.canteen.mapper.CanteenOnlineCategoryMapper; import com.bsd.pjcloud.canteen.service.CanteenOnlineCategoryService; +import com.bsd.pjcloud.canteen.util.CanteenMerchantScopeHelper; import com.bsd.pjcloud.common.data.datascope.DataScope; -import com.bsd.pjcloud.wallet.api.feign.RemoteMchService; -import com.bsd.pjcloud.wallet.api.vo.MchManageVo; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; @@ -25,7 +23,7 @@ import java.util.List; @AllArgsConstructor public class CanteenOnlineCategoryServiceImpl extends ServiceImpl implements CanteenOnlineCategoryService { - private final RemoteMchService remoteMchService; + private final CanteenMerchantScopeHelper merchantScopeHelper; @Override public Object pageScope(Page page, LambdaQueryWrapper wrapper) { @@ -42,11 +40,7 @@ public class CanteenOnlineCategoryServiceImpl extends ServiceImpllambdaQuery() .eq(CanteenOnlineComboItemEntity::getComboSpuId, entity.getId())); @@ -586,7 +587,7 @@ public class CanteenOnlineSpuServiceImpl extends ServiceImpl canteenCategoryEntityMap = canteenCategoryEntities.stream() .collect(Collectors.toMap(CanteenCategoryEntity::getId, c -> c)); - Long deptId = SecurityUtils.getUser().getDeptId(); + Long deptId = merchantScopeHelper.requireDeptIdByMerchantId(merchantId); for (CanteenOnlineSpuEntity c : canteenSpuEntities) { if (c.getChoiceGroupId() != null) { CanteenOnlineSpuChoiceGroupEntity g = canteenOnlineSpuChoiceGroupService.getById(c.getChoiceGroupId()); diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuImportHelper.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuImportHelper.java new file mode 100644 index 000000000..3df4662e9 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuImportHelper.java @@ -0,0 +1,1188 @@ +package com.bsd.pjcloud.canteen.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.bsd.pjcloud.canteen.constant.StockTypeEnum; +import com.bsd.pjcloud.canteen.dto.CanteenSpuImportExcel; +import com.bsd.pjcloud.canteen.entity.CanteenCategoryEntity; +import com.bsd.pjcloud.canteen.entity.CanteenSpuComboItemEntity; +import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity; +import com.bsd.pjcloud.canteen.entity.CanteenStockEntity; +import com.bsd.pjcloud.canteen.mapper.CanteenCategoryMapper; +import com.bsd.pjcloud.canteen.service.CanteenSpuService; +import com.bsd.pjcloud.canteen.service.CanteenStockService; +import com.bsd.pjcloud.canteen.util.CanteenMerchantScopeHelper; +import com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO; +import com.bsd.pjcloud.common.core.util.R; +import lombok.RequiredArgsConstructor; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.ss.usermodel.DateUtil; +import org.apache.poi.ss.usermodel.FormulaEvaluator; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.ss.util.CellRangeAddress; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * 菜品列表 Excel 导入/导出(单品 + 套餐) + *

+ */ +@Service +@RequiredArgsConstructor +public class CanteenSpuImportHelper { + + private final CanteenSpuService canteenSpuService; + private final CanteenCategoryMapper canteenCategoryMapper; + private final CanteenStockService canteenStockService; + private final CanteenMerchantScopeHelper merchantScopeHelper; + + public List templateRows() { + List list = new ArrayList<>(); + list.add(row("A套餐", "糖醋排骨", "荤菜", "12", "21", "15", 999, 1, "上架", "是", "青莲仔排", + "套餐:同套餐名多行=一套;套餐价只填首行。菜品单价每行可填。名称已存在则按导入方式复用/更新。移动端显示:是=0,否=1")); + list.add(row("A套餐", "蒜苔炒腊肉", "荤菜", "8", "21", "10", 999, 1, "上架", "是", "蒜苔,腊肉", null)); + list.add(row("A套餐", "清炒时蔬", "素菜", "3", "21", "5", 999, 1, "上架", "是", "空心菜", null)); + list.add(row("B套餐", "红烧肉", "荤菜", "10", "18", "12", 999, 1, "上架", "是", "五花肉", + "另一套餐示例")); + list.add(row("B套餐", "土豆丝", "素菜", "4", "18", "5", 999, 1, "上架", "是", "土豆", null)); + list.add(row("无", "千张大骨汤", "汤类", "0", null, "0", 500, 1, "上架", "是", null, + "单品:套餐名填「无」。改价请先导出(含菜品ID)再覆盖导入,勿删ID列")); + list.add(row("无", "豆浆", "早餐", "2", null, "2", 500, 2, "下架", "否", null, null)); + return list; + } + + private static CanteenSpuImportExcel row(String combo, String name, String category, String dishPrice, + String comboPrice, String market, Integer stock, Integer sort, String status, String show, + String desc, String tip) { + CanteenSpuImportExcel r = new CanteenSpuImportExcel(); + r.setComboName(combo); + r.setName(name); + r.setCategoryName(category); + if (StrUtil.isNotBlank(dishPrice)) { + r.setPriceYuan(new BigDecimal(dishPrice)); + } + if (StrUtil.isNotBlank(comboPrice)) { + r.setComboPriceYuan(new BigDecimal(comboPrice)); + } + if (StrUtil.isNotBlank(market)) { + r.setMarketPriceYuan(new BigDecimal(market)); + } + r.setStock(stock); + r.setSort(sort); + r.setStatusName(status); + r.setIsShowName(show); + r.setDescription(desc); + r.setTip(tip); + return r; + } + + public List toExportRows(List list) { + if (CollUtil.isEmpty(list)) { + return List.of(); + } + Map categoryNameMap = loadCategoryNameMap( + list.stream().map(CanteenSpuEntity::getCategoryId).filter(Objects::nonNull).distinct().toList()); + + List comboIds = list.stream().filter(s -> Objects.equals(s.getSpuType(), 2)).map(CanteenSpuEntity::getId) + .toList(); + Map> comboMap = canteenSpuService.loadComboItemVos(comboIds); + List childIds = comboMap.values().stream().flatMap(List::stream) + .map(OnlineSpuComboItemVO::getChildSpuId).filter(Objects::nonNull).distinct().toList(); + Map childCache = new HashMap<>(); + if (CollUtil.isNotEmpty(childIds)) { + for (CanteenSpuEntity c : canteenSpuService.listByIds(childIds)) { + childCache.put(c.getId(), c); + } + categoryNameMap.putAll(loadCategoryNameMap(childCache.values().stream() + .map(CanteenSpuEntity::getCategoryId).filter(Objects::nonNull).distinct().toList())); + } + + List rows = new ArrayList<>(); + for (CanteenSpuEntity spu : list) { + if (spu == null) { + continue; + } + if (Objects.equals(spu.getSpuType(), 2)) { + List items = comboMap.getOrDefault(spu.getId(), List.of()); + if (CollUtil.isEmpty(items)) { + CanteenSpuImportExcel row = baseExportRow(spu, categoryNameMap); + row.setComboName(spu.getName()); + row.setComboId(String.valueOf(spu.getId())); + row.setDishId(null); + row.setName(""); + row.setComboPriceYuan(fenToYuan(spu.getPrice())); + rows.add(row); + continue; + } + for (int i = 0; i < items.size(); i++) { + OnlineSpuComboItemVO item = items.get(i); + CanteenSpuEntity child = childCache.get(item.getChildSpuId()); + CanteenSpuImportExcel row = new CanteenSpuImportExcel(); + row.setComboName(spu.getName()); + row.setComboId(String.valueOf(spu.getId())); + row.setName(item.getName() != null ? item.getName() : (child != null ? child.getName() : null)); + Long childId = child != null ? child.getId() : item.getChildSpuId(); + if (childId != null) { + row.setDishId(String.valueOf(childId)); + } + if (child != null) { + row.setCategoryName(categoryNameMap.get(child.getCategoryId())); + row.setPriceYuan(fenToYuan(child.getPrice())); + row.setMarketPriceYuan(fenToYuan(child.getMarketPrice())); + row.setDescription(child.getDescription()); + } + row.setComboPriceYuan(fenToYuan(spu.getPrice())); + row.setStock(spu.getStock()); + row.setSort(spu.getSort()); + row.setStatusName(statusToName(spu.getStatus())); + row.setIsShowName(showToName(spu.getIsShow())); + if (StrUtil.isBlank(row.getCategoryName())) { + row.setCategoryName(categoryNameMap.get(spu.getCategoryId())); + } + rows.add(row); + } + } + else { + CanteenSpuImportExcel row = baseExportRow(spu, categoryNameMap); + row.setComboName("无"); + rows.add(row); + } + } + return rows; + } + + private CanteenSpuImportExcel baseExportRow(CanteenSpuEntity spu, Map categoryNameMap) { + CanteenSpuImportExcel row = new CanteenSpuImportExcel(); + if (spu.getId() != null) { + row.setDishId(String.valueOf(spu.getId())); + } + row.setName(spu.getName()); + row.setCategoryName(categoryNameMap.get(spu.getCategoryId())); + row.setPriceYuan(fenToYuan(spu.getPrice())); + row.setMarketPriceYuan(fenToYuan(spu.getMarketPrice())); + row.setStock(spu.getStock()); + row.setSort(spu.getSort()); + row.setStatusName(statusToName(spu.getStatus())); + row.setIsShowName(showToName(spu.getIsShow())); + row.setDescription(spu.getDescription()); + return row; + } + + @Transactional(rollbackFor = Exception.class) + public R importExcel(MultipartFile file, Long merchantId, Long defaultCategoryId, boolean cover) + throws IOException { + if (merchantId == null) { + return R.failed("请选择商户"); + } + if (file == null || file.isEmpty()) { + return R.failed("请上传 Excel 文件"); + } + // POI 按列读取:避开 EasyExcel 数字格→DTO 转换丢值问题(单品「菜品单价」常见) + List rows = readRowsByPoi(file); + if (CollUtil.isEmpty(rows)) { + return R.failed("导入数据为空"); + } + normalizeMergedCells(rows); + fillComboLevelFields(rows); + normalizeSinglePriceColumns(rows); + + Long deptId = merchantScopeHelper.requireDeptIdByMerchantId(merchantId); + Map categoryByName = loadCategoryMap(merchantId); + + Map> groups = new LinkedHashMap<>(); + List errors = new ArrayList<>(); + int line = 1; + int singlePriceParsed = 0; + int singlePriceMissing = 0; + for (CanteenSpuImportExcel row : rows) { + line++; + if (isBlankRow(row)) { + continue; + } + String dish = StrUtil.trim(row.getName()); + if (StrUtil.isBlank(dish)) { + errors.add("第" + line + "行:菜品名称不能为空"); + continue; + } + String combo = normalizeComboKey(row.getComboName()); + if (StrUtil.isBlank(combo)) { + if (row.getPriceYuan() != null) { + singlePriceParsed++; + } + else { + singlePriceMissing++; + } + } + groups.computeIfAbsent(combo, k -> new ArrayList<>()).add(row); + } + if (CollUtil.isNotEmpty(errors) && groups.isEmpty()) { + return R.failed(String.join(";", errors.subList(0, Math.min(errors.size(), 20)))); + } + + // 同菜既出现在「单品行」又出现在「套餐组成」时:单品行单价优先(后处理单品,避免被套餐行旧价盖回) + Set standaloneDishIds = new HashSet<>(); + Set standaloneNames = new HashSet<>(); + List singleRows = groups.getOrDefault("", List.of()); + for (CanteenSpuImportExcel s : singleRows) { + Long sid = parseLongId(s.getDishId()); + if (sid != null) { + standaloneDishIds.add(sid); + } + if (StrUtil.isNotBlank(s.getName())) { + standaloneNames.add(s.getName().trim()); + } + } + + ImportCache cache = preloadCache(merchantId, rows); + List pendingUpdates = new ArrayList<>(); + + int created = 0; + int updated = 0; + int skipped = 0; + int priceChanged = 0; + // 先套餐(挂关系),后单品(单价最终以单品行为准) + for (Map.Entry> e : groups.entrySet()) { + if (StrUtil.isBlank(e.getKey())) { + continue; + } + try { + int[] c = upsertCombo(e.getKey(), e.getValue(), merchantId, deptId, defaultCategoryId, categoryByName, + cover, errors, cache, pendingUpdates, standaloneDishIds, standaloneNames); + created += c[0]; + updated += c[1]; + skipped += c[2]; + priceChanged += c[3]; + } + catch (IllegalArgumentException ex) { + errors.add(ex.getMessage()); + } + } + if (CollUtil.isNotEmpty(singleRows)) { + try { + int[] c = upsertSingles(singleRows, merchantId, deptId, defaultCategoryId, categoryByName, cover, + errors, cache, pendingUpdates); + created += c[0]; + updated += c[1]; + skipped += c[2]; + priceChanged += c[3]; + } + catch (IllegalArgumentException ex) { + errors.add(ex.getMessage()); + } + } + flushPendingUpdates(pendingUpdates); + + if (CollUtil.isNotEmpty(errors) && created + updated + skipped == 0) { + return R.failed(String.join(";", errors.subList(0, Math.min(errors.size(), 20)))); + } + String msg = String.format("导入完成:新增 %d,更新 %d,跳过 %d,价格已改 %d", created, updated, skipped, + priceChanged); + if (!cover && skipped > 0) { + msg += ";当前为「新增导入」,同名已跳过。改价格请选「覆盖导入」"; + } + if (cover && singlePriceMissing > 0 && singlePriceParsed == 0) { + msg += ";警告:单品行均未读到「菜品单价(元)」,请确认第4列有数值"; + } + else if (cover && singlePriceMissing > 0) { + msg += ";其中 " + singlePriceMissing + " 行单品未填菜品单价"; + } + if (CollUtil.isNotEmpty(errors)) { + msg += ";部分失败:" + String.join(";", errors.subList(0, Math.min(errors.size(), 10))); + } + return R.ok(null, msg); + } + + /** + * 单品行:若误把市场价填在「套餐价」列,则回填到市场价,避免只改了套餐价列却看菜品价不变的错觉。 + */ + private static void normalizeSinglePriceColumns(List rows) { + for (CanteenSpuImportExcel row : rows) { + if (row == null || !StrUtil.isBlank(normalizeComboKey(row.getComboName()))) { + continue; + } + if (row.getMarketPriceYuan() == null && row.getComboPriceYuan() != null) { + row.setMarketPriceYuan(row.getComboPriceYuan()); + } + } + } + + /** + * 按固定列序读取(跳过表头),金额兼容数字/文本/公式/合并单元格。 + * 列:0套餐名 1菜品名称 2分类 3菜品单价 4套餐价 5市场价 6库存 7排序 8状态 9移动端 10详情 11说明 12菜品ID 13套餐ID + */ + private static List readRowsByPoi(MultipartFile file) throws IOException { + List rows = new ArrayList<>(); + try (Workbook wb = WorkbookFactory.create(file.getInputStream())) { + Sheet sheet = wb.getNumberOfSheets() > 0 ? wb.getSheetAt(0) : null; + if (sheet == null) { + return rows; + } + DataFormatter formatter = new DataFormatter(); + FormulaEvaluator evaluator = wb.getCreationHelper().createFormulaEvaluator(); + int last = sheet.getLastRowNum(); + for (int r = 1; r <= last; r++) { + Row row = sheet.getRow(r); + if (row == null) { + continue; + } + CanteenSpuImportExcel dto = new CanteenSpuImportExcel(); + dto.setComboName(cellString(sheet, row, 0, formatter, evaluator)); + dto.setName(cellString(sheet, row, 1, formatter, evaluator)); + dto.setCategoryName(cellString(sheet, row, 2, formatter, evaluator)); + dto.setPriceYuan(cellDecimal(sheet, row, 3, formatter, evaluator)); + dto.setComboPriceYuan(cellDecimal(sheet, row, 4, formatter, evaluator)); + dto.setMarketPriceYuan(cellDecimal(sheet, row, 5, formatter, evaluator)); + dto.setStock(cellInt(sheet, row, 6, formatter, evaluator)); + dto.setSort(cellInt(sheet, row, 7, formatter, evaluator)); + dto.setStatusName(cellString(sheet, row, 8, formatter, evaluator)); + dto.setIsShowName(cellString(sheet, row, 9, formatter, evaluator)); + dto.setDescription(cellString(sheet, row, 10, formatter, evaluator)); + dto.setTip(cellString(sheet, row, 11, formatter, evaluator)); + dto.setDishId(cellIdString(sheet, row, 12, formatter, evaluator)); + dto.setComboId(cellIdString(sheet, row, 13, formatter, evaluator)); + rows.add(dto); + } + } + return rows; + } + + private static Cell resolveCell(Sheet sheet, Row row, int col) { + if (row == null) { + return null; + } + int rowIdx = row.getRowNum(); + List merged = sheet.getMergedRegions(); + if (CollUtil.isNotEmpty(merged)) { + for (CellRangeAddress range : merged) { + if (range.isInRange(rowIdx, col)) { + Row first = sheet.getRow(range.getFirstRow()); + return first == null ? null : first.getCell(range.getFirstColumn()); + } + } + } + return row.getCell(col); + } + + private static String cellString(Sheet sheet, Row row, int col, DataFormatter formatter, + FormulaEvaluator evaluator) { + Cell cell = resolveCell(sheet, row, col); + if (cell == null || cell.getCellType() == CellType.BLANK) { + return null; + } + String v = formatter.formatCellValue(cell, evaluator); + return StrUtil.isBlank(v) ? null : v.trim(); + } + + /** 雪花ID:优先按文本读;数字格用 BigDecimal 避免科学计数法截断 */ + private static String cellIdString(Sheet sheet, Row row, int col, DataFormatter formatter, + FormulaEvaluator evaluator) { + Cell cell = resolveCell(sheet, row, col); + if (cell == null || cell.getCellType() == CellType.BLANK) { + return null; + } + CellType type = cell.getCellType(); + if (type == CellType.FORMULA) { + type = cell.getCachedFormulaResultType(); + } + if (type == CellType.NUMERIC && !DateUtil.isCellDateFormatted(cell)) { + return BigDecimal.valueOf(cell.getNumericCellValue()).toPlainString().split("\\.")[0]; + } + String v = formatter.formatCellValue(cell, evaluator); + if (StrUtil.isBlank(v)) { + return null; + } + v = v.trim().replace(",", ""); + if (v.endsWith(".0")) { + v = v.substring(0, v.length() - 2); + } + return v; + } + + private static Long parseLongId(String raw) { + if (StrUtil.isBlank(raw)) { + return null; + } + String n = raw.trim().replace(",", ""); + if (n.endsWith(".0")) { + n = n.substring(0, n.length() - 2); + } + try { + return new BigDecimal(n).longValueExact(); + } + catch (Exception ex) { + return null; + } + } + + private static BigDecimal cellDecimal(Sheet sheet, Row row, int col, DataFormatter formatter, + FormulaEvaluator evaluator) { + Cell cell = resolveCell(sheet, row, col); + if (cell == null || cell.getCellType() == CellType.BLANK) { + return null; + } + CellType type = cell.getCellType(); + if (type == CellType.FORMULA) { + type = cell.getCachedFormulaResultType(); + } + if (type == CellType.NUMERIC && !DateUtil.isCellDateFormatted(cell)) { + return BigDecimal.valueOf(cell.getNumericCellValue()); + } + String raw = formatter.formatCellValue(cell, evaluator); + return parseYuan(raw); + } + + private static Integer cellInt(Sheet sheet, Row row, int col, DataFormatter formatter, + FormulaEvaluator evaluator) { + BigDecimal bd = cellDecimal(sheet, row, col, formatter, evaluator); + return bd == null ? null : bd.setScale(0, RoundingMode.HALF_UP).intValue(); + } + + private static BigDecimal parseYuan(String raw) { + if (StrUtil.isBlank(raw)) { + return null; + } + String n = raw.trim().replace(",", "").replace("¥", "").replace("¥", "").replace("元", ""); + if (n.isEmpty()) { + return null; + } + try { + return new BigDecimal(n); + } + catch (NumberFormatException ex) { + return null; + } + } + + /** @return [created, updated, skipped, priceChanged] */ + private int[] upsertSingles(List items, Long merchantId, Long deptId, + Long defaultCategoryId, Map categoryByName, boolean cover, List errors, + ImportCache cache, List pendingUpdates) { + int created = 0; + int updated = 0; + int skipped = 0; + int priceChanged = 0; + for (CanteenSpuImportExcel item : items) { + try { + Long categoryId = requireOrCreateCategory(item, defaultCategoryId, categoryByName, merchantId, + deptId); + List exists = resolveDishes(merchantId, item, cache); + if (CollUtil.isNotEmpty(exists)) { + if (!cover) { + skipped++; + continue; + } + Integer dishPriceFen = resolveDishPriceFen(item); + for (CanteenSpuEntity exist : exists) { + Integer oldPrice = exist.getPrice(); + applyDish(exist, item, categoryId, deptId, merchantId); + if (dishPriceFen != null) { + exist.setPrice(dishPriceFen); + Integer marketFen = toFen(item.getMarketPriceYuan()); + exist.setMarketPrice(marketFen != null ? marketFen : dishPriceFen); + } + queueUpdate(pendingUpdates, exist); + if (dishPriceFen != null && !Objects.equals(oldPrice, dishPriceFen)) { + priceChanged++; + } + updated++; + } + } + else { + CanteenSpuEntity spu = newDish(merchantId, deptId, categoryId, item); + canteenSpuService.save(spu); + createStock(spu); + cache.put(spu); + created++; + if (resolveDishPriceFen(item) != null) { + priceChanged++; + } + } + } + catch (IllegalArgumentException ex) { + errors.add(ex.getMessage()); + } + } + return new int[] { created, updated, skipped, priceChanged }; + } + + /** @return [created, updated, skipped, priceChanged] */ + private int[] upsertCombo(String comboName, List items, Long merchantId, Long deptId, + Long defaultCategoryId, Map categoryByName, boolean cover, List errors, + ImportCache cache, List pendingUpdates, Set standaloneDishIds, + Set standaloneNames) { + List children = new ArrayList<>(); + int created = 0; + int updated = 0; + int skipped = 0; + int priceChanged = 0; + for (CanteenSpuImportExcel item : items) { + try { + Long categoryId = requireOrCreateCategory(item, defaultCategoryId, categoryByName, merchantId, + deptId); + List exists = resolveDishes(merchantId, item, cache); + if (CollUtil.isNotEmpty(exists)) { + CanteenSpuEntity exist = exists.get(0); + boolean dishOwnedBySingleRow = standaloneDishIds.contains(exist.getId()) + || (StrUtil.isNotBlank(exist.getName()) && standaloneNames.contains(exist.getName().trim())); + if (cover && !dishOwnedBySingleRow) { + Integer oldPrice = exist.getPrice(); + applyDish(exist, item, categoryId, deptId, merchantId); + Integer dishPriceFen = resolveDishPriceFen(item); + if (dishPriceFen != null) { + exist.setPrice(dishPriceFen); + Integer marketFen = toFen(item.getMarketPriceYuan()); + exist.setMarketPrice(marketFen != null ? marketFen : dishPriceFen); + } + queueUpdate(pendingUpdates, exist); + if (dishPriceFen != null && !Objects.equals(oldPrice, dishPriceFen)) { + priceChanged++; + } + updated++; + } + else if (!cover) { + skipped++; + } + children.add(exist); + } + else { + CanteenSpuEntity spu = newDish(merchantId, deptId, categoryId, item); + canteenSpuService.save(spu); + createStock(spu); + cache.put(spu); + children.add(spu); + created++; + if (resolveDishPriceFen(item) != null) { + priceChanged++; + } + } + } + catch (IllegalArgumentException ex) { + errors.add(ex.getMessage()); + } + } + if (children.isEmpty()) { + throw new IllegalArgumentException("套餐「" + comboName + "」至少需要 1 个组成菜品"); + } + + CanteenSpuImportExcel first = items.get(0); + Long comboCategoryId = requireOrCreateCategory(first, defaultCategoryId, categoryByName, merchantId, deptId); + Integer comboPriceFen = firstNonNullComboPriceFen(items); + Integer comboMarketFen = firstNonNullMarketFen(items); + if (comboMarketFen == null) { + comboMarketFen = comboPriceFen; + } + + List comboItems = new ArrayList<>(); + int sort = 0; + for (CanteenSpuEntity child : children) { + CanteenSpuComboItemEntity line = new CanteenSpuComboItemEntity(); + line.setChildSpuId(child.getId()); + line.setQuantity(1); + line.setSortOrder(sort++); + comboItems.add(line); + } + + CanteenSpuEntity existCombo = resolveCombo(merchantId, comboName, first.getComboId(), cache); + if (existCombo != null) { + if (!cover) { + skipped++; + return new int[] { created, updated, skipped, priceChanged }; + } + Integer oldComboPrice = existCombo.getPrice(); + existCombo.setCategoryId(comboCategoryId); + existCombo.setDeptId(deptId); + if (comboPriceFen != null) { + existCombo.setPrice(comboPriceFen); + } + if (comboMarketFen != null) { + existCombo.setMarketPrice(comboMarketFen); + } + else if (comboPriceFen != null) { + existCombo.setMarketPrice(comboPriceFen); + } + existCombo.setStock(first.getStock() != null ? first.getStock() : existCombo.getStock()); + existCombo.setSort(first.getSort() != null ? first.getSort() : existCombo.getSort()); + if (StrUtil.isNotBlank(first.getStatusName())) { + existCombo.setStatus(parseStatus(first.getStatusName())); + } + if (StrUtil.isNotBlank(first.getIsShowName())) { + existCombo.setIsShow(parseIsShow(first.getIsShowName())); + } + existCombo.setSpuType(2); + existCombo.setComboItems(comboItems); + if (existCombo.getPicUrl() == null) { + existCombo.setPicUrl(""); + } + canteenSpuService.updateDeep(existCombo); + cache.put(existCombo); + if (comboPriceFen != null && !Objects.equals(oldComboPrice, comboPriceFen)) { + priceChanged++; + } + updated++; + } + else { + CanteenSpuEntity combo = newBase(merchantId, deptId, comboCategoryId, comboName, 2); + combo.setPrice(comboPriceFen != null ? comboPriceFen : 0); + combo.setMarketPrice(comboMarketFen != null ? comboMarketFen : combo.getPrice()); + combo.setStock(first.getStock() != null ? first.getStock() : 9999); + combo.setSort(first.getSort() != null ? first.getSort() : 0); + combo.setStatus(parseStatus(first.getStatusName())); + combo.setIsShow(parseIsShow(first.getIsShowName())); + combo.setIntroduction(comboName); + combo.setComboItems(comboItems); + canteenSpuService.saveDeep(combo); + createStock(combo); + cache.put(combo); + created++; + if (comboPriceFen != null) { + priceChanged++; + } + } + return new int[] { created, updated, skipped, priceChanged }; + } + + /** 单品单价:菜品单价优先;为空时用市场价 */ + private static Integer resolveDishPriceFen(CanteenSpuImportExcel item) { + Integer dish = toFen(item.getPriceYuan()); + if (dish != null) { + return dish; + } + return toFen(item.getMarketPriceYuan()); + } + + private ImportCache preloadCache(Long merchantId, List rows) { + Set ids = new HashSet<>(); + Set names = new HashSet<>(); + for (CanteenSpuImportExcel row : rows) { + if (row == null) { + continue; + } + Long dishId = parseLongId(row.getDishId()); + if (dishId != null) { + ids.add(dishId); + } + Long comboId = parseLongId(row.getComboId()); + if (comboId != null) { + ids.add(comboId); + } + if (StrUtil.isNotBlank(row.getName())) { + names.add(row.getName().trim()); + } + String comboKey = normalizeComboKey(row.getComboName()); + if (StrUtil.isNotBlank(comboKey)) { + names.add(comboKey); + } + } + ImportCache cache = new ImportCache(); + if (CollUtil.isNotEmpty(ids)) { + for (CanteenSpuEntity e : canteenSpuService.listByIds(ids)) { + if (e != null && Objects.equals(e.getMerchantId(), merchantId)) { + cache.put(e); + } + } + } + if (CollUtil.isNotEmpty(names)) { + List byName = canteenSpuService.list(Wrappers.lambdaQuery() + .eq(CanteenSpuEntity::getMerchantId, merchantId) + .in(CanteenSpuEntity::getName, names)); + for (CanteenSpuEntity e : byName) { + cache.put(e); + } + } + return cache; + } + + private static void queueUpdate(List pending, CanteenSpuEntity exist) { + exist.setPicUrl(StrUtil.nullToEmpty(exist.getPicUrl())); + pending.add(exist); + } + + private void flushPendingUpdates(List pending) { + if (CollUtil.isEmpty(pending)) { + return; + } + Map latest = new LinkedHashMap<>(); + for (CanteenSpuEntity e : pending) { + if (e != null && e.getId() != null) { + latest.put(e.getId(), e); + } + } + List list = new ArrayList<>(latest.values()); + final int batch = 200; + for (int i = 0; i < list.size(); i += batch) { + List part = list.subList(i, Math.min(i + batch, list.size())); + // price/marketPrice 已 ALWAYS;批量更新减少往返 + canteenSpuService.updateBatchById(part); + } + pending.clear(); + } + + private List resolveDishes(Long merchantId, CanteenSpuImportExcel item, ImportCache cache) { + Long id = parseLongId(item.getDishId()); + if (id != null) { + CanteenSpuEntity byId = cache.getById(id); + if (byId == null) { + byId = canteenSpuService.getById(id); + if (byId != null) { + cache.put(byId); + } + } + if (byId != null && Objects.equals(byId.getMerchantId(), merchantId) + && !Objects.equals(byId.getSpuType(), 2)) { + return List.of(byId); + } + } + return cache.findDishesByName(item.getName()); + } + + private CanteenSpuEntity resolveCombo(Long merchantId, String comboName, String comboIdRaw, ImportCache cache) { + Long id = parseLongId(comboIdRaw); + if (id != null) { + CanteenSpuEntity byId = cache.getById(id); + if (byId == null) { + byId = canteenSpuService.getById(id); + if (byId != null) { + cache.put(byId); + } + } + if (byId != null && Objects.equals(byId.getMerchantId(), merchantId) + && Objects.equals(byId.getSpuType(), 2)) { + return byId; + } + } + return cache.findComboByName(comboName); + } + + /** 导入期内存缓存,减少逐行查库 */ + private static final class ImportCache { + private final Map byId = new HashMap<>(); + private final Map> dishesByName = new HashMap<>(); + private final Map comboByName = new HashMap<>(); + + void put(CanteenSpuEntity e) { + if (e == null || e.getId() == null) { + return; + } + byId.put(e.getId(), e); + String name = StrUtil.trim(e.getName()); + if (StrUtil.isBlank(name)) { + return; + } + if (Objects.equals(e.getSpuType(), 2)) { + comboByName.putIfAbsent(name, e); + } + else { + dishesByName.computeIfAbsent(name, k -> new ArrayList<>()); + List list = dishesByName.get(name); + list.removeIf(x -> Objects.equals(x.getId(), e.getId())); + list.add(e); + } + } + + CanteenSpuEntity getById(Long id) { + return id == null ? null : byId.get(id); + } + + List findDishesByName(String name) { + if (StrUtil.isBlank(name)) { + return List.of(); + } + List list = dishesByName.get(name.trim()); + return list == null ? List.of() : List.copyOf(list); + } + + CanteenSpuEntity findComboByName(String name) { + if (StrUtil.isBlank(name)) { + return null; + } + return comboByName.get(name.trim()); + } + } + + private Long requireOrCreateCategory(CanteenSpuImportExcel item, Long defaultCategoryId, + Map map, Long merchantId, Long deptId) { + String categoryName = StrUtil.trim(item.getCategoryName()); + if (StrUtil.isNotBlank(categoryName)) { + Long id = map.get(categoryName); + if (id != null) { + return id; + } + CanteenCategoryEntity cat = new CanteenCategoryEntity(); + cat.setName(categoryName); + cat.setMerchantId(merchantId); + cat.setDeptId(deptId); + cat.setParentId(0L); + cat.setSort(0); + cat.setStatus(1); + cat.setPicUrl(""); + // 导入批量建分类:直接 insert,避免每条都打 MQTT + canteenCategoryMapper.insert(cat); + Long newId = cat.getId(); + map.put(categoryName, newId); + return newId; + } + if (defaultCategoryId != null) { + return defaultCategoryId; + } + throw new IllegalArgumentException( + "分类为空且未选默认分类(菜品:" + StrUtil.blankToDefault(item.getName(), "") + ")"); + } + + private CanteenSpuEntity newDish(Long merchantId, Long deptId, Long categoryId, CanteenSpuImportExcel item) { + CanteenSpuEntity spu = newBase(merchantId, deptId, categoryId, StrUtil.trim(item.getName()), 1); + applyDish(spu, item, categoryId, deptId, merchantId); + return spu; + } + + private CanteenSpuEntity newBase(Long merchantId, Long deptId, Long categoryId, String name, int spuType) { + CanteenSpuEntity spu = new CanteenSpuEntity(); + spu.setName(name); + spu.setMerchantId(merchantId); + spu.setDeptId(deptId); + spu.setCategoryId(categoryId); + spu.setPicUrl(""); + spu.setSpuType(spuType); + spu.setSpecType(Boolean.FALSE); + spu.setSalesCount(0); + spu.setVirtualSalesCount(0); + spu.setBrowseCount(0); + spu.setCostPrice(0); + spu.setSystemType(2L); + spu.setStatus(0); + spu.setIsShow("0"); + spu.setStock(9999); + spu.setSort(0); + return spu; + } + + private void applyDish(CanteenSpuEntity spu, CanteenSpuImportExcel row, Long categoryId, Long deptId, + Long merchantId) { + spu.setName(StrUtil.trim(row.getName())); + spu.setMerchantId(merchantId); + spu.setDeptId(deptId); + spu.setCategoryId(categoryId); + spu.setSpuType(1); + Integer priceFen = toFen(row.getPriceYuan()); + if (priceFen != null) { + spu.setPrice(priceFen); + } + else if (spu.getPrice() == null) { + spu.setPrice(0); + } + Integer marketFen = toFen(row.getMarketPriceYuan()); + if (marketFen != null) { + spu.setMarketPrice(marketFen); + } + else if (priceFen != null) { + spu.setMarketPrice(priceFen); + } + else if (spu.getMarketPrice() == null) { + spu.setMarketPrice(spu.getPrice() != null ? spu.getPrice() : 0); + } + if (row.getStock() != null) { + spu.setStock(row.getStock()); + } + else if (spu.getStock() == null) { + spu.setStock(9999); + } + if (row.getSort() != null) { + spu.setSort(row.getSort()); + } + else if (spu.getSort() == null) { + spu.setSort(0); + } + if (StrUtil.isNotBlank(row.getStatusName())) { + spu.setStatus(parseStatus(row.getStatusName())); + } + else if (spu.getStatus() == null) { + spu.setStatus(0); + } + if (StrUtil.isNotBlank(row.getIsShowName())) { + spu.setIsShow(parseIsShow(row.getIsShowName())); + } + else if (spu.getIsShow() == null) { + spu.setIsShow("0"); + } + if (row.getDescription() != null) { + spu.setDescription(StrUtil.trim(row.getDescription())); + } + spu.setPicUrl(StrUtil.nullToEmpty(spu.getPicUrl())); + } + + private void createStock(CanteenSpuEntity spu) { + CanteenStockEntity stock = new CanteenStockEntity(); + stock.setItemId(spu.getId()); + stock.setItemType(StockTypeEnum.DISH.getType()); + stock.setBaseStock(0D); + stock.setInventoryUpperLimit(0); + stock.setInventoryLowerLimit(0); + stock.setCurrentStock(spu.getStock() == null ? 0D : spu.getStock().doubleValue()); + stock.setDeptId(spu.getDeptId()); + canteenStockService.save(stock); + } + + /** + * 合并单元格:套餐名向下填充;「无」表示单品。 + */ + private static void normalizeMergedCells(List rows) { + String lastCombo = null; + for (CanteenSpuImportExcel row : rows) { + if (isBlankRow(row)) { + continue; + } + String combo = StrUtil.trim(row.getComboName()); + if (StrUtil.isBlank(combo)) { + row.setComboName(lastCombo); + } + else if (isSingleMarker(combo)) { + row.setComboName(null); + lastCombo = null; + } + else { + lastCombo = combo; + row.setComboName(combo); + } + } + } + + /** + * 套餐级字段(套餐价/库存/排序/状态/移动端)取组内首个非空,回填到组内各行,避免合并单元格只改了非首行导致不更新。 + */ + private static void fillComboLevelFields(List rows) { + int i = 0; + while (i < rows.size()) { + CanteenSpuImportExcel cur = rows.get(i); + if (isBlankRow(cur)) { + i++; + continue; + } + String key = normalizeComboKey(cur.getComboName()); + if (StrUtil.isBlank(key)) { + i++; + continue; + } + int j = i + 1; + while (j < rows.size()) { + CanteenSpuImportExcel next = rows.get(j); + if (isBlankRow(next)) { + j++; + continue; + } + if (!key.equals(normalizeComboKey(next.getComboName()))) { + break; + } + j++; + } + String comboPrice = null; + String comboId = null; + Integer stock = null; + Integer sort = null; + String status = null; + String show = null; + for (int k = i; k < j; k++) { + CanteenSpuImportExcel r = rows.get(k); + if (isBlankRow(r)) { + continue; + } + if (comboPrice == null && r.getComboPriceYuan() != null) { + comboPrice = r.getComboPriceYuan().toPlainString(); + } + if (comboId == null && StrUtil.isNotBlank(r.getComboId())) { + comboId = r.getComboId().trim(); + } + if (stock == null && r.getStock() != null) { + stock = r.getStock(); + } + if (sort == null && r.getSort() != null) { + sort = r.getSort(); + } + if (status == null && StrUtil.isNotBlank(r.getStatusName())) { + status = r.getStatusName().trim(); + } + if (show == null && StrUtil.isNotBlank(r.getIsShowName())) { + show = r.getIsShowName().trim(); + } + } + for (int k = i; k < j; k++) { + CanteenSpuImportExcel r = rows.get(k); + if (isBlankRow(r)) { + continue; + } + if (comboPrice != null) { + r.setComboPriceYuan(new BigDecimal(comboPrice)); + } + if (comboId != null) { + r.setComboId(comboId); + } + if (stock != null) { + r.setStock(stock); + } + if (sort != null) { + r.setSort(sort); + } + if (status != null) { + r.setStatusName(status); + } + if (show != null) { + r.setIsShowName(show); + } + } + i = j; + } + } + + private static Integer firstNonNullComboPriceFen(List items) { + for (CanteenSpuImportExcel item : items) { + Integer fen = toFen(item.getComboPriceYuan()); + if (fen != null) { + return fen; + } + } + return null; + } + + private static Integer firstNonNullMarketFen(List items) { + for (CanteenSpuImportExcel item : items) { + Integer fen = toFen(item.getMarketPriceYuan()); + if (fen != null) { + return fen; + } + } + return null; + } + + private static String normalizeComboKey(String comboName) { + if (StrUtil.isBlank(comboName) || isSingleMarker(comboName)) { + return ""; + } + return comboName.trim(); + } + + private static boolean isSingleMarker(String combo) { + return "无".equals(combo) || "-".equals(combo) || "单品".equals(combo) + || "(无)".equals(combo) || "(无)".equals(combo); + } + + private Map loadCategoryNameMap(List categoryIds) { + Map map = new HashMap<>(); + if (CollUtil.isEmpty(categoryIds)) { + return map; + } + List cats = canteenCategoryMapper.selectList( + Wrappers.lambdaQuery().in(CanteenCategoryEntity::getId, categoryIds)); + if (CollUtil.isNotEmpty(cats)) { + for (CanteenCategoryEntity c : cats) { + map.put(c.getId(), c.getName()); + } + } + return map; + } + + private Map loadCategoryMap(Long merchantId) { + List list = canteenCategoryMapper.selectList(Wrappers.lambdaQuery() + .eq(CanteenCategoryEntity::getMerchantId, merchantId)); + Map map = new HashMap<>(); + if (CollUtil.isEmpty(list)) { + return map; + } + for (CanteenCategoryEntity c : list) { + if (c != null && StrUtil.isNotBlank(c.getName()) && c.getId() != null) { + map.put(c.getName().trim(), c.getId()); + } + } + return map; + } + + private static boolean isBlankRow(CanteenSpuImportExcel row) { + return StrUtil.isBlank(row.getName()) && StrUtil.isBlank(row.getComboName()) + && StrUtil.isBlank(row.getCategoryName()) && row.getPriceYuan() == null; + } + + /** 元 → 分;空白返回 null */ + private static Integer toFen(BigDecimal yuan) { + if (yuan == null) { + return null; + } + return Math.max(yuan.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP).intValue(), 0); + } + + private static BigDecimal fenToYuan(Integer fen) { + if (fen == null) { + return null; + } + return BigDecimal.valueOf(fen).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); + } + + private static int parseStatus(String name) { + if (StrUtil.isBlank(name)) { + return 0; + } + String n = name.trim(); + if (n.contains("回收") || "-1".equals(n)) { + return -1; + } + // 食堂约定(与 PC 字典 spu_status、表单默认一致):0=上架,1=下架 + if (n.contains("下") || "1".equals(n)) { + return 1; + } + return 0; + } + + private static String statusToName(Integer status) { + if (status == null || status == 0) { + return "上架"; + } + if (status == 1) { + return "下架"; + } + if (status == -1) { + return "回收"; + } + return "上架"; + } + + /** + * 食堂约定(与每日菜谱 is_show=0 一致):0=移动端显示(是),1=不显示(否)。 + */ + private static String parseIsShow(String name) { + if (StrUtil.isBlank(name)) { + return "0"; + } + String n = name.trim(); + if ("否".equals(n) || "1".equals(n) || "N".equalsIgnoreCase(n) || "false".equalsIgnoreCase(n) + || "不显示".equals(n) || "隐藏".equals(n)) { + return "1"; + } + return "0"; + } + + /** 0/空 → 是;1 → 否 */ + private static String showToName(String isShow) { + if ("1".equals(isShow)) { + return "否"; + } + return "是"; + } +} diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuServiceImpl.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuServiceImpl.java index a6022dbc4..209134287 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuServiceImpl.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuServiceImpl.java @@ -134,6 +134,31 @@ public class CanteenSpuServiceImpl extends ServiceImpl listForExport(String name, Long categoryId, Long merchantId, Long[] ids) { + // 与 selectPage(菜品列表页)保持同一套筛选,避免导出比列表多/变成“全部” + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.like(StrUtil.isNotBlank(name), CanteenSpuEntity::getName, name); + wrapper.eq(Objects.nonNull(categoryId), CanteenSpuEntity::getCategoryId, categoryId); + merchantScopeHelper.applyMerchantIdScope(wrapper, merchantId, CanteenSpuEntity::getMerchantId); + + LambdaQueryWrapper onlineQw = new LambdaQueryWrapper<>(); + onlineQw.isNotNull(CanteenOnlineSpuEntity::getLinkSpuId); + merchantScopeHelper.applyMerchantIdScope(onlineQw, merchantId, CanteenOnlineSpuEntity::getMerchantId); + List onlineList = canteenOnlineSpuMapper.selectListByScope(onlineQw, DataScope.of()); + Set linkedIds = onlineList.stream() + .map(CanteenOnlineSpuEntity::getLinkSpuId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + wrapper.notIn(!linkedIds.isEmpty(), CanteenSpuEntity::getId, linkedIds); + + if (ids != null && ids.length > 0) { + wrapper.in(CanteenSpuEntity::getId, Arrays.asList(ids)); + } + wrapper.orderByAsc(CanteenSpuEntity::getSort).orderByDesc(CanteenSpuEntity::getId); + return baseMapper.selectListByScope(wrapper, DataScope.of()); + } + @Override public Object pageDailyScope(Page page, CanteenDailySpuDTO canteenDailySpuDTO) { LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); @@ -297,6 +322,12 @@ public class CanteenSpuServiceImpl extends ServiceImpl有 merchantId 时必须能解析到商户部门,失败直接抛错,禁止静默落成管理员部门。 */ public void fillDeptIdByMerchantId(Long merchantId, Consumer deptIdSetter) { if (merchantId == null || deptIdSetter == null) { return; } + deptIdSetter.accept(requireDeptIdByMerchantId(merchantId)); + } + + /** + * merchantId 为字符串时的回填(排菜菜谱 / 排餐计划等) + */ + public void fillDeptIdByMerchantId(String merchantId, Consumer deptIdSetter) { + if (StrUtil.isBlank(merchantId) || deptIdSetter == null) { + return; + } + try { + fillDeptIdByMerchantId(Long.parseLong(merchantId.trim()), deptIdSetter); + } + catch (NumberFormatException e) { + throw new IllegalArgumentException("商户ID格式错误:" + merchantId); + } + } + + /** + * 有商户则写商户部门;无商户则写当前登录人部门(仅商户可选场景) + */ + public void fillDeptIdByMerchantIdOrCurrentUser(Long merchantId, Consumer deptIdSetter) { + if (deptIdSetter == null) { + return; + } + if (merchantId != null) { + deptIdSetter.accept(requireDeptIdByMerchantId(merchantId)); + return; + } + PjcloudUser user = SecurityUtils.getUser(); + if (user == null || user.getDeptId() == null) { + throw new IllegalArgumentException("无法确定所属部门,请选择商户或检查账号部门"); + } + deptIdSetter.accept(user.getDeptId()); + } + + public void fillDeptIdByMerchantIdOrCurrentUser(String merchantId, Consumer deptIdSetter) { + if (StrUtil.isBlank(merchantId)) { + fillDeptIdByMerchantIdOrCurrentUser((Long) null, deptIdSetter); + return; + } + try { + fillDeptIdByMerchantIdOrCurrentUser(Long.parseLong(merchantId.trim()), deptIdSetter); + } + catch (NumberFormatException e) { + throw new IllegalArgumentException("商户ID格式错误:" + merchantId); + } + } + + /** + * 解析商户绑定部门;查不到或未配置部门时抛错,避免写入当前管理员 dept_id + */ + public Long requireDeptIdByMerchantId(Long merchantId) { + if (merchantId == null) { + throw new IllegalArgumentException("商户不能为空"); + } try { R r = remoteMchService.getById(merchantId); - if (r != null && r.isOk() && ObjectUtil.isNotEmpty(r.getData()) && r.getData().getDeptId() != null) { - deptIdSetter.accept(r.getData().getDeptId()); + if (r == null || !r.isOk() || ObjectUtil.isEmpty(r.getData())) { + throw new IllegalArgumentException("商户不存在:" + merchantId); } + Long deptId = r.getData().getDeptId(); + if (deptId == null) { + throw new IllegalArgumentException("商户未绑定部门,请先在商户管理中配置部门:" + merchantId); + } + return deptId; + } + catch (IllegalArgumentException e) { + throw e; } catch (Exception e) { log.warn("按商户回填部门失败, merchantId={}", merchantId, e); + throw new IllegalArgumentException("按商户获取部门失败,请稍后重试"); } } diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/UserExcelVO.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/UserExcelVO.java index f3d5b49b7..694e3e307 100644 --- a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/UserExcelVO.java +++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/UserExcelVO.java @@ -101,9 +101,9 @@ public class UserExcelVO implements Serializable { private String staffingTypeParse; /** - * 锁定标记 + * 启用状态(导出:启用/禁用;导入仍兼容 0/9) */ - @ExcelProperty("锁定标记,0:正常,9:已锁定") + @ExcelProperty("启用状态") private String lockFlag; /** diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserImportExportServiceImpl.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserImportExportServiceImpl.java index 02dafddd8..e2e6b12bc 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserImportExportServiceImpl.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserImportExportServiceImpl.java @@ -166,6 +166,13 @@ public class SysUserImportExportServiceImpl implements SysUserImportExportServic excelVO.setStaffingType(staffingType.getDescription()); } } + // 启用状态:0=启用,9=禁用(与列表开关一致) + if ("0".equals(userVO.getLockFlag())) { + excelVO.setLockFlag("启用"); + } + else if ("9".equals(userVO.getLockFlag())) { + excelVO.setLockFlag("禁用"); + } return excelVO; }).toList(); } @@ -729,7 +736,7 @@ public class SysUserImportExportServiceImpl implements SysUserImportExportServic user.setNickname(excel.getNickname()); user.setName(excel.getName()); user.setEmail(excel.getEmail()); - user.setLockFlag(excel.getLockFlag()); + user.setLockFlag(parseLockFlag(excel.getLockFlag())); user.setStaffingType(excel.getStaffingTypeParse()); user.setPasswordModifyTime(LocalDateTime.now()); user.setPassword(excel.getPhone()); @@ -739,6 +746,21 @@ public class SysUserImportExportServiceImpl implements SysUserImportExportServic sysUserService.saveUser(user); } + /** 启用状态:启用/0 → 0;禁用/9 → 9;空默认启用 */ + private static String parseLockFlag(String raw) { + if (StrUtil.isBlank(raw)) { + return "0"; + } + String v = raw.trim(); + if ("启用".equals(v) || "正常".equals(v) || "0".equals(v)) { + return "0"; + } + if ("禁用".equals(v) || "已锁定".equals(v) || "锁定".equals(v) || "9".equals(v)) { + return "9"; + } + return v; + } + /** * 创建学生用户及监护关系,学生扩展信息只属于 STUDENT 身份。 * diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserMapper.xml b/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserMapper.xml index 45feeaad8..2e7c36245 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserMapper.xml +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserMapper.xml @@ -219,6 +219,9 @@ AND u.staffing_type = #{query.staffingType} + + AND u.lock_flag = #{query.lockFlag} + and urole.role_id IN @@ -300,13 +303,33 @@ LEFT JOIN sys_user_role urole ON urole.user_id = u.user_id u.del_flag = '0' + + AND u.user_id = #{query.userId} + + + AND u.process_status = #{query.processStatus} + AND u.username LIKE #{usernameLike} + + + AND u.name LIKE #{nameLike} + AND u.dept_id = #{query.deptId} + + AND u.staffing_type = #{query.staffingType} + + + AND u.lock_flag = #{query.lockFlag} + + + + AND u.phone LIKE #{phoneLike} + and urole.role_id IN @@ -335,6 +358,12 @@ #{id} + + and u.user_id IN + + #{v} + + group by u.user_id