feat: 各模块支持导入导出及用户列表优化

* canteen
  - 每日菜谱:支持模板下载与数据导出
  - 菜品列表:支持模板下载与数据导出

* umps
  - 用户管理:筛选条件增加「启用状态」
  - 用户列表导出:新增「启用状态」字段

fix(canteen): 修复管理员维护其他商户时的部门归属问题

管理员账号维护非本商户数据时,部门统一归属到当前商户部门,
避免越权及数据归属混乱。
This commit is contained in:
zjf committed 2026-08-14 16:23:01 +08:00
1 parent 3989a4f95b
commit deb28ffb22
28 files changed
+2402 -67

No files matched your search

@@ -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;
}
@@ -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<String> ids;
}
@@ -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 导入/导出/模板行(单品 + 套餐)
* <p>套餐名留空或「无」=单品;套餐名相同多行=同一套餐的组成菜。
* <p>导出含「菜品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;
}
@@ -60,6 +60,7 @@ public class CanteenSpuEntity extends Model<CanteenSpuEntity> {
/**
* 商品封面图
*/
@TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS)
@Schema(description="商品封面图")
private String picUrl;
@@ -78,9 +79,9 @@ public class CanteenSpuEntity extends Model<CanteenSpuEntity> {
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<CanteenSpuEntity> {
/**
* 商品价格,单位使用:分
*/
@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;
/**
@@ -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 金额(元):兼容数字单元格与文本单元格。
* <p>字段上指定本 converter 后,按单元格实际类型解析,不依赖 supportExcelTypeKey 匹配。
*/
public class ExcelYuanConverter implements Converter<BigDecimal> {
@Override
public Class<BigDecimal> 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());
}
}
@@ -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));
}
/**
@@ -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());
}
}
/**
@@ -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<CanteenDailyMenuImportExcel> 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
@@ -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));
}
/**
@@ -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("修改排菜菜谱")
@@ -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) {
@@ -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<CanteenSpuImportExcel> 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<CanteenSpuEntity> export(CanteenSpuEntity canteenSpu, Long[] ids) {
return canteenSpuService.list(Wrappers.lambdaQuery(canteenSpu).in(ArrayUtil.isNotEmpty(ids), CanteenSpuEntity::getId, ids));
public List<CanteenSpuImportExcel> 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;
}
}
@@ -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;
/**
* 每日菜谱导入模板:套餐多行合并日期/餐段/套餐名/套餐价/说明;菜品单价不合并。
* <p>列:0日期 1餐段 2套餐名 3菜品名 4配料 5菜品单价 6套餐价 7填写说明
* <p>数据行(表头第 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);
}
}
}
}
}
@@ -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:套餐多行时合并套餐级字段(模板/导出通用)。
* <p>列:0套餐名 1菜品名称 2分类 3菜品单价 4套餐价 5市场价 6库存 7排序 8状态 9移动端 10详情 11说明 12菜品ID 13套餐ID
* <p>须配合 {@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);
}
}
@@ -49,6 +49,10 @@ public interface CanteenSpuService extends IService<CanteenSpuEntity> {
*/
Object pageScope(Page page, CanteenSpuEntity canteenSpu);
/**
* 导出列表查询(与列表页 selectPage 同筛选:商户/名称/分类,并排除已关联线上菜品)
*/
List<CanteenSpuEntity> listForExport(String name, Long categoryId, Long merchantId, Long[] ids);
/**
* 分页查询,并且过滤已选择的菜品
@@ -42,6 +42,9 @@ public class CanteenCategoryServiceImpl extends ServiceImpl<CanteenCategoryMappe
@Override
public Long create(CanteenCategoryEntity canteenCategory) {
if (canteenCategory.getMerchantId() == null) {
throw new IllegalArgumentException("商户不能为空");
}
merchantScopeHelper.fillDeptIdByMerchantId(canteenCategory.getMerchantId(), canteenCategory::setDeptId);
baseMapper.insert(canteenCategory);
// 发送mq消息
@@ -53,7 +56,12 @@ public class CanteenCategoryServiceImpl extends ServiceImpl<CanteenCategoryMappe
public Boolean edit(CanteenCategoryEntity canteenCategory) {
CanteenCategoryEntity detail = baseMapper.selectById(canteenCategory.getId());
if (ObjectUtil.isEmpty(detail)) throw new RuntimeException("菜品分类不存在");
merchantScopeHelper.fillDeptIdByMerchantId(canteenCategory.getMerchantId(), canteenCategory::setDeptId);
Long merchantId = canteenCategory.getMerchantId() != null ? canteenCategory.getMerchantId() : detail.getMerchantId();
if (merchantId == null) {
throw new IllegalArgumentException("商户不能为空");
}
canteenCategory.setMerchantId(merchantId);
merchantScopeHelper.fillDeptIdByMerchantId(merchantId, canteenCategory::setDeptId);
baseMapper.updateById(canteenCategory);
// 发送mq消息
remoteMqttEventService.sendCanteenClassifyChangeMsg(
@@ -0,0 +1,437 @@
package com.bsd.pjcloud.canteen.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.bsd.pjcloud.canteen.dto.CanteenDailyMenuImportExcel;
import com.bsd.pjcloud.canteen.entity.CanteenDailyMenuEntity;
import com.bsd.pjcloud.canteen.entity.CanteenSpuComboItemEntity;
import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity;
import com.bsd.pjcloud.canteen.service.CanteenDailyMenuService;
import com.bsd.pjcloud.canteen.service.CanteenSpuService;
import com.bsd.pjcloud.canteen.util.CanteenMerchantScopeHelper;
import com.bsd.pjcloud.common.core.util.R;
import lombok.RequiredArgsConstructor;
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.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 每日菜谱 Excel 导入(规范模板)
*/
@Service
@RequiredArgsConstructor
public class CanteenDailyMenuImportHelper {
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private final CanteenSpuService canteenSpuService;
private final CanteenDailyMenuService canteenDailyMenuService;
private final CanteenMerchantScopeHelper merchantScopeHelper;
public List<CanteenDailyMenuImportExcel> templateRows() {
List<CanteenDailyMenuImportExcel> 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<CanteenDailyMenuImportExcel> 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<String, List<CanteenDailyMenuImportExcel>> groups = new LinkedHashMap<>();
List<String> 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<String> clearedDayHour = new LinkedHashSet<>();
int createdDish = 0;
int createdCombo = 0;
int scheduled = 0;
for (Map.Entry<String, List<CanteenDailyMenuImportExcel>> 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<CanteenDailyMenuImportExcel> 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<CanteenSpuEntity> 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<String> clearedDayHour) {
String dh = date + "|" + hourType;
if (replaceDay && !clearedDayHour.contains(dh)) {
canteenDailyMenuService.remove(Wrappers.<CanteenDailyMenuEntity>lambdaQuery()
.eq(CanteenDailyMenuEntity::getMerchantId, merchantId)
.eq(CanteenDailyMenuEntity::getStartTime, date)
.eq(CanteenDailyMenuEntity::getHourType, hourType));
clearedDayHour.add(dh);
}
boolean exists = canteenDailyMenuService.count(Wrappers.<CanteenDailyMenuEntity>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.<CanteenSpuEntity>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<CanteenSpuEntity> children) {
CanteenSpuEntity exist = canteenSpuService.getOne(Wrappers.<CanteenSpuEntity>lambdaQuery()
.eq(CanteenSpuEntity::getMerchantId, merchantId)
.eq(CanteenSpuEntity::getName, fullName)
.eq(CanteenSpuEntity::getSpuType, 2)
.last("limit 1"), false);
List<CanteenSpuComboItemEntity> 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<CanteenDailyMenuImportExcel> 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;
}
}
}
@@ -57,10 +57,8 @@ public class CanteenInventoryRecordsServiceImpl extends ServiceImpl<CanteenInven
public Object saveCanteenInventoryRecordsdDTO(CanteenInventoryRecordsdDTO canteenInventoryRecordsdDTO) {
CanteenInventoryRecordsEntity canteenInventoryRecordsEntity= BeanUtils.toBean(canteenInventoryRecordsdDTO, CanteenInventoryRecordsEntity.class);
merchantScopeHelper.fillDeptIdByMerchantId(canteenInventoryRecordsEntity.getMerchantId(), canteenInventoryRecordsEntity::setDeptId);
if (canteenInventoryRecordsEntity.getDeptId() == null) {
canteenInventoryRecordsEntity.setDeptId(SecurityUtils.getUser().getDeptId());
}
merchantScopeHelper.fillDeptIdByMerchantId(canteenInventoryRecordsEntity.getMerchantId(),
canteenInventoryRecordsEntity::setDeptId);
canteenInventoryRecordsEntity.setCheckStatus(TaskCheckStatusEnum.PADDING.getCode());
// canteenInventoryRecordsEntity.setCheckStatus(TaskCheckStatusEnum.RESULT.getCode());
baseMapper.insert(canteenInventoryRecordsEntity);
@@ -22,6 +22,7 @@ import com.bsd.pjcloud.canteen.mapper.CanteenMenuRecipeMapper;
import com.bsd.pjcloud.canteen.service.CanteenMealPlanService;
import com.bsd.pjcloud.canteen.service.CanteenMenuRecipeCategoryService;
import com.bsd.pjcloud.canteen.service.CanteenMenuRecipeService;
import com.bsd.pjcloud.canteen.util.CanteenMerchantScopeHelper;
import com.bsd.pjcloud.canteen.vo.mealplan.CanteenMealPlanExportRow;
import com.bsd.pjcloud.canteen.vo.mealplan.CanteenMealPlanMaterialBriefVO;
import com.bsd.pjcloud.canteen.vo.mealplan.CanteenMealPlanVO;
@@ -29,7 +30,6 @@ import com.bsd.pjcloud.canteen.vo.menurecipe.CanteenMenuRecipeVO;
import com.bsd.pjcloud.common.core.util.BeanUtils;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.data.datascope.DataScope;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
import com.bsd.pjcloud.wallet.api.feign.RemoteMchService;
import com.bsd.pjcloud.wallet.api.vo.MchManageVo;
import jakarta.servlet.http.HttpServletResponse;
@@ -65,6 +65,7 @@ public class CanteenMealPlanServiceImpl extends ServiceImpl<CanteenMealPlanMappe
private final CanteenMenuRecipeMapper menuRecipeMapper;
private final CanteenMenuRecipeCategoryService recipeCategoryService;
private final RemoteMchService remoteMchService;
private final CanteenMerchantScopeHelper merchantScopeHelper;
@Override
public Page<CanteenMealPlanEntity> pageScope(Page<CanteenMealPlanEntity> page,
@@ -78,7 +79,7 @@ public class CanteenMealPlanServiceImpl extends ServiceImpl<CanteenMealPlanMappe
@Transactional(rollbackFor = Exception.class)
public Long savePlan(CanteenMealPlanSaveDTO dto) {
CanteenMealPlanEntity entity = toEntity(dto);
entity.setDeptId(SecurityUtils.getUser().getDeptId());
merchantScopeHelper.fillDeptIdByMerchantIdOrCurrentUser(entity.getMerchantId(), entity::setDeptId);
if (entity.getMatchMode() == null) {
entity.setMatchMode(1);
}
@@ -18,13 +18,13 @@ import com.bsd.pjcloud.canteen.mapper.CanteenMenuRecipeMaterialMapper;
import com.bsd.pjcloud.canteen.mapper.CanteenOnlineSpuMapper;
import com.bsd.pjcloud.canteen.service.CanteenMenuRecipeCategoryService;
import com.bsd.pjcloud.canteen.service.CanteenMenuRecipeService;
import com.bsd.pjcloud.canteen.util.CanteenMerchantScopeHelper;
import com.bsd.pjcloud.canteen.util.CustomerRecipeExcelParser;
import com.bsd.pjcloud.canteen.vo.menurecipe.CanteenMenuRecipeImportResultVO;
import com.bsd.pjcloud.canteen.vo.menurecipe.CanteenMenuRecipeMaterialVO;
import com.bsd.pjcloud.canteen.vo.menurecipe.CanteenMenuRecipeVO;
import com.bsd.pjcloud.common.core.util.BeanUtils;
import com.bsd.pjcloud.common.data.datascope.DataScope;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
@@ -43,6 +43,7 @@ public class CanteenMenuRecipeServiceImpl extends ServiceImpl<CanteenMenuRecipeM
private final CanteenErpMaterialMapper erpMaterialMapper;
private final CanteenOnlineSpuMapper onlineSpuMapper;
private final CanteenMenuRecipeCategoryService recipeCategoryService;
private final CanteenMerchantScopeHelper merchantScopeHelper;
@Override
public Page<CanteenMenuRecipeEntity> pageScope(Page<CanteenMenuRecipeEntity> page,
@@ -252,7 +253,9 @@ public class CanteenMenuRecipeServiceImpl extends ServiceImpl<CanteenMenuRecipeM
CanteenMenuRecipeImportResultVO result = new CanteenMenuRecipeImportResultVO();
result.setCategory(cat);
Long deptId = SecurityUtils.getUser().getDeptId();
final Long[] deptHolder = new Long[1];
merchantScopeHelper.fillDeptIdByMerchantIdOrCurrentUser(merchantId, d -> deptHolder[0] = d);
Long deptId = deptHolder[0];
String mch = StrUtil.blankToDefault(merchantId, null);
Map<String, CanteenErpMaterialEntity> materialCache = new HashMap<>();
@@ -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<CanteenOnlineCategoryMapper, CanteenOnlineCategoryEntity> implements CanteenOnlineCategoryService {
private final RemoteMchService remoteMchService;
private final CanteenMerchantScopeHelper merchantScopeHelper;
@Override
public Object pageScope(Page page, LambdaQueryWrapper<CanteenOnlineCategoryEntity> wrapper) {
@@ -42,11 +40,7 @@ public class CanteenOnlineCategoryServiceImpl extends ServiceImpl<CanteenOnlineC
*/
@Override
public Long create(CanteenOnlineCategoryEntity dto) {
// 查询商户对应的部门id
MchManageVo mchVo = remoteMchService.getById(dto.getMerchantId()).getData();
if (ObjectUtil.isNotEmpty(mchVo)) {
dto.setDeptId(mchVo.getDeptId());
}
merchantScopeHelper.fillDeptIdByMerchantId(dto.getMerchantId(), dto::setDeptId);
baseMapper.insert(dto);
return dto.getId();
}
@@ -56,11 +50,7 @@ public class CanteenOnlineCategoryServiceImpl extends ServiceImpl<CanteenOnlineC
*/
@Override
public Long edit(CanteenOnlineCategoryEntity dto) {
// 查询商户对应的部门id
MchManageVo mchVo = remoteMchService.getById(dto.getMerchantId()).getData();
if (ObjectUtil.isNotEmpty(mchVo)) {
dto.setDeptId(mchVo.getDeptId());
}
merchantScopeHelper.fillDeptIdByMerchantId(dto.getMerchantId(), dto::setDeptId);
baseMapper.updateById(dto);
return dto.getId();
}
@@ -34,7 +34,6 @@ import com.bsd.pjcloud.canteen.vo.erp.CanteenOnlineSpuSimpleVO;
import com.bsd.pjcloud.common.core.util.BeanUtils;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.data.datascope.DataScope;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
@@ -107,6 +106,7 @@ public class CanteenOnlineSpuServiceImpl extends ServiceImpl<CanteenOnlineSpuMap
public Boolean saveDeep(CanteenOnlineSpuEntity entity) {
normalizeSpuType(entity);
validateCombo(entity, null);
merchantScopeHelper.fillDeptIdByMerchantId(entity.getMerchantId(), entity::setDeptId);
baseMapper.insert(entity);
saveComboItems(entity.getId(), entity.getSpuType(), entity.getComboItems());
return Boolean.TRUE;
@@ -117,6 +117,7 @@ public class CanteenOnlineSpuServiceImpl extends ServiceImpl<CanteenOnlineSpuMap
public Boolean updateDeep(CanteenOnlineSpuEntity entity) {
normalizeSpuType(entity);
validateCombo(entity, entity.getId());
merchantScopeHelper.fillDeptIdByMerchantId(entity.getMerchantId(), entity::setDeptId);
baseMapper.updateById(entity);
comboItemMapper.delete(Wrappers.<CanteenOnlineComboItemEntity>lambdaQuery()
.eq(CanteenOnlineComboItemEntity::getComboSpuId, entity.getId()));
@@ -586,7 +587,7 @@ public class CanteenOnlineSpuServiceImpl extends ServiceImpl<CanteenOnlineSpuMap
Map<Long, CanteenCategoryEntity> 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());
@@ -134,6 +134,31 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
return pageData;
}
@Override
public List<CanteenSpuEntity> listForExport(String name, Long categoryId, Long merchantId, Long[] ids) {
// 与 selectPage(菜品列表页)保持同一套筛选,避免导出比列表多/变成“全部”
LambdaQueryWrapper<CanteenSpuEntity> 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<CanteenOnlineSpuEntity> onlineQw = new LambdaQueryWrapper<>();
onlineQw.isNotNull(CanteenOnlineSpuEntity::getLinkSpuId);
merchantScopeHelper.applyMerchantIdScope(onlineQw, merchantId, CanteenOnlineSpuEntity::getMerchantId);
List<CanteenOnlineSpuEntity> onlineList = canteenOnlineSpuMapper.selectListByScope(onlineQw, DataScope.of());
Set<Long> 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<CanteenSpuEntity> wrapper = Wrappers.lambdaQuery();
@@ -297,6 +322,12 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
public Boolean saveDeep(CanteenSpuEntity entity) {
normalizeSpuType(entity);
validateCombo(entity, null);
if (entity.getMerchantId() == null) {
throw new IllegalArgumentException("商户不能为空");
}
if (entity.getPicUrl() == null) {
entity.setPicUrl("");
}
merchantScopeHelper.fillDeptIdByMerchantId(entity.getMerchantId(), entity::setDeptId);
baseMapper.insert(entity);
saveComboItems(entity.getId(), entity.getSpuType(), entity.getComboItems());
@@ -161,20 +161,86 @@ public class CanteenMerchantScopeHelper {
}
/**
* 按商户回填 deptId(写入时与商户部门对齐,保证 DataScope 与商户隔离一致)
* 按商户回填 deptId(写入时与商户部门对齐,保证 DataScope 与商户隔离一致)。
* <p>有 merchantId 时必须能解析到商户部门,失败直接抛错,禁止静默落成管理员部门。
*/
public void fillDeptIdByMerchantId(Long merchantId, Consumer<Long> deptIdSetter) {
if (merchantId == null || deptIdSetter == null) {
return;
}
deptIdSetter.accept(requireDeptIdByMerchantId(merchantId));
}
/**
* merchantId 为字符串时的回填(排菜菜谱 / 排餐计划等)
*/
public void fillDeptIdByMerchantId(String merchantId, Consumer<Long> 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<Long> 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<Long> 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<MchManageVo> 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("按商户获取部门失败,请稍后重试");
}
}
@@ -101,9 +101,9 @@ public class UserExcelVO implements Serializable {
private String staffingTypeParse;
/**
* 锁定标记
* 启用状态(导出:启用/禁用;导入仍兼容 0/9)
*/
@ExcelProperty("锁定标记,0:正常,9:已锁定")
@ExcelProperty("启用状态")
private String lockFlag;
/**
@@ -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 身份。
*
@@ -219,6 +219,9 @@
<if test="query.staffingType != null">
AND u.staffing_type = #{query.staffingType}
</if>
<if test="query.lockFlag != null and query.lockFlag != ''">
AND u.lock_flag = #{query.lockFlag}
</if>
<if test="query.role != null and query.role.size() != 0">
and urole.role_id IN
<foreach collection="query.role" item="v" open="(" close=")" separator=",">
@@ -300,13 +303,33 @@
LEFT JOIN sys_user_role urole ON urole.user_id = u.user_id
<where>
u.del_flag = '0'
<if test="query.userId != null">
AND u.user_id = #{query.userId}
</if>
<if test="query.processStatus != null and query.processStatus != '' ">
AND u.process_status = #{query.processStatus}
</if>
<if test="query.username != null and query.username != ''">
<bind name="usernameLike" value="'%'+query.username+'%'"/>
AND u.username LIKE #{usernameLike}
</if>
<if test="query.name != null and query.name != ''">
<bind name="nameLike" value="'%'+query.name+'%'"/>
AND u.name LIKE #{nameLike}
</if>
<if test="query.deptId != null and query.deptId != ''">
AND u.dept_id = #{query.deptId}
</if>
<if test="query.staffingType != null">
AND u.staffing_type = #{query.staffingType}
</if>
<if test="query.lockFlag != null and query.lockFlag != ''">
AND u.lock_flag = #{query.lockFlag}
</if>
<if test="query.phone != null and query.phone != ''">
<bind name="phoneLike" value="'%'+query.phone+'%'"/>
AND u.phone LIKE #{phoneLike}
</if>
<if test="query.role != null and query.role.size() != 0">
and urole.role_id IN
<foreach collection="query.role" item="v" open="(" close=")" separator=",">
@@ -335,6 +358,12 @@
#{id}
</foreach>
</if>
<if test="query.userIds != null and query.userIds.size() != 0">
and u.user_id IN
<foreach collection="query.userIds" item="v" open="(" close=")" separator=",">
#{v}
</foreach>
</if>
</where>
group by u.user_id
<if test="_databaseId != 'mssql'">