菜谱排餐
This commit is contained in:
1 parent
d43fe27242
commit
41eb7952b0
30 files changed
+1422
-36
No files matched your search
+45
@@ -0,0 +1,45 @@
|
||||
package com.bsd.pjcloud.canteen.dto.mealplan;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Schema(description = "按餐次分类配额随机排餐")
|
||||
public class CanteenMealPlanRandomDTO {
|
||||
|
||||
@Schema(description = "商户ID(可选,用于过滤通用/本商户菜谱)")
|
||||
private String merchantId;
|
||||
|
||||
@NotNull(message = "请选择开始日期")
|
||||
private LocalDate startDate;
|
||||
|
||||
@NotNull(message = "请填写排餐天数")
|
||||
private Integer dayCount;
|
||||
|
||||
@NotEmpty(message = "请勾选餐次")
|
||||
private List<Integer> mealSegments;
|
||||
|
||||
@Schema(description = "已选食材;有则只在可做菜谱中随机")
|
||||
private List<Long> materialIds;
|
||||
|
||||
@NotEmpty(message = "请配置各餐次分类配额")
|
||||
@Schema(description = "餐次配额,例如 [{mealSegment:1, quotas:{素菜:2,小荤:1}}]")
|
||||
private List<SegmentQuota> segmentQuotas;
|
||||
|
||||
@Data
|
||||
@Schema(description = "单餐次分类配额")
|
||||
public static class SegmentQuota {
|
||||
|
||||
@NotNull
|
||||
private Integer mealSegment;
|
||||
|
||||
@Schema(description = "分类 -> 份数,如 {大荤:1,小荤:1,素菜:2}")
|
||||
private Map<String, Integer> quotas;
|
||||
}
|
||||
}
|
||||
+3
@@ -32,6 +32,9 @@ public class CanteenMealPlanSaveDTO {
|
||||
|
||||
private List<Long> selectedMaterialIds;
|
||||
|
||||
@Schema(description = "餐次分类配额(随机排餐配置)")
|
||||
private List<CanteenMealPlanRandomDTO.SegmentQuota> segmentCategoryQuotas;
|
||||
|
||||
private Integer matchMode;
|
||||
|
||||
private Integer status;
|
||||
|
||||
+5
@@ -4,6 +4,7 @@ import com.alibaba.excel.annotation.ExcelIgnore;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.baomidou.mybatisplus.extension.activerecord.Model;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import com.bsd.pjcloud.canteen.dto.mealplan.CanteenMealPlanRandomDTO;
|
||||
import com.bsd.pjcloud.common.core.util.TenantTable;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
@@ -41,6 +42,10 @@ public class CanteenMealPlanEntity extends Model<CanteenMealPlanEntity> {
|
||||
@TableField(value = "selected_material_ids_json", typeHandler = JacksonTypeHandler.class)
|
||||
private List<Long> selectedMaterialIds;
|
||||
|
||||
@Schema(description = "餐次分类配额")
|
||||
@TableField(value = "segment_category_quotas_json", typeHandler = JacksonTypeHandler.class)
|
||||
private List<CanteenMealPlanRandomDTO.SegmentQuota> segmentCategoryQuotas;
|
||||
|
||||
private Integer matchMode;
|
||||
|
||||
@Schema(description = "0草稿 1已定稿")
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package com.bsd.pjcloud.canteen.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.baomidou.mybatisplus.extension.activerecord.Model;
|
||||
import com.bsd.pjcloud.common.core.util.TenantTable;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TenantTable
|
||||
@TableName("canteen_menu_recipe_category")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "排菜菜谱分类")
|
||||
public class CanteenMenuRecipeCategoryEntity extends Model<CanteenMenuRecipeCategoryEntity> {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
private Long tenantId;
|
||||
|
||||
private Long deptId;
|
||||
|
||||
@Schema(description = "分类名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "排序,越小越靠前")
|
||||
private Integer sortOrder;
|
||||
|
||||
@Schema(description = "0启用 1停用")
|
||||
private Integer status;
|
||||
|
||||
private String remark;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private String updateBy;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@TableLogic
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String delFlag;
|
||||
}
|
||||
+3
@@ -32,6 +32,9 @@ public class CanteenMenuRecipeEntity extends Model<CanteenMenuRecipeEntity> {
|
||||
@Schema(description = "菜名")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "菜谱分类(可维护)")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "图片")
|
||||
private String picUrl;
|
||||
|
||||
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
package com.bsd.pjcloud.canteen.util;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 客户菜谱 Excel 单元格解析(与 gen-customer-recipe-sql.js 规则一致)
|
||||
*/
|
||||
public final class CustomerRecipeExcelParser {
|
||||
|
||||
private static final Pattern TRAIL_RE = Pattern.compile("^(.+?)\\s*(\\d+(?:\\.\\d+)?)\\s*(斤|只|包|瓶|盒|袋|根)$");
|
||||
|
||||
private static final Pattern LEAD_NUM_RE = Pattern.compile("^(\\d+(?:\\.\\d+)?)\\s*(斤|只|条|包|瓶|盒)?(.+)$");
|
||||
|
||||
private static final Pattern LEAD_CN_RE = Pattern.compile("^([一二三四五六七八九十两]+)\\s*(条|斤|只|包)?(.+)$");
|
||||
|
||||
private static final Pattern QTY_RE = Pattern.compile("^(\\d+(?:\\.\\d+)?)\\s*(斤|只|包|瓶|盒|袋|个)?$");
|
||||
|
||||
private static final Map<String, Integer> CN_NUM = new HashMap<>();
|
||||
|
||||
static {
|
||||
CN_NUM.put("一", 1);
|
||||
CN_NUM.put("二", 2);
|
||||
CN_NUM.put("两", 2);
|
||||
CN_NUM.put("三", 3);
|
||||
CN_NUM.put("四", 4);
|
||||
CN_NUM.put("五", 5);
|
||||
CN_NUM.put("六", 6);
|
||||
CN_NUM.put("七", 7);
|
||||
CN_NUM.put("八", 8);
|
||||
CN_NUM.put("九", 9);
|
||||
CN_NUM.put("十", 10);
|
||||
}
|
||||
|
||||
private CustomerRecipeExcelParser() {
|
||||
}
|
||||
|
||||
public static String cleanName(String raw) {
|
||||
if (raw == null) {
|
||||
return "";
|
||||
}
|
||||
return raw.replace('(', '(')
|
||||
.replace(')', ')')
|
||||
.replaceAll("\\s+", " ")
|
||||
.trim()
|
||||
.replace("(斤)", "")
|
||||
.replace("(盒)", "")
|
||||
.replace("(块)", "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
public static ParsedMaterial parseMaterialCell(Object nameRaw, Object qtyRaw) {
|
||||
String name = nameRaw == null ? "" : String.valueOf(nameRaw).trim();
|
||||
if (StrUtil.isBlank(name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BigDecimal qty = null;
|
||||
String unitHint = null;
|
||||
if (qtyRaw != null && StrUtil.isNotBlank(String.valueOf(qtyRaw))) {
|
||||
String qtyStr = String.valueOf(qtyRaw).trim();
|
||||
Matcher qm = QTY_RE.matcher(qtyStr);
|
||||
if (qm.matches()) {
|
||||
qty = new BigDecimal(qm.group(1));
|
||||
if (qm.group(2) != null) {
|
||||
unitHint = qm.group(2);
|
||||
}
|
||||
}
|
||||
else if (qtyStr.contains("包")) {
|
||||
Matcher m = Pattern.compile("(\\d+)").matcher(qtyStr);
|
||||
if (m.find()) {
|
||||
qty = new BigDecimal(m.group(1));
|
||||
}
|
||||
unitHint = "包";
|
||||
}
|
||||
else {
|
||||
try {
|
||||
qty = new BigDecimal(qtyStr);
|
||||
}
|
||||
catch (Exception ignored) {
|
||||
qty = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Matcher m = TRAIL_RE.matcher(name);
|
||||
if (m.matches()) {
|
||||
return new ParsedMaterial(cleanName(m.group(1)),
|
||||
qty != null ? qty : new BigDecimal(m.group(2)),
|
||||
m.group(3));
|
||||
}
|
||||
|
||||
if (!name.isEmpty() && Character.isDigit(name.charAt(0))) {
|
||||
m = LEAD_NUM_RE.matcher(name);
|
||||
if (m.matches() && m.group(3) != null && m.group(3).matches(".*[\\u4e00-\\u9fa5].*")) {
|
||||
String unitPart = m.group(2);
|
||||
String matName = "条".equals(unitPart) ? cleanName(unitPart + m.group(3)) : cleanName(m.group(3));
|
||||
String unit = "条".equals(unitPart) ? "条" : (unitPart != null ? unitPart : "斤");
|
||||
return new ParsedMaterial(matName, qty != null ? qty : new BigDecimal(m.group(1)), unit);
|
||||
}
|
||||
}
|
||||
|
||||
m = LEAD_CN_RE.matcher(name);
|
||||
if (m.matches() && m.group(3) != null && CN_NUM.containsKey(m.group(1))) {
|
||||
String unitPart = m.group(2);
|
||||
String matName = "条".equals(unitPart) ? cleanName(unitPart + m.group(3)) : cleanName(m.group(3));
|
||||
String unit = unitPart != null ? unitPart : "条";
|
||||
return new ParsedMaterial(matName, qty != null ? qty : BigDecimal.valueOf(CN_NUM.get(m.group(1))), unit);
|
||||
}
|
||||
|
||||
name = name.replaceAll("([\\u4e00-\\u9fa5])0+$", "$1").trim();
|
||||
String n = cleanName(name);
|
||||
if (StrUtil.isBlank(n)) {
|
||||
return null;
|
||||
}
|
||||
String unit = unitHint != null ? unitHint : "斤";
|
||||
if (("鸡蛋".equals(n) || "鹌鹑蛋".equals(n)) && qty != null && qty.compareTo(BigDecimal.ONE) >= 0) {
|
||||
unit = "个";
|
||||
}
|
||||
return new ParsedMaterial(n, qty, unit);
|
||||
}
|
||||
|
||||
public record ParsedMaterial(String name, BigDecimal quantity, String unit) {
|
||||
}
|
||||
}
|
||||
+3
@@ -1,6 +1,7 @@
|
||||
package com.bsd.pjcloud.canteen.vo.mealplan;
|
||||
|
||||
import com.bsd.pjcloud.canteen.dto.mealplan.CanteenMealPlanItemDTO;
|
||||
import com.bsd.pjcloud.canteen.dto.mealplan.CanteenMealPlanRandomDTO;
|
||||
import com.bsd.pjcloud.canteen.vo.menurecipe.CanteenMenuRecipeVO;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
@@ -29,6 +30,8 @@ public class CanteenMealPlanVO {
|
||||
|
||||
private List<Long> selectedMaterialIds;
|
||||
|
||||
private List<CanteenMealPlanRandomDTO.SegmentQuota> segmentCategoryQuotas;
|
||||
|
||||
private Integer matchMode;
|
||||
|
||||
private Integer status;
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.bsd.pjcloud.canteen.vo.menurecipe;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "菜谱 Excel 导入结果")
|
||||
public class CanteenMenuRecipeImportResultVO {
|
||||
|
||||
@Schema(description = "新建食材数")
|
||||
private int materialCreated;
|
||||
|
||||
@Schema(description = "复用已有食材数")
|
||||
private int materialReused;
|
||||
|
||||
@Schema(description = "新建菜谱数")
|
||||
private int recipeCreated;
|
||||
|
||||
@Schema(description = "跳过行数(无菜名/重复等)")
|
||||
private int skipped;
|
||||
|
||||
@Schema(description = "分类")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "提示信息")
|
||||
private String message;
|
||||
}
|
||||
+3
@@ -18,6 +18,9 @@ public class CanteenMenuRecipeVO {
|
||||
|
||||
private String name;
|
||||
|
||||
@Schema(description = "菜谱分类:大荤/小荤/素菜")
|
||||
private String category;
|
||||
|
||||
private String picUrl;
|
||||
|
||||
private String remark;
|
||||
|
||||
+9
@@ -3,6 +3,8 @@ package com.bsd.pjcloud.canteen.controller;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.canteen.dto.mealplan.CanteenMealPlanItemDTO;
|
||||
import com.bsd.pjcloud.canteen.dto.mealplan.CanteenMealPlanRandomDTO;
|
||||
import com.bsd.pjcloud.canteen.dto.mealplan.CanteenMealPlanSaveDTO;
|
||||
import com.bsd.pjcloud.canteen.entity.CanteenMealPlanEntity;
|
||||
import com.bsd.pjcloud.canteen.service.CanteenMealPlanService;
|
||||
@@ -77,6 +79,13 @@ public class CanteenMealPlanController {
|
||||
return R.ok(canteenMealPlanService.removePlans(ids));
|
||||
}
|
||||
|
||||
@Operation(summary = "按餐次分类配额随机排餐", description = "返回明细项,不直接落库;前端可覆盖课表后再保存")
|
||||
@PostMapping("/generate-random")
|
||||
@PreAuthorize("@pms.hasPermission('canteen_canteenMealPlan_add') or @pms.hasPermission('canteen_canteenMealPlan_edit')")
|
||||
public R<List<CanteenMealPlanItemDTO>> generateRandom(@Valid @RequestBody CanteenMealPlanRandomDTO dto) {
|
||||
return R.ok(canteenMealPlanService.generateRandom(dto));
|
||||
}
|
||||
|
||||
@ResponseExcel(name = "排餐明细")
|
||||
@Operation(summary = "导出明细(日期+餐次+菜名)")
|
||||
@GetMapping("/export/{id}")
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package com.bsd.pjcloud.canteen.controller;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.canteen.entity.CanteenMenuRecipeCategoryEntity;
|
||||
import com.bsd.pjcloud.canteen.service.CanteenMenuRecipeCategoryService;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.log.annotation.SysLog;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/canteenMenuRecipeCategory")
|
||||
@Tag(name = "排菜菜谱分类")
|
||||
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
|
||||
public class CanteenMenuRecipeCategoryController {
|
||||
|
||||
private final CanteenMenuRecipeCategoryService categoryService;
|
||||
|
||||
@Operation(summary = "分页查询")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@pms.hasPermission('canteen_canteenMenuRecipeCategory_view')")
|
||||
public R<Page<CanteenMenuRecipeCategoryEntity>> page(@ParameterObject Page<CanteenMenuRecipeCategoryEntity> page,
|
||||
@ParameterObject CanteenMenuRecipeCategoryEntity query) {
|
||||
var wrapper = Wrappers.<CanteenMenuRecipeCategoryEntity>lambdaQuery()
|
||||
.like(StrUtil.isNotBlank(query.getName()), CanteenMenuRecipeCategoryEntity::getName, query.getName())
|
||||
.eq(query.getStatus() != null, CanteenMenuRecipeCategoryEntity::getStatus, query.getStatus())
|
||||
.orderByAsc(CanteenMenuRecipeCategoryEntity::getSortOrder)
|
||||
.orderByDesc(CanteenMenuRecipeCategoryEntity::getCreateTime);
|
||||
return R.ok(categoryService.pageScope(page, wrapper));
|
||||
}
|
||||
|
||||
@Operation(summary = "启用分类列表(下拉)")
|
||||
@GetMapping("/list-enabled")
|
||||
@PreAuthorize("@pms.hasPermission('canteen_canteenMenuRecipeCategory_view') or @pms.hasPermission('canteen_canteenMenuRecipe_view') or @pms.hasPermission('canteen_canteenMealPlan_view')")
|
||||
public R<List<CanteenMenuRecipeCategoryEntity>> listEnabled() {
|
||||
return R.ok(categoryService.listEnabled());
|
||||
}
|
||||
|
||||
@Operation(summary = "详情")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@pms.hasPermission('canteen_canteenMenuRecipeCategory_view')")
|
||||
public R getById(@PathVariable Long id) {
|
||||
return R.ok(categoryService.getById(id));
|
||||
}
|
||||
|
||||
@SysLog("新增菜谱分类")
|
||||
@Operation(summary = "新增")
|
||||
@PostMapping
|
||||
@PreAuthorize("@pms.hasPermission('canteen_canteenMenuRecipeCategory_add')")
|
||||
public R save(@RequestBody CanteenMenuRecipeCategoryEntity entity) {
|
||||
return R.ok(categoryService.saveCategory(entity));
|
||||
}
|
||||
|
||||
@SysLog("修改菜谱分类")
|
||||
@Operation(summary = "修改")
|
||||
@PutMapping
|
||||
@PreAuthorize("@pms.hasPermission('canteen_canteenMenuRecipeCategory_edit')")
|
||||
public R update(@RequestBody CanteenMenuRecipeCategoryEntity entity) {
|
||||
return R.ok(categoryService.updateCategory(entity));
|
||||
}
|
||||
|
||||
@SysLog("删除菜谱分类")
|
||||
@Operation(summary = "删除")
|
||||
@DeleteMapping
|
||||
@PreAuthorize("@pms.hasPermission('canteen_canteenMenuRecipeCategory_del')")
|
||||
public R remove(@RequestBody Long[] ids) {
|
||||
return R.ok(categoryService.removeCategories(ids));
|
||||
}
|
||||
}
|
||||
+14
@@ -6,6 +6,7 @@ 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.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;
|
||||
@@ -17,6 +18,7 @@ import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -36,6 +38,7 @@ public class CanteenMenuRecipeController {
|
||||
@ParameterObject CanteenMenuRecipeEntity query) {
|
||||
var wrapper = Wrappers.<CanteenMenuRecipeEntity>lambdaQuery()
|
||||
.like(StrUtil.isNotBlank(query.getName()), CanteenMenuRecipeEntity::getName, query.getName())
|
||||
.eq(StrUtil.isNotBlank(query.getCategory()), CanteenMenuRecipeEntity::getCategory, query.getCategory())
|
||||
.eq(StrUtil.isNotBlank(query.getMerchantId()), CanteenMenuRecipeEntity::getMerchantId,
|
||||
query.getMerchantId())
|
||||
.eq(query.getStatus() != null, CanteenMenuRecipeEntity::getStatus, query.getStatus())
|
||||
@@ -81,4 +84,15 @@ public class CanteenMenuRecipeController {
|
||||
public R<List<?>> matchByMaterials(@RequestBody CanteenMenuRecipeMatchDTO dto) {
|
||||
return R.ok(canteenMenuRecipeService.matchByMaterials(dto));
|
||||
}
|
||||
|
||||
@SysLog("导入客户菜谱Excel")
|
||||
@Operation(summary = "导入客户菜谱 Excel", description = "表头:序号/菜品/食材1/克重1...;整批归入指定分类(大荤/小荤/素菜)")
|
||||
@PostMapping("/import-customer-excel")
|
||||
@PreAuthorize("@pms.hasPermission('canteen_canteenMenuRecipe_add')")
|
||||
public R<CanteenMenuRecipeImportResultVO> importCustomerExcel(
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("category") String category,
|
||||
@RequestParam(value = "merchantId", required = false) String merchantId) {
|
||||
return R.ok(canteenMenuRecipeService.importCustomerExcel(file, category, merchantId));
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.bsd.pjcloud.canteen.mapper;
|
||||
|
||||
import com.bsd.pjcloud.canteen.entity.CanteenMenuRecipeCategoryEntity;
|
||||
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface CanteenMenuRecipeCategoryMapper extends PjcloudBaseMapper<CanteenMenuRecipeCategoryEntity> {
|
||||
}
|
||||
+7
@@ -3,6 +3,8 @@ package com.bsd.pjcloud.canteen.service;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.bsd.pjcloud.canteen.dto.mealplan.CanteenMealPlanItemDTO;
|
||||
import com.bsd.pjcloud.canteen.dto.mealplan.CanteenMealPlanRandomDTO;
|
||||
import com.bsd.pjcloud.canteen.dto.mealplan.CanteenMealPlanSaveDTO;
|
||||
import com.bsd.pjcloud.canteen.entity.CanteenMealPlanEntity;
|
||||
import com.bsd.pjcloud.canteen.vo.mealplan.CanteenMealPlanExportRow;
|
||||
@@ -26,4 +28,9 @@ public interface CanteenMealPlanService extends IService<CanteenMealPlanEntity>
|
||||
List<CanteenMealPlanExportRow> exportRows(Long id);
|
||||
|
||||
void exportGridExcel(Long id, HttpServletResponse response);
|
||||
|
||||
/**
|
||||
* 按餐次分类配额随机生成排餐明细(不落库,由前端确认后再保存)
|
||||
*/
|
||||
List<CanteenMealPlanItemDTO> generateRandom(CanteenMealPlanRandomDTO dto);
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.bsd.pjcloud.canteen.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.bsd.pjcloud.canteen.entity.CanteenMenuRecipeCategoryEntity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public interface CanteenMenuRecipeCategoryService extends IService<CanteenMenuRecipeCategoryEntity> {
|
||||
|
||||
Page<CanteenMenuRecipeCategoryEntity> pageScope(Page<CanteenMenuRecipeCategoryEntity> page,
|
||||
LambdaQueryWrapper<CanteenMenuRecipeCategoryEntity> wrapper);
|
||||
|
||||
Boolean saveCategory(CanteenMenuRecipeCategoryEntity entity);
|
||||
|
||||
Boolean updateCategory(CanteenMenuRecipeCategoryEntity entity);
|
||||
|
||||
Boolean removeCategories(Long[] ids);
|
||||
|
||||
/** 启用中的分类名称列表(排菜/排餐下拉) */
|
||||
List<CanteenMenuRecipeCategoryEntity> listEnabled();
|
||||
|
||||
/** 启用中的分类名称集合(一次查出,避免循环打库) */
|
||||
Set<String> enabledNameSet();
|
||||
|
||||
/** 校验并归一化分类名;不存在或已停用返回 null */
|
||||
String normalize(String category);
|
||||
|
||||
/** 使用已缓存名称集归一化,避免重复查库 */
|
||||
String normalize(String category, Set<String> enabledNames);
|
||||
|
||||
boolean existsEnabledName(String name);
|
||||
}
|
||||
+16
-1
@@ -5,7 +5,9 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.bsd.pjcloud.canteen.dto.menurecipe.CanteenMenuRecipeMatchDTO;
|
||||
import com.bsd.pjcloud.canteen.entity.CanteenMenuRecipeEntity;
|
||||
import com.bsd.pjcloud.canteen.vo.menurecipe.CanteenMenuRecipeImportResultVO;
|
||||
import com.bsd.pjcloud.canteen.vo.menurecipe.CanteenMenuRecipeVO;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -21,5 +23,18 @@ public interface CanteenMenuRecipeService extends IService<CanteenMenuRecipeEnti
|
||||
|
||||
CanteenMenuRecipeVO getDetail(Long id);
|
||||
|
||||
List<CanteenMenuRecipeVO> matchByMaterials(CanteenMenuRecipeMatchDTO dto);
|
||||
/**
|
||||
* 按选用食材匹配可做菜谱(批量查询,无 N+1)
|
||||
* @param brief true 时仅返回 id/name/category/summary,供排餐匹配与随机使用
|
||||
*/
|
||||
List<CanteenMenuRecipeVO> matchByMaterials(CanteenMenuRecipeMatchDTO dto, boolean brief);
|
||||
|
||||
default List<CanteenMenuRecipeVO> matchByMaterials(CanteenMenuRecipeMatchDTO dto) {
|
||||
return matchByMaterials(dto, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入客户菜谱 Excel(列:序号、菜品、食材1、克重1…),整批归入指定分类
|
||||
*/
|
||||
CanteenMenuRecipeImportResultVO importCustomerExcel(MultipartFile file, String category, String merchantId);
|
||||
}
|
||||
+125
-1
@@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bsd.pjcloud.canteen.dto.mealplan.CanteenMealPlanItemDTO;
|
||||
import com.bsd.pjcloud.canteen.dto.mealplan.CanteenMealPlanRandomDTO;
|
||||
import com.bsd.pjcloud.canteen.dto.mealplan.CanteenMealPlanSaveDTO;
|
||||
import com.bsd.pjcloud.canteen.dto.menurecipe.CanteenMenuRecipeMatchDTO;
|
||||
import com.bsd.pjcloud.canteen.entity.CanteenErpMaterialEntity;
|
||||
@@ -19,6 +20,7 @@ import com.bsd.pjcloud.canteen.mapper.CanteenMealPlanItemMapper;
|
||||
import com.bsd.pjcloud.canteen.mapper.CanteenMealPlanMapper;
|
||||
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.vo.mealplan.CanteenMealPlanExportRow;
|
||||
import com.bsd.pjcloud.canteen.vo.mealplan.CanteenMealPlanMaterialBriefVO;
|
||||
@@ -61,6 +63,7 @@ public class CanteenMealPlanServiceImpl extends ServiceImpl<CanteenMealPlanMappe
|
||||
private final CanteenErpMaterialMapper erpMaterialMapper;
|
||||
private final CanteenMenuRecipeService menuRecipeService;
|
||||
private final CanteenMenuRecipeMapper menuRecipeMapper;
|
||||
private final CanteenMenuRecipeCategoryService recipeCategoryService;
|
||||
private final RemoteMchService remoteMchService;
|
||||
|
||||
@Override
|
||||
@@ -200,11 +203,132 @@ public class CanteenMealPlanServiceImpl extends ServiceImpl<CanteenMealPlanMappe
|
||||
response.setCharacterEncoding("utf-8");
|
||||
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName);
|
||||
EasyExcel.write(response.getOutputStream()).sheet("排餐餐表").doWrite(rows);
|
||||
} catch (IOException e) {
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new RuntimeException("导出失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CanteenMealPlanItemDTO> generateRandom(CanteenMealPlanRandomDTO dto) {
|
||||
if (dto.getDayCount() == null || dto.getDayCount() < 1) {
|
||||
throw new IllegalArgumentException("排餐天数无效");
|
||||
}
|
||||
List<CanteenMenuRecipeVO> pool;
|
||||
if (CollUtil.isNotEmpty(dto.getMaterialIds())) {
|
||||
CanteenMenuRecipeMatchDTO matchDTO = new CanteenMenuRecipeMatchDTO();
|
||||
matchDTO.setMaterialIds(dto.getMaterialIds());
|
||||
matchDTO.setMerchantId(dto.getMerchantId());
|
||||
pool = menuRecipeService.matchByMaterials(matchDTO, true);
|
||||
}
|
||||
else {
|
||||
LambdaQueryWrapper<CanteenMenuRecipeEntity> wrapper = Wrappers.<CanteenMenuRecipeEntity>lambdaQuery()
|
||||
.eq(CanteenMenuRecipeEntity::getStatus, 0)
|
||||
.select(CanteenMenuRecipeEntity::getId, CanteenMenuRecipeEntity::getName,
|
||||
CanteenMenuRecipeEntity::getCategory);
|
||||
if (StrUtil.isNotBlank(dto.getMerchantId())) {
|
||||
wrapper.and(w -> w.eq(CanteenMenuRecipeEntity::getMerchantId, dto.getMerchantId())
|
||||
.or().isNull(CanteenMenuRecipeEntity::getMerchantId)
|
||||
.or().eq(CanteenMenuRecipeEntity::getMerchantId, ""));
|
||||
}
|
||||
List<CanteenMenuRecipeEntity> recipes = menuRecipeMapper.selectListByScope(wrapper, DataScope.of());
|
||||
pool = recipes.stream().map(r -> {
|
||||
CanteenMenuRecipeVO vo = new CanteenMenuRecipeVO();
|
||||
vo.setId(r.getId());
|
||||
vo.setName(r.getName());
|
||||
vo.setCategory(r.getCategory());
|
||||
return vo;
|
||||
}).toList();
|
||||
}
|
||||
if (CollUtil.isEmpty(pool)) {
|
||||
throw new IllegalArgumentException("没有可排的菜谱,请先导入/维护菜谱或调整食材");
|
||||
}
|
||||
|
||||
Set<String> enabledCats = recipeCategoryService.enabledNameSet();
|
||||
Map<String, List<CanteenMenuRecipeVO>> byCategory = new HashMap<>();
|
||||
for (CanteenMenuRecipeVO r : pool) {
|
||||
String cat = recipeCategoryService.normalize(r.getCategory(), enabledCats);
|
||||
if (cat == null) {
|
||||
continue;
|
||||
}
|
||||
byCategory.computeIfAbsent(cat, k -> new ArrayList<>()).add(r);
|
||||
}
|
||||
|
||||
Map<Integer, Map<String, Integer>> quotaMap = new LinkedHashMap<>();
|
||||
for (CanteenMealPlanRandomDTO.SegmentQuota sq : dto.getSegmentQuotas()) {
|
||||
if (sq.getMealSegment() == null || sq.getQuotas() == null || sq.getQuotas().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Integer> normalized = new LinkedHashMap<>();
|
||||
sq.getQuotas().forEach((k, v) -> {
|
||||
String cat = recipeCategoryService.normalize(k, enabledCats);
|
||||
if (cat != null && v != null && v > 0) {
|
||||
normalized.put(cat, v);
|
||||
}
|
||||
});
|
||||
if (!normalized.isEmpty()) {
|
||||
quotaMap.put(sq.getMealSegment(), normalized);
|
||||
}
|
||||
}
|
||||
if (quotaMap.isEmpty()) {
|
||||
throw new IllegalArgumentException("请配置各餐次分类配额(如早餐:素菜2、小荤1)");
|
||||
}
|
||||
|
||||
Random random = new Random();
|
||||
Map<String, Deque<Long>> recentByCat = new HashMap<>();
|
||||
Map<String, Set<Long>> recentSetByCat = new HashMap<>();
|
||||
List<CanteenMealPlanItemDTO> items = new ArrayList<>();
|
||||
|
||||
for (int day = 0; day < dto.getDayCount(); day++) {
|
||||
LocalDate planDate = dto.getStartDate().plusDays(day);
|
||||
for (Integer seg : dto.getMealSegments()) {
|
||||
Map<String, Integer> quotas = quotaMap.get(seg);
|
||||
if (quotas == null || quotas.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
Set<Long> usedInCell = new HashSet<>();
|
||||
int sort = 0;
|
||||
for (Map.Entry<String, Integer> e : quotas.entrySet()) {
|
||||
String cat = e.getKey();
|
||||
int need = e.getValue();
|
||||
List<CanteenMenuRecipeVO> candidates = byCategory.get(cat);
|
||||
if (CollUtil.isEmpty(candidates)) {
|
||||
continue;
|
||||
}
|
||||
candidates = new ArrayList<>(candidates);
|
||||
Collections.shuffle(candidates, random);
|
||||
Deque<Long> recent = recentByCat.computeIfAbsent(cat, k -> new ArrayDeque<>());
|
||||
Set<Long> recentSet = recentSetByCat.computeIfAbsent(cat, k -> new HashSet<>());
|
||||
candidates.sort((a, b) -> Boolean.compare(recentSet.contains(a.getId()), recentSet.contains(b.getId())));
|
||||
int picked = 0;
|
||||
for (CanteenMenuRecipeVO recipe : candidates) {
|
||||
if (picked >= need) {
|
||||
break;
|
||||
}
|
||||
if (!usedInCell.add(recipe.getId())) {
|
||||
continue;
|
||||
}
|
||||
CanteenMealPlanItemDTO item = new CanteenMealPlanItemDTO();
|
||||
item.setPlanDate(planDate);
|
||||
item.setMealSegment(seg);
|
||||
item.setRecipeId(recipe.getId());
|
||||
item.setRecipeName(recipe.getName());
|
||||
item.setSortOrder(sort++);
|
||||
items.add(item);
|
||||
recent.addLast(recipe.getId());
|
||||
recentSet.add(recipe.getId());
|
||||
while (recent.size() > 12) {
|
||||
Long old = recent.removeFirst();
|
||||
recentSet.remove(old);
|
||||
}
|
||||
picked++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private CanteenMealPlanEntity toEntity(CanteenMealPlanSaveDTO dto) {
|
||||
CanteenMealPlanEntity entity = BeanUtils.toBean(dto, CanteenMealPlanEntity.class);
|
||||
if (entity.getDayCount() == null || entity.getDayCount() < 1) {
|
||||
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
package com.bsd.pjcloud.canteen.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bsd.pjcloud.canteen.entity.CanteenMenuRecipeCategoryEntity;
|
||||
import com.bsd.pjcloud.canteen.entity.CanteenMenuRecipeEntity;
|
||||
import com.bsd.pjcloud.canteen.mapper.CanteenMenuRecipeCategoryMapper;
|
||||
import com.bsd.pjcloud.canteen.mapper.CanteenMenuRecipeMapper;
|
||||
import com.bsd.pjcloud.canteen.service.CanteenMenuRecipeCategoryService;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScope;
|
||||
import com.bsd.pjcloud.common.security.util.SecurityUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CanteenMenuRecipeCategoryServiceImpl
|
||||
extends ServiceImpl<CanteenMenuRecipeCategoryMapper, CanteenMenuRecipeCategoryEntity>
|
||||
implements CanteenMenuRecipeCategoryService {
|
||||
|
||||
private final CanteenMenuRecipeMapper menuRecipeMapper;
|
||||
|
||||
@Override
|
||||
public Page<CanteenMenuRecipeCategoryEntity> pageScope(Page<CanteenMenuRecipeCategoryEntity> page,
|
||||
LambdaQueryWrapper<CanteenMenuRecipeCategoryEntity> wrapper) {
|
||||
return baseMapper.selectPageByScope(page, wrapper, DataScope.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean saveCategory(CanteenMenuRecipeCategoryEntity entity) {
|
||||
String name = StrUtil.trim(entity.getName());
|
||||
if (StrUtil.isBlank(name)) {
|
||||
throw new IllegalArgumentException("分类名称不能为空");
|
||||
}
|
||||
if (existsName(name, null)) {
|
||||
throw new IllegalArgumentException("分类名称已存在:" + name);
|
||||
}
|
||||
entity.setName(name);
|
||||
if (entity.getSortOrder() == null) {
|
||||
entity.setSortOrder(0);
|
||||
}
|
||||
if (entity.getStatus() == null) {
|
||||
entity.setStatus(0);
|
||||
}
|
||||
if (entity.getDeptId() == null) {
|
||||
entity.setDeptId(SecurityUtils.getUser().getDeptId());
|
||||
}
|
||||
return baseMapper.insert(entity) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean updateCategory(CanteenMenuRecipeCategoryEntity entity) {
|
||||
if (entity.getId() == null) {
|
||||
throw new IllegalArgumentException("分类ID不能为空");
|
||||
}
|
||||
CanteenMenuRecipeCategoryEntity old = baseMapper.selectById(entity.getId());
|
||||
if (old == null) {
|
||||
throw new IllegalArgumentException("分类不存在");
|
||||
}
|
||||
String name = StrUtil.trim(entity.getName());
|
||||
if (StrUtil.isBlank(name)) {
|
||||
throw new IllegalArgumentException("分类名称不能为空");
|
||||
}
|
||||
if (existsName(name, entity.getId())) {
|
||||
throw new IllegalArgumentException("分类名称已存在:" + name);
|
||||
}
|
||||
String oldName = old.getName();
|
||||
entity.setName(name);
|
||||
baseMapper.updateById(entity);
|
||||
// 改名时同步菜谱上的分类字段
|
||||
if (!name.equals(oldName)) {
|
||||
CanteenMenuRecipeEntity patch = new CanteenMenuRecipeEntity();
|
||||
patch.setCategory(name);
|
||||
menuRecipeMapper.update(patch, Wrappers.<CanteenMenuRecipeEntity>lambdaUpdate()
|
||||
.eq(CanteenMenuRecipeEntity::getCategory, oldName));
|
||||
}
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean removeCategories(Long[] ids) {
|
||||
if (ids == null || ids.length == 0) {
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
List<CanteenMenuRecipeCategoryEntity> list = baseMapper.selectBatchIds(CollUtil.toList(ids));
|
||||
for (CanteenMenuRecipeCategoryEntity cat : list) {
|
||||
long used = menuRecipeMapper.selectCount(Wrappers.<CanteenMenuRecipeEntity>lambdaQuery()
|
||||
.eq(CanteenMenuRecipeEntity::getCategory, cat.getName()));
|
||||
if (used > 0) {
|
||||
throw new IllegalArgumentException("分类「" + cat.getName() + "」已被 " + used + " 道菜谱使用,无法删除");
|
||||
}
|
||||
}
|
||||
return removeByIds(CollUtil.toList(ids));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CanteenMenuRecipeCategoryEntity> listEnabled() {
|
||||
return baseMapper.selectList(Wrappers.<CanteenMenuRecipeCategoryEntity>lambdaQuery()
|
||||
.eq(CanteenMenuRecipeCategoryEntity::getStatus, 0)
|
||||
.orderByAsc(CanteenMenuRecipeCategoryEntity::getSortOrder)
|
||||
.orderByAsc(CanteenMenuRecipeCategoryEntity::getId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> enabledNameSet() {
|
||||
return listEnabled().stream()
|
||||
.map(CanteenMenuRecipeCategoryEntity::getName)
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.map(String::trim)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String normalize(String category) {
|
||||
return normalize(category, enabledNameSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String normalize(String category, Set<String> enabledNames) {
|
||||
if (StrUtil.isBlank(category)) {
|
||||
return null;
|
||||
}
|
||||
String name = category.trim();
|
||||
if (enabledNames == null || enabledNames.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return enabledNames.contains(name) ? name : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsEnabledName(String name) {
|
||||
if (StrUtil.isBlank(name)) {
|
||||
return false;
|
||||
}
|
||||
return enabledNameSet().contains(name.trim());
|
||||
}
|
||||
|
||||
private boolean existsName(String name, Long excludeId) {
|
||||
LambdaQueryWrapper<CanteenMenuRecipeCategoryEntity> w = Wrappers.<CanteenMenuRecipeCategoryEntity>lambdaQuery()
|
||||
.eq(CanteenMenuRecipeCategoryEntity::getName, name);
|
||||
if (excludeId != null) {
|
||||
w.ne(CanteenMenuRecipeCategoryEntity::getId, excludeId);
|
||||
}
|
||||
return baseMapper.selectCount(w) > 0;
|
||||
}
|
||||
}
|
||||
+242
-15
@@ -1,6 +1,8 @@
|
||||
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.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
@@ -14,15 +16,20 @@ import com.bsd.pjcloud.canteen.mapper.CanteenErpMaterialMapper;
|
||||
import com.bsd.pjcloud.canteen.mapper.CanteenMenuRecipeMapper;
|
||||
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.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;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -35,6 +42,7 @@ public class CanteenMenuRecipeServiceImpl extends ServiceImpl<CanteenMenuRecipeM
|
||||
private final CanteenMenuRecipeMaterialMapper materialMapper;
|
||||
private final CanteenErpMaterialMapper erpMaterialMapper;
|
||||
private final CanteenOnlineSpuMapper onlineSpuMapper;
|
||||
private final CanteenMenuRecipeCategoryService recipeCategoryService;
|
||||
|
||||
@Override
|
||||
public Page<CanteenMenuRecipeEntity> pageScope(Page<CanteenMenuRecipeEntity> page,
|
||||
@@ -50,6 +58,7 @@ public class CanteenMenuRecipeServiceImpl extends ServiceImpl<CanteenMenuRecipeM
|
||||
if (entity.getStatus() == null) {
|
||||
entity.setStatus(0);
|
||||
}
|
||||
entity.setCategory(recipeCategoryService.normalize(entity.getCategory()));
|
||||
baseMapper.insert(entity);
|
||||
saveMaterials(entity.getId(), entity.getMaterialList());
|
||||
return Boolean.TRUE;
|
||||
@@ -58,6 +67,7 @@ public class CanteenMenuRecipeServiceImpl extends ServiceImpl<CanteenMenuRecipeM
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean updateDeep(CanteenMenuRecipeEntity entity) {
|
||||
entity.setCategory(recipeCategoryService.normalize(entity.getCategory()));
|
||||
baseMapper.updateById(entity);
|
||||
materialMapper.delete(Wrappers.<CanteenMenuRecipeMaterialEntity>lambdaQuery()
|
||||
.eq(CanteenMenuRecipeMaterialEntity::getRecipeId, entity.getId()));
|
||||
@@ -96,7 +106,7 @@ public class CanteenMenuRecipeServiceImpl extends ServiceImpl<CanteenMenuRecipeM
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CanteenMenuRecipeVO> matchByMaterials(CanteenMenuRecipeMatchDTO dto) {
|
||||
public List<CanteenMenuRecipeVO> matchByMaterials(CanteenMenuRecipeMatchDTO dto, boolean brief) {
|
||||
if (dto == null || CollUtil.isEmpty(dto.getMaterialIds())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
@@ -112,26 +122,243 @@ public class CanteenMenuRecipeServiceImpl extends ServiceImpl<CanteenMenuRecipeM
|
||||
if (CollUtil.isEmpty(recipes)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Long> recipeIds = recipes.stream().map(CanteenMenuRecipeEntity::getId).toList();
|
||||
List<CanteenMenuRecipeMaterialEntity> allLines = materialMapper.selectList(
|
||||
Wrappers.<CanteenMenuRecipeMaterialEntity>lambdaQuery()
|
||||
.in(CanteenMenuRecipeMaterialEntity::getRecipeId, recipeIds)
|
||||
.orderByAsc(CanteenMenuRecipeMaterialEntity::getSortOrder));
|
||||
Map<Long, List<Long>> recipeMaterialMap = allLines.stream()
|
||||
.collect(Collectors.groupingBy(CanteenMenuRecipeMaterialEntity::getRecipeId,
|
||||
Collectors.mapping(CanteenMenuRecipeMaterialEntity::getMaterialId, Collectors.toList())));
|
||||
|
||||
List<CanteenMenuRecipeVO> matched = new ArrayList<>();
|
||||
List<Long> recipeIds = recipes.stream().map(CanteenMenuRecipeEntity::getId).toList();
|
||||
List<CanteenMenuRecipeMaterialEntity> allLines = selectMaterialsByRecipeIds(recipeIds);
|
||||
Map<Long, List<CanteenMenuRecipeMaterialEntity>> linesByRecipe = allLines.stream()
|
||||
.collect(Collectors.groupingBy(CanteenMenuRecipeMaterialEntity::getRecipeId));
|
||||
|
||||
List<CanteenMenuRecipeEntity> matchedRecipes = new ArrayList<>();
|
||||
for (CanteenMenuRecipeEntity recipe : recipes) {
|
||||
List<Long> needIds = recipeMaterialMap.getOrDefault(recipe.getId(), Collections.emptyList());
|
||||
if (CollUtil.isEmpty(needIds)) {
|
||||
List<CanteenMenuRecipeMaterialEntity> lines = linesByRecipe.getOrDefault(recipe.getId(), Collections.emptyList());
|
||||
if (CollUtil.isEmpty(lines)) {
|
||||
continue;
|
||||
}
|
||||
if (selected.containsAll(needIds)) {
|
||||
matched.add(getDetail(recipe.getId()));
|
||||
boolean coverAll = true;
|
||||
for (CanteenMenuRecipeMaterialEntity line : lines) {
|
||||
if (line.getMaterialId() == null || !selected.contains(line.getMaterialId())) {
|
||||
coverAll = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (coverAll) {
|
||||
matchedRecipes.add(recipe);
|
||||
}
|
||||
}
|
||||
return matched;
|
||||
if (matchedRecipes.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Set<Long> materialIds = matchedRecipes.stream()
|
||||
.flatMap(r -> linesByRecipe.getOrDefault(r.getId(), Collections.emptyList()).stream())
|
||||
.map(CanteenMenuRecipeMaterialEntity::getMaterialId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
Map<Long, CanteenErpMaterialEntity> materialMap = materialIds.isEmpty() ? Collections.emptyMap()
|
||||
: erpMaterialMapper.selectBatchIds(materialIds).stream()
|
||||
.collect(Collectors.toMap(CanteenErpMaterialEntity::getId, m -> m, (a, b) -> a));
|
||||
|
||||
Map<Long, String> spuNameMap = Collections.emptyMap();
|
||||
if (!brief) {
|
||||
Set<Long> spuIds = matchedRecipes.stream().map(CanteenMenuRecipeEntity::getOnlineSpuId)
|
||||
.filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
if (!spuIds.isEmpty()) {
|
||||
spuNameMap = onlineSpuMapper.selectBatchIds(spuIds).stream()
|
||||
.collect(Collectors.toMap(CanteenOnlineSpuEntity::getId, CanteenOnlineSpuEntity::getName, (a, b) -> a));
|
||||
}
|
||||
}
|
||||
|
||||
List<CanteenMenuRecipeVO> result = new ArrayList<>(matchedRecipes.size());
|
||||
for (CanteenMenuRecipeEntity recipe : matchedRecipes) {
|
||||
List<CanteenMenuRecipeMaterialEntity> lines = linesByRecipe.getOrDefault(recipe.getId(), Collections.emptyList());
|
||||
CanteenMenuRecipeVO vo = new CanteenMenuRecipeVO();
|
||||
vo.setId(recipe.getId());
|
||||
vo.setName(recipe.getName());
|
||||
vo.setCategory(recipe.getCategory());
|
||||
vo.setStatus(recipe.getStatus());
|
||||
vo.setMerchantId(recipe.getMerchantId());
|
||||
if (!brief) {
|
||||
vo.setDeptId(recipe.getDeptId());
|
||||
vo.setPicUrl(recipe.getPicUrl());
|
||||
vo.setRemark(recipe.getRemark());
|
||||
vo.setOnlineSpuId(recipe.getOnlineSpuId());
|
||||
vo.setCreateTime(recipe.getCreateTime());
|
||||
if (recipe.getOnlineSpuId() != null) {
|
||||
vo.setOnlineSpuName(spuNameMap.get(recipe.getOnlineSpuId()));
|
||||
}
|
||||
List<CanteenMenuRecipeMaterialVO> materialVos = new ArrayList<>(lines.size());
|
||||
for (CanteenMenuRecipeMaterialEntity line : lines) {
|
||||
CanteenMenuRecipeMaterialVO mv = new CanteenMenuRecipeMaterialVO();
|
||||
mv.setId(line.getId());
|
||||
mv.setMaterialId(line.getMaterialId());
|
||||
mv.setQuantity(line.getQuantity());
|
||||
mv.setSortOrder(line.getSortOrder());
|
||||
CanteenErpMaterialEntity mat = materialMap.get(line.getMaterialId());
|
||||
if (mat != null) {
|
||||
mv.setMaterialName(mat.getName());
|
||||
mv.setUnit(mat.getUnit());
|
||||
}
|
||||
materialVos.add(mv);
|
||||
}
|
||||
vo.setMaterialList(materialVos);
|
||||
}
|
||||
String summary = lines.stream().map(line -> {
|
||||
CanteenErpMaterialEntity m = materialMap.get(line.getMaterialId());
|
||||
return m != null ? m.getName() : String.valueOf(line.getMaterialId());
|
||||
}).collect(Collectors.joining("、"));
|
||||
vo.setMaterialSummary(summary);
|
||||
result.add(vo);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 按菜谱ID分批拉取明细,避免超长 IN */
|
||||
private List<CanteenMenuRecipeMaterialEntity> selectMaterialsByRecipeIds(List<Long> recipeIds) {
|
||||
if (CollUtil.isEmpty(recipeIds)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<CanteenMenuRecipeMaterialEntity> all = new ArrayList<>();
|
||||
final int batchSize = 500;
|
||||
for (int i = 0; i < recipeIds.size(); i += batchSize) {
|
||||
List<Long> batch = recipeIds.subList(i, Math.min(i + batchSize, recipeIds.size()));
|
||||
all.addAll(materialMapper.selectList(Wrappers.<CanteenMenuRecipeMaterialEntity>lambdaQuery()
|
||||
.in(CanteenMenuRecipeMaterialEntity::getRecipeId, batch)
|
||||
.orderByAsc(CanteenMenuRecipeMaterialEntity::getSortOrder)));
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public CanteenMenuRecipeImportResultVO importCustomerExcel(MultipartFile file, String category, String merchantId) {
|
||||
String cat = recipeCategoryService.normalize(category);
|
||||
if (cat == null) {
|
||||
throw new IllegalArgumentException("分类无效或已停用,请先在「菜谱分类」中维护");
|
||||
}
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new IllegalArgumentException("请上传 Excel 文件");
|
||||
}
|
||||
|
||||
List<Map<Integer, String>> rows;
|
||||
try {
|
||||
rows = EasyExcel.read(file.getInputStream()).sheet().headRowNumber(0).doReadSync();
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new IllegalArgumentException("Excel 读取失败:" + e.getMessage());
|
||||
}
|
||||
if (CollUtil.isEmpty(rows) || rows.size() < 2) {
|
||||
throw new IllegalArgumentException("Excel 无有效数据(需含表头+至少一行)");
|
||||
}
|
||||
|
||||
CanteenMenuRecipeImportResultVO result = new CanteenMenuRecipeImportResultVO();
|
||||
result.setCategory(cat);
|
||||
Long deptId = SecurityUtils.getUser().getDeptId();
|
||||
String mch = StrUtil.blankToDefault(merchantId, null);
|
||||
|
||||
Map<String, CanteenErpMaterialEntity> materialCache = new HashMap<>();
|
||||
Set<String> existingRecipeNames = baseMapper.selectList(Wrappers.<CanteenMenuRecipeEntity>lambdaQuery()
|
||||
.eq(CanteenMenuRecipeEntity::getCategory, cat)
|
||||
.select(CanteenMenuRecipeEntity::getName))
|
||||
.stream()
|
||||
.map(CanteenMenuRecipeEntity::getName)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
int created = 0;
|
||||
int skipped = 0;
|
||||
for (int i = 1; i < rows.size(); i++) {
|
||||
Map<Integer, String> row = rows.get(i);
|
||||
if (row == null || row.isEmpty()) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
String dishName = StrUtil.trim(cell(row, 1));
|
||||
if (StrUtil.isBlank(dishName)) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
if (existingRecipeNames.contains(dishName)) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
List<CanteenMenuRecipeMaterialEntity> lines = new ArrayList<>();
|
||||
for (int c = 2; c <= 10; c += 2) {
|
||||
CustomerRecipeExcelParser.ParsedMaterial parsed = CustomerRecipeExcelParser.parseMaterialCell(
|
||||
cell(row, c), cell(row, c + 1));
|
||||
if (parsed == null || StrUtil.isBlank(parsed.name())) {
|
||||
continue;
|
||||
}
|
||||
CanteenErpMaterialEntity material = resolveOrCreateMaterial(parsed, materialCache, result, deptId);
|
||||
CanteenMenuRecipeMaterialEntity line = new CanteenMenuRecipeMaterialEntity();
|
||||
line.setMaterialId(material.getId());
|
||||
line.setQuantity(parsed.quantity());
|
||||
line.setSortOrder(lines.size());
|
||||
lines.add(line);
|
||||
}
|
||||
|
||||
CanteenMenuRecipeEntity recipe = new CanteenMenuRecipeEntity();
|
||||
recipe.setName(dishName);
|
||||
recipe.setCategory(cat);
|
||||
recipe.setMerchantId(mch);
|
||||
recipe.setDeptId(deptId);
|
||||
recipe.setStatus(0);
|
||||
recipe.setMaterialList(lines);
|
||||
saveDeep(recipe);
|
||||
existingRecipeNames.add(dishName);
|
||||
created++;
|
||||
}
|
||||
|
||||
result.setRecipeCreated(created);
|
||||
result.setSkipped(skipped);
|
||||
result.setMessage(String.format("分类[%s]:新建菜谱%d道,新建食材%d种,复用食材%d种,跳过%d行",
|
||||
cat, created, result.getMaterialCreated(), result.getMaterialReused(), skipped));
|
||||
return result;
|
||||
}
|
||||
|
||||
private CanteenErpMaterialEntity resolveOrCreateMaterial(CustomerRecipeExcelParser.ParsedMaterial parsed,
|
||||
Map<String, CanteenErpMaterialEntity> cache, CanteenMenuRecipeImportResultVO result, Long deptId) {
|
||||
String key = parsed.name();
|
||||
CanteenErpMaterialEntity cached = cache.get(key);
|
||||
if (cached != null) {
|
||||
result.setMaterialReused(result.getMaterialReused() + 1);
|
||||
upgradeUnitIfNeeded(cached, parsed.unit());
|
||||
return cached;
|
||||
}
|
||||
CanteenErpMaterialEntity existing = erpMaterialMapper.selectOne(Wrappers.<CanteenErpMaterialEntity>lambdaQuery()
|
||||
.eq(CanteenErpMaterialEntity::getName, key)
|
||||
.eq(CanteenErpMaterialEntity::getMaterialType, 2)
|
||||
.last("LIMIT 1"));
|
||||
if (existing != null) {
|
||||
cache.put(key, existing);
|
||||
result.setMaterialReused(result.getMaterialReused() + 1);
|
||||
upgradeUnitIfNeeded(existing, parsed.unit());
|
||||
return existing;
|
||||
}
|
||||
CanteenErpMaterialEntity created = new CanteenErpMaterialEntity();
|
||||
created.setName(key);
|
||||
created.setUnit(StrUtil.blankToDefault(parsed.unit(), "斤"));
|
||||
created.setMaterialType(2);
|
||||
created.setDeptId(deptId);
|
||||
erpMaterialMapper.insert(created);
|
||||
cache.put(key, created);
|
||||
result.setMaterialCreated(result.getMaterialCreated() + 1);
|
||||
return created;
|
||||
}
|
||||
|
||||
private void upgradeUnitIfNeeded(CanteenErpMaterialEntity material, String unit) {
|
||||
if (material == null || StrUtil.isBlank(unit)) {
|
||||
return;
|
||||
}
|
||||
if ("斤".equals(material.getUnit()) && !"斤".equals(unit)) {
|
||||
material.setUnit(unit);
|
||||
erpMaterialMapper.updateById(material);
|
||||
}
|
||||
}
|
||||
|
||||
private static String cell(Map<Integer, String> row, int col) {
|
||||
Object v = row.get(col);
|
||||
return v == null ? null : String.valueOf(v);
|
||||
}
|
||||
|
||||
private void saveMaterials(Long recipeId, List<CanteenMenuRecipeMaterialEntity> list) {
|
||||
|
||||
+6
@@ -36,6 +36,12 @@ public class MallProductSkuEntity extends Model<MallProductSkuEntity> {
|
||||
@Schema(description="SPU ID")
|
||||
private Long productId;
|
||||
|
||||
/**
|
||||
* 商户ID(冗余,SKU编码在商户内唯一)
|
||||
*/
|
||||
@Schema(description="商户ID")
|
||||
private Long merchantId;
|
||||
|
||||
/**
|
||||
* SKU编码
|
||||
*/
|
||||
|
||||
+9
@@ -6,9 +6,18 @@ import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface MallProductSkuMapper extends PjcloudBaseMapper<MallProductSkuEntity> {
|
||||
|
||||
/**
|
||||
* 统计商户下已存在的 SKU 编码数量(含已软删记录,与唯一索引一致)
|
||||
*/
|
||||
long countExistingSkuCodes(@Param("merchantId") Long merchantId,
|
||||
@Param("skuCodes") List<String> skuCodes,
|
||||
@Param("excludeProductId") Long excludeProductId);
|
||||
|
||||
/**
|
||||
* 更新库存
|
||||
* @param skuId SKU ID
|
||||
|
||||
+7
@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.bsd.pjcloud.supermarket.entity.MallProductSkuEntity;
|
||||
import com.bsd.pjcloud.supermarket.dto.SkuStockChangeDTO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface MallProductSkuService extends IService<MallProductSkuEntity> {
|
||||
|
||||
/**
|
||||
@@ -19,4 +21,9 @@ public interface MallProductSkuService extends IService<MallProductSkuEntity> {
|
||||
* 批量增减库存(正数增加,负数扣减),任一失败则返回false
|
||||
*/
|
||||
boolean deductStockBatch(SkuStockChangeDTO changes);
|
||||
|
||||
/**
|
||||
* 统计商户下已存在的 SKU 编码数量(含已软删记录)
|
||||
*/
|
||||
long countExistingSkuCodes(Long merchantId, List<String> skuCodes, Long excludeProductId);
|
||||
}
|
||||
+26
-14
@@ -63,10 +63,12 @@ public class MallProductServiceImpl extends ServiceImpl<MallProductMapper, MallP
|
||||
|
||||
// 3. 处理SKU(如果有)
|
||||
if (CollUtil.isNotEmpty(productCreateDTO.getSkuList())) {
|
||||
Long merchantId = productCreateDTO.getMerchantId();
|
||||
List<MallProductSkuEntity> skuList = productCreateDTO.getSkuList().stream()
|
||||
.map(skuDTO -> {
|
||||
MallProductSkuEntity sku = new MallProductSkuEntity();
|
||||
sku.setProductId(product.getId());
|
||||
sku.setMerchantId(merchantId);
|
||||
sku.setSkuCode(skuDTO.getSkuCode());
|
||||
sku.setSkuName(skuDTO.getSkuName());
|
||||
sku.setPrice(skuDTO.getPrice());
|
||||
@@ -135,7 +137,14 @@ public class MallProductServiceImpl extends ServiceImpl<MallProductMapper, MallP
|
||||
|
||||
// 3. 处理SKU(精确处理SKU的增删改)
|
||||
if (CollUtil.isNotEmpty(productCreateDTO.getSkuList())) {
|
||||
updateSkusForProduct(product.getId(), productCreateDTO.getSkuList());
|
||||
updateSkusForProduct(product.getId(), product.getMerchantId(), productCreateDTO.getSkuList());
|
||||
|
||||
// 商户变更时同步 SKU 冗余字段
|
||||
mallProductSkuService.update(
|
||||
Wrappers.<MallProductSkuEntity>lambdaUpdate()
|
||||
.set(MallProductSkuEntity::getMerchantId, product.getMerchantId())
|
||||
.eq(MallProductSkuEntity::getProductId, product.getId())
|
||||
);
|
||||
|
||||
// 计算总库存
|
||||
List<MallProductSkuEntity> currentSkus = mallProductSkuService.list(
|
||||
@@ -165,7 +174,7 @@ public class MallProductServiceImpl extends ServiceImpl<MallProductMapper, MallP
|
||||
* @param productId 商品ID
|
||||
* @param newSkuList 新的SKU列表
|
||||
*/
|
||||
private void updateSkusForProduct(Long productId, List<MallProductSkuCreateDTO> newSkuList) {
|
||||
private void updateSkusForProduct(Long productId, Long merchantId, List<MallProductSkuCreateDTO> newSkuList) {
|
||||
// 1. 获取数据库中现有的SKU列表
|
||||
List<MallProductSkuEntity> existingSkus = mallProductSkuService.list(
|
||||
Wrappers.<MallProductSkuEntity>lambdaQuery()
|
||||
@@ -200,6 +209,7 @@ public class MallProductServiceImpl extends ServiceImpl<MallProductMapper, MallP
|
||||
.map(skuDTO -> {
|
||||
MallProductSkuEntity sku = new MallProductSkuEntity();
|
||||
sku.setProductId(productId);
|
||||
sku.setMerchantId(merchantId);
|
||||
sku.setSkuCode(skuDTO.getSkuCode());
|
||||
sku.setSkuName(skuDTO.getSkuName());
|
||||
sku.setPrice(skuDTO.getPrice());
|
||||
@@ -220,6 +230,7 @@ public class MallProductServiceImpl extends ServiceImpl<MallProductMapper, MallP
|
||||
MallProductSkuEntity existingSku = existingSkuMap.get(skuDTO.getSkuCode());
|
||||
// 更新字段
|
||||
existingSku.setSkuName(skuDTO.getSkuName());
|
||||
existingSku.setMerchantId(merchantId);
|
||||
existingSku.setPrice(skuDTO.getPrice());
|
||||
existingSku.setCostPrice(skuDTO.getCostPrice());
|
||||
existingSku.setStock(skuDTO.getStock());
|
||||
@@ -310,6 +321,10 @@ public class MallProductServiceImpl extends ServiceImpl<MallProductMapper, MallP
|
||||
* @param productCreateDTO 商品创建DTO
|
||||
*/
|
||||
private void validateProductData(MallProductCreateDTO productCreateDTO) {
|
||||
if (productCreateDTO.getMerchantId() == null) {
|
||||
throw new RuntimeException("商户ID不能为空");
|
||||
}
|
||||
|
||||
// 如果有SKU,验证SKU数据
|
||||
if (CollUtil.isNotEmpty(productCreateDTO.getSkuList())) {
|
||||
// 验证SKU编码在当前请求中的唯一性
|
||||
@@ -320,22 +335,19 @@ public class MallProductServiceImpl extends ServiceImpl<MallProductMapper, MallP
|
||||
if (distinctSkuCodes != productCreateDTO.getSkuList().size()) {
|
||||
throw new RuntimeException("SKU编码不能重复");
|
||||
}
|
||||
|
||||
// 验证SKU编码全局唯一性(排除当前商品)
|
||||
|
||||
// 验证SKU编码在商户内唯一(排除当前商品)
|
||||
List<String> skuCodes = productCreateDTO.getSkuList().stream()
|
||||
.map(MallProductSkuCreateDTO::getSkuCode)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
LambdaQueryWrapper<MallProductSkuEntity> wrapper = Wrappers.lambdaQuery();
|
||||
wrapper.in(MallProductSkuEntity::getSkuCode, skuCodes);
|
||||
if (productCreateDTO.getId() != null) {
|
||||
// 编辑时排除当前商品的SKU
|
||||
wrapper.ne(MallProductSkuEntity::getProductId, productCreateDTO.getId());
|
||||
}
|
||||
|
||||
long existingCount = mallProductSkuService.count(wrapper);
|
||||
|
||||
long existingCount = mallProductSkuService.countExistingSkuCodes(
|
||||
productCreateDTO.getMerchantId(),
|
||||
skuCodes,
|
||||
productCreateDTO.getId()
|
||||
);
|
||||
if (existingCount > 0) {
|
||||
throw new RuntimeException("SKU编码已存在,请使用不同的编码");
|
||||
throw new RuntimeException("该商户下SKU编码已存在,请使用不同的编码");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -9,6 +9,8 @@ import com.bsd.pjcloud.supermarket.dto.SkuStockChangeDTO;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 共富商城商品SKU
|
||||
*
|
||||
@@ -59,4 +61,12 @@ public class MallProductSkuServiceImpl extends ServiceImpl<MallProductSkuMapper,
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countExistingSkuCodes(Long merchantId, List<String> skuCodes, Long excludeProductId) {
|
||||
if (merchantId == null || skuCodes == null || skuCodes.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
return baseMapper.countExistingSkuCodes(merchantId, skuCodes, excludeProductId);
|
||||
}
|
||||
}
|
||||
+14
@@ -6,6 +6,7 @@
|
||||
<resultMap id="mallProductSkuMap" type="com.bsd.pjcloud.supermarket.entity.MallProductSkuEntity">
|
||||
<id property="id" column="id"/>
|
||||
<result property="productId" column="product_id"/>
|
||||
<result property="merchantId" column="merchant_id"/>
|
||||
<result property="skuCode" column="sku_code"/>
|
||||
<result property="skuName" column="sku_name"/>
|
||||
<result property="price" column="price"/>
|
||||
@@ -23,4 +24,17 @@
|
||||
<result property="delFlag" column="del_flag"/>
|
||||
<result property="tenantId" column="tenant_id"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="countExistingSkuCodes" resultType="long">
|
||||
SELECT COUNT(1)
|
||||
FROM mall_product_sku
|
||||
WHERE merchant_id = #{merchantId}
|
||||
AND sku_code IN
|
||||
<foreach collection="skuCodes" item="code" open="(" separator="," close=")">
|
||||
#{code}
|
||||
</foreach>
|
||||
<if test="excludeProductId != null">
|
||||
AND product_id != #{excludeProductId}
|
||||
</if>
|
||||
</select>
|
||||
</mapper>
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package com.bsd.pjcloud.admin.api.util;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 姓名/昵称:支持中文,或英文(单词间可空格,如 John Smith)
|
||||
*/
|
||||
public final class PersonNameUtils {
|
||||
|
||||
public static final String CODE_NAME_EMPTY = "sys.person.name.empty";
|
||||
|
||||
public static final String CODE_NICKNAME_EMPTY = "sys.person.nickname.empty";
|
||||
|
||||
public static final String CODE_NAME_INVALID = "sys.person.name.invalid";
|
||||
|
||||
public static final String CODE_NICKNAME_INVALID = "sys.person.nickname.invalid";
|
||||
|
||||
public static final String CODE_NAME_TOO_LONG = "sys.person.name.too.long";
|
||||
|
||||
private static final int MAX_LEN = 32;
|
||||
|
||||
private static final Pattern PERSON_NAME_PATTERN = Pattern.compile(
|
||||
"^(?:[\\u4E00-\\u9FA5]{1,32}|[A-Za-z]+(?:\\s[A-Za-z]+){0,8}|[\\u4E00-\\u9FA5A-Za-z]+(?:\\s[\\u4E00-\\u9FA5A-Za-z]+){0,8})$");
|
||||
|
||||
private PersonNameUtils() {
|
||||
}
|
||||
|
||||
public static String normalize(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return value.trim().replaceAll("\\s+", " ");
|
||||
}
|
||||
|
||||
public static boolean isValid(String value) {
|
||||
if (StrUtil.isBlank(value)) {
|
||||
return false;
|
||||
}
|
||||
String normalized = normalize(value);
|
||||
return normalized.length() <= MAX_LEN && PERSON_NAME_PATTERN.matcher(normalized).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return i18n 错误码,无错误返回 null
|
||||
*/
|
||||
public static String validateErrorCode(String value, boolean nickname) {
|
||||
if (StrUtil.isBlank(value)) {
|
||||
return nickname ? CODE_NICKNAME_EMPTY : CODE_NAME_EMPTY;
|
||||
}
|
||||
String normalized = normalize(value);
|
||||
if (normalized.length() > MAX_LEN) {
|
||||
return CODE_NAME_TOO_LONG;
|
||||
}
|
||||
if (!PERSON_NAME_PATTERN.matcher(normalized).matches()) {
|
||||
return nickname ? CODE_NICKNAME_INVALID : CODE_NAME_INVALID;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @deprecated 使用 validateErrorCode + MsgUtils */
|
||||
@Deprecated
|
||||
public static String validateOrNull(String value, String fieldLabel) {
|
||||
String code = validateErrorCode(value, "昵称".equals(fieldLabel));
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
if (CODE_NAME_TOO_LONG.equals(code)) {
|
||||
return fieldLabel + "不能超过" + MAX_LEN + "个字符";
|
||||
}
|
||||
if (CODE_NAME_EMPTY.equals(code) || CODE_NICKNAME_EMPTY.equals(code)) {
|
||||
return fieldLabel + "不能为空";
|
||||
}
|
||||
return fieldLabel + "仅支持中文,或英文(单词间可用空格,如 John Smith)";
|
||||
}
|
||||
|
||||
}
|
||||
+4
@@ -22,6 +22,7 @@ package com.bsd.pjcloud.admin.controller;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
@@ -325,6 +326,9 @@ public class SysUserController {
|
||||
@SysLog("修改个人信息")
|
||||
@PutMapping("/personal/edit")
|
||||
public R updateUserInfo(@Valid @RequestBody UserDTO userDto) {
|
||||
if (StrUtil.isBlank(userDto.getUsername())) {
|
||||
userDto.setUsername(SecurityUtils.getUser().getUsername());
|
||||
}
|
||||
return userService.updateUserInfo(userDto);
|
||||
}
|
||||
|
||||
|
||||
+84
-4
@@ -40,6 +40,7 @@ import com.bsd.pjcloud.admin.api.constant.UserStateEnum;
|
||||
import com.bsd.pjcloud.admin.api.dto.ForgotPasswordDTO;
|
||||
import com.bsd.pjcloud.admin.api.dto.SysUserFaceDto;
|
||||
import com.bsd.pjcloud.admin.api.dto.UserDTO;
|
||||
import com.bsd.pjcloud.admin.api.util.PersonNameUtils;
|
||||
import com.bsd.pjcloud.admin.api.dto.UserInfo;
|
||||
import com.bsd.pjcloud.admin.api.entity.*;
|
||||
import com.bsd.pjcloud.admin.api.entity.user.SysUserTagEntity;
|
||||
@@ -389,14 +390,28 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
||||
@Override
|
||||
@CacheEvict(value = CacheConstants.USER_DETAILS, key = "#userDto.username")
|
||||
public R<Boolean> updateUserInfo(UserDTO userDto) {
|
||||
if (StrUtil.isBlank(userDto.getUsername())) {
|
||||
userDto.setUsername(SecurityUtils.getUser().getUsername());
|
||||
}
|
||||
String nameErr = PersonNameUtils.validateOrNull(userDto.getName(), "姓名");
|
||||
if (nameErr != null) {
|
||||
return R.failed(nameErr);
|
||||
}
|
||||
String nicknameErr = PersonNameUtils.validateOrNull(userDto.getNickname(), "昵称");
|
||||
if (nicknameErr != null) {
|
||||
return R.failed(nicknameErr);
|
||||
}
|
||||
Long userId = SecurityUtils.getUser().getId();
|
||||
SysUser sysUser = new SysUser();
|
||||
sysUser.setPhone(userDto.getPhone());
|
||||
sysUser.setUserId(userId);
|
||||
sysUser.setAvatar(userDto.getAvatar());
|
||||
sysUser.setNickname(userDto.getNickname());
|
||||
sysUser.setName(userDto.getName());
|
||||
sysUser.setNickname(PersonNameUtils.normalize(userDto.getNickname()));
|
||||
sysUser.setName(PersonNameUtils.normalize(userDto.getName()));
|
||||
sysUser.setEmail(userDto.getEmail());
|
||||
// 个人资料:未传手机号时不修改(移动端手机号不可编辑)
|
||||
if (StrUtil.isNotBlank(userDto.getPhone()) && !userDto.getPhone().contains("*")) {
|
||||
sysUser.setPhone(userDto.getPhone());
|
||||
}
|
||||
return R.ok(this.updateById(sysUser));
|
||||
}
|
||||
|
||||
@@ -405,9 +420,16 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
||||
@Audit(name = "用户更新", spel = "@sysUserMapper.selectById(#userDto.userId)")
|
||||
@CacheEvict(value = CacheConstants.USER_DETAILS, key = "#userDto.username")
|
||||
public Boolean updateUser(UserDTO userDto) {
|
||||
Long userId = resolveUserIdForUpdate(userDto);
|
||||
if (userId == null) {
|
||||
throw new RuntimeException("用户ID不能为空");
|
||||
}
|
||||
userDto.setUserId(userId);
|
||||
|
||||
// 更新用户表信息
|
||||
SysUser sysUser = new SysUser();
|
||||
BeanUtils.copyProperties(userDto, sysUser);
|
||||
sysUser.setUserId(userId);
|
||||
sysUser.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
if (StrUtil.isNotBlank(userDto.getPassword())) {
|
||||
@@ -416,10 +438,20 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
||||
sysUser.setPasswordExpireFlag(UserExpireEnum.EXPIRE.getCode());
|
||||
}
|
||||
|
||||
// 脱敏手机号不参与更新,避免把 138****1234 写回库
|
||||
if (StrUtil.isNotBlank(userDto.getPhone()) && userDto.getPhone().contains("*")) {
|
||||
SysUser dbUser = this.getById(userId);
|
||||
sysUser.setPhone(dbUser != null ? dbUser.getPhone() : null);
|
||||
}
|
||||
|
||||
// 判断用户username是否重复
|
||||
if (existUsername(userDto.getUserId(), userDto.getUsername())) {
|
||||
if (existUsername(userId, userDto.getUsername())) {
|
||||
throw new RuntimeException("该用户名已存在");
|
||||
}
|
||||
// 判断手机号是否重复
|
||||
if (StrUtil.isNotBlank(sysUser.getPhone()) && existPhone(userId, sysUser.getPhone())) {
|
||||
throw new RuntimeException(MsgUtils.getMessage(ErrorCodes.SYS_USER_PHONE_EXISTING, sysUser.getPhone()));
|
||||
}
|
||||
|
||||
this.updateById(sysUser);
|
||||
|
||||
@@ -801,6 +833,20 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
||||
String message = MsgUtils.getMessage(ErrorCodes.SYS_USER_PHONE_EXISTING, userDto.getPhone());
|
||||
throw new RuntimeException(message);
|
||||
}
|
||||
if (StrUtil.isNotBlank(userDto.getName())) {
|
||||
String nameErr = PersonNameUtils.validateOrNull(userDto.getName(), "姓名");
|
||||
if (nameErr != null) {
|
||||
throw new RuntimeException(nameErr);
|
||||
}
|
||||
userDto.setName(PersonNameUtils.normalize(userDto.getName()));
|
||||
}
|
||||
if (StrUtil.isNotBlank(userDto.getNickname())) {
|
||||
String nicknameErr = PersonNameUtils.validateOrNull(userDto.getNickname(), "昵称");
|
||||
if (nicknameErr != null) {
|
||||
throw new RuntimeException(nicknameErr);
|
||||
}
|
||||
userDto.setNickname(PersonNameUtils.normalize(userDto.getNickname()));
|
||||
}
|
||||
SysUser sysUser = new SysUser();
|
||||
BeanUtils.copyProperties(userDto, sysUser);
|
||||
|
||||
@@ -1150,4 +1196,38 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
||||
}
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
private boolean existPhone(Long userId, String phone) {
|
||||
if (StrUtil.isBlank(phone) || phone.contains("*")) {
|
||||
return false;
|
||||
}
|
||||
Long count = baseMapper.selectCount(new LambdaQueryWrapper<SysUser>()
|
||||
.eq(SysUser::getPhone, phone)
|
||||
.ne(ObjectUtil.isNotEmpty(userId), SysUser::getUserId, userId)
|
||||
);
|
||||
if (ObjectUtil.isNotEmpty(userId)) {
|
||||
return count >= 1;
|
||||
}
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* UserDTO 子类重复声明了 userId,Jackson 可能只写入父类字段,需兼容读取。
|
||||
*/
|
||||
private Long resolveUserIdForUpdate(UserDTO userDto) {
|
||||
if (userDto.getUserId() != null) {
|
||||
return userDto.getUserId();
|
||||
}
|
||||
try {
|
||||
java.lang.reflect.Field field = SysUser.class.getDeclaredField("userId");
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(userDto);
|
||||
if (value instanceof Long) {
|
||||
return (Long) value;
|
||||
}
|
||||
} catch (ReflectiveOperationException ignored) {
|
||||
// ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -243,7 +243,7 @@
|
||||
</if>
|
||||
<if test="query.nameOrPhone != null and query.nameOrPhone != ''">
|
||||
<bind name="nameOrPhone" value="'%'+query.nameOrPhone+'%'"/>
|
||||
AND (u.phone LIKE #{nameOrPhone} OR u.name LIKE #{nameOrPhone} )
|
||||
AND (u.phone LIKE #{nameOrPhone} OR u.name LIKE #{nameOrPhone} OR u.username LIKE #{nameOrPhone})
|
||||
</if>
|
||||
<if test="query.userIds != null and query.userIds.size() != 0">
|
||||
and u.user_id IN
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
const fs = require('fs');
|
||||
const XLSX = require('xlsx');
|
||||
|
||||
const files = [
|
||||
{ file: '大荤.xlsx', cat: '大荤' },
|
||||
{ file: '小荤.xlsx', cat: '小荤' },
|
||||
{ file: '素菜.xlsx', cat: '素菜' },
|
||||
];
|
||||
|
||||
const TRAIL_RE = /^(.+?)\s*(\d+(?:\.\d+)?)\s*(斤|只|包|瓶|盒|袋|根)$/;
|
||||
const LEAD_NUM_RE = /^(\d+(?:\.\d+)?)\s*(斤|只|条|包|瓶|盒)?(.+)$/;
|
||||
const LEAD_CN_RE = /^([一二三四五六七八九十两]+)\s*(条|斤|只|包)?(.+)$/;
|
||||
const CN_NUM = { 一: 1, 二: 2, 两: 2, 三: 3, 四: 4, 五: 5, 六: 6, 七: 7, 八: 8, 九: 9, 十: 10 };
|
||||
|
||||
function escapeSql(s) {
|
||||
return String(s).replace(/\\/g, '\\\\').replace(/'/g, "''");
|
||||
}
|
||||
|
||||
function cleanName(raw) {
|
||||
return String(raw || '')
|
||||
.replace(/(/g, '(')
|
||||
.replace(/)/g, ')')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.replace(/\(斤\)/g, '')
|
||||
.replace(/\(盒\)/g, '')
|
||||
.replace(/\(块\)/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function parseMaterialCell(nameRaw, qtyRaw) {
|
||||
let name = String(nameRaw || '').trim();
|
||||
if (!name) return null;
|
||||
|
||||
let qty = qtyRaw === '' || qtyRaw === null || qtyRaw === undefined ? null : qtyRaw;
|
||||
let unitHint = null;
|
||||
|
||||
if (typeof qty === 'string') {
|
||||
const qm = String(qty).trim().match(/^(\d+(?:\.\d+)?)\s*(斤|只|包|瓶|盒|袋|个)?$/);
|
||||
if (qm) {
|
||||
qty = Number(qm[1]);
|
||||
if (qm[2]) unitHint = qm[2];
|
||||
} else if (String(qty).includes('包')) {
|
||||
const m = String(qty).match(/(\d+)/);
|
||||
qty = m ? Number(m[1]) : null;
|
||||
unitHint = '包';
|
||||
} else {
|
||||
qty = null;
|
||||
}
|
||||
}
|
||||
|
||||
let m = name.match(TRAIL_RE);
|
||||
if (m) {
|
||||
return {
|
||||
name: cleanName(m[1]),
|
||||
quantity: qty != null ? Number(qty) : Number(m[2]),
|
||||
unit: m[3],
|
||||
};
|
||||
}
|
||||
|
||||
if (/^\d/.test(name)) {
|
||||
m = name.match(LEAD_NUM_RE);
|
||||
if (m && m[3] && /[\u4e00-\u9fa5]/.test(m[3])) {
|
||||
// 1.5条肉 -> 条肉
|
||||
const matName = m[2] === '条' ? cleanName(m[2] + m[3]) : cleanName(m[3]);
|
||||
return {
|
||||
name: matName,
|
||||
quantity: qty != null ? Number(qty) : Number(m[1]),
|
||||
unit: m[2] === '条' ? '条' : m[2] || '斤',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
m = name.match(LEAD_CN_RE);
|
||||
if (m && m[3] && CN_NUM[m[1]]) {
|
||||
// 两条肉 -> 条肉
|
||||
const matName = m[2] === '条' ? cleanName(m[2] + m[3]) : cleanName(m[3]);
|
||||
return {
|
||||
name: matName,
|
||||
quantity: qty != null ? Number(qty) : CN_NUM[m[1]],
|
||||
unit: m[2] || '条',
|
||||
};
|
||||
}
|
||||
|
||||
// 黑木耳0
|
||||
name = name.replace(/([\u4e00-\u9fa5])0+$/u, '$1').trim();
|
||||
const n = cleanName(name);
|
||||
if (!n) return null;
|
||||
|
||||
let unit = unitHint || '斤';
|
||||
if ((n === '鸡蛋' || n === '鹌鹑蛋') && qty != null && Number(qty) >= 1) unit = '个';
|
||||
|
||||
return {
|
||||
name: n,
|
||||
quantity: qty != null && !Number.isNaN(Number(qty)) ? Number(qty) : null,
|
||||
unit,
|
||||
};
|
||||
}
|
||||
|
||||
const materialMap = new Map();
|
||||
const recipes = [];
|
||||
let matSeq = 904000001;
|
||||
let recipeSeq = 905000001;
|
||||
let linkSeq = 906000001;
|
||||
|
||||
function getOrCreateMaterial(parsed) {
|
||||
const key = parsed.name;
|
||||
if (!materialMap.has(key)) {
|
||||
materialMap.set(key, { id: matSeq++, name: key, unit: parsed.unit || '斤' });
|
||||
} else {
|
||||
const cur = materialMap.get(key);
|
||||
if (cur.unit === '斤' && parsed.unit && parsed.unit !== '斤') cur.unit = parsed.unit;
|
||||
}
|
||||
return materialMap.get(key);
|
||||
}
|
||||
|
||||
for (const f of files) {
|
||||
const wb = XLSX.readFile('C:/Users/Administrator/Desktop/' + f.file);
|
||||
const rows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { header: 1, defval: '' });
|
||||
for (let i = 1; i < rows.length; i++) {
|
||||
const r = rows[i];
|
||||
const dishName = String(r[1] || '').trim();
|
||||
if (!dishName) continue;
|
||||
|
||||
const mats = [];
|
||||
for (let c = 2; c <= 10; c += 2) {
|
||||
const parsed = parseMaterialCell(r[c], r[c + 1]);
|
||||
if (!parsed || !parsed.name) continue;
|
||||
const mat = getOrCreateMaterial(parsed);
|
||||
mats.push({
|
||||
materialId: mat.id,
|
||||
name: mat.name,
|
||||
quantity: parsed.quantity,
|
||||
sort: mats.length,
|
||||
});
|
||||
}
|
||||
|
||||
recipes.push({
|
||||
id: recipeSeq++,
|
||||
name: dishName,
|
||||
category: f.cat,
|
||||
materials: mats,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const out = [];
|
||||
out.push('-- =============================================');
|
||||
out.push('-- 客户食材+菜谱导入(大荤/小荤/素菜 Excel)');
|
||||
out.push('-- 生成时间: ' + new Date().toISOString());
|
||||
out.push('-- ID 段: 食材 904xxxxxx / 菜谱 905xxxxxx / 明细 906xxxxxx');
|
||||
out.push('-- 请确认与库中已有 ID 不冲突;tenant_id=1;material_type=2(线上食材)');
|
||||
out.push('-- 用量:按客户原表“克重”数值写入(业务习惯多为斤),单位写入食材 unit');
|
||||
out.push('-- 菜谱 remark = 分类(大荤/小荤/素菜);无食材的菜谱仍导入菜名');
|
||||
out.push('-- =============================================');
|
||||
out.push('');
|
||||
out.push('-- ---------- 1. 食材(' + materialMap.size + ' 种) ----------');
|
||||
out.push('INSERT INTO `canteen_erp_material`');
|
||||
out.push('(`id`, `tenant_id`, `dept_id`, `name`, `unit`, `material_type`, `nutrition_tags`, `del_flag`, `create_by`, `create_time`)');
|
||||
out.push('VALUES');
|
||||
|
||||
const mats = [...materialMap.values()].sort((a, b) => a.id - b.id);
|
||||
mats.forEach((m, idx) => {
|
||||
const comma = idx === mats.length - 1 ? ';' : ',';
|
||||
out.push(`(${m.id}, 1, NULL, '${escapeSql(m.name)}', '${escapeSql(m.unit)}', 2, NULL, '0', 'admin', NOW())${comma}`);
|
||||
});
|
||||
|
||||
out.push('');
|
||||
out.push('-- ---------- 2. 排菜菜谱(' + recipes.length + ' 道) ----------');
|
||||
out.push('INSERT INTO `canteen_menu_recipe`');
|
||||
out.push('(`id`, `tenant_id`, `dept_id`, `merchant_id`, `name`, `remark`, `status`, `del_flag`, `create_by`, `create_time`)');
|
||||
out.push('VALUES');
|
||||
recipes.forEach((r, idx) => {
|
||||
const comma = idx === recipes.length - 1 ? ';' : ',';
|
||||
out.push(`(${r.id}, 1, NULL, NULL, '${escapeSql(r.name)}', '${escapeSql(r.category)}', 0, '0', 'admin', NOW())${comma}`);
|
||||
});
|
||||
|
||||
out.push('');
|
||||
out.push('-- ---------- 3. 菜谱食材明细 ----------');
|
||||
const links = [];
|
||||
for (const r of recipes) {
|
||||
for (const m of r.materials) {
|
||||
links.push({
|
||||
id: linkSeq++,
|
||||
recipeId: r.id,
|
||||
materialId: m.materialId,
|
||||
quantity: m.quantity,
|
||||
sort: m.sort,
|
||||
comment: `${r.category}-${r.name}-${m.name}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
out.push('INSERT INTO `canteen_menu_recipe_material` (`id`, `recipe_id`, `material_id`, `quantity`, `sort_order`, `create_time`) VALUES');
|
||||
links.forEach((l, idx) => {
|
||||
let qtySql = 'NULL';
|
||||
if (l.quantity != null && !Number.isNaN(l.quantity)) {
|
||||
qtySql = Number.isInteger(l.quantity)
|
||||
? l.quantity.toFixed(2)
|
||||
: String(Math.round(l.quantity * 1000) / 1000);
|
||||
}
|
||||
const comma = idx === links.length - 1 ? ';' : ',';
|
||||
out.push(`(${l.id}, ${l.recipeId}, ${l.materialId}, ${qtySql}, ${l.sort}, NOW())${comma} -- ${escapeSql(l.comment)}`);
|
||||
});
|
||||
|
||||
out.push('');
|
||||
out.push('-- 统计: 食材 ' + mats.length + ' / 菜谱 ' + recipes.length + ' / 明细 ' + links.length);
|
||||
out.push('-- 按分类: ' + files.map((f) => f.cat + '=' + recipes.filter((r) => r.category === f.cat).length).join(', '));
|
||||
|
||||
const path = 'C:/workcode/java/biz-api-cloud-zax/db/1030pjcloudx_canteen_customer_recipe_import.sql';
|
||||
fs.writeFileSync(path, out.join('\n'), 'utf8');
|
||||
console.log('written', path);
|
||||
console.log('materials', mats.length, 'recipes', recipes.length, 'links', links.length);
|
||||
console.log(
|
||||
'empty-material recipes:',
|
||||
recipes.filter((r) => r.materials.length === 0).map((r) => r.category + ':' + r.name).join(', '),
|
||||
);
|
||||
console.log('sample materials:', mats.slice(0, 20).map((m) => m.name + '(' + m.unit + ')').join(', '));
|
||||
Reference in new issue
Block a user